The string Copy Constructor

Assuming you've followed this so far, you might have noticed one loose end. What if we want to pass a string as a value argument to a function? As we have seen, with the current setup bad things will happen since the compiler-generated copy constructor doesn't copy strings correctly. Well, you'll be relieved to learn that this, too, can be fixed. The answer is to implement our own version of the copy constructor. Let's take another look at the header file for our string class, in Figure 8.5.

Figure 8.5. The string class interface (codestring1.h)
class string
{
public:
  string();
  string(const string& Str);
  string& operator = (const string& Str);
  ~string();

  string(char* p);

private:
  short m_Length;
  char* m_Data;
};

The line we're interested in here is string(const string& Str);. This is a constructor, since its name is the class name string. It takes one argument, the type of which is const string& (a reference to a constant string). This means that we're not going to change the argument's value "through" the reference, as we could do via a non-const reference. The code in Figure 8.6 implements this new constructor.

Figure 8.6. The copy constructor for the string class (from codestring1.cpp)
string::string(const string& Str)
: m_Length(Str.m_Length),
  m_Data(new char [m_Length])
{
  memcpy(m_Data,Str.m_Data,m_Length);
}

This function's job is similar to that of operator =, which makes sense because both of these functions are in the copying business. However, there are also some differences; otherwise, we wouldn't need two separate functions.

One difference is that because a copy constructor is a constructor, we can use a member initialization list to initialize the member variables; this convenience is not available to operator =, as it is not a constructor.

The other difference is that we don't have to delete any previously held storage that might have been assigned to m_Data. Of course, this is also because we're building a new string, not reusing one that already exists. Therefore, we know that m_Data has never had any storage assigned to it previously.

One fine point that might have slipped past you is why we can't use a value argument rather than a reference argument to our copy constructor. The reason is that using a value argument of a class type requires a copy of the actual argument to be made, using ... the copy constructor! Obviously this won't work when we're writing the copy constructor for that type, so the compiler will let us know if we try to do this accidentally.

Now that we have a correct copy constructor, we can use a string as a value argument to a function, and the copy that's made by the compiler when execution of the function starts will be an independent string, not connected to the caller's variable. When this copy is destroyed at the end of the function, it will go away quietly and the caller's original variable won't be disturbed. This is all very well in theory, but it's time to see some practice. Let's write a function that we can call with a string to do some useful work, like displaying the characters in the string on the screen.

Screen Output

As I hope you remember from the previous chapters, we can send output to the screen via cout, a predefined output destination. For example, to write the character 'a' to the screen we could use the statement cout << 'a';. Although we have previously used cout and << to display string variables of the standard library string class, the current version of our string class doesn't support such output. If we want this ability, we'll have to provide it ourselves. Since variables that can't be displayed are limited in usefulness, we're going to start to do just that right now. Figure 8.7 is the updated header file.

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 string3.h, string3.cpp, and strtst3.cpp, respectively.

Figure 8.7. The string class interface, with Display function (codestring3.h)
class string
{
public:
  string();
  string(const string& Str);
  string& operator = (const string& Str);
  ~string();

  string(char* p);
  void Display();

private:
  short m_Length;
  char* m_Data;
};

As you can see, the new function is declared as void Display();. This means that it returns no value, its name is Display, and it takes no arguments. This last characteristic may seem odd at first, because surely Display needs to know which string we want to display. However, as we've already seen, each object has its own copy of all of the variables defined in the class interface. In this case, the data that are to be displayed are the characters pointed to by m_Data.

Figure 8.8 is an example of how Display can be used.

Figure 8.8. The latest version of the string class test program, using the Display function (codestrtst3.cpp)
#include <iostream>
#include "string3.h"
int main()
{
  string s;
  string n("Test");
  string x;

  s = n;
  n = "My name is Susan";

  n.Display();

  return 0;
}

See the line that says, n.Display();? That is how our new Display function is called. Remember, it's a member function, so it is always called with respect to a particular string variable; in this case, that variable is n.

