![]() |
![]() |
![]() |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
![]() |
![]() |
To access the contents, click the chapter and section titles.
C++ in Plain English
The Copy Constructor and the Reference Operator (&)The copy constructor CStr(CStr&) turns out to be another constructor thats difficult to live without. As mentioned earlier, the copy constructor is invoked when you use one object to initialize another object: CStr name(Joe Bloe); CStr name2(name); // Call copy constructor. In the first line of code, name is defined and initialized from the string Joe Bloe. C++ calls the constructor CStr(char*) to initialize this string. Then in the second line, name2 is initialized as a copy of name. The object name2 is the same kind of object as name and is to be initialized to contain the same information. The constructor that tells how to make a copy from the same kind of object is the classs copy constructor. This constructor is very important, because, in addition to variable definition, it is automatically invoked in the following situations:
These situations are all too common, so the compiler supplies a hidden copy constructor if you dont provide one. However, this compiler-supplied constructor is simplistic. All it does is to perform a simple member-by-member copy. In some cases, this is fine. But CStr is a good example of a class in which a simple member-by-member class is inadequate. In fact, the results can be disastrous. Figure 6.1 shows what happens when this approach is used to copy one CStr object to another.
After the member-by-member copying is performed, the second object (name2) has a pointer to the same memory block that the first object points to. Such a situation works for now, but any change to either object could invalidate the other object. There are many ways this could happen, but the most obvious occurs if one of the objects is deleted? pString = new CStr; pString->cpy(John Q. Public); CStr name2(*pString); ... delete pString; puts(name2.get()); // What happens now? The basic idea of copying, I think youll agree, is that if you lose or destroy the original, the copy should live on and be fully usable. But thats not what happens here. When the copy constructor is invoked (in the third line of this example), it initializes the pointer name2.pData to the same address as pString-&>pData. When *pString is deleted, the memory block is freed. But name2.pData still points to this same memory block, which is now invalid! What happens with the last line, then, is an illegal memory reference. Clearly, the copy constructor should do what the CStr(char*) constructor does: allocate a new memory block and then make a physical copy of the string data. This copy constructor is easy to write, because it should look almost exactly like CStr(char*). First, however, you need to understand what the reference operator (&) is and how its used in copy construction. Figure 6.2 illustrates correct copy construction.
References: The Address Operator(&) Used in a New WayTo write a copy constructor, you must use the reference operator. This operator uses an ampersand (&), just as the address operator does. The reference operator isnt the same as the address operator, although both operators have a strong relationship to pointers. C++ may be confusing here because there are a total of three different uses for the ampersand. In addition to being the reference operator and the address operator, its also used for bitwise AND, a binary operation. But the reference operator is easy to identify because it occurs in only one place: declarations. The syntax is similar to that of pointers: the ampersand occurs just to the left of a variable or argument name, just as would a pointer symbol (*). In a simple variable definition, the reference operator indicates that one variable is to be used as an alias for another variable. int a; int & b = a; // b is an alias for a a = 10; b++; // This increments a In this example, the reference operator is used to make b an alias for a. This code is functionally equivalent to the following example, using pointers: int a, *b; a = 10; b = &a; // *b is an alias for a (*b)++; // This increments a If you compare the code examples closely, youll see one critical difference: when b is defined as a reference (&b), it is not treated as a pointer in the source code. The compiler may implement b as a pointer, but this fact is hidden by the C++ syntax; b looks like a normal integer. You can use the reference operator (&) simply to create aliases for variables, but the most important uses tend to occur in function calls. Look at the prototype for CStrs copy constructor: CStr(CStr &str); This constructor takes one argument, str, of type CStr&. This indicates that the argument is actually passed as a pointer, but the language syntax pretends that a pointer is not involved. Avoiding the pointer syntax in this case keeps the C++ code much cleaner and simpler. In simple terms, to make a copy of an object, the copy constructor gets a pointer to the original object and accesses members through that pointer. The object is passed by reference, without explicit pointer syntax, just as you might do in Basic, Pascal, or FORTRAN. If you think about it, this passing by reference is required here. If the copy constructor had to get a copy of an object before it could create a copy, then it would have to invoke itself before it ever got started! The result would be a self-defeating infinite regress. Writing the Copy ConstructorAt this point, all you need to remember about the reference operator is this: treat the argument as a value and not a pointer, even though you know its being passed by reference. The correct copy constructor for CStr is as follows: CStr::CStr(CStr &str) { char* s = str.get(); int n = str.getlength(); pData = (char *) malloc(n); strcpy(pData, s); nLength = n; } This function is similar to the CStr(char*) constructor. The principal difference is that the string data, s, is extracted first. The length is also extracted, because this is more efficient than making a call to strlen. By the way, the header of the function definition may seem confusing at first. The class name, CStr, is used three times, each time in a slightly different way. Figure 6.3 analyzes and explains each occurrence. The three uses are closely related, but each fulfills a different role in the syntaxscope qualifier, name, or argument type.
The Finishing Touch: The const KeywordOne of the effects of passing by reference is to enable the function to change the arguments value. If youve studied Basic, FORTRAN, or Pascal, youve no doubt learned that this is the main reason for passing an argument by reference. But the last thing the copy constructor should ever do is to change its argument. Here, passing by reference is for efficiencys sake. One would like to pass by reference but prevent the possibility of ever changing the argument. Fortunately, C++ provides such a techniqueplace the const keyword in front of the argument declaration: class CStr { CStr(const CStr &str); ... }; // Copy constructor with const arg. // All the code is the same except for arg declaration. CStr::CStr(const CStr &str) { char* s = str.get(); int n = str.getlength(); pData = (char *) malloc(n); strcpy(pData, s); nLength = n; } Now the copy constructor cannot change the value of any member of str. Any attempt to assign values to str or any of its data members would be flagged as an error by the compiler. Nor can str be passed to any other function unless that function also takes a const argument. Using a const argument is the preferred way to write a copy constructor, because it prevents a cause of run-time errors. Unfortunately, if you attempt to compile the code with this use of const, youll get errors. The problem is that the first two lines of the body of the function make calls through the argument, str: how does the compiler know that these function calls wont corrupt str? The solution is to declare these functions, get and getlength, as const functions. Doing so means that these functions have a contract to not change any data members (a condition that does hold in these cases), so they are safe to call for a const object: int getlength(void) const {return nLength;} char *get(void) const {return pData;} Whew! This extra work may make const seem like more trouble than its worth, but all the rules make sense, if you think about them. For more information on the fine points of the const keyword, see Part III.
|
![]() |
Products | Contact Us | About Us | Privacy | Ad Info | Home
Use of this site is subject to certain Terms & Conditions, Copyright © 1996-2000 EarthWeb Inc. All rights reserved. Reproduction whole or in part in any form or medium without express written permission of EarthWeb is prohibited. Read EarthWeb's privacy statement. |