1. Trang chủ
  2. » Công Nghệ Thông Tin

Thinking in C plus plus (P11) pdf

50 295 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 50
Dung lượng 154,48 KB

Nội dung

480 Thinking in C++ www.BruceEckel.com defined types by value during function calls. It’s so important, in fact, that the compiler will automatically synthesize a copy- constructor if you don’t provide one yourself, as you will see. Passing & returning by value To understand the need for the copy-constructor, consider the way C handles passing and returning variables by value during function calls. If you declare a function and make a function call, int f(int x, char c); int g = f(a, b); how does the compiler know how to pass and return those variables? It just knows! The range of the types it must deal with is so small – char , int , float , double , and their variations – that this information is built into the compiler. If you figure out how to generate assembly code with your compiler and determine the statements generated by the function call to f( ) , you’ll get the equivalent of: push b push a call f() add sp,4 mov g, register a This code has been cleaned up significantly to make it generic; the expressions for b and a will be different depending on whether the variables are global (in which case they will be _b and _a ) or local (the compiler will index them off the stack pointer). This is also true for the expression for g . The appearance of the call to f( ) will depend on your name-decoration scheme, and “register a” depends on how the CPU registers are named within your assembler. The logic behind the code, however, will remain the same. In C and C++, arguments are first pushed on the stack from right to left, then the function call is made. The calling code is responsible 11: References & the Copy-Constructor 481 for cleaning the arguments off the stack (which accounts for the add sp,4 ). But notice that to pass the arguments by value, the compiler simply pushes copies on the stack – it knows how big they are and that pushing those arguments makes accurate copies of them. The return value of f( ) is placed in a register. Again, the compiler knows everything there is to know about the return value type because that type is built into the language, so the compiler can return it by placing it in a register. With the primitive data types in C, the simple act of copying the bits of the value is equivalent to copying the object. Passing & returning large objects But now consider user-defined types. If you create a class and you want to pass an object of that class by value, how is the compiler supposed to know what to do? This is not a type built into the compiler; it’s a type you have created. To investigate this, you can start with a simple structure that is clearly too large to return in registers: //: C11:PassingBigStructures.cpp struct Big { char buf[100]; int i; long d; } B, B2; Big bigfun(Big b) { b.i = 100; // Do something to the argument return b; } int main() { B2 = bigfun(B); } ///:~ Decoding the assembly output is a little more complicated here because most compilers use “helper” functions instead of putting 482 Thinking in C++ www.BruceEckel.com all functionality inline. In main( ) , the call to bigfun( ) starts as you might guess – the entire contents of B is pushed on the stack. (Here, you might see some compilers load registers with the address of the Big and its size, then call a helper function to push the Big onto the stack.) In the previous code fragment, pushing the arguments onto the stack was all that was required before making the function call. In PassingBigStructures.cpp , however, you’ll see an additional action: the address of B2 is pushed before making the call, even though it’s obviously not an argument. To comprehend what’s going on here, you need to understand the constraints on the compiler when it’s making a function call. Function-call stack frame When the compiler generates code for a function call, it first pushes all the arguments on the stack, then makes the call. Inside the function, code is generated to move the stack pointer down even farther to provide storage for the function’s local variables. (“Down” is relative here; your machine may increment or decrement the stack pointer during a push.) But during the assembly-language CALL, the CPU pushes the address in the program code where the function call came from , so the assembly- language RETURN can use that address to return to the calling point. This address is of course sacred, because without it your program will get completely lost. Here’s what the stack frame looks like after the CALL and the allocation of local variable storage in the function: Function arguments Return address Local variables 11: References & the Copy-Constructor 483 The code generated for the rest of the function expects the memory to be laid out exactly this way, so that it can carefully pick from the function arguments and local variables without touching the return address. I shall call this block of memory, which is everything used by a function in the process of the function call, the function frame . You might think it reasonable to try to return values on the stack. The compiler could simply push it, and the function could return an offset to indicate how far down in the stack the return value begins. Re-entrancy The problem occurs because functions in C and C++ support interrupts; that is, the languages are re-entrant . They also support recursive function calls. This means that at any point in the execution of a program an interrupt can occur without breaking the program. Of course, the person who writes the interrupt service routine (ISR) is responsible for saving and restoring all the registers that are used in the ISR, but if the ISR needs to use any memory further down on the stack, this must be a safe thing to do. (You can think of an ISR as an ordinary function with no arguments and void return value that saves and restores the CPU state. An ISR function call is triggered by some hardware event instead of an explicit call from within a program.) Now imagine what would happen if an ordinary function tried to return values on the stack. You can’t touch any part of the stack that’s above the return address, so the function would have to push the values below the return address. But when the assembly- language RETURN is executed, the stack pointer must be pointing to the return address (or right below it, depending on your machine), so right before the RETURN, the function must move the stack pointer up, thus clearing off all its local variables. If you’re trying to return values on the stack below the return address, you become vulnerable at that moment because an interrupt could come along. The ISR would move the stack pointer down to hold 484 Thinking in C++ www.BruceEckel.com its return address and its local variables and overwrite your return value. To solve this problem, the caller could be responsible for allocating the extra storage on the stack for the return values before calling the function. However, C was not designed this way, and C++ must be compatible. As you’ll see shortly, the C++ compiler uses a more efficient scheme. Your next idea might be to return the value in some global data area, but this doesn’t work either. Reentrancy means that any function can be an interrupt routine for any other function, including the same function you’re currently inside . Thus, if you put the return value in a global area, you might return into the same function, which would overwrite that return value. The same logic applies to recursion. The only safe place to return values is in the registers, so you’re back to the problem of what to do when the registers aren’t large enough to hold the return value. The answer is to push the address of the return value’s destination on the stack as one of the function arguments, and let the function copy the return information directly into the destination. This not only solves all the problems, it’s more efficient. It’s also the reason that, in PassingBigStructures.cpp , the compiler pushes the address of B2 before the call to bigfun( ) in main( ) . If you look at the assembly output for bigfun( ) , you can see it expects this hidden argument and performs the copy to the destination inside the function. Bitcopy versus initialization So far, so good. There’s a workable process for passing and returning large simple structures. But notice that all you have is a way to copy the bits from one place to another, which certainly works fine for the primitive way that C looks at variables. But in C++ objects can be much more sophisticated than a patch of bits; they have meaning. This meaning may not respond well to having its bits copied. 11: References & the Copy-Constructor 485 Consider a simple example: a class that knows how many objects of its type exist at any one time. From Chapter 10, you know the way to do this is by including a static data member: //: C11:HowMany.cpp // A class that counts its objects #include <fstream> #include <string> using namespace std; ofstream out("HowMany.out"); class HowMany { static int objectCount; public: HowMany() { objectCount++; } static void print(const string& msg = "") { if(msg.size() != 0) out << msg << ": "; out << "objectCount = " << objectCount << endl; } ~HowMany() { objectCount ; print("~HowMany()"); } }; int HowMany::objectCount = 0; // Pass and return BY VALUE: HowMany f(HowMany x) { x.print("x argument inside f()"); return x; } int main() { HowMany h; HowMany::print("after construction of h"); HowMany h2 = f(h); HowMany::print("after call to f()"); } ///:~ The class HowMany contains a static int objectCount and a static member function print( ) to report the value of that objectCount , 486 Thinking in C++ www.BruceEckel.com along with an optional message argument. The constructor increments the count each time an object is created, and the destructor decrements it. The output, however, is not what you would expect: after construction of h: objectCount = 1 x argument inside f(): objectCount = 1 ~HowMany(): objectCount = 0 after call to f(): objectCount = 0 ~HowMany(): objectCount = -1 ~HowMany(): objectCount = -2 After h is created, the object count is one, which is fine. But after the call to f( ) you would expect to have an object count of two, because h2 is now in scope as well. Instead, the count is zero, which indicates something has gone horribly wrong. This is confirmed by the fact that the two destructors at the end make the object count go negative, something that should never happen. Look at the point inside f( ) , which occurs after the argument is passed by value. This means the original object h exists outside the function frame, and there’s an additional object inside the function frame, which is the copy that has been passed by value. However, the argument has been passed using C’s primitive notion of bitcopying, whereas the C++ HowMany class requires true initialization to maintain its integrity, so the default bitcopy fails to produce the desired effect. When the local object goes out of scope at the end of the call to f( ) , the destructor is called, which decrements objectCount , so outside the function, objectCount is zero. The creation of h2 is also performed using a bitcopy, so the constructor isn’t called there either, and when h and h2 go out of scope, their destructors cause the negative values of objectCount . 11: References & the Copy-Constructor 487 Copy-construction The problem occurs because the compiler makes an assumption about how to create a new object from an existing object . When you pass an object by value, you create a new object, the passed object inside the function frame, from an existing object, the original object outside the function frame. This is also often true when returning an object from a function. In the expression HowMany h2 = f(h); h2 , a previously unconstructed object, is created from the return value of f( ) , so again a new object is created from an existing one. The compiler’s assumption is that you want to perform this creation using a bitcopy, and in many cases this may work fine, but in HowMany it doesn’t fly because the meaning of initialization goes beyond simply copying. Another common example occurs if the class contains pointers – what do they point to, and should you copy them or should they be connected to some new piece of memory? Fortunately, you can intervene in this process and prevent the compiler from doing a bitcopy. You do this by defining your own function to be used whenever the compiler needs to make a new object from an existing object. Logically enough, you’re making a new object, so this function is a constructor, and also logically enough, the single argument to this constructor has to do with the object you’re constructing from. But that object can’t be passed into the constructor by value because you’re trying to define the function that handles passing by value, and syntactically it doesn’t make sense to pass a pointer because, after all, you’re creating the new object from an existing object. Here, references come to the rescue, so you take the reference of the source object. This function is called the copy-constructor and is often referred to as X(X&) , which is its appearance for a class called X . 488 Thinking in C++ www.BruceEckel.com If you create a copy-constructor, the compiler will not perform a bitcopy when creating a new object from an existing one. It will always call your copy-constructor. So, if you don’t create a copy- constructor, the compiler will do something sensible, but you have the choice of taking over complete control of the process. Now it’s possible to fix the problem in HowMany.cpp : //: C11:HowMany2.cpp // The copy-constructor #include <fstream> #include <string> using namespace std; ofstream out("HowMany2.out"); class HowMany2 { string name; // Object identifier static int objectCount; public: HowMany2(const string& id = "") : name(id) { ++objectCount; print("HowMany2()"); } ~HowMany2() { objectCount; print("~HowMany2()"); } // The copy-constructor: HowMany2(const HowMany2& h) : name(h.name) { name += " copy"; ++objectCount; print("HowMany2(const HowMany2&)"); } void print(const string& msg = "") const { if(msg.size() != 0) out << msg << endl; out << '\t' << name << ": " << "objectCount = " << objectCount << endl; } }; int HowMany2::objectCount = 0; 11: References & the Copy-Constructor 489 // Pass and return BY VALUE: HowMany2 f(HowMany2 x) { x.print("x argument inside f()"); out << "Returning from f()" << endl; return x; } int main() { HowMany2 h("h"); out << "Entering f()" << endl; HowMany2 h2 = f(h); h2.print("h2 after call to f()"); out << "Call f(), no return value" << endl; f(h); out << "After call to f()" << endl; } ///:~ There are a number of new twists thrown in here so you can get a better idea of what’s happening. First, the string name acts as an object identifier when information about that object is printed. In the constructor, you can put an identifier string (usually the name of the object) that is copied to name using the string constructor. The default = "" creates an empty string . The constructor increments the objectCount as before, and the destructor decrements it. Next is the copy-constructor, HowMany2(const HowMany2&) . The copy-constructor can create a new object only from an existing one, so the existing object’s name is copied to name , followed by the word “copy” so you can see where it came from. If you look closely, you’ll see that the call name(h.name) in the constructor initializer list is actually calling the string copy-constructor. Inside the copy-constructor, the object count is incremented just as it is inside the normal constructor. This means you’ll now get an accurate object count when passing and returning by value. The print( ) function has been modified to print out a message, the object identifier, and the object count. It must now access the name [...]... wocc("Composite()") {} void print(const string& msg = "") const { wocc.print(msg); 494 Thinking in C+ + www.BruceEckel.com } }; int main() { Composite c; c. print("Contents of c" ); cout . defined copy-constructor. However, in main( ) an object is created using the copy-constructor in the definition: Composite c2 = c; 496 Thinking in C+ + www.BruceEckel.com The copy-constructor. // Preventing copy-construction class NoCC { int i; NoCC(const NoCC&); // No definition public: NoCC(int ii = 0) : i(ii) {} }; void f(NoCC); int main() { NoCC n; //! f(n);. Error: copy-constructor called //! NoCC n2 = n; // Error: c- c called //! NoCC n3(n); // Error: c- c called } ///:~ Notice the use of the more general form NoCC(const NoCC&); using the const .

Ngày đăng: 05/07/2014, 19:20

TỪ KHÓA LIÊN QUAN

TÀI LIỆU CÙNG NGƯỜI DÙNG

TÀI LIỆU LIÊN QUAN