As is often the case, Susan thought she didn't understand this idea, but actually did.

Susan: This Display stuff... I don't get it. Do you also have to write the code the classes need to display data on the screen?

Steve: Yes.

Before we get to the code for the Display function, let's take a look at the first few lines of the implementation file for this new version of the string class, which is shown in Figure 8.9.

Figure 8.9. The first few lines of the latest implementation of the string class (from string3.cpp)
#include <iostream>
#include <string.h>
using std::cout;

The first two lines of this file aren't anything new: all they do is specify two header files from the standard C++ library to allow us to access the streams and C string functions from that library. But the third line, “using std::cout;” is new. What does it do?

Up until now, our only use of using has been to import all the names from the std namespace, where the names from the standard library are defined. When we are writing our own string class, however, we can't import all those names without causing confusion between our own string class and the one from the standard library.

Luckily, there is a way to import only specific names from the standard library. That's what that line does: it tells the compiler to import the specific name cout from the standard library. Therefore, whenever we refer to cout without qualifying which cout we mean, the compiler will include std::cout in the list of possible matches for that name. We'll have to do that once in a while, whenever we want to use some standard library names, and some of our own.

Susan had some questions about this new use of using.

Susan: What does this new using thing do that's different from the old one?

Steve: When we say using namespace std;, that means that we want all the names from the standard library to be “imported” into the present scope. In this case, however, we don't want it to do that because we have written our own string class. If we wrote using namespace std;, the compiler would get confused between the standard library string class and our string class. For that reason, we will just tell the compiler about individual names from the standard library that we want it to import, rather than doing it wholesale.

Now that that's cleared up, let's look at the implementation of this new member function, Display, in Figure 8.10.

Figure 8.10. The string class implementation of the Display function (from string3.cpp)
void string::Display()
{
  short i;

  for (i = 0; i < m_Length-1; i ++)
     cout << m_Data[i];
}

This should be looking almost sensible by now; here's the detailed analysis. We start out with the function declaration, which says we're defining a void function (i.e., one that returns no value) that is a member function of the class string. This function is named Display, and it takes no arguments. Then we define a short called i. The main part of the function is a for loop, which is executed with the index starting at 0 and incremented by one for each character, as usual. The for loop continues while the index is less than the number of displayable characters that we use to store the value of our string; of course, we don't need to display the null byte at the end of the string. So far, so good. Now comes the tricky part. The next statement, which is the controlled block of the for loop, says to send something to cout; that is, display it on the screen. This makes sense, because after all that's the purpose of this function. But what is it that is being sent to cout?

The Array

It's just a char but that may not be obvious from the way it's written. This expression m_Data[i] looks just like a Vec element, doesn't it? In fact, m_Data[i] is an element, but not of a Vec. Instead, it's an element of an array, the C equivalent of a C++ Vec.

What's an array? Well, it's a bunch of data items (elements) of the same type; in this case, it's an array of chars. The array name, m_Data in this case, corresponds to the address of the first of these elements; the other elements follow the first one immediately in memory. If this sounds familiar, it should; it's very much like Susan's old nemesis, a pointer. However, like a Vec, we can also refer to the individual elements by their indexes; so, m_Data[i] refers to the ith element of the array, which in the case of a char array is, oddly enough, a char.

So now it should be clear that each time through the loop, we're sending out the ith element of the array of chars where we stored our string data.

Susan and I had quite a discussion about this topic, and here it is.

Susan: If this is an array or a Vec (I can't tell which), how does the class know about it if you haven't written a constructor for it?

Steve: Arrays are a native feature of C++, left over from C. Thus, we don't have to (and can't) create a constructor and the like for them.

Susan: The best I can figure out from this discussion is that an array is like a Vec but instead of numbers it indexes char data and uses a pointer to do it?

Steve: Very close. It's just like a Vec except that it's missing some rather important features of a Vec. The most important one from our perspective is that an array doesn't have any error-checking; if you give it a silly index you'll get something back, but exactly what is hard to determine.

