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
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 were 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, were 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
Previous | Table of Contents | Next |