1

After I run this program, and input the Value: value in main(), I get the error "HEAP CORRUPTION DETECTED":

I've spent about an hour trying to figure out what's causing this, would appreciate help in what caused this and what exactly I need to do to fix this.

Thanks!

#include <iostream>

using namespace std;

template <class t>
class dynamicDataHolder
{
    t* ptr = NULL;
    int size = 0;
    int elements = 0;

public:

    dynamicDataHolder() {
        ptr = new t[5];
        size = 5;
        elements = 0;
    }

    dynamicDataHolder(int arraySize) {
        ptr = new t[size];
        size = arraySize;
        elements = 0;
    }

    dynamicDataHolder(const dynamicDataHolder<t>& obj) {
        ptr = new t[obj.size];

        if (ptr != NULL) {
            for (int i = 0; i < obj.size; i++) {
                ptr[i] = obj.ptr[i];
            }
        }
        else {
            cout << "\nPointer to the holder was found to be NULL." << endl;
        }
        elements = obj.elements;
        size = obj.size;
    }

    t getData(int pos) {
        if (pos < 0 || pos > size)
        {
            cout << "\nError: Invalid index!" << endl;
            return 0;
        }

        return ptr[pos];
    }

    int sizeA() {
        return size;
    }

    int capacity() {
        int capacity = size - elements;
        return capacity;
    }

    void insert(t data) {
        int pos;

        if (elements >= size) {
            int updatedSize = (2 * size);

            t* tempPtr = ptr;
            ptr = new t[updatedSize];

            for (int i = 0; i < size; ++i) {
                ptr[i] = tempPtr[i];
            }

            delete[] tempPtr;
            tempPtr = NULL;

            size = updatedSize;
            pos = elements;
            ptr[pos] = data;
            incrementElement();
        }
        else {
            if (elements == 0) {
                pos = 0;
                ptr[pos] = data;
                incrementElement();
            }
            else {
                pos = elements;
                ptr[pos] = data;
                incrementElement();
            }
        }
    }

    bool compare(const dynamicDataHolder<t>& obj)
    {
        bool result = false;

        if (obj.size != size || obj.elements != elements) {
            return result;
        }

        for (int i = 0; i < size; i++) {
            result = false;

            if (ptr[i] == obj.ptr[i])
            {
                result = true;
            }
        }

        return result;
    }

    void resize(int updatedSize) {
        t* tempPtr = ptr;
        ptr = new t[updatedSize];

        if (updatedSize < size) {
            cout << "\nWarning! Entered size is less than the current size." << endl
                << "Loss of data may occur. Continue? (Y/N)" << endl;
        Select:
            char userChoice; cin >> userChoice;
            
            if (userChoice == 'y' || userChoice == 'Y') {
                goto Continue;
            }
            else if (userChoice == 'n' || userChoice == 'N') {
                return;
            }
            else {
                cout << "\nInvalid selection - please try again." << endl;
                goto Select;
            }
        }
        Continue:
        if (ptr != NULL) {
            for (int i = 0; i < size; i++) {
                ptr[i] = tempPtr[i];
            }

            delete[] tempPtr;
            tempPtr = NULL;

            size = updatedSize;
        }
        else {
            cout << "\nPointer to the holder was found to be NULL." << endl;
        }
    }

    void insertAtNthPos()
    {
        cout << "\nEnter the details below to insert values:-\n" << endl;
        cout << "\nPosition: ";
        int pos = 0; cin >> pos;
        cout << "Value: ";
        t value; cin >> value;

        if (pos <= size)
        {
            ptr[pos] = value;

            if (pos > elements) {
                incrementElement();
            }
        }
        else {
            resize(2 * size);
            ptr[pos] = value;
            incrementElement();
        }
    }

    dynamicDataHolder& operator=(const dynamicDataHolder& rhs) {
        delete[] ptr;
        ptr = new t[rhs.size];
        if (this != &rhs)
        {
            for (int i = 0; i < rhs.size; i++)
            {
                ptr[i] = rhs.ptr[i];
            }
        }
        return *this;
    }

    ~dynamicDataHolder()
    {
        delete[] ptr;
        ptr = NULL;
    }