Susan: If it is just like a Vec and it is not as useful as a real Vec, then why use it? What can it do that a Vec can't?

Steve: A Vec isn't a native data type, whereas an array is. Therefore, you can use arrays to make Vecs, which is in fact how a Vec is implemented at the lowest level. We also wouldn't want to use Vecs to hold our string data because they're much more "expensive" (i.e., large and slow) to use. I'm trying to illustrate how we could make a string class that would resemble one that would actually be usable in a real program and although simplicity is important, I didn't want to go off the deep end in hiding arrays from the reader.

Susan: So when you say that "we're sending out the ith element of the array of chars where we stored our value" does that mean that the "ith" element would be the pointer to some memory address where char data is stored?

Steve: Not exactly. The ith element of the array is just like the ith element of a Vec. If we had a Vec of four chars called x, we'd declare it as follows:

Vec<char> x(4);

Then we could refer to the individual chars in that Vec as x[0], x[1], x[2], or x[3]. It's much the same for an array. If we had an array of four chars called y, we'd declare it as follows:

char y[4];

Then we could refer to the individual chars in that array as y[0], y[1], y[2], or y[3].

That's all very well, but there's one loose end. We defined m_Data as a char*, which is a pointer to (i.e., the address of) a char. As is common in C and C++, this particular char* is the first of a bunch of chars one after the other in memory. So where did the array come from?

The Equivalence of Arrays and Pointers

Brace yourself for this one. In C++, a pointer and the address of an array are for almost all purposes the same thing. You can treat an array address as a pointer and a pointer as an array address, pretty much as you please. This is a holdover from C and is necessary for compatibility with C programs. People who like C will tell you how "flexible" the equivalence of pointers and arrays is in C. That's true, but it's also extremely dangerous because it means that arrays have no error checking whatsoever. You can use whatever index you feel like, and the compiled code will happily try to access the memory location that would have corresponded to that index. The program in Figure 8.11 is an example of what can go wrong when using arrays.

Figure 8.11. Dangerous characters (codedangchar.cpp)
#include <iostream>
using std::cout;
using std::endl;

int main()
{
   char High[10];
   char Middle[10];
   char Low[10];
   char* Alias;
   short i;

   for (i = 0; i < 10; i ++)
      {
      Middle[i] = 'A' + i;
      High[i] = '0';
      Low[i] ='1';
      }

   Alias = Middle;

   for (i = 10; i < 20; i ++)
      {
      Alias[i] = 'a' + i;
      }

   cout << "Low: ";
   for (i = 0; i < 10; i ++)
      cout << Low[i];
   cout << endl;

   cout << "Middle: ";
   for (i = 0; i < 10; i ++)
      cout << Middle[i];

   cout << endl;

   cout << "Alias: ";
   for (i = 0; i < 10; i ++)
      cout << Alias[i];

   cout << endl;

   cout << "High: ";
   for (i = 0; i < 10; i ++)
      cout << High[i];

   cout << endl;
}

First, before trying to analyze this program, I should point out that it contains two using statements to tell the compiler that we want to import individual names from the standard library. In this case, we want the compiler to consider the names std::cout and std::endl as matching the unqualified names cout and endl, respectively.

Now, let's look at what this program does when it's executed. First we define three variables, High, Middle, and Low, each as an array of 10 chars. Then we define a variable Alias as a char*; as you may recall, this is how we specify a pointer to a char. Such a pointer is essentially equivalent to a plain old memory address.

Susan wanted to know something about this program before we got any further into it.

Susan: Why are these arrays called High, Low and Middle?

Steve: Good question. They are named after their relative positions on the stack when the function is executing. We'll see why that is important later.

In the next part of the program, we use a for loop to set each element of the arrays High, Middle, and Low to a value. So far, so good, except that the statement Middle[i] = 'A' + i; may look a bit odd. How can we add a char value like 'A' and a short value such as i?

The char As a Very Short Numeric Variable

