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 tokens[15] in the original program and the call to the member function should be fairly obvious, as we see them in Figure 7.14.

[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.

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.

[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.

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 codestring1.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 423.[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.

[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.

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]

[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.

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
try
  {
  p = new char[1000];
  }
 catch (...)
  {
  cout << "You're hosed!" << endl;
  exit(1);
  }

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]

[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.

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 446).

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);
 delete [ ] this->m_Data;

 this->m_Data = temp;

 return *this;
}

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]

[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.

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 405, ~string();. Its implementation looks like Figure 7.18.

[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.

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.

[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.

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.

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

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