CHAPTER 7 Creating a Homegrown string class
You may recall the discussion near the beginning of Chapter 6 of native vs. user-defined variable types. I provided a list of native C++ variable types: char, short, long, float, double, bool, and int. We've already created several classes for our inventory control project, and now it's time to apply what we've learned to a more generally useful type, the string. We've been using strings for a long time and now it's time to see exactly how to implement our own string class. This class is similar, although not identical, to the standard string class we've been using.
First, though, you may be wondering why we should go through the trouble of learning how to implement a string class, when the standard library provides one for us. The answer is simple: creating a string class illustrates many of the subtle points of creating any class significantly more complicated than the StockItem class in the previous chapter. Since the purpose of this book is to teach you C++ programming, we might as well use instructional classes that at least resemble actual, useful classes like the string class.
7.1. Objectives of This Chapter
By the end of this chapter, you should
1. Understand how variables of a string class are created, destroyed, and assigned to one another.
2. Understand how to assign memory to variables where the amount of memory needed is not known until the program is running.
3. Understand how C strings work and how we can use them in our string class implementation.
4. Understand how C string literals can be used to initialize variables of a string class.
7.2. C String Literals vs. strings
Susan had some questions about these objectives. Here's the discussion.
Susan: What is the difference between C string literals and variables of the string class?
Steve: A variable of the standard string class is what you've been using to store variable-length alphanumeric data. You can copy them, input them from the keyboard, assign values to them, and the like. By contrast, a C string literal is just a bunch of characters in a row; all you can do with it is display it or assign it to a string variable.
Susan: OK, then you are saying that variables of the string class are what I am used to working with. On the other hand, a C string literal is just some nonsense that you want me to learn to assign to something that might make sense? OK, this is great; sure, this is logical. Hey, a C string literal must be a part of the native language?
Steve: Right all the way along.
Susan: Yes, but why would something so basic as string not be part of the native language? This is what I don't understand. And Vecs too; even though they are difficult, I can see that they are a very necessary evil. So tell me why those basic things would not be part of the native language?
Steve: That's a very good question. That decision was made to keep the C++ language itself as simple as possible.1 So rather than include those data types directly in the language, they were added as part of the standard library.
Before we get into how to create a string class like the one we've been using in this book, I should expand on the answer I gave Susan as to why string isn't a native type in the first place. One of the design goals of C++, as of C, was to allow the language to be moved, or ported, from one machine type to another as easily as possible. Since strings, vectors, and so on can be written in C++ (i.e., created out of the more elementary parts of the language), they don't have to be built in. This reduces the amount of effort needed to port C++ to a new machine or operating system. In addition, some applications don't need and can't afford anything but the barest essentials; "embedded" CPUs such as those in cameras, VCRs, elevators, or microwave ovens, are probably the most important examples of such applications, and such devices are much more common than "real" computers.
Even though the standard library strings aren't native, we've been using them for some time already without having to concern ourselves with that fact, so it should be fairly obvious that such a class provides the facilities of a concrete data type; that is, one whose objects can be created, copied, assigned, and destroyed as though they were native variables. You may recall from the discussion starting in the section entitled "Concrete Data Types" on page 309 that such data types need a default constructor, a copy constructor, an assignment operator, and a destructor. To refresh your memory, here's the description of each of these member functions:
1. A default constructor creates an object when there is no initial value specified for the object.
2. A copy constructor makes a new object with the same contents as an existing object of the same type.
3. An assignment operator sets an existing object to the value of another object of the same type.
4. A destructor cleans up when an object expires; for a local object, this occurs at the end of the block where it was created.
In the StockItem and Inventory class definitions that we've created up to this point, the compiler-generated versions of these functions were fine for all but the default constructor. In the case of our string class, though, we're going to have to create our own versions of all four of these functions, for reasons that will become apparent as we examine their implementations in this chapter and the next one.
Before we can implement these member functions for our string class, though, we have to define exactly what a string is. A string class is a data type that gives us the following capabilities in addition to those facilities that every concrete data type provides:
1. We can set a string to a literal value like "abc".
2. We can display a string on the screen with the << operator.
3. We can read a string in from the keyboard with the >> operator.
4. We can compare two strings to find out whether they are equal.
5. We can compare two strings to find out which is "less than" the other; that is, which one would come first in the dictionary.
We'll see how all of these capabilities work sometime in this chapter or the next one. But for now, let's start with Figure 7.1, a simplified version of the interface specification for our string class that includes the specification of the four member functions needed for a concrete data type, as well as a special constructor that is specific to the string class. I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you are going through this part of the chapter. Those files are string1.h, string1.cpp, and strtst1.cpp, respectively.
FIGURE 7.1. Our string class interface, initial version (code\string1.h)
string& operator = (const string& Str);
The first four member functions in that interface are the standard concrete data type functions. In order, they are
1. The default constructor
2. The copy constructor
3. The assignment operator, operator =
4. The destructor
I've been instructed by Susan to let you see all of the code that implements this initial version of our string class at once before we start to analyze it. Of course I've done so, and Figure 7.2 is the result.
FIGURE 7.2. The initial implementation for the string class (code\string1.cpp)
#include <cstring>
using std::memcpy;
using std::strlen;
#include "string1.h"
string::string()
: m_Length(1),
m_Data(new char [m_Length])
{
memcpy(m_Data,"",m_Length);
}
string::string(const string& Str)
: m_Length(Str.m_Length),
m_Data(new char [m_Length])
{
memcpy(m_Data,Str.m_Data,m_Length);
}
string::string(char* p)
: m_Length(strlen(p) + 1),
m_Data(new char [m_Length])
{
memcpy(m_Data,p,m_Length);
}
string& string::operator = (const string& Str)
{
char* temp = new char[Str.m_Length];
m_Length = Str.m_Length;
memcpy(temp,Str.m_Data,m_Length);
delete [ ] m_Data;
m_Data = temp;
return *this;
}
string::~string()
{
delete [ ] m_Data;
}
The first odd thing about that implementation file is the #include <cstring>. So far, we've been using #include <string> to tell the compiler that we want to use the standard C++ library string class. So what is <cstring>?
It's a leftover from C that defines a number of functions that we will need to implement our own string class, primarily having to do with memory allocation and copying of data from one place to another. It used to be called <string.h>, and in fact you can still refer to it by that name, but all of the C standard library header files have now been renamed to follow the new C++ standard of no extension. For your reference, the new name for every C standard library header file consists of the old name with the ".h" removed and a "c" added to the beginning.
As for #include "string1.h", as you can tell from the "" around the name, that's one of our own header files; in this case, it's the header file where we declare the interface for the first version of our string class.
There's one more thing I should explain about the programs in this chapter and the next: they don't include the usual line using namespace std;. This is because we're not using the standard library string class in these programs. If we were to include that line, the compiler would complain that it couldn't tell which string class we were referring to, the one from the standard library or the one that we are defining ourselves.
Now that I hope I've cleared up any possible confusion about those topics, let's start our examination of our version of string by looking at the default constructor. Figure 7.3 shows its implementation.
FIGURE 7.3. The default constructor for our string class (from code\string1.cpp)
string::string()
: m_Length(1),
m_Data(new char [m_Length])
{
memcpy(m_Data,"",m_Length);
}
The member initialization list in this constructor contains two expressions. The first of them, m_Length(1), isn't very complicated at all. It simply sets the length of our new string to 1. However, this may seem a bit odd; why do we need any characters at all for a string that has no value? The answer to this riddle is quite simple, but to understand it you'll need to know something about a data type we haven't really discussed fully as yet: the C string.
We've been using one specific variety of that type, the C string literal, for quite awhile now. A C string literal is just a literal sequence of characters terminated by a null byte. A C string is exactly the same, except that it isn't necessarily defined literally. That is, a C string is a sequence of characters, whether or not literally specified, terminated by a null byte.
Why do we need to worry about this now when we have been able to ignore it up to this point? Because many pre-existing C functions that we may want to use in our programs use C strings rather than C++ strings. So, to make our strings as compatible as possible with those pre-existing C functions, we need to include the null byte that terminates all C strings. This means that when we calculate the number of bytes needed to hold the data for a string, we need to reserve one extra byte of memory beyond the number needed to hold the actual contents of the string. In the current case of a zero-character string, this means that we need one byte of storage for the null byte.
Next, it's important to note that this is an example where the order of execution of member initializer expressions is important: we definitely want m_Length to be initialized before m_Data, because the amount of data being assigned to m_Data depends on the value of m_Length. As you may remember from Chapter 6, the order in which the initialization expressions are executed is dependent not on the order in which they are written in the list, but on the order in which the member variables being initialized are declared in the class interface definition. Therefore, it's important to make sure that the order in which those member variables are declared is correct. In this case, it is, because m_Length is declared before m_Data in string1.h.
Susan had a very good question at this point.
Susan: If the program might not work right if we mess up the order of initialization, why isn't it an error to do that? Can't the compiler tell?
Steve: Very good point. It seems to me that the compiler should be able to tell and perhaps some of them do. But the one on the CD in the back of the book doesn't seem to mind if I write the initialization expressions in a different order than the way they will actually be executed.
Before proceeding to the next member initialization expression, let's take a look at the characteristics of the variables that we're using here. The scope of these variables, as we know from our previous discussion of the StockItem class, is class scope; therefore, each object of the string class has its own set of these variables, and they are accessible from any member functions of the class as though they were global variables.
However, an equally important characteristic of each of these variables is its data type. The type of m_Length is short, which is a type we've encountered before (a 16-bit integer variable that can hold a number between -32768 and 32767). But what about the type of the other member variable, m_Data, which is listed in Figure 7.1 as char*? We know what a char is, but what does that * mean?
Pointers
The star means pointer, which is just another term for a memory address. In particular, char* (pronounced "char star") means "pointer to a char".2 The pointer is considered one of the most difficult concepts for beginning programmers to grasp, but you shouldn't have any trouble understanding its definition if you've been following the discussion so far. A pointer is the address of some data item in memory. That is, to say "a variable points to a memory location" is almost exactly the same as saying "a variable's value is the address of a memory location". In the specific case of a variable x of type char*, for example, to say "x points to a C string" is exactly the same as saying "x contains the address of the first byte of the C string."3 The m_Data variable is used to hold the address of the first char of the data that a string contains; the rest of the characters follow the first character at consecutively higher locations in memory.
If this sounds familiar, it should. A C string literal like "hello" consists of a number of chars in consecutive memory locations; it should come as no surprise, then, when I tell you that a C string literal has the type char*.4
As you might infer from these cases, our use of one char* to refer to multiple chars isn't an isolated example. Actually, it's quite a widespread practice in C++, which brings up an important point: a char*, or any other type of pointer for that matter, has two different possible meanings in C++.5 One of these meanings is the obvious one of signifying the address of a single item of the type the pointer points to. In the case of a char*, that means the address of a char. However, in the case of a C string literal, as well as in the case of our m_Data member variable, we use a char* to indicate the address of the first char of an indeterminate number of chars; any chars after the first one occupy consecutively higher addresses in memory. Most of the time, this distinction has little effect on the way we write programs, but sometimes we have to be sensitive to this "multiple personality" of pointers. We'll run across one of these cases later in this chapter.
Susan had some questions (and I had some answers) on this topic of the true meaning of a char*:
Susan: What I get from this is that char* points to a char address either singularly or as the beginning of a string of multiple addresses. Is that right?
Steve: Yes, except that it's a string of several characters, not addresses.
Susan: Oh, here we go again; this is so confusing. So if I use a string "my name is" then a char* points to the address that holds the string of all those letters. But if the number of letters exceeds what the address can hold, won't it take up the next available address in memory and the char* point to it after it points to the first address?
Steve: Each memory address can hold 1 byte; in the case of a string, that byte is the value of one char of the string's data. So a char*, as we use it, will always point to the first char of the chars that hold our string's value; the other chars follow that one immediately in memory.
Susan: Let me ask this: When you show an example of a string with the value "Test" (Figure 7.8 on page 435), the pointer at address 12340002 containing the address 1234febc is really pointing at the T as that would be the first char and the rest of the letters will actually be in the other immediately following bytes of memory?
Steve: Absolutely correct.
While we're on the subject of that earlier discussion of C string literals, you may recall that I bemoaned the fact that such C string literals use a 0 byte to mark the end of the literal value, rather than keeping track of the length separately. Nothing can be done about that decision now, at least as it applies to C string literals. In the case of our string class, however, the implementation is under our control rather than the language designer's; therefore, I've decided to use a length variable (m_Length) along with the variable that holds the address of the first char of the data (m_Data).
To recap, what we're doing in this chapter and the next one is synthesizing a data type called string. A string needs a length and a set of characters to represent the actual data in the string. The short named m_Length is used in the string class to keep track of the number of characters in the data part of the string; the char* named m_Data is used to hold the address of the first character of the data part of the string.
The next member initialization expression, m_Data(new char [m_Length]), takes us on another of our side trips. This one has to do with the (dreaded) topic of dynamic memory allocation.
7.3. Dynamic Memory Allocation via new and delete
So far, we've encountered two storage classes: static and auto. As you might recall from the discussion in Chapter 5, static variables are allocated memory when the program is linked, while the memory for auto variables is assigned to them at entry to the block where they are defined. However, both mechanisms have a major limitation; the amount of memory needed is fixed when the program is compiled. In the case of a string, we need to allocate an amount of memory that in general cannot be known until the program is executed, so we need another storage class.
As you will be happy to learn, there is indeed another storage class called dynamic storage that enables us to decide the amount of memory to allocate at run time.6 To allocate memory dynamically, we use the new operator, specifying the data type of the memory to be allocated and the count of elements that we need. In the member initialization expression m_Data(new char [m_Length]), the type is char and the count is m_Length. The result of calling new is a pointer to the specified data type; in this case, since we want to store chars, the result of calling new is a pointer to a char; that is, a char*. This is a good thing, because char* is the type of the variable m_Data that we're initializing to the address that is returned from new. So the result of the member initialization expression we're examining is to set m_Data to the value returned from calling new; that value is the address of a newly assigned block of memory that can hold m_Length chars. In the case of the default constructor, we've asked for a block of 1 byte, which is just what we need to hold the contents of the zero-length C string that represents the value of our empty string.
It may not be obvious why we need to call new to get the address where we will store our data. Doesn't a char* always point to a byte in memory? Yes, it does; the problem is which byte. We can't use static (link time) or auto (function entry time) allocation for our string class, because each string can have a different number of characters. Therefore, we have to assign the memory after we find out how many characters we need to store the value of the string. The new operator reserves some memory and returns the address of the beginning of that memory. In this case, we assign that address to our char* variable called m_Data. An important point to note here is that in addition to giving us the address of a section of memory, new also gives us the right to use that memory for our own purposes. That same memory area will not be made available for any other use until we say we're done with it by calling another operator called delete.
Susan had some questions about how (and why) we use new. Here's the discussion:
Susan: OK, so all Figure 7.3 does is lay the foundation to be able to acquire memory to store the C string "" and then copy that information that will go into m_Data that starts at a certain location in memory?
Steve: Right. Figure 7.6 is the code for the constructor that accomplishes that task.
Susan: When you say that "the amount of memory needed is fixed when the program is compiled" that bothers me. I don't understand that in terms of auto variables, or is this because that type is known such as a short?
Steve: Right. As long as the types and the quantity of the data items in a class definition are known at compile time, as is the case with auto and static variables, the compiler can figure out the amount of memory they need. The addresses of auto variables aren't known at compile time, but how much space they use is.
Susan: OK, I understand the types of the data items. However, I am not sure what you mean by the quantity; can you give me an example?
Steve: Sure. You might have three chars and four shorts in a particular class definition; in that case, the compiler would add up three times the length of a char and four times the length of a short and allocate that much memory (more or less). Actually, some other considerations affect the size of a class object that aren't relevant to the discussion here, but they can all be handled at compile time and therefore still allow the compiler to figure out the amount of memory needed to store an object of any class.
The statement inside the {} in the default constructor, namely memcpy(m_Data,"",m_Length);, is responsible for copying the null byte from the C string "" to our newly allocated area of memory. The function memcpy (short for "memory copy") is one of the C standard library functions for C string and memory manipulation; it is declared in <cstring>. As you can see, memcpy takes three arguments. The first argument is a pointer to the destination, that is, the address that will receive the data. The second argument is a pointer to the source of the data; this, of course, is the address that we're copying from (i.e., the address of the null byte in the "", in our example). The last argument is the number of bytes to copy.
In other words, memcpy reads the bytes that start at the address specified by its input argument (in this case, "") and writes a copy of those bytes to addresses starting at the address specified by its output argument (in this case, m_Data). The amount copied is specified by the length argument (in this case, m_Length). Effectively, therefore, memcpy copies a certain amount of data from one place in memory to another. In this case, it copies 1 byte (which happens to be a null byte) from the address of the C string literal "" to the address pointed to by m_Data (that is, the place where we're storing the data that make up the value of our string).
This notion of dynamic allocation was the subject of some more discussion with Susan.
Susan: This stuff with operator new: I have no idea what you are talking about. I am totally gone, left in the dust. What is this stuff? Why do you need new to point to memory locations? I thought that is what char* did?
Steve: You're right that char* points to a memory location. But which one? The purpose of new is to get some memory for us from the operating system and return the address of the first byte of that memory. In this case, we assign that address to our char* variable called m_Data. Afterward, we can store data at that address.
Susan: I am not getting this because I just don't get the purpose of char*, and don't just tell me that it points to an address in memory. I want to know why we need it to point to a specific address in memory rather than let it use just any random address in memory.
Steve: Because then there would be no way of guaranteeing that the memory that it points to won't be used by some other part of the program, or indeed some other program entirely in a multitasking system that doesn't provide a completely different memory space for each program. We need to claim ownership of some memory by calling new before we can use it.
Susan: I think I understand now why we need to use new, but why should the result of calling new be a pointer? I am missing this completely. How does new result in char*?
Steve: Because that's how new is defined: it gives you an address (pointer) to a place where you can store some chars (or whatever type you requested).
Susan: OK, but in the statement m_Data = new char [m_Length], why is char in this statement not char*? I am so confused on this.
Steve: Because you're asking for an address (pointer) to a place where you can store a bunch of chars.
Susan: But then wouldn't it be necessary to specify char* rather than char in the statement?
Steve: I admit that I find that syntax unclear as well. Yes, in my opinion, the type should be stated as char*, but apparently Bjarne thought otherwise.
Susan: OK, so then m_Data is the pointer address where new (memory from the free store) is going to store data of m_Length?
Steve: Almost right. The value assigned to m_Data in the constructor is the value returned from operator new; this value is the address of an area of memory allocated to this use. The area of memory is of length m_Length.
Susan: Well, I thought that the address stored in m_Data was the first place where you stored your chars. So is new just what goes and gets that memory to put the address in m_Data?
Steve: Exactly.
Susan: Here's what I understand about the purpose of char*. It functions as a pointer to a specific memory address. We need to do that because the computer doesn't know where to put the char data, therefore we need char* to say "hey you, computer, look over here, this is where we are going to put the data for you to find and use".
Steve: That's fine.
Susan: We need to use char* for variable length memory. This is because we don't know how much memory we will need until it is used. For this we need the variable m_Data to hold the first address in memory for our char data. Then we need the variable m_Length that we have set to the length of the C string that will be used to get the initial data for the string. Then we have to have that nifty little helper guy new to get some memory from the free store for the memory of our C string data.
Steve: Sounds good to me.
Susan: Now about memcpy: This appears to be the same thing as initializing the variable. I am so confused.
Steve: That's correct. Maybe you shouldn't get unconfused!
As the call to memcpy is the only statement in the constructor proper, it's time to see what we have accomplished. The constructor has initialized a string by:
1. Setting the length of the string to the effective length of a null C string, "", including the terminating null byte (i.e., 1 byte).
2. Allocating memory for a null C string.
3. Copying the contents of a null C string to the allocated memory.
The final result of all this work is a string with the value "", whose memory layout might look like Figure 7.4.
FIGURE 7.4. An empty string in memory![]()
Using the default constructor is considerably easier than defining it. As we have seen in Chapter 6, the default constructor is called whenever we declare an object without specifying any data to initialize it with (for example, in the line string s; in Figure 7.5). Although this program doesn't do anything useful, it does illustrate how we can use the member functions of our string class, so you should pay attention to it.
FIGURE 7.5. Our first test program for the string class (code\strtst1.cpp)
I should point out here that the only file the compiler needs to figure out how to compile the line string s; is the header file, string1.h. The actual implementation of the string class in string1.cpp isn't required here, because all the compiler cares about when compiling a program using classes is the contract between the class implementer and the user; that is, the header file. The actual implementation in string1.cpp that fulfills this contract isn't needed until the program is linked to make an executable; at that point, the linker will complain if it can't find an implementation of any function that we've referred to.
7.4. Constructing a string from a C String
Now that we've disposed of the default constructor, let's take a look at the line in our string interface definition (Figure 7.1 on page 409): string(char* p);.7 This is the declaration for another constructor; unlike the default constructor we've already examined, this one has an argument, namely, char* p.8
As we saw in Chapter 6, the combination of the function name and argument types is called the signature of a function. Two functions that have the same name but differ in the type of at least one argument are distinct functions, and the compiler will use the difference(s) in the type(s) of the argument(s) to figure out which function with a given name should be called in any particular case. Of course, this leads to the question of why we would need more than one string constructor; they all make strings, don't they?
Yes, they do, but not from the same "raw material". It's true that every constructor in our string class makes a string, but each constructor has a unique argument list that determines exactly how the new string will be constructed. The default constructor always makes an empty string (like the C string literal ""), whereas the constructor string(char* p) takes a C string as an argument and makes a string that has the same value as that argument.
Susan wasn't going to accept this without a struggle.
Susan: I don't get "whereas the string(char* p) constructor takes a C string and makes a string that has the same value as the C string does."
Steve: Well, when the compiler looks at the statement string n("Test"); it has to follow some steps to figure it out.
1. The compiler knows that you want to create a string because you've defined a variable called n with the type string; that's what string n means.
2. Therefore, since string is not a native data type, the compiler looks for a function called string::string, which would create a string.
3. However, there can be several functions named string::string, with different argument lists, because there are several possible ways to get the initial data for the string you're creating. In this case, you are supplying data in the form of a C string literal, whose type is char*; therefore, a constructor with the signature string::string(char*) will match.
4. Since a function with the signature string::string(char*) has been declared in the header file, the line string n("Test"); is translated to a call to that function.
Susan: So string(char* p) is just there in case you need it for "any given situation"; what situation is this?
Steve: It depends on what kind of data (if any) we're supplying to the constructor. If we don't supply any data, then the default constructor is used. If we supply a C string (such as a C string literal), then the constructor that takes a char* is used, because the type of a C string is char*.
Susan: So string s; is the default constructor in case you need something that uses uninitialized objects?
Steve: Not quite; that line calls the default constructor for the string class, string::string(), which doesn't need any arguments, because it constructs an empty string.
Susan: And the string n ("Test"); is a constructor that finally gets around to telling us what we are trying to accomplish here?
Steve: Again, not quite. That line calls the constructor string::string(char* p); to create a string with the value "Test".
Susan: See, you are talking first about string n("Test"); in Figure 7.5 on page 423 and then you get all excited that you just happen to have string::string(char* p) hanging around which is way over in Figure 7.1 on page 409.
Steve: Now that you know that a C string literal such as "Test" has the data type char*, does this make sense?
Susan: OK, I think this helped. I understand it better. Only now that I do, it raises other questions that I accepted before but now don't make sense due to what I do understand. Does that make sense to you? I didn't think so.
Steve: Sure, why not? You've reached a higher level of understanding, so you can now see confusions that were obscured before.
Susan: So this is just the constructor part? What about the default constructor, what happened to it?
Steve: We can't use it with the statement string n("Test"), because we have some data to assign to the string when the string is created. A default constructor is used only when there is no initial value for a variable.
Susan: So was the whole point of discussion about default constructors just to let us know that they exist even though you aren't really using them here?
Steve: We are using it in the statement string s; to create a string with no initial value, as discussed before.
Susan wasn't clear on why the C string "Test" would be of type char*, which is understandable because that's anything but obvious. Here's the discussion we entered into on this point.
Susan: When you say "Test" is a C string literal of type char* and that the compiler happily finds that declaration, that is fine. But see, it is not obvious to me that it is type char*; I can see char but not char*. Something is missing here so that I would be able to follow the jump from char to char*.
Steve: A C string literal isn't a single char, but a bunch of chars. Therefore, we need to get the address of the first one; that gives us the addresses of the ones after it.
Now that the reason why a C string literal is of type char* is a bit clearer, Figure 7.6 shows the implementation for the constructor that takes a char* argument.
FIGURE 7.6. The char* constructor for our string class (from code\string1.cpp)
string::string(char* p)
: m_Length(strlen(p) + 1),
m_Data(new char [m_Length])
{
memcpy(m_Data,p,m_Length);
}
You should be able to decode the header string::string(char* p). This function is a constructor for class string (because its class is string and its name is also string) and its argument, named p, is of type char*. The first member initialization expression is m_Length(strlen(p) + 1). This is obviously initializing the string's length (m_Length) to something, but what?
As you may recall, C strings are stored as a series of characters terminated by a null byte (i.e., one with a 0 value). Therefore, unlike the case with our strings, where the length is available by looking at a member variable (m_Length), the only way to find the length of a C string is to search from the beginning of the C string until you get to a null byte. Since this is such a common operation in C, the C standard library (which is a subset of the C++ standard library) provides the function strlen (short for "string length") for this purpose; it returns a result indicating the number of characters in the C string, not including the null byte. So the member initialization expression m_Length(strlen(p) + 1) initializes our member variable m_Length to the length of the C string p, which we compute as the length reported by strlen (which doesn't include the terminating null byte) + 1 for the terminating null byte. We need this information because we've decided to store the length explicitly in our string class rather than relying solely on a null byte to mark the end of the string, as is done in C.9
Susan had some questions about the implementation of this function, and I supplied some answers.
Susan: What is strlen?
Steve: A function left over from C; it tells us how long a C string is.
Susan: Where did it come from?
Steve: It's from the C standard library, which is part of the C++ standard library.
Susan: What are you using it for here?
Steve: Finding out how long the C string is that we're supposed to copy into our string.
Susan: Is this C or C++?
Steve: Both.
Susan: Why is char* so special that it deserves a pointer? What makes it different?
Steve: The * means "pointer". In C++, char* means "pointer to a char".
Susan: I just don't understand the need for the pointer in char. See when we were using it (char) before, it didn't have a pointer, so why now? Well, I guess it was because I thought it was native back then when I didn't know that there was any other way. So why don't you have a pointer to strings then? Are all variables in classes going to have to be pointed to? I guess that is what I am asking.
Steve: We need pointers whenever we want to allocate an amount of memory that isn't known until the program is executing. If we wanted to have a rule that all strings were 10 characters in length (for example), then we could allocate the space for those characters in the string. However, we want to be able to handle strings of any length, so we have to decide how much space to allocate for the data when the constructor string::string(char* p) is called; the only way to do that is to use a pointer to memory that is allocated at run time, namely m_Data. Then we can use that memory to hold a copy of the C string pointed to by the parameter p.
Susan: Oh, no! Here we go again. Is m_Data a pointer? I thought it was just a variable that held an address.
Steve: Those are equivalent statements.
Susan: Why does it point? (Do you know how much I am beginning to hate that word?) I think you are going to have to clarify this.
Steve: It "points" in a metaphorical sense, but one that is second nature to programmers in languages like C. In fact, it merely holds the address of some memory location. Is that clearer?
Susan: So the purpose of m_Data is just a starting off point in memory?
Steve: Right. It's the address of the first char used to store the value of the string.
Susan: So the purpose of m_Length is to allot the length of memory that starts at the location where m_Data is?
Steve: Close; actually, it's to keep track of the amount of memory that has been allocated for the data.
Susan: But I see here that you are setting m_Length to strlen, so that in effect makes m_Length do the same thing?
Steve: Right; m_Length is the length of the string because it is set to the result returned by strlen (after adding 1 for the null byte at the end of the C string).
Susan: Why would you want a string with no data, anyway? What purpose does that serve?
Steve: So you can define a string before knowing what value it will eventually have. For example, the statement string s; defines a string with no data; the value can be assigned to the string later.
Susan: Oh yeah, just as you would have short x;. I forgot.
Steve: Yep.
Susan: Anyway, the first thing that helped me understand the need for pointers is variable-length data. I am sure that you mentioned this somewhere, but I certainly missed it. So this is a highlight. Once the need for it is understood then the rest falls in place. Well, almost; it is still hard to visualize, but I can.
Steve: I'll make sure to stress that point until it screams for mercy.
Susan: I think you might be able to take this information and draw a schematic for it. That would help. And show the code next to each of the steps involved.
Steve: Don't worry, we'll see lots of diagrams later.
Susan: So strlen is a function like any member function?
Steve: Yes, except that it is a global function rather than a member function belonging to a particular class. That's because it's a leftover from C, which doesn't have classes.
Susan: So it is what I can consider as a native function? Now I am getting confused again. I thought that just variables can be either made up (classes) or native. Why are we talking about functions in the same way? But then I remember that, in a backward way, functions belong to variables in classes rather than the other way around. This is just so crazy.
Steve: Functions are never native in the way that variables are; that is, built into the language. A lot of functions come with the language, in the form of the libraries, but they have no special characteristics that make them "better" than ones you write yourself. However, this is not true of variables in C, because C doesn't provide the necessary facilities for the programmer to add variable types that have the appearance and behavior of the native types.
Susan: You see I think it is hard for me to imagine a function as one word, because I am so used to main() with a bunch of code following it and I think of that as the whole function; see where I am getting confused?
Steve: When we call a function like strlen, that's not the whole function, it's just the name of the function. This is exactly like the situation where we wrote Average and then called it later to average some numbers.
Susan: A function has to "do something", so you will have to define what the function does; then when we use the function, we just call the name and that sets the action in gear?
Steve: Exactly.
Susan: Now, about this char* thing. . . (don't go ballistic, please) . . . exactly what is it? I mean it is a pointer to char, so what is *? Is it like an assignment operator? How is it classified?
Steve: After a type name, * means "pointer to the type preceding". So char* means "pointer to char", short* means "pointer to short", and so on.
Susan: So that would be for a short with variable-length data? And that would be a different kind of short than a native short?
Steve: Almost but not quite correct. It would be for variable-length data consisting of one or more shorts, just as a C string literal is variable-length data consisting of one or more chars.
Susan: So it would be variable by virtue of the number of shorts?
Steve: Actually, by virtue of the possibility of having a number of shorts other than exactly one. If you might need two or three (or 100, for that matter) shorts (or any other type), and you don't know how many when the program is compiled, then you pretty much have to use a pointer.
Susan: OK, yes, you said that about * and what it means to use a char*, but I thought it would work only with char so I didn't know I would be seeing it again with other variable types. I can't wait.
Steve: When we use pointers to other types, they will be to user-defined types, and we'll be using them for a different purpose than we are using char*. That won't be necessary until Chapter 10, so you have a reprieve for the time being.
The next member initialization expression in the constructor, m_Data(new char [m_Length]), is the same as the corresponding expression in the default constructor. In this case, of course, the amount of memory being allocated is equal to the length of the input C string (including its terminating null byte), rather than the fixed value 1.
Now that we have the address of some memory that belongs to us, we can use it to hold the characters that make up the value of our string. The literal value that our test program uses to call this constructor is "Test", which is four characters long, not counting the null byte at the end. Since we have to make room for that null byte, the total is 5 bytes, so that's what we'd ask new to give us. Assuming that the return value from new was 1234febc, Figure 7.7 illustrates what our new string looks like at this point.
FIGURE 7.7. string n during construction![]()
The reason for the ???? is that we haven't copied the character data for our string to that location yet, so we don't know what that location contains. Actually, this brings up a point we've skipped so far: where new gets the memory it allocates. The answer is that much, if not all, of the "free" memory in your machine (i.e., memory that isn't used to store the operating system, the code for your program, statically allocated variables, and the stack) is lumped into a large area called the free store, which is where dynamically allocated memory "lives".10 When you call new, it cordons off part of the free store as being "in use" and returns a pointer to that portion.
It's possible that the idea of a variable that holds a memory address but which is itself stored in memory isn't that obvious. It wasn't to Susan:
Susan: I don't get this stuff about a pointer being stored in a memory address and having a memory address in it. So what's the deal?
Steve: Here's an analogy that might help. What happens when there is something too large to fit into a post office box? One solution is to put the larger object into one of a few large mailboxes, and leave the key to the larger mailbox in your regular mailbox. In this analogy, the small mailbox is like a pointer variable and the key is like the contents of that pointer. The large mailbox corresponds to the memory dynamically allocated by new.
So at this point, we have allocated m_Length bytes of memory, which start at the address in the pointer variable m_Data. Now we need to copy the current value of the input C string (pointed to by p) into that newly allocated area of memory. This is the job of the sole statement inside the brackets of the constructor proper,
which copies the data from the C string pointed to by p to our newly allocated memory.The final result is that we have made (constructed) a string variable and set it to a value specified by a C string. Figure 7.8 shows what that string might look like in memory.
FIGURE 7.8. string n in memory![]()
But how would this string::string(char* p) constructor operate in a program? To answer that question, Figure 7.9 gives us another look at our sample program.
FIGURE 7.9. A simple test program for the string class (code\strtst1.cpp)
#include "string1.h"
int main()
{
string s;
string n("Test");
string x;
s = n;
n = "My name is Susan";
x = n;
return 0;
}
Calling the string(char*) Constructor
How does the compiler interpret the line string n("Test");? First, it determines that string is the name of a class. A function with the name of a class, as we have already seen, is always a constructor for that class. The question is which constructor to call; the answer is determined by the type(s) of the argument(s). In this case, the argument is a C string literal, which has the type char*; therefore, the compiler looks for a constructor for class string that has an argument of type char*. Since there is such a constructor, the one we have just examined, the compiler generates a call to it. Figure 7.10 shows this constructor again for reference, while we analyze it.
FIGURE 7.10. The char* constructor for the string class, again (from code\string1.cpp)
string::string(char* p)
: m_Length(strlen(p) + 1),
m_Data(new char [m_Length])
{
memcpy(m_Data,p,m_Length);
}
When the program executes, string::string(char* p) is called with the argument "Test". Let's trace the execution of the constructor, remembering that member initialization expressions are executed in the order in which the member variables being initialized are listed in the class interface, not necessarily in the order in which they are written in the member initialization list.
1. The first member initialization expression to be executed is m_Length(strlen(p) + 1). This initializes the member variable m_Length to the length of the C string whose address is in p, with 1 added for the null byte that terminates the string. In this case, the C string is "Test", and its length, including the null byte, is 5.
2. Next, the member initialization expression m_Data(new char [m_Length]) is executed. This allocates m_Length (5, in this case) bytes of memory from the free store and initializes the variable m_Data to the address of that memory.
3. Finally, the statement memcpy(m_Data,p,m_Length); copies m_Length bytes (5, in this case) of data from the C string pointed to by p to the memory pointed to by m_Data.
When the constructor is finished, the string variable n has a length, 5, and contents, "Test", as shown in Figure 7.8. It's now ready for use in the rest of the program. After all the discussion, Susan provided this rendition of the char* constructor for the string class.
Susan: So first we define a class. This means that we will have to have one or more constructors, which are functions with the same name as the class, used to create objects of that class. The char* constructor we're dealing with here goes through three steps, as follows: Step 1 sets the length of the string; step 2 gets the memory to store the data, and provides the address of that memory; step 3 does the work; it copies what you want.
Steve: Right.
7.5. Assignment Operator Issues
Now, let's look at the next line: s = n;. That looks harmless enough; it just copies one string, n, to another string, s. But wait a second; how does the compiler know how to assign a value to a variable of a type we've made up?
Just as the compiler will generate a version of the default constructor if we don't define one, because every object has to be initialized somehow, the ability to assign one value of a given type to a variable of the same type is essential to being a concrete data type. Therefore, the compiler will supply a version of operator =, the assignment operator, if we don't define one ourselves. In Chapter 6, we were able to rely on the compiler-generated operator =, which simply copies every member variable from the source object to the target object. Unfortunately, that won't work here. The reason is that the member variable m_Data isn't really the data for the string; it's a pointer to (i.e., the address of) the data. The compiler-generated operator =, however, wouldn't be able to figure out how we're using m_Data, so it would copy the pointer rather than the data. In our example, s = n;, the member variable m_Data in s would end up pointing to the same place in memory as the member variable m_Data in n. Thus, if either s or n did something to change "its" data, both strings would have their values changed, which isn't how we expect variables to behave.
To make this more concrete, let's look back at Figure 7.8. So far, we have an object of type string that contains a length and a pointer to dynamically allocated memory where its actual data are stored. However, if we use the compiler-generated operator = to execute the statement s = n;, the result looks like Figure 7.11.
FIGURE 7.11. strings n and s in memory after compiler-generated =![]()
In other words, the two strings s and n are like Siamese twins; whatever affects one of them affects the other, since they share one copy of the data "Test". What we really want is two strings that are independent of one another, so that we can change the contents of one without affecting the other one. Very shortly, we'll see how to accomplish this.
As you might suspect, Susan didn't think the need for us to define our own operator = was obvious at all. Here's how I started talking her into it.
Susan: I have a little note to you off to the side in the margins about this operator =, it says "If it was good enough for native data then why not class data?" I think that is a very good question, and I don't care about that pointy thing. I don't understand why m_Data isn't really data for the string.
Steve: It isn't the data itself but the address where the data starts.
Susan: Actually, looking at these figures makes this whole idea more understandable. Yes, I see somewhat your meaning in Figure 7.11; that pointy thing is pointing all over the place. Oh no, I don't want to see how to make two independent strings! Just eliminate the pointy thing and it will be all better. OK?
Steve: Sorry, that isn't possible. You'll just have to bear with me until I can explain it to you better.
Susan: Well, let me ask you this: Is the whole point of writing the statement s = n just to sneak your way into this conversation about this use of operator =? Otherwise, I don't see where it would make sense for the sample program.
Steve: Yes, that's correct.
Susan: And the chief reason for creating a new = is that the new one makes a copy of the data using a new memory address off the free store, rather than having the pointer pointing to the same address while using the compiler-generated operator =? If so, why? Getting a little fuzzy around that point. With StockItem, the compiler-generated operator = was good enough. Why not now?
Steve: Yes, that's why we need to create our own operator =. We didn't need one before because the components of a StockItem are all concrete data types, so we don't have to worry about "sharing" the data as we do with the string class, which contains a char*.
Susan: So when you use char* or anything with a pointer, that is outside the realm of concrete data types?
Steve: Right. However, the reason that we can't allow pointers to be copied as with the compiler-generated operator = isn't that they aren't concrete data types, but that they aren't the actual data of the strings. They're the address of the actual data; therefore, if we copy the pointer in the process of copying a variable, both pointers hold the same address. This means that changes to one of the variables affects the other one, which is not how concrete data types behave.
Susan: I think I actually understand this now. At least, I'm not as confused as I was before.
Steve: Good; it's working.
7.6. Solving the Assignment Operator Problem
Although it's actually possible to get the effect of two independent strings without the extra work of allocating memory and copying data every time an assignment is done, the mechanisms needed to do that are beyond the scope of this book.11 By far the easiest way to have the effect of two independent strings is to actually make another copy of a string's data whenever we copy the string, and that's how we'll do it here. The results will be as indicated in Figure 7.12.
With this arrangement, a change to one of the string variables will leave the other one unaffected, as the user would expect. To make this happen, we have to implement our own operator =, which will copy the data rather than just the pointer to the data. That's the operator declared in Figure 7.1 by the line:
string& operator = (const string& Str);
What exactly does this mean? Well, as with all function declarations, the first part of the function declaration indicates the return type of the function. In this case, we're going to return a reference to the string to which we're assigning a value; that is, the string on the left of the = sign in an assignment statement. While this may seem reasonable at first glance, actually it's not at all obvious why we should return anything from operator =. After all, if we say a = b;, after a has been set to the same value as b, we're done; that operation is performed by the = operator, so no return value is needed after the assignment is completed.
FIGURE 7.12. strings n and s in memory after custom =![]()
However, there are two reasons why assignment of native types returns a value, which is equal to the value that was assigned to the left hand argument of =. First, it allows us to write an if statement such as if (a = b), when we really meant if (a == b); of course, this will cause a bug in the program, since these two statements don't have the same meaning. The first one sets a to b and returns the value of a; if a isn't 0, then the if condition is considered true. The latter statement, of course, compares a and b for equality and makes the if condition true if they are equal. To help prevent the error of substituting = for == in this situation, many compilers have a warning that indicates your use of, say, if (a = b); unfortunately, this is a legal construct with native types, and so cannot generate a compiler error. As it happens, using = in this way is an illegal operation with class objects, so even if you want to use this error-prone construct, you can't. Since I never use that construct with native variables, I don't mind not having it for class objects.
The other potential use of the return value from operator = is to allow statements such as a = b = c;, where the current value of c is assigned to b and the return value from that assignment is assigned to a. Although I don't use that construct either, since I find it more confusing than useful, I have been told that this return value is required to use some of the library facilities specified in the C++ standard. Therefore, it is my obligation to teach you the "right" way to write assignment operators so that you will be able to use these standard facilities with your classes.
Now we're up to the mysterious-looking construct operator =. This portion of the function declaration tells the compiler the name of the function we're defining; namely, operator =. The operator keyword lets the compiler know that the "name" of this function is actually an operator name, rather than a "normal" function name. We have to say operator = rather than merely =, for two reasons. First, because normal function names can't have a = character in them, but are limited to upper and lower case letters, numbers, and the underscore (_). Second, because when we're redefining any operator, even one like new whose name is made of characters allowed in identifiers, we have to tell the compiler that we're doing that on purpose. Otherwise, we'll get an error telling us that we're trying to define a function or variable with the same name as a keyword.12
We're ready to look at the argument to this function, specified by the text inside the parentheses, const string& Str. We've already seen in Chapter 6 that & in this context means that the argument to which it refers is a reference argument rather than a value argument.13 In other words, the variable Str is actually just another name for the argument provided by the caller of this function, rather than being a separate local variable with the same value as the caller's argument. However, there is a new keyword in this expression, const, which is short for "constant". In this context, it means that we promise that this function will not modify the argument to which const refers, namely string& Str. This is essential in the current situation, but it will take some discussion to explain why.
7.7. The const Modifier for Reference Arguments
As you may recall from our discussion of types of arguments in earlier chapters, when you call a function using a value argument, the argument that you supply in the calling function isn't the one that the called function receives. Instead, a copy is made of the calling function's argument, and the called function works on the copy. While this is fine most of the time, in this case it won't work properly for reasons that will be apparent shortly; instead, we have to use a reference argument. As we saw in the discussion of reference arguments in Chapter 6, such an argument is not a copy of the caller's argument, but another name for the actual argument provided by the caller. This has a number of consequences. First, it's often more efficient than a "normal" argument, because the usual processing time needed to make a copy for the called function isn't required. Second, any changes made to the reference argument change the caller's argument as well. The use of this mechanism should be limited to those cases where it is really necessary, since it can confuse the readers of the calling function. There's no way to tell just by looking at the calling function that some of its variables can be changed by calling another function.
In this case, however, we have no intention of changing the input argument. All we want to do is to copy its length and data into the output string, the one for which operator = was called. Therefore, we tell the compiler, by using the const modifier, that we aren't going to change the input argument. This removes the drawback of non-const reference arguments: that they can change variables in the calling function with no indication of that possibility in the calling function. Therefore, using const reference arguments is quite a useful and safe way to reduce the number of time-consuming copying operations needed to make function calls.
However in this case, the use of a const reference argument is more than just efficient. As we'll see in the discussion starting under the heading "The Compiler Generates a Temporary Variable" on page 478 in Chapter 8, such an argument allows us to assign a C string (i.e., bytes pointed to by a char*) to one of our string variables without having to write a special operator = for that purpose.14
You might be surprised to hear that Susan didn't have too much trouble accepting all this stuff about const reference arguments. Obviously her resistance to new ideas was weakening by this point.
Susan: OK, so the reference operator just renames the argument and doesn't make a copy of it; that is why it is important to promise not to change it?
Steve: Right. A non-const reference argument can be changed in the called function, because unlike a "regular" (i.e., value) argument, which is really a copy of the calling function's variable, a reference argument is just another name for the caller's variable. Therefore, if we change the reference argument we're really changing the caller's variable, which is generally not a good idea.
Susan: OK. But in this case since we are going to want to change the meaning of = in all strings it is OK?
Steve: Not quite. Every time we define an operator we're changing the meaning of that operator for all objects of that class. The question is whether we're intending to change the value of the caller's variable that is referred to by the reference argument. If we are, then we can't use const to qualify the reference; if not, we can use const. Does that answer your question?
Susan: Well, yes and no. I think I have it now: When you write that code it is for that class only and won't affect other classes that you may have written, because it is contained within that particular class code. Right?
Steve: Correct.
Susan: So we don't want to change the input argument because we are basically defining a new = for this class, right?
Steve: Right. The input argument is where we get the data to copy to the string we're assigning to. We don't want to change the input argument, just the string we're assigning to.
Back to the discussion of the function declaration, we now have enough information to decode the function declaration
string& string::operator = (const string& Str)
as illustrated in Figure 7.13.
FIGURE 7.13. The declaration of operator = for the string class
Putting it all together, we're defining a function belonging to class string that returns a reference to a string. This function implements operator = and takes an argument named Str that's a constant reference to a string. That is, the argument Str is another name for the string passed to us by the caller, not a copy of the caller's string. Furthermore, we're vowing not to use this argument to change the caller's variable.
7.8. Calling operator=
Now that we've dissected the header into its atomic components, the actual implementation of the function should be trivial by comparison. But first there's a loose end to be tied up. That is, why was this function named string::operator = called in the first place? The line that caused the call was very simple: s = n;. There's no explicit mention of string or operator.
This is another of the ways in which C++ supports classes. Because you can use the = operator to assign one variable of a native type to another variable of the same type, C++ provides the same syntax for user defined variable types. Similar reasoning applies to operators like >, <, and so on, for classes where these operators make sense.
When the compiler sees the statement s = n;, it proceeds as follows:
1. The variable s is an object of class string.
2. The statement appears to be an assignment statement (i.e., an invocation of the C++ operator named operator =) setting s equal to the value of another string value named n.
3. Is there a definition of a member function of class string that implements operator = and takes one argument of class string?
4. Yes, there is. Therefore, translate the statement s = n; into a call to operator = for class string.
5. Compile that statement as though it were the one in the program.
Susan was appreciative of the reminder that we started out discussing the statement s = n;.
Susan: Oh, my gosh, I totally forgot about s = n; thanks for the reminder. We did digress a bit, didn't we? Are you saying you have to go through the same thing to define other operators in classes?
Steve: Yes.
Susan: So are you saying that when you write the simple statement s = n; that the = calls the function that we just went through?
Steve: Right.
Following this procedure, the correspondence between the tokens15 in the original program and the call to the member function should be fairly obvious, as we see them in Figure 7.14.
FIGURE 7.14. Calling the operator = implementation
But we've left out something. What does the string s correspond to in the function call to operator =?
The Keyword this
The string s corresponds to a hidden argument whose name is the keyword this. Such an argument is automatically included in every call to a member function in C++.16 The type of this is always a constant pointer to an object of the class that a member function belongs to. In the case of the string class, its type is const string*; that is, a constant pointer to a string; the const means that we can't change the value of this by assigning a new value to it. The value of this is the address of the class object for which the member function call was made. In this case, the statement s = n; was translated into s.operator = (n); by the compiler; therefore, when the statement s = n; is being executed, the value of this is the address of the string s.
We'll see why we need to be concerned about this at the end of our analysis of the implementation of operator = (Figure 7.15).
FIGURE 7.15. The assignment operator (operator =) for the string class (from code\string1.cpp)
string& string::operator = (const string& Str)
{
char* temp = new char[Str.m_Length];
m_Length = Str.m_Length;
memcpy(temp,Str.m_Data,m_Length);
delete [ ] m_Data;
m_Data = temp;
return *this;
}
This function starts out with char* temp = new char[Str.m_Length], which we use to acquire the address of some memory that we will use to store our new copy of the data from Str. Along with the address, new gives us the right to use that memory until we free it with delete.
The next statement is m_Length = Str.m_Length;. This is the first time we've used the . operator to access a member variable of an object other than the object for which the member function was called. Up until now, we've been satisfied to refer to a member variable such as m_Length just by that simple name, as we would with a local or global variable. The name m_Length is called an unqualified name because it doesn't specify which object we're referring to. The expression m_Length by itself refers to the occurrence of the member variable m_Length in the object for which the current function was called; i.e., the string whose address is this (the string s in our example line s = n;).
If you think about it, this is a good default because member functions refer to member variables of their "own" object more than any other kinds of variables. Therefore, to reduce the amount of typing the programmer has to do, whenever we refer to a member variable without specifying the object to which it belongs, the compiler will assume that we mean the variable that belongs to the object for which the member function was called (i.e, the one whose address is the current value of this).
However, when we want to refer to a member variable of an object other than the one pointed to by this, we have to indicate which object we're referring to, which we do by using the . operator. This operator means that we want to access the member variable (or function) whose name is on the right of the . for the object whose name is on the left of the ".". Hence, the expression Str.m_Length specifies that we're talking about the occurrence of m_Length that's in the variable Str, and the whole statement m_Length = Str.m_Length; means that we want to set the length of "our" string (i.e., the one pointed to by this) to the length of the argument string Str.
Then we use memcpy to copy the data from Str (i.e., the group of characters starting at the address stored in Str.m_Data) to our newly allocated memory, which at this point in the function is referred to by temp (we'll see why in a moment).
Next, we use the statement delete [ ] m_Data; to free the memory previously used to store our string data. This corresponds to the new statement that we used to allocate memory for a string in the constructor string::string(char* p), as shown in Figure 7.6 on page 427.17 That is, the delete operator returns the memory to the available pool called the free store. There are actually two versions of the delete operator: one version frees memory for a single data item, and the other frees memory for a group of items that are stored consecutively in memory. Here, we're using the version of the delete operator that frees a group of items rather than a single item, which we indicate by means of the [] after the keyword delete; the version of delete that frees only one item doesn't have the [].18 So after this statement is executed, the memory that was allocated in the constructor to hold the characters in our string has been handed back to the memory allocation routines for possible reuse at a later time.
Susan had a few minor questions about this topic, but nothing too alarming.
Susan: So delete just takes out the memory new allocated for m_Data?
Steve: Right.
Susan: What do you mean by "frees a group of items"?
Steve: It returns the memory to the free store, so it can be used for some other purpose.
Susan: Is that all the addresses in memory that contain the length of the string?
Steve: Not the length of the string, but the data for the string, such as "Test".
Memory Allocation Errors
A point that we should not overlook is the possibility of calling delete for a pointer that has never been assigned a value. Calling delete on a pointer that doesn't point to a valid block of memory allocated by new will cause the system to malfunction in some bizarre way, usually at a time considerably after the improper call to delete.19 This occurs because the dynamic memory allocation system will try to reclaim the "allocated" memory pointed to by the invalid pointer by adding it back to the free store. Eventually, some other function will come along, ask for some memory, and be handed a pointer to this "available" block that is actually nothing of the sort. The result of trying to use this area of memory depends on which of three cases the erroneous address falls into: the first is that the memory at that address is nonexistent, the second is that the memory is already in use for some other purpose, and the third is that the invalid address points to an area in the free store that is already marked as available for allocation. In the first case, the function that tries to store its data in this nonexistent area of memory will cause a system crash or error message, depending on the system's ability and willingness to check for such errors. In the second case, the function that is the "legal" owner of the memory will find its stored values changed mysteriously and will misbehave as a result. In the third case, the free store management routines will probably get confused and start handing out wrong addresses. Errors of this kind are common (and are extremely difficult to find) in programs that use pointers heavily in uncontrolled ways.20
Susan was interested in this topic of errors in memory allocation, so we discussed it.
Susan: Can you give me an example of what an "invalid pointer" would be? Would it be an address in memory that is in use for something else rather than something that can be returned to the free store?
Steve: That's one kind of invalid pointer. Another type would be an address that doesn't exist at all; that is, one that is past the end of the possible legal addresses.
Susan: Oh, wait, so it would be returned to the free store but later if it is allocated to something else, it will cause just a tiny little problem because it is actually in use somewhere else?
Steve: You bet.
Susan: Oh yeah, this is cool, this is exciting. So this is what really happens when a crash occurs?
Steve: Yes, that is one of the major causes of crashes.
Susan: I like this. So when you try to access memory where there is no memory you get an error message?
Steve: Yes, or if it belongs to someone else. Of course, you'll be lucky to get anything other than a hard crash if it's a DOS program; at least in Windows you'll probably get an error message instead.
Another way to go wrong with dynamic memory allocation is the opposite one. Instead of trying to delete something that was never dynamically allocated, you can forget to delete something that has been dynamically allocated. This is called a memory leak; it's very insidious, because the program appears to work correctly when tested casually. The usual way to find these errors is to notice that the program apparently runs correctly for a (possibly long) time and then fails due to running out of available memory. I should mention here how we can tell that we've run out of memory: the new operator, rather than returning a value, will "throw an exception" if there is no free memory left. This will cause the program to terminate if we don't do anything to handle it.
Given all of the ways to misuse dynamic memory allocation, we'll use it only when its benefits clearly outweigh the risks. To be exact, we'll restrict its use to controlled circumstances inside class implementations, to reduce the probability of such errors.
Susan had some questions about the idea of new throwing an exception if no memory is left.
Susan: What is "throwing an exception"?
Steve: That's what new does when it doesn't have anything to give you. It causes your program to be interrupted rather than continuing along without noticing anything has happened.
Susan: How does a real program check for this?
Steve: By code that looks something like Figure 7.16.
FIGURE 7.16. Checking for an exception from new
cout << "You're hosed!" << endl;
The try keyword means "try to execute the following statements (called a try block)", and "catch (...)" means "if any of the statements in the previous try block generated an exception, execute the following statements". If the statements in the try block don't cause an exception to be generated, then the catch block is ignored and execution continues at the next statement after the end of the catch block.
Finally, exit means "bail out of the program right now, without returning to the calling function, if any". The argument to exit is reported back to DOS as the return value from the program; 0 means OK, anything else means some sort of error. Of course, it's better to take some other action besides just quitting when you run into an exception, if possible, but that would take us too far afield from the discussion here.
The error prone nature of dynamic memory allocation is ironic, since it would be entirely possible for the library implementers who write the functions that are used by new and delete to prevent, or at least detect, the problem of deleting something you haven't allocated or failing to delete something that you have allocated. After all, those routines handle all of the memory allocation and deallocation for a C++ program, so there's no reason that they couldn't keep track of what has been allocated and released.21
Of course, an ounce of prevention is worth a pound of cure, so avoiding these problems by proper design is the best solution. Luckily, it is possible to write programs so that this type of error is much less likely, by keeping all dynamic memory allocation inside class implementations rather than exposing it to the application programmer. We're following this approach with our string class, and it can also be applied to other situations where it is less straightforward, as we'll see when we get back to the inventory control application.
Susan was intrigued by the possible results of forgetting to deallocate resources such as memory. Here's the resulting discussion:
Susan: So when programs leak system resources, is that the result of just forgetting to delete something that is dynamically allocated?
Steve: Yes.
Susan: Then that would be basically a programming error or at least sloppiness on the part of the programmer?
Steve: Yes.
More on operator =
Having discussed some of the possible problems with dynamic allocation, let's continue with the code for operator = (Figure 7.15 on page 450).
The next line in the function, m_Data = temp;, makes our char* pointer refer to the newly allocated memory that holds a copy of the data from the string that we received as an argument (i.e., the one on the right of the =). Now our target string is a fully independent entity with the same value as the string that was passed in.
Finally, as is standard with assignment operators, we return *this, which means "the object to which this points", i.e., a reference to the string whose value we have just set, so that it can be used in further operations.
Susan had some questions about operator = and how it is implemented.
Susan: Why would you want to copy a variable into another variable anyway?
Steve: Well, let's say we have an application that keeps track of 300 CDs in a CD jukebox. One of the things people would probably like to know is the name of the CD that is playing right now. So you might have a CurrentSelection variable that would be changed whenever the CD currently playing changed. The CD loading function would copy the name of the CD being loaded to the CurrentSelection variable.
Steve: Okay. But I still don't get the . thingy.
Steve: All . does is separate the object (on the left) from the member variable or function (on the right). So s.operator=(n); might be roughly translated as "apply the operator = to the object s, with the argument n".
Susan: So wait: the . does more than separate; it allows access to other string member variables?
Steve: Right. It separates an object's name from the particular variable or function that we're accessing for that object. In other words, Str.m_Length means "the instance of m_Length that is part of the object Str."
Susan: So in the statement m_Length = Str.m_Length; what we are doing is creating a new m_Length equal to the length of Str's m_Length for the = operator?
Steve: Not exactly. What we're doing is setting the value of the length (m_Length) for the string being assigned to (the left-hand string) to the same value as the length of the string being copied from (the right-hand string).
Susan: But it is going to be specific for this string?
Steve: If I understand your question, the value of m_Length will be set for the particular string that we're assigning a new value to.
Susan: When we say Str, does that mean that we are not using the variable pointed to by this? I am now officially lost.
Steve: Yes, that's what it means. In a member function, if we don't specify the object we are talking about, it's the one pointed to by this; of course, if we do specify which object we mean, then we get the one we specify.
Although the individual statements weren't too much of a problem, Susan didn't get the big picture. Here's how I explained it:
Susan: I don't get this whole code thing for Figure 7.15, now that I think about it. Why does this stuff make a new operator =? This is weird.
Steve: Well, what does operator = do? It makes the object on the left side have the same value as the object on the right side. In the case of a string, this means that the left-hand string should have the same length as the one on the right, and all the chars used to store the data for the right-hand string need to be copied to the address pointed to by m_Data for the left-hand string. That's what our custom = does.
Susan: Let's see. First we have to get some new memory for the new m_Data; then we have to make a copy. . . So then the entire purpose of writing a new operator = is to make sure that variables of that class can be made into separate entities when using the = sign rather than sharing the same memory address for their data?
Steve: Right.
Susan: I forget now why we did that.
Steve: We did it so that we could change the value of one of the variables without affecting the other one.
Before we move on to the next member function, I should mention that Susan and I had quite a lengthy correspondence about the notion of this. Here are some more of the highlights of that discussion.
Susan: I still don't understand this.
Steve: this refers to the object that a member function is being called for. For example, in the statement xyz.Read();, when the function named Read is called, the value of this will be the address of the object xyz.
Susan: OK, then, is this the result of calling a function? Or the address of the result?
Steve: Not quite either of those; this is the address of the object for which a class function is called.
Susan: Now that I have really paid attention to this and tried to commit it to memory it makes more sense. I think that what is so mysterious is that it is a hidden argument. When I think of an argument I think of something in (), as an input argument.
Steve: It actually is being passed as though it were specified in every call to a member function. The reason it is hidden is not to make it mysterious, but to reduce the amount of work the programmer has to do. Since almost every member function needs to access something via this, supplying it automatically is a serious convenience.
Susan: Now as far as my understanding of the meaning of this, it is the address of the object whose value is the result of calling a member function.
Steve: Almost exactly right; this is the address of the object for which a member function is called. Is this merely a semantic difference?
Susan: Not quite. Is there not a value to the object? Other than that we are speaking the same language.
Steve: Yes, the object has a value. However, this is merely the address of the object, not its value.
Susan: How about writing this as if it were not hidden and was in the argument list; then show me how it would look. See what I mean? Show me what you think it would look like if you were to write it out and not hide it.
Steve: OK, that sounds good. I was thinking of doing that anyway.
That hypothetical operator = implementation uses a new notation: ->, which is the "pointer member access" operator. It separates a pointer to an object or variable name, on its left, from the member variable or member function on its right. In other words, -> does the same thing for pointer variables that . does for objects. That is, if the token on the right of -> is a member variable, that token refers to the specific member variable belonging to the object pointed to by the pointer on the left of ->; if the token on the right of -> is a member function, then it is called for the object pointed to by the pointer on the left of ->. For example, this->m_Data means "the m_Data that belongs to the object pointed to by this".
Given that new notation, Figure 7.17 shows what the code for operator = might look like if the this pointer weren't hidden, both in the function declaration and as a qualifier for the member variable names.
FIGURE 7.17. A hypothetical assignment operator (operator =) for the string class with explicit this
string& string::operator =(const string* this, const string& Str)
char* temp = new char[Str.m_Length];
this->m_Length = Str.m_Length;
memcpy(temp,Str.m_Data,this->m_Length);
Note that every reference to a member variable of the current object would have to specify this. That would actually be more significant in writing the code than the fact that we would have to supply this in the call. Of course, how we would actually supply this when calling the operator = function is also a good question. Clearly the necessity of passing this explicitly would make for a messier syntax than just s = n;.
The Destructor
Now that we have seen how operator = works in detail, let's look at the next member function in the initial version of our string class, the destructor. A destructor is the opposite of a constructor; that is, it is responsible for deallocating any memory allocated by the constructor and performing whatever other functions have to be done before a variable dies. It's quite rare to call the destructor for a variable explicitly; as a rule, the destructor is called automatically when the variable goes out of scope. As we've seen, the most common way for this to happen is that a function returns to its calling function; at that time, destructors are called for all local variables that have destructors, whereas local variables that don't have destructors, such as those of native types, just disappear silently.22
Susan had some questions about how variables are allocated and deallocated. Here's the discussion that ensued.
Susan: I remember we talked about the stack pointer and how it refers to addresses in memory but I don't remember deallocating anything. What is that?
Steve: Deallocating variables on the stack merely means that the same memory locations can be reused for different local variables.
Susan: Oh, that is right, the data stays in the memory locations until the location is used by something else. It really isn't meaningful after it has been used unless it is initialized again, right?
Steve: Yes, that's right.
Susan: When I first read about the destructor my reaction was, "well, what is the difference between this and delete?" But basically it just is a function that makes delete go into auto-pilot?
Steve: Basically correct, for variables that allocate memory dynamically. More generally, it performs whatever cleanup is needed when a function goes out of scope.
Susan: How does it know you are done with the variable, so that it can put the memory back?
Steve: By definition, when the destructor is called, the variable is history. This happens automatically when it goes out of scope. For an auto variable, whether of native type or class type, this occurs at the end of the block where the variable was defined.
Susan: I don't understand this. I reread your explanation of "going out of scope" and it is unclear to me what is happening and what the alternatives are. How does a scope "disappear"?
Steve: The scope doesn't disappear, but the execution of the program leaves it. For example, when a function terminates, the local variables (which have local scope), go out of scope and disappear. That is, they no longer have memory locations assigned to them, until and unless the function starts execution again.
Susan: What if you need the variable again?
Steve: Then don't let it go out of scope.
Because destructors are almost always called automatically when a variable goes out of scope, rather than by an explicit statement written by the programmer, the only information guaranteed to be available to a destructor is the address of the variable to be destroyed. For this reason, the C++ language specifies that a destructor cannot have arguments. This in turn means that there can be only one destructor for any class, since there can be at most one function in a given class with a given name and the same type(s) of argument(s) (or, as in this case, no arguments).
As with the constructor(s), the destructor has a special name to identify it to the compiler. In this case, it's the name of the class with the token ~ (the tilde) prefixed to it, so the destructor for class string is named ~string.23 The declaration of this function is the next line in Figure 7.1 on page 409, ~string();. Its implementation looks like Figure 7.18.
FIGURE 7.18. The destructor for the string class (from code/string1.cpp)
string::~string()
{
delete [ ] m_Data;
}
This function doesn't use any new constructs other than the odd notation for the name of the destructor; we've already seen that the delete [ ] operator frees the memory allocated to the pointer variable it operates on.24 In this case, that variable is m_Data, which holds the address of the first one of the group of characters that make up the actual data contained by the string.
Now that we've covered nearly all of the member functions in the initial version of the string class, it's time for some review.
7.9. Review
We've almost finished building our own version of a concrete data type called string, which provides a means of storing and processing a group of characters in a more convenient form than a C string. The fact that string is a concrete data type means that a string that is defined as a local variable in a block should be created when the code in the block begins execution and automatically destroyed when the block ends. Another requirement for a concrete data type is to be able to copy one variable of that type to another variable of the same type and have the two copies behave like independent variables, not linked together in the manner of Siamese twins.
As we've previously noted, the creation of an object is performed by a special member function called a constructor. Any class can have several constructors, one for each possible way that a newly created object can be initialized. So far, we've examined the interface and implementation of the default constructor, which takes no arguments, and a constructor that takes a char* argument. The former is needed to create a string that doesn't have a specified initial value, while the latter allows us to create a string that has the same contents as a C string. The default constructor is one of the required member functions in a concrete data type.
We've also seen that in the case of our string constructors, we need to know the order in which the member initialization expressions are executed. Since this is dependent on the order of declaration of member variables, we have to make sure that those member variables are declared in the correct order for our member initialization expressions to work properly.
Continuing with the requirements for a concrete data type, we've implemented our own version of operator =, which can set one string to the same value as another string while leaving them independent.
We've also created one other required member function for a concrete data type, the destructor, which is used to clean up after a string when it expires. This function is called automatically for an auto variable at the end of the block where the variable is defined.
We're still short a copy constructor, which can create a string that has the same value as another pre-existing string. This may sound just like operator =, but it's not exactly the same. While operator = is used to set a string that already exists to the same value as another extant string, the copy constructor creates a brand-new string with the same value as one that already exists. We'll see how this works in the next chapter; in the meantime, let's take a look at some exercises intended to test your understanding of this material.
7.10. Exercises
1. What would happen if we compiled the program in Figure 7.19? Why?
FIGURE 7.19. Exercise 1 (code\strex1.cpp)
string& operator = (const string& Str);
2. What would happen if we compiled the program in Figure 7.20? Why?
FIGURE 7.20. Exercise 2 (code\strex2.cpp)
class string
{
public:
string(const string& Str);
string(char* p);
string& operator=(const string& Str);
~string();
private:
string();
short m_Length;
char* m_Data;
};
int main()
{
string s("Test");
string n;
n = s;
return 0;
}
3. What would happen if we compiled the program in Figure 7.21? Why?
FIGURE 7.21. Exercise 3 (code\strex3.cpp)
class string
{
public:
string();
string(const string& Str);
string(char* p);
string& operator=(const string& Str);
private:
~string();
short m_Length;
char* m_Data;
};
int main()
{
string s("Test");
return 0;
}
4. What would happen if a user of our string class wrote an expression that tried to set a string variable to itself (e.g., a = a;)?
7.11. Conclusion
We've covered a lot of material about how a real, generally useful class such as string works in this chapter. In the next chapter, we'll continue with the saga of our string class, finishing up the additional functionality needed to turn it into a full-fledged concrete data type. We'll put this new functionality to the test in a modified version of the sorting algorithm from the early chapters that sorts strings rather than numeric values.
7.12. Answers to Exercises
1. The output of the compiler should look something like this:
Error E2247 STREX1.cpp 21: `string::m_Length' is not accessible in function main()
Warning W8004 STREX1.cpp 28: `Length' is assigned a value that is never used in function main()
This one is simple; since m_Length is a private member variable of string, a nonmember function such as main can't access it. Also, the compiler is warning us that we are never using the value of the Length variable.
2. The output of the compiler should look something like this:
Error E2247 STREX2.cpp 17: `string::string()' is not accessible in function main()
This is also pretty simple. Since the default constructor string::string() is in the private area, it's impossible for a nonmember function such as main to use it. Notice that there was no error message about string::string(char* p); that constructor is in the public area, so main is permitted to create a string from a C string. It's just the default constructor that's inaccessible.
3. The output of the compiler should look something like this:
Error E2166 STREX3.cpp 16: Destructor for `string' is not accessible in function main()
This answer is considerably less obvious than the previous ones. To be sure, the destructor is private and therefore can't be called from main, but that doesn't explain why main is trying to call the destructor in the first place. The reason is that every auto variable of a type that has a destructor must have its destructor called at the end of the function where the auto variable is defined. That's part of the mechanism that makes our objects act like "normal" variables, which also lose their values at the end of the function where they are declared.25 In the case of a user defined variable, though, more cleanup may be required; this is certainly true for strings, which have to deallocate the memory that they allocated to store their character data.
Therefore, you cannot create an object of a class whose destructor is private as an auto variable, as the automatic call of the destructor at the end of the scope would be illegal.
Susan didn't get this one exactly right, but she was close.
Susan: I have a note here that this program would not work because the ~string () thingy should be public and that, if this were to run, it would cause a memory leak. Am I on the right track?
Steve: Yes, you're close. Actually, what would happen is that the compiler would refuse to compile this program because it wouldn't be able to call ~string at the end of the function, since ~string is private. If the compiler would compile this program without calling the destructor, there would indeed be a memory leak.
4. Let's take a look at the sequence of events that would have transpired if the user had typed a = a;.26 The first statement to be executed would be char* temp = new char[Str.m_Length], which would allocate some new memory to store the contents of the string.
The second statement to be executed would be m_Length = Str.m_Length;. Since m_Length and Str.m_Length are actually the same memory location in this case, this statement wouldn't do anything.
Then the statement memcpy(temp,Str.m_Data,m_Length); would be executed. This would copy m_Length bytes of data to the address stored in temp, which points to the newly allocated piece of memory, from the address stored in Str.m_Data.
The next statement to be executed would be delete [ ] m_Data;, which would free the memory previously allocated to our string.
The next statement to be executed would be m_Data = temp;. This would cause our string to refer to the newly allocated memory, which contains a copy of the data from the right hand string (in this case, the same string we are assigning to).
Finally, we would return *this, as we normally do from an assignment operator.
The net result of all of this is that the m_Data member variable of string a would point to a copy of the same data that it originally pointed to; in other words, the assignment statement would work correctly even for this case.
Susan had a very concise summary of this process:
Susan: So the data for the original string was stolen and replaced with an exact replica?
Steve: Wright.27
1 Which, unfortunately for compiler writers, isn't very simple.
2 By the way, char* can also be written as char *, but I find it clearer to attach the * to the data type being pointed to.
3 C programmers are likely to object that a pointer has some properties that differ from those of a memory address. Technically, they're right, but in the specific case of char* the differences between a pointer and a memory address will never matter to us.
4 Actually, this isn't quite correct. The type of a C string literal is slightly different from char*, but that type is close enough for our purposes here. We'll see on page 446 what the exact type is and why it doesn't matter for our purposes.
5 As this implies, it's possible to have a pointer to any type of variable, not just to a char. For example, a pointer to a short would have the type short*, and similarly for pointers to any other data type, including user-defined types. As we will see in Chapter 10, pointers to user-defined types are very important in some circumstances, but we don't need to worry about them right now.
6 This terminology doesn't exactly match the official nomenclature used by Bjarne Stroustrup to describe dynamic memory allocation. However, every C++ programmer will understand you if you talk about dynamic storage, and I think this terminology is easier to understand than the official terminology.
7 I know we've skipped the copy constructor, the assignment operator, and the destructor. Don't worry, we'll get to them later.
8 There's nothing magical about the name p for a pointer. You could call it George if you wanted to, but it would just confuse people. The letter p is often used for pointers, especially by programmers who can't type, which unfortunately is fairly common.
9 This is probably a good place to clear up any confusion you might have about whether there are native and user defined functions; there is no such distinction. Functions are never native in the way that variables are: built into the language. Quite a few functions such as strlen and memcpy come with the language; that is, they are supplied in the standard libraries that you get when you buy the compiler. However, these functions are not privileged relative to the functions you can write yourself, unlike the case with native variables in C. In other words, you can write a function in C or C++ that looks and behaves exactly like one in the library, whereas it's impossible in C to add a type of variable that has the same appearance and behavior as the native types; the knowledge of the native variable types is built into the C compiler and cannot be changed or added to by the programmer.
But why aren't there any native functions? Because the language was designed to be easy to move (or port) from one machine to another. This is easier if the compiler is simpler; hence, most of the functionality of the language is provided by functions that can be written in the "base language" the compiler knows about. This includes basic functions such as strlen and memcpy, which can be written in C. For purposes of performance, they are often written in assembly language instead, but that's not necessary to get the language running on a new machine.
10 I'm assuming that you are using an operating system that can access all of the memory in your computer. If not, the free store may be much smaller than this suggests.
11 We'll see how to implement a similar feature in another context when we get back to the discussion of inventory control later.
12 As this explanation may suggest, we can't make up our own operators with strange names by prefixing those names with operator; we're limited to those operators that already exist in the C++ language.
13 In this section, you're going to see a lot of hedging of the form "in this context, x means y". The reason is that C and C++ both reuse keywords and symbols in many different situations, often with different meanings in each situation. In my opinion, this is a flaw in the design of these languages as it makes learning them more difficult. The reason for this reuse is that every time a keyword is added to the language, it's possible that formerly working code that contains a variable or function with the same name as the keyword will fail to compile. Personally, I think the problem of breaking existing code is overrated compared to the problems caused by overuse of the same keywords; however, I don't have a lot of old C or C++ code to maintain, so maybe I'm biased.
14 Now I can tell you what the real type of a C string literal is. Before the adoption of the C++ standard, the type actually was char*, as I said on page 414. Now, however, it's actually a const char*, because you really shouldn't change literal values. The reason this difference doesn't matter is that C++ will automatically convert the type of a C string literal to char* whenever necessary, to preserve the behavior of prestandard programs. This shouldn't affect us, because we know better than to change the value of a C string literal.
15 A token is the smallest part of a program that the compiler treats as a separate unit; it's analogous to a word in English, with a statement being more like a sentence. For example, string is a token, as are :: and (. On the other hand, x = 5; is a statement.
16 Actually, there is a kind of member function called a static member function that doesn't get a this pointer when it is called. We'll discuss this type of member function later, starting in Chapter 9.
17 Or any other constructor that allocates memory in which to store characters. I'm just referring to the char* constructor because we've already analyzed that one.
18 By the way, this is one of the previously mentioned times when we have to explicitly deal with the difference between a pointer used as "the address of an item" and one used as "the address of some number of items"; the [] after delete tells the compiler that the latter is the current situation. The C++ standard specifies that any memory that was allocated via a new expression containing [] must be deleted via delete []. Unfortunately, the compiler probably can't check this. If you get it wrong, your program probably won't work as intended and you may have a great deal of difficulty figuring out why. This is one of the reasons why it's important to use pointers only inside class implementations, where you have some chance of using them correctly.
19 There's an exception to this rule: calling delete for a pointer with the value 0 will not cause any untoward effects, as such a pointer is recognized as "pointing to nowhere".
20 If you are going to develop commercial software someday, you'll discover that you may need a utility program to help you find these problems, especially if you have to work on software designed and written by people who don't realize that pointers are dangerous. I've had pretty good luck with one called Purify, which is a product of Rational Software.
21 Actually, most compilers now give you the option of being informed at the end of execution of your program whether you have had any memory leaks, if you are running under a debugger, and some will even tell you if you delete memory that you haven't allocated (at least in some cases). However, to really be sure you don't have such problems, you'll need to use a utility such as the one I mentioned before.
22 If we use new to allocate memory for a variable that has a destructor, then the destructor is called when that variable is freed by delete. We'll discuss this when we get back to the inventory control application.
23 In case you're wondering, this somewhat obscure notation was chosen because the tilde is used to indicate logical negation; that is, if some expression x has the logical value true, then ~x will have the logical value false, and vice-versa.
24 By the way, in case you were wondering what happened to the old values of the m_Data and m_Length member variables, we don't have to worry about those because the string being destroyed won't ever be used again.
25 To be more precise, the destructor is called at the end of the scope in which the variable was defined. It's possible for a variable to have scope smaller than an entire function; in that case, the variable is destroyed when its scope expires.
26 See Figure 7.15 on page 450 for the code for operator =.
27 See http://www.wam.umd.edu/~stwright/right/StevenWright.html for more Steve Wright jokes.