2

I've been working on an assignment and now I'm stuck with buggy destructors. I have to create a generic binary tree with all the usual member functions and some special operators. There's also a restriction: everything must work iteratively so no nasty recursive hacks this time.

There is obviously something very wrong with the destructor of BinTreeNode class because if I delete the node like this:

BinTreeNode<int> * node = new BinTreeNode<int>();
delete node; 

I can still access its data:

node->getData(); //should fail miserably

so deletion has no effect but I have no usable idea how I should correct the destructor. It seems to me that the algorithm should be about right so I suspect there's something wrong with how I use pointers but at this point I'm so confused that I don't even understand my own code.

Code I have this far:

BinTree.h

#ifndef BINTREE_H_
#define BINTREE_H_

#ifndef NULL
#define NULL 0
#endif

#include "BinTreeNode.h"

template <class T>
class BinTree
{
    private:
        BinTreeNode<T> * root;
    public:
        //constructors and destructor
        BinTree():
            root(NULL){}

        BinTree(T data):
            root(new BinTreeNode<T>(data)){}

        ~BinTree();

        //search
        BinTreeNode<T> * search(T data);

        //insert
        bool insert(T data);

        //remove
        bool remove(T data);
};

template <class T>
BinTree<T>::~BinTree()
{
    delete root;
}

template <class T>
BinTreeNode<T> * BinTree<T>::search(T data)
{
    BinTreeNode<T> * node = new BinTreeNode<T>(data);
    BinTreeNode<T> * current = root;

    while (current != NULL)
    {
        if (*current == *node)
        {
            delete node;
            return root;
        }
        else if (*node < *current)
        {
            current = current->getLeft();
        }
        else
        {
            current = current->getRight();
        }
    }
    delete node;
    return NULL;
}

template <class T>
bool BinTree<T>::insert(T data)
{
    BinTreeNode<T> * node = new BinTreeNode<T>(data);
    BinTreeNode<T> * current = root;

    while (current != NULL)
    {
        if (*current == *node)
        {
            delete node;
            return false;
        }
        else if (*node < *current)
        {
            if (current->getLeft() == NULL)
            {
                current->setLeft(node);
                return true;
            }
            else
            {
                current = current->getLeft();
            }
        }
        else
        {
            if (current->getRight() == NULL)
            {
                current->setRight(node);
                return true;
            }
            else
            {
                current = current->getRight();
            }
        }
    }
    return false;
}

#endif

BinTreeNode.h

#ifndef BINTREENODE_H_
#define BINTREENODE_H_

#ifndef NULL
#define NULL 0
#endif

template <class T>
class BinTreeNode
{
    private:
        T data;
        BinTreeNode<T> *left, *right, *parent;
    public:
        //constructors and destructor
        BinTreeNode():
            data(NULL), left(NULL), right(NULL), parent(NULL){}

        BinTreeNode(T data):
            data(data), left(NULL), right(NULL), parent(NULL){}

        ~BinTreeNode();

        //set and get data member
        T getData() const;

        void setData(T data);

        //set and get left and right branches
        BinTreeNode<T> * getLeft() const;

        BinTreeNode<T> * getRight() const;

        void setLeft(BinTreeNode<T> * node);

        void setRight(BinTreeNode<T> * node);

        //set and get parent
        BinTreeNode<T> * getParent() const;

        void setParent(BinTreeNode<T> * node);

        //comparison operators
        bool operator<(const BinTreeNode<T>& node) const;
        bool operator>(const BinTreeNode<T>& node) const;
        bool operator==(const BinTreeNode<T>& node) const;
};

template <class T>
BinTreeNode<T>::~BinTreeNode()
{
    BinTreeNode<T> * current = this;
    BinTreeNode<T> * parent = NULL;
    while (current != NULL)
    {
        parent = current->getParent();
        if (current->getLeft() == NULL)
            current = current->getLeft();
        else if (current->getRight() == NULL)
            current = current->getRight();
        else
        {
            if (parent->getRight() == current)
                parent->setRight(NULL);
            else
                parent->setLeft(NULL);
             current = NULL; // this line (among others) is very suspicious
        }
        current = parent;
    }
}

template <class T>
T BinTreeNode<T>::getData() const
{
    return data;
}

template <class T>
void BinTreeNode<T>::setData(T data)
{
    this->data = data;
}