    void incrementElement()
    {
        elements += 1;
    }

};


int main()
{
    cout << "What do you want to do?" << endl
        << "\t1. Run Test Sequence" << endl 
        << "\t0. Exit Program" << endl;
Select:
    int userChoice = 0; cin >> userChoice;

    if (userChoice == 1) {
        goto testSequence;
    }
    else if (userChoice == 0) {
        return 0;
    }
    else {
        cout << "\nInvalid selection - please try again." << endl;
        goto Select;
    }


testSequence:
    system("CLS");

    dynamicDataHolder<int> obj;
    dynamicDataHolder<char> obj2(5);
    dynamicDataHolder<char> obj3 = obj2;

    bool result = (obj3.compare(obj2));
    if (result == 1) {
        cout << "True" << endl;
    }
    else {
        cout << "\nFalse" << endl;
    }

    for (int i = 0; i < 5; i++) {
        obj.insert(i * 2);
    }

    cout << "Capacity: " << obj.capacity() << endl;
    cout << "Size: " << obj.sizeA() << endl;

    obj.resize(15);

    cout << obj2.getData(4) << endl;

    obj2.insertAtNthPos();

    dynamicDataHolder<int> obj4;
    obj4 = obj;
}
sehe
  • 374,641
  • 47
  • 450
  • 633
Hexed
  • 45
  • 5
  • 1
    Enable your compiler warnings. Then your compiler will tell you that in parameterized constructor (`dynamicDataHolder(int arraySize)`) you are using `size` before it is initialized. – Yksisarvinen Apr 09 '21 at 13:50
  • Thank you so much! What a noob mistake haha – Hexed Apr 09 '21 at 14:07

1 Answers1

2

Free code review.

