Dynamic Bag Class


Pointer Member Variables

The original bag has a member variable that is a static array named data. The new, dynamic bag has a member variable named data that is a pointer to a dynamic array. In both cases, the array is a partially filled array so we need to keep the variable, used.

Here is a comparison of the two class definitions:

    // From bag1.h
    class bag
    {
       ...
    private:
       value_type data[CAPACITY];
       size_type  used;
    }
    
// From bag2.h
    class bag
    {
       ...
    private:
       value_type *data;
       size_type  used;
       size_type  capacity;
    };
    

The static array can never hold more than 30 items, but we will arrange for the dynamic bag to hold as many items as our user ever inserts into the bag. We'll begin with an array of size 30 (we have to choose some initial size!) but if the user calls insert( ) more than 30 times, we'll just make a larger array and copy all the stuff from the old array into the new one, then insert the user's new item.

Since the size of the array can actually change, we will need to add a data member called capacity that can store the current size of the array. Then insert can check to see if there is room for a new item (or if it needs to create a bigger array) by comparing used to the current capacity.

Invariant

  1. The number of items in the bag is in the member variable used.
  2. The actual items of the bag are stored in a partially filled array. The array is a dynamic array, pointed to by the member variable data.
  3. The total size of the dynamic array is in the member variable capacity.

Constructor

The constructor allocates the actual array that the pointer named data points to. We will have it make the array be of size 30 to start with unless the user who declares the bag sends the constructor a different value. The constructor's prototype in the header file will look like this:

class bag
{
public:
        ...
        static const size_type DEFAULT_CAPACITY = 30;        

        // CONSTRUCTORS and DESTRUCTOR
        bag(size_type initial_capacity = DEFAULT_CAPACITY);
        ...
};
Here is the constructor as it appears in the .C file:
    bag::bag(size_type initial_capacity)
    {
        data     = new value_type[initial_capacity];
        capacity = initial_capacity;
        used     = 0;
    }
If a user wants his bag to start out with room for 500 items, he can declare a bag this way:
bag mybag(500);

The "reserve" Function

We will have a function that can be called to change the size of the array when a program is running. A user can call the function or a member function such as insert can call the function. The function's name is reserve and it gets sent the size that it is supposed to make the array become. Container classes in the Standard Template Library all have function's like this one named reserve. The reserve function's prototype in the header file looks like this:
    void reserve(size_type new_capacity);
The actual function definition looks like this:
    void bag::reserve(size_type new_capacity)
    // Library facilities used: algorithm
    {
        value_type *larger_array;

        if (new_capacity == capacity)
            return; // The allocated memory is already the right size.

        if (new_capacity < used)
            new_capacity = used; // Canít allocate less than we are using.

        larger_array = new value_type[new_capacity];
        copy(data, data + used, larger_array);
        delete [ ] data;
        data     = larger_array;
        capacity = new_capacity;
    }
The line that says "delete [ ] data" deallocates the memory that the pointer named data points to.

Value Semantics

When a class uses dynamic memory allocation, the assignment operator and the copy constructor provided for free by the compiler no longer work. They do a straight member to member copy. Thus, if a user has two bags, b and c, and says
b = c;
then copies are made of c's three variables and placed into b's three variables. Now b's data pointer points to the same area of memory as c's data pointer. Not good! What we need to do is make a new array for b and copy all the stuff from c's array over to b's array. If the array that b already has is the same size as c's array, we don't have to make a new one. We can just do the copy. But if b's array is a different size than c's, we first deallocate b's current array and then make one of the correct size and do the copy.

The free copy constructor does not work either. We have to write both our own copy constructor and our own assignment operator. In the header file these two functions look like this:

    bag(const bag& source);
    
    void operator =(const bag& source);
Here are the actual function implementations:

    bag::bag(const bag& source)
    // Library facilities used: algorithm
    {
        data     = new value_type[source.capacity];
        capacity = source.capacity;
        used     = source.used;

        copy(source.data, source.data + used, data);
    }


    void bag::operator =(const bag& source)
    // Library facilities used: algorithm
    {
        value_type *new_data;

        // Check for possible self-assignment:
        if (this == &source)
            return;

        // If needed, allocate an array with a different size:
        if (capacity != source.capacity)
        { 
            new_data = new value_type[source.capacity];
            delete [ ] data;
            data     = new_data;
            capacity = source.capacity;
        }

        // Copy the data from the source array:
        used = source.used;
        copy(source.data, source.data + used, data);
    }

The Destructor

Any class that uses dynamic memory also needs to have a destructor written for it. The job of this destructor is to return an object's dynamic memory to the heap when the object is no longer in use. This happens any time that an object is "going away", e.g., when a function is ending and it has a local variable or a value parameter that is an object, or when a user calls "delete" for an object.

A destructor has these features:

  1. The name of the destructor is always the character ~ followed by the class name. In our example the name of the destructor is ~bag.
  2. The destructor has no parameters and no return value. You must put empty parentheses after its name, however.
Here is the destructor for the bag class, as it looks in the .h file and as it looks in the .C file:
    ~bag( );
    
    
    
    bag::~bag( )
    {
        delete [ ] data;
    }

Changes to Existing Functions

The only changes we need to make to existing functions when we change the way that the array is allocated are those dealing with the size of the array. In insert, for example, we used to have an assert statement that made sure that the bag was big enough to hold the new item being inserted. We no longer need that assert. In its place we will put an if statement that says "if the array isn't big enough to hold another item, call reserve and make it bigger." Here is the new insert function:
    void bag::insert(const value_type& entry)
    {   
        if (used == capacity)
            reserve(used+1);
        data[used] = entry;
        ++used;
    }
Your instructor disagrees with the author's decision to make the array bigger by only one element's space. I think we should add enough space to hold at least 10 new elements.

Here is the new operator +=:

    void bag::operator +=(const bag& addend)
    // Library facilities used: algorithm
    {
        if (used + addend.used > capacity)
            reserve(used + addend.used);
        
        copy(addend.data, addend.data + addend.used, data + used);
        used += addend.used;
    }
And here is the new operator +:
    bag operator +(const bag& b1, const bag& b2)
    {
        bag answer(b1.size( ) + b2.size( ));

        answer += b1; 
        answer += b2;

        return answer;
    }