Let us return to those thrilling days of yesteryear, or at least Chapter 3. Since then, we've been using chars to hold ASCII values, which is their most common use. However, every char variable actually has a "double life"; it can also be thought of as a "really short" numeric variable that can take on any of 256 values. Thus, we can add and subtract chars and shorts as long as we're careful not to try to use a char to hold a number greater than 255 (or greater than 127, for a signed char). In this case, there's no problem with the magnitude of the result, since we're starting out with the value A and adding a number between 0 and 9 to it; the highest possible result is J, which is still well below the maximum value that can be stored in a char.

With that detail taken care of, let's proceed with the analysis of this program. The next statement after the end of the first for loop is the seemingly simple line Alias = Middle;. This is obviously an assignment statement, but what is being assigned?

The value that Alias receives is the address of the first element of the array Middle. That is, after the assignment statement is executed, Alias is effectively another name for Middle. Therefore, the next loop, which assigns values to elements 10 through 19 of the "array" Alias, actually operates on the array Middle, setting those elements to the values k through t.

The rest of the program is pretty simple; it just displays the characters from each of the Low, Middle, Alias, and High arrays. Of course, Alias isn't really an array, but it acts just like one. To be precise, it acts just like Middle, since it points to the first character in Middle. Therefore, the Alias and Middle loops will display the same characters. Then the final loop displays the values in the High array.

Running off the End of an Array

That's pretty simple, isn't it? Not quite as simple as it looks. If you've been following along closely, you're probably thinking I've gone off the deep end. First, I said that the array Middle had 10 elements (which are numbered 0 through 9 as always in C++); now I'm assigning values to elements numbered 10 through 19. Am I nuts?

No, but the program is. When you run it, you'll discover that it produces the output shown in Figure 8.12.

Figure 8.12. Reaping the whirlwind
Low:1111111111
Middle: ABCDEFGHIJ
Alias: ABCDEFGHIJ
High: mnopqrst00

Most of these results are pretty reasonable; Low is just as it was when we initialized it, and Middle and Alias have the expected portion of the alphabet. But look at High. Shouldn't it be all 0s?

Yes, it should. However, we have broken the rules by writing "past the end" of an array, and the result is that we have overwritten some other data in our program, which in this case turned out to be most of the original values of High. You may wonder why we didn't get an error message as we did when we tried to write to a nonexistent Vec element in an earlier chapter. The reason is that in C, the name of an array is translated into the address of the first element of a number of elements stored consecutively in memory. In other words, an array acts just like a pointer, except that the address of the first element it refers to can't be changed at run time.

In case this equivalence of arrays and pointers isn't immediately obvious to you, you're not alone; it wasn't obvious to Susan, either.

Susan: And when you say that "that is, a pointer (i.e., the address of) a char, which may be the first of a bunch of chars one after another in memory", does that mean the char* points to the first address and then the second and then the third individually, and an array will point at all of them at the same time?

Steve: No, the char* points to the first char, but we (and the compiler) can figure out the addresses of the other chars because they follow the first char sequentially in memory. The same is true of the array; the array name refers to the address of the first char, and the other chars in the array can be addressed with the index added to the array name. In other words, y[2] in the example means "the char that is 2 bytes past the beginning of the array called y".

You might think that this near-identity between pointers and arrays means that the compiler does not keep track of how many elements are in an array. Actually, it does, and it is possible to retrieve that information in the same function where the array is declared, via a mechanism we won't get into in this book. However, array access has no bounds-checking built into it, so the fact that the compiler knows how many elements are in an array doesn't help you when you run off the end of an array. Also, whenever you pass an array as an argument, the information on the number of elements in the array is not available in the called function. So the theoretical possibility of finding out this information in some cases isn't much help in most practical situations.