template <class T>
BinTreeNode<T> * BinTreeNode<T>::getLeft() const
{
    return left;
}

template <class T>
BinTreeNode<T> * BinTreeNode<T>::getRight() const
{
    return right;
}

template <class T>
void BinTreeNode<T>::setLeft(BinTreeNode<T> * node)
{
    node->setParent(this);
    left = node;
}

template <class T>
void BinTreeNode<T>::setRight(BinTreeNode<T> * node)
{
    node->setParent(this);
    right = node;
}

template <class T>
BinTreeNode<T> * BinTreeNode<T>::getParent() const
{
    return parent;
}

template <class T>
void BinTreeNode<T>::setParent(BinTreeNode<T> * node)
{
    parent = node;
}

template <class T>
bool BinTreeNode<T>::operator<(const BinTreeNode<T>& node) const
{
        return this->data < node.data;
}

template <class T>
bool BinTreeNode<T>::operator>(const BinTreeNode<T>& node) const
{
    return this->data > node.data;
}

template <class T>
bool BinTreeNode<T>::operator==(const BinTreeNode<T>& node) const
{
    return this->data == node.data;
}

#endif /* BINTREENODE_H_ */
Athelionas
  • 181
  • 3
  • 12

1 Answers1

7

Your BinTreeNode destructor should simply be:

template <class T>
BinTreeNode<T>::~BinTreeNode() {
    delete left;
    delete right;
}

That will call left and right's destructors recursively, freeing the memory allocated for those nodes and their child nodes. This will as a consequence free the entire tree.

Assigning NULL to a pointer does not free the memory pointed by it.

On the other hand, what you mention, that after deletion, this line:

node->getData();

Still returns data, is perfectly normal. Deletion frees the memory, but the data stored in it might still be available for a while, until something new is written in that memory address. Accessing an already free'd memory address implies undefined behaviour.

BTW, you should use "0"(without quotes) in C++ instead of NULL. Therefore, there it's not necessary to use the #ifndef NULL(...).

EDIT: I hadn't seen the "no recursion" comment. Here's a non-recursive algorithm:

#include <deque>

/* ... */

template <class T>
BinTreeNode<T>::~BinTreeNode() {
    std::deque deq;
    // we're going to delete our children
    deq.push_back(this);
    while(deq.size()) {
        BinTreeNode<T> *ptr = deq.front();
        deq.pop_front();
        if(ptr) {
            deq.push_back(ptr->left);
            deq.push_back(ptr->right);
            // we don't want the child nodes
            // to double delete the children
            ptr->left = 0;
            ptr->right = 0;
            // avoid deleteing ourselves
            if(ptr != this)
                delete ptr;
        }
    }
}

I haven't tested it, but it should work.

mfontanini
  • 21,410
  • 4
  • 65
  • 73
  • 1
    It is worth adding that whilst it is highly likely this will access the data that used to be stored in the now unallocated memory it is technically undefined behaviour, so it can do anything. – Charles Keepax Mar 16 '12 at 23:38
  • @Charles Keepax i added a line mentioning the undefined behaviour associated with accessing a free'd address. – mfontanini Mar 16 '12 at 23:40
  • This recursive method is indeed very neat but as I stated it earlier I'm not allowed to use recursion in any of my functions. This is IMHO a very stupid restriction (given the ease that recursion means for binary trees) but I have to obey my teacher's wish. – Athelionas Mar 16 '12 at 23:52
  • By the way I had no idea that data could be accessed after deletion (however it seems obvious now) so thanks for the clarification. Using NULL is a bad habit from C times so yes, I should really give up on using it. – Athelionas Mar 17 '12 at 00:05
  • Sorry, i hadn't seen the "no recursion" part. I've eddited my answer, it now contains a non-recursive algorithm. – mfontanini Mar 17 '12 at 00:17
  • Nice algorithm but I think that using a stack somewhat kills the purpose of not having recursion. Maybe I can modify this algorithm to avoid using stack. Thank you for your effort anyway. I'm sure I can figure out something on my own now. – Athelionas Mar 17 '12 at 00:57
  • 1
    Typically one avoids recursion to avoid filling up the primary stack. Creating one's own stack-like structure in allocated memory (like in the example use of std::deque) serves this purpose quite well. – Michael Urman Mar 17 '12 at 01:19