Here's a simplified version of your code

  • replacing goto with loops (this revolution in language development happened in 1970, get with the times!)

  • use the right names! Naming is important.

    In particular

    • capacity means "limit to potential size" (what you names "size" or "arraySize" before)
    • size means "count of elements in the collection" (e.g. size grows on insertion)
    • (capacity - size) is free space/room/available capacity. This is what you named "capacity" before

    Violating expectations of identifier meanings leads to bugs. In fact, you had that, because your own test sequence used getData(4) on a holder that contained 0 elements (_size == 0).

  • Same for operations. insert(...) should insert, not overwrite (see below), resize(...) should change the size, not the capacity.

    To change the capacity, the standard library uses the term reserve(...), which I also used for consistency and least-surprise.

  • Also, names should be descriptive. In a strong typed language naming the variable after its type is redundant information. Also, you can names variables strA, strB, strC but it's more informative to name them givenName, middleNames, surName. Note that suddenly we have semantic information (is it lastName, surName, birthName, familyName. So much information is conveyed).

    In this sense, I renamed ptr to _data

    While we're at it, we might rename the class to be descriptive: DynaArray<T> tells you more than dynamicDataHolder<T> (that could be std::optional<T>, std::unique_ptr<T>, std::vector<T>, harddiskVolume etc).

    As a minor footnote it is a good idea to have a naming convention for (private) member variables, so you will know what variables are local and which are member. I used _data for _ptr

  • You had a number of places (2 if I remember correctly) where you had off-by-one boundary checks (think pos > capacity instead of >=).

  • Separation Of Concerns. dynamicDataHolder<T> is a data holder, not an interactiveUserInterviewAboutErrorConditions<T> or databaseUpdateUserInterface<T>. So, don't do UI in the class. Change void insertAtNthPos() to void setData(size_t pos, T value).

    Again, naming is important. Don't promise to insert if really you don't unless it's beyond current size bounds. "Insert 7 at pos 1 in {1,2,3}" should result in "{1,7,2,3}", not "{1,7,3}". The name setData

    • does what is expected and
    • separates UI concerns and
    • nicely mirrors getData(size_t pos)
  • Oh, use consistent index types (size_t in my version) so that you avoid funny looking checks (pos > 0?) and bugs due to mixed signed/unsigned comparisons.

    Yes, you had them. No you wouldn't have caught them because you didn't have compiler warnings enabled. Let this reinforce the mantra: "Use The Force Diagnostics"

  • Don't do "using namespace" at global scope. Or not at all (Why is "using namespace std;" considered bad practice?)

  • DRY. DRY. DRY. (Don't Repeat Yourself). Sooo many examples of this. You had at least 3 copy-element-data loops. You had several places that resize the allocated array. You had funny stuff like:

     void insert(t data) {
         int pos;
    
         if (elements >= size) {
             int updatedSize = (2 * size);
    
             t* tempPtr = ptr;
             ptr = new t[updatedSize];
    
             for (int i = 0; i < size; ++i) {
                 ptr[i] = tempPtr[i];
             }
    
             delete[] tempPtr;
             tempPtr = NULL;
    
             size = updatedSize;
             pos = elements;
             ptr[pos] = data;
             incrementElement();
         }
         else {
             if (elements == 0) {
                 pos = 0;
                 ptr[pos] = data;
                 incrementElement();
             }
             else {
                 pos = elements;
                 ptr[pos] = data;
                 incrementElement();
             }
         }
    

    It should be obvious that this does the exact same thing three times. The if (elements == 0) check is completely useless, because if you don't have the check, the else branch does the same anyways. For comparison, the new insert looks like:

     void insert(T data) { setData(_size, data); }
    

    Oh oops, that got completely zapped, because really, there is nothing special about inserting. But setData is also only ~4 lines of code.

  • when you add items you have to be careful: you had a very helpful "capacity check". And if the capacity was not enough you doubled capacity. HOWEVER. Who said that was enough? You need to either keep doubling until space is adequate:

     void setData(size_t pos, T value) {
         while (pos >= _capacity) {
             grow();
         }
         _size      = std::max(_size, pos + 1);
         _data[pos] = value;
     }
    

    or reserve sufficient size at once:

     size_t required = _capacity;
     while (pos >= required)
         required *= 2;
     reserve(required);
    
  • Const-Correctness: all the observers should be marked const so the compiler knows what it can optimize and the user has guarantees what will never change:

     size_t size()      const { return _size;     }
     size_t capacity()  const { return _capacity; }
     size_t available() const { return _capacity  - _size; }
    
     bool compare(const DynArray<T>& obj) const;
    
  • The repeated copy/initialization loops can be fixed by using the copy constructor. In related context, make the constructor use a base/member initializer list:

     explicit DynArray(size_t initialCapacity = 5)
         : _capacity(initialCapacity),
           _data(initialCapacity != 0u ? new T[_capacity]{} : nullptr) {}
    

    Now the copy constructor can be:

     DynArray(DynArray const& obj) : DynArray(obj.capacity()) {
         _size = obj.size();
         for (size_t i = 0; i < capacity(); ++i)
             _data[i] = obj._data[i];
     }
    

    That's the last time you will ever see a copy loop. Why? Observe:

  • How would you reserve more / less space and without manually copying items? Wouldn't you need a dynamic array class of some kind?

    Oh wait. You're implementing it! Don't forget the code you already have:

     void reserve(size_t updatedSize) {
         DynArray tmp(updatedSize);
         for (size_t i = 0; i < std::min(capacity(), tmp.capacity()); ++i) {
             tmp._data[i] = _data[i];
         }
         *this = std::move(tmp);
     }
    
     void grow() { reserve(2 * _capacity); }
    

    Now the real magic is in the move constructor/assignment operations.

  • Copy-and-Swap idiom to the rescue. Turns out you can implement these operations really efficiently and generally with little code:

     DynArray(DynArray&& obj) : DynArray(0) { swap(obj); }
     DynArray& operator=(DynArray rhs) {
         swap(rhs);
         return *this;
     }
    

    See. Simplicity is a driver. The key is that you just swap with a temporary. The temporary gets destructed, as you want. The swap is pretty simple in its own right:

     void swap(DynArray& rhs) {
         assert(rhs._data);
         std::swap(_capacity, rhs._capacity);
         std::swap(_size, rhs._size);
         std::swap(_data, rhs._data);
     }
    

    There's a lot more thinking behind Copy-And-Swap, but you can find that on your own if you really want: What is the copy-and-swap idiom?

  • compare checked capacities. I'd say that from a logical standpoint that shouldn't matter.

      DynArray<int> a(20), b(12);
      for (int v : {1,2,3}) { a.insert(v); b.insert(v); }
      assert(a == b); // should pass
    
  • it also had bugs, a spurious

     result = false;
    

    and erroneous

     if (ptr[i] == obj.ptr[i])
     {
         result = true;
     }
    

    That made no sense. If a single item matches, doesn't mean all the previous mismatches are gone.

The above has removed a lot of code, and many many lurking bugs or maintenance head-aches. I reworded your test code to be ... readable. The gotos are obviously gone, the main is now "sane":

int main() {
    std::cout << "What do you want to do?\n"
              << "\t1. Run Test Sequence\n"
              << "\t0. Exit Program\n";

    for (int userChoice = 0; std::cin >> userChoice;) {
        switch (userChoice) {
            case 1: testSequence(); break;
            case 0: return 0;
            default: std::cout << "\nInvalid selection - please try again.\n";
        }
    }
}

And the information displayed is more complete and much less error prone. Naming can still be improved (obj2? obj4?!??) but I had no semantic information to go by, so I left that as an exorcism for the reader, and also so that you will have an easy time cross-referencing your own test sequence code with mine.

Live Demo

Live On Coliru

#include <iostream>
#include <stdexcept>
#include <cassert>

template <class T> class DynArray {
    size_t _capacity = 0;
    size_t _size     = 0;
    T* _data         = nullptr;

  public:
    explicit DynArray(size_t initialCapacity = 5)
        : _capacity(initialCapacity),
          _data(initialCapacity != 0u ? new T[_capacity]{} : nullptr) {}

    DynArray(DynArray const& obj) : DynArray(obj.capacity()) {
        _size = obj.size();
        for (size_t i = 0; i < capacity(); ++i)
            _data[i] = obj._data[i];
    }

    ~DynArray() { delete[] _data; }

    void swap(DynArray& rhs) {
        assert(rhs._data);
        std::swap(_capacity, rhs._capacity);
        std::swap(_size, rhs._size);
        std::swap(_data, rhs._data);
    }

    DynArray(DynArray&& obj) : DynArray(0) { swap(obj); }
    DynArray& operator=(DynArray rhs) {
        swap(rhs);
        return *this;
    }

    T getData(size_t pos) const {
        if (pos >= _size) {
            throw std::out_of_range("pos");
        }

        return _data[pos];
    }

    void setData(size_t pos, T value) {
        while (pos >= _capacity) 
            grow();
        _size = std::max(_size, pos+1);
        _data[pos] = value;
    }

    void insert(T data) { setData(_size, data); }
    size_t size()      const { return _size;     }
    size_t capacity()  const { return _capacity; }
    size_t available() const { return _capacity  - _size; }

    bool compare(const DynArray& obj) const {
        if (obj._size != _size) {
            return false;
        }

        for (size_t i = 0; i < _size; i++) {
            if (_data[i] != obj._data[i])
                return false;
        }

        return true;
    }

    void reserve(size_t updatedSize) {
        DynArray tmp(updatedSize);
        for (size_t i = 0; i < std::min(capacity(), tmp.capacity()); ++i) {
            tmp._data[i] = _data[i];
        }
        *this = std::move(tmp);
    }

    void grow() { reserve(2 * _capacity); }
};

template <typename T>
void dump(std::string caption, DynArray<T> const& da) {
    std::cout << caption << " Capacity: "  << da.capacity()  << ","
                         << " Size: "      << da.size()      << ","
                         << " Available: " << da.available() << ","
                         << " Data: {";
    for (size_t i = 0; i < da.size(); i++)
        std::cout << " " << static_cast<int>(da.getData(i));
    std::cout << " }\n";
}

void testSequence() {
    {
        DynArray<int> obj;
        for (size_t i = 0; i < 5; i++) {
            obj.insert(i * 2);
        }

        dump("obj", obj);

        if (15 < obj.capacity()) {
            std::cout << "\nWarning! Entered size is less than the current size.\n"
                << "Loss of data may occur. Continue? (Y/N)\n";

            for (char userChoice; std::cin >> userChoice;) {
                if (userChoice == 'y' || userChoice == 'Y') {
                    obj.reserve(15);
                    break;
                }
                if (userChoice == 'n' || userChoice == 'N') {
                    break;
                }
                std::cout << "\nInvalid selection - please try again.\n";
            }
        }

        DynArray<int> obj4;
        obj4 = obj;

        dump("obj4", obj);
    }

    DynArray<char> obj2(5);
    DynArray<char> obj3 = obj2;

    dump("obj2", obj2);
    dump("obj3", obj3);
    std::cout << "obj2 == obj3: " << std::boolalpha << obj3.compare(obj2) << "\n";

    try {
        std::cout << "obj2.getData(4): " << obj2.getData(4) << std::endl;
    } catch(std::out_of_range const& e) {
        std::cout << "out of range: " << e.what() << std::endl;
    }

    while (true) {
        std::cout << "\nEnter the details below to insert obj2 values:-\n";
        std::cout << "\nPosition: ";
        long pos = 0;
        if (std::cin >> pos) {
            if (pos < 0)
                break;
            std::cout << "Value: ";
            int value = 0;
            if (std::cin >> value) {
                obj2.setData(pos, value);
            }
        }
        if (!std::cin.eof())
            std::cin.clear();
        else
            break;
        
        dump("obj2", obj2);
    }
}

int main() {
    do {
        std::cout << "What do you want to do?\n"
                  << "\t1. Run Test Sequence\n"
                  << "\t0. Exit Program\n";
        if (int userChoice = 0; std::cin >> userChoice) {
            switch (userChoice) {
                case 1: testSequence(); continue;
                case 0: std::cout << "Goodbye\n"; return 0;
                default: std::cout << "\nInvalid selection - please try again.\n";
            }
        }
        if (!std::cin.eof())
            std::cin.clear();
    } while (std::cin.good());
}

When run with e.g.

g++ -std=c++17 -O2 -Wall -Wextra -pedantic -pthread -fsanitize=address,undefined main.cpp -o sotest
./sotest <<< "1 30 99 3 42 0 -1 0 8 -1 1 -1 0"

You will see the following output:

What do you want to do?
1. Run Test Sequence
0. Exit Program
obj Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj4 Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj2 Capacity: 5, Size: 0, Available: 5, Data: { }
obj3 Capacity: 5,
Size: 0, Available: 5, Data: { } obj2 == obj3: true
obj2.getData(4): out of range: pos

Enter the details below to insert obj2 values:-

Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }

Enter the details below to insert obj2 values:-

Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { 0
0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }

Enter the details below to insert obj2 values:-

Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { -1
0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }

Enter the details below to insert obj2 values:-

Position: Value: obj2 Capacity: 40, Size: 31, Available: 9, Data: { 8
0 0 42 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 99 }

Enter the details below to insert obj2 values:-

Position: What do you want to do?
1. Run Test Sequence
0. Exit Program
obj Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj4 Capacity: 5, Size: 5, Available: 0, Data: { 0 2 4 6 8 }
obj2 Capacity: 5, Size: 0, Available: 5, Data: { }
obj3 Capacity: 5,
Size: 0, Available: 5, Data: { }
obj2 == obj3: true
obj2.getData(4): out of range: pos

Enter the details below to insert obj2 values:-

Position: What do you want to do?
1. Run Test Sequence
0. Exit Program
Goodbye

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Trivia: the reviewed code is 172 lines of code, as opposed to 252 lines previously. [All while displaying much more detailed output]. This is a good mantra: less code is better – sehe Apr 09 '21 at 22:16
  • Thank you so much! I'm still a beginner so there's still a long way to go. Much appreciated! – Hexed Apr 11 '21 at 15:54
  • Also thought to let you know that some stuff was required (in an assignment) to be that way and I kind of had to make it like so. Maybe later they will teach us how to improve this code. Anyways, again, thank you for taking the time to explain everything! – Hexed Apr 11 '21 at 16:26
  • That's okay. Schools are "the worst" - this is very common. And probably fine. As long as you realize that schools teach basics, concepts, and not often in a realistic way. It's very useful to realize this early on, because I've seen it turn a lot of people off from programming ("Is this what it is about? I hate that" and just give up) – sehe Apr 11 '21 at 18:20