This is why pointers and arrays are the single most error-prone construct in C (and C++, when they're used recklessly). It's also why we're not going to use either of these constructs except when there's no other reasonable way to accomplish our goals; even then, we'll confine them to tightly controlled circumstances in the implementation of a user defined data type. For example, we don't have to worry about going "off the end" of the array in our Display function, because we know exactly how many characters we've stored (m_Length), and we've written the function to send exactly that many characters to the screen via cout. In fact, all of the member functions of our string class are carefully designed to allocate, use, and dispose of the memory pointed to by m_Data so that the user of this class doesn't have to worry about pointers or arrays, or the problems they can cause. After all, one of the main benefits of using C++ is that the users of a class don't have to concern themselves with the way it works, just with what it does.

Assuming that you've installed the software from the CD in the back of this book, you can try out this program. First, you have to compile it by following the compilation instructions on the CD. Then type dangchar to run the program. You'll see that it indeed prints out the erroneous data shown in Figure 8.12. You can also run it under the debugger, by following the usual instructions for that method.

Susan and I had quite a discussion about this program:

Susan: It is still not clear to me why you assigned values to elements numbered 10–19. Was that for demonstration purposes to force "writing past the end"?

Steve: Yes.

Susan: So by doing this to Middle then it alters the place the pointer is going to point for High?

Steve: No, it doesn't change the address of High. Instead, it uses some of the same addresses that High uses, overwriting some of the data in High.

Susan: So then when High runs there isn't any memory to put its results in? Why does Middle overwrite High instead of High overwriting Middle? But it was Alias that took up high's memory?

Steve: Actually High is filled up with the correct values, but then they're overwritten by the loop that stores via Alias, which is just another name for Middle.

Susan: Is that why High took on the lower case letters? Because Middle took the first loop and then Alias is the same as middle, so that is why it also has the upper case letters but then when High looped it picked up the pointer where Alias left off and that is why it is in lower case? But how did it manage two zeros at the end? I better stop talking, this is getting too weird. You are going to think I am nuts.

Steve: No. You're not nuts, but the program is. We're breaking the rules by writing "past the end" of the array Middle, using the pointer Alias to do so. We could have gotten the same result by storing data into elements 10 through 19 of Middle, but I wanted to show the equivalence of pointers and arrays.

Susan: I was just going to ask if Middle alone would have been sufficient to do the job.

Steve: Yes, it would have been. However, it would not have made the point that arrays and pointers are almost identical in C++.

Susan: I am confused as to why the lower case letters are in High and why k and l are missing and two zeros made their way in. You never told me why those zeros were there.

Steve: Because the end of one array isn't necessarily immediately followed by the beginning of the next array; this depends on the sizes of the arrays and on how the compiler allocates local variables on the stack. What we're doing here is breaking the rules of the language so it shouldn't be a surprise that the result isn't very sensible.

Susan: OK, so if you are breaking the rules you can't predict an outcome? Here I was trying to figure out what was going on by looking at the results even knowing it was erroneous. Ugh.

Steve: Indeed. I guess I should have a couple of diagrams showing what the memory layout looks like before and after the data is overwritten. Let's start with Figure 8.13. Most of that should be fairly obvious, but what are those boxes with question marks in them after the data that we put in the various arrays?

Figure 8.13. The memory layout before overwriting the data

Those are just bytes of memory that aren't being used to hold anything at the moment. You see, the compiler doesn't necessarily use every consecutive byte of memory to store all of the variables that we declare. Sometimes, for various reasons which aren't relevant here, it leaves gaps between the memory addresses allocated to our variables.

One more point about this diagram is that I've indicated the location referred to by the expression Alias[10], which as we've already seen is an invalid pointer/array reference. That's where the illegal array/pointer operations will start to mess things up.

Now let's see what the memory layout looks like after executing the loop where we try to store something into memory through Alias[10] through Alias[19].

Figure 8.14. The memory layout after overwriting the data

What we have done here is write off the end of the array called Middle and overwrite most of the data for High in the process. Does that clear it up?

Susan: Yes, I think I've got it now. It's sort of like if you dump garbage in a stream running through your back yard and it ends up on your neighbor's property.

Steve: Exactly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset