Previous Table of Contents Next


However, a constructor does not merely assign values to members; it creates those members as well. Therefore, the cleanest way to exploit the similarity between the constructors, destructor, and assignment operator is to define auxiliary member functions that will implement the more primitive operations.

In the case of a String, those operations turn out to be

  Given a pointer to the first character of a null-terminated character array,
Allocate enough dynamic memory to contain a copy of the characters in that array.
Copy the characters from the array into the new memory
Set our pointer to point to the initial character of the new memory.
  Delete the memory to which our pointer points.

With the benefit of all this hindsight, then, we can start implementing our String class from the bottom up:

   class String {
   public:
       // The interface member functions go here
   private:
       char* data;

       void init(const char* p) {
           data = new char[strlen(p) + 1];
           strcpy(data, p);
       }
       void destroy() {
           delete[] data;
       }
   };

Once we have defined these primitive operations, we can write the constructors, destructor, and assignment operator in terms of them:

   class String {
    public:
       String() { init(“”); }
       String(const char* p) { init(p); }
       String(const String& s) { init(s.data); }
       ~String() { destroy(); } String& operator=(const String& s) {
           if (this != &s) {
               destroy();
               init(s.data);
           }
           return *this;
      }
   private:
       // as before
   };

This code is all we need to capture the essentials of variable-length strings.

The first constructor says how to construct a string when we have not given an initial value:

   String s;

Calling init(“”) is an easy way to cause data to point to a dynamically allocated null character, which is how we represent a string with no characters. The second constructor says how to construct a String from a string literal or other character array; the third is the copy constructor. The destructor is trivial; it just calls destroy. In fact, the only part of this class that is not trivial is the assignment operator.

The reason that the assignment operator is slightly tricky is that it has to cater to the possibility of assigning a string to itself:

   String s(“foo”);
   s = s;

Here we used the copy constructor to give s an initial value. There is no problem there. But when we execute s = s, we have a problem: The left- and right-hand sides of the assignment refer to the same object. If we are not careful, we will destroy the data and then try to copy the characters we have just destroyed.

We avoid this problem by checking for self-assignment explicitly, and doing nothing in that particular case. When we’re done, we return *this as our value, whether or not we did any work.

Of course, as it stands, this class is useless because, although we can put characters into an object of that class, there is no way to get any data out of the object. To ensure that the class is not completely useless, we will make it possible to print String objects.

To do that, we must define an overloaded operator<< that will be executed when we write expressions such as

   cout << s

where s is a String object. To do that, we need to know that the type of cout is ostream, and that an output operator << must have a reference to ostream as its first parameter. We must also know that output operators are expected to return their left argument as their result, so that expressions such as

   cout << s << endl

can work. With that knowledge, we can write

   ostream& operator<<(ostream& ostrm, const String& s)
   {
       ostrm << s.data;
       return ostrm;
   }

To print a String, we print the pointer that the String contains. This must work in the same way that

   cout << “Hello, world”

works, because the string literal is little more than a pointer. So we define printing our String class in terms of printing string literals, and we are done.

Well, we’re almost done, anyway. The last thing we need to do is to give our operator<< permission to know about the representation of a String object, by saying

   class String {
       friend ostream& operator<<(ostream&, const String&);
       // as before
   };

Of course, this String class is much simpler than the version in the standard library. For example, for such a class to be useful in practice, it must support input as well as output. It also needs operations to manipulate the contents of Strings, such as fetching individual characters, concatenating Strings, and so on. Such complexities are beyond the scope of this chapter.

The important points to remember about C++ data abstraction are

  C++ supports data abstraction with classes.
  A class definition describes the contents of objects of that class.
  Class objects have members, which can be functions or data.
  Members can be public or private, depending on whether they are part of the interface or the implementation.
  Constructors, destructors, and assignment operators allow fine-grained control over how objects can be used.
  The data-abstraction facilities of C++ make it possible to define new types that are almost as easy to use as the built-in types. In particular, they make it unnecessary to have types such as variable-length strings built into the language. Instead, such types can be—and are—part of a library.


Previous Table of Contents Next