Classes with Pointer Members

You can use the new and delete operators from within the member functions of a class. Suppose you wanted to write a String class, where each object contains a character string. It's inappropriate to store the strings as arrays, since you don't know how long they'll be. Instead, you can give each object a character pointer as a member, and dynamically allocate an appropriate amount of memory for each object. For example:

#include <iostream.h>

#include <string.h>

// ------- A string class

class String

{

public:

String();

String( const char *s );

String( char c, int n );

void set( int index, char newchar );

char get( int index ) const;

int getLength() const { return length; }

void display() const { cout << buf; }

~String();

private:

int length;

char *buf;

};

// Default constructor

String::String()

{

buf = 0;

length = 0;

}

// ---------- Constructor that takes a const char *

String::String( const char *s )

{

length = strlen( s );

buf = new char[length + 1];

strcpy( buf, s );

}

// ---------- Constructor that takes a char and an int

String::String( char c, int n )

{

length = n;

buf = new char[length + 1];

memset( buf, c, length );

buf[length] = '\0'; }

// ---------- Set a character in a String

void String::set( int index, char newchar )

{

if( (index > 0) && (index <= length) )

buf[index - 1] = newchar;

}

// ---------- Get a character in a String

char String::get( int index ) const

{

if( (index > 0) && (index <= length) )

return buf[index - 1];

else

return 0;

}

// ---------- Destructor for a String

String::~String()

{

delete buf; // Works even for empty String; delete 0 is safe

}

main()

{

String myString( "here's my string" );

myString.set( 1, 'H' );

}

The String constructor that takes a character pointer uses the new operator to allocate a buffer to contain the string. It then copies the contents of the string into the buffer. As a result, a String object is not a contiguous block of memory the way a structure variable is. Each String object consists of two blocks of memory, one that contains length and buf, and another that stores the characters themselves.

If you call sizeof to find the size of a String object, you get only the size of the block containing the integer and the pointer. However, different String objects may have character buffers of different lengths.

In fact, you can write a member function that changes the length of a String object's character buffer. For example:

void String::append( const char *addition )

{

char *temp;

length += strlen( addition );

temp = new char[length + 1]; // Allocate new buffer

strcpy( temp, buf ); // Copy contents of old buffer

strcat( temp, addition ); // Append new string

delete buf; // Deallocate old buffer

buf = temp;

}

This function appends a new string to the contents of an existing String object. For example:

String myString( "here's my string" );

myString.append( " and here's more of it" );

// myString now holds "here's my string and here's more of it"

A String object is thus dynamically resizeable. In C, most programmers resize a character buffer explicitly, which requires a lot of work. In C++, however, you can resize String objects very easily; all the details of the resizing are handled by the member functions.

The String class is an example of a class that requires a destructor. When a String object goes out of scope, the block of memory containing length and buf is deallocated automatically. However, the character buffer was allocated with new, so it must be deallocated explicitly. As a result, the String class defines a destructor that uses the delete operator to deallocate the character buffer. If the class didn't have a destructor, the character buffers would never be deallocated and the program might eventually run out of memory.

The String class has potential problems, however. Suppose you added the following code to the main function in this example:

String yourString( "here's your string" );

yourString = myString;

The program constructs a String object named yourString and then assigns the contents of myString to it. This looks harmless enough, but it actually causes problems.

When you assign one object to another, the compiler performs a memberwise assignment; that is, it does the equivalent of the following:

// Hypothetical equivalent of yourString = myString

yourString.length = myString.length;

yourString.buf = myString.buf;

The assignment of the length member is no problem. However, the buf member is a pointer. The result of the pointer assignment is that yourString.buf and myString.buf point to the same location in memory. The two objects share the same character buffer. This is illustrated in Figure 5.1.

This means that any modifications to one of the String objects affects both of them. If you call myString.set(), you will modify yourString as well. This behavior probably isn't what you desired.

More serious problems arise when the objects go out of scope. When the String class's destructor is called for myString, it deletes the object's buf pointer, deallocating the memory that it points to. Then the destructor is called again for yourString, and it deletes that object's buf pointer. But both buf members have the same value, which means the pointer is deleted twice. This can cause unpredictable results. Furthermore, the original buffer in yourString, containing “here's your string,” is lost. That block of memory is never deleted.

These problems occur for any class that has pointer members and allocates memory from the free store. The compiler's default behavior for assigning one object to another is unsatisfactory for such classes. The solution is to replace the compiler's default behavior by writing a special function to perform the assignment, called the “assignment operator.”