Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 88 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
88
Dung lượng
291,45 KB
Nội dung
332 Thinking in C++ www.BruceEckel.com header file (perhaps one that is out of date), the name decoration provides a safety net that is often referred to as type-safe linkage . Consider the following scenario. In one file is the definition for a function: //: C07:Def.cpp {O} // Function definition void f(int) {} ///:~ In the second file, the function is misdeclared and then called: //: C07:Use.cpp //{L} Def // Function misdeclaration void f(char); int main() { //! f(1); // Causes a linker error } ///:~ Even though you can see that the function is actually f(int) , the compiler doesn’t know this because it was told – through an explicit declaration – that the function is f(char) . Thus, the compilation is successful. In C, the linker would also be successful, but not in C++. Because the compiler decorates the names, the definition becomes something like f_int , whereas the use of the function is f_char . When the linker tries to resolve the reference to f_char , it can only find f_int , and it gives you an error message. This is type-safe linkage. Although the problem doesn’t occur all that often, when it does it can be incredibly difficult to find, especially in a large project. This is one of the cases where you can easily find a difficult error in a C program simply by running it through the C++ compiler. 7: Function Overloading & Default Arguments 333 Overloading example We can now modify earlier examples to use function overloading. As stated before, an immediately useful place for overloading is in constructors. You can see this in the following version of the Stash class: //: C07:Stash3.h // Function overloading #ifndef STASH3_H #define STASH3_H class Stash { int size; // Size of each space int quantity; // Number of storage spaces int next; // Next empty space // Dynamically allocated array of bytes: unsigned char* storage; void inflate(int increase); public: Stash(int size); // Zero quantity Stash(int size, int initQuantity); ~Stash(); int add(void* element); void* fetch(int index); int count(); }; #endif // STASH3_H ///:~ The first Stash( ) constructor is the same as before, but the second one has a Quantity argument to indicate the initial number of storage places to be allocated. In the definition, you can see that the internal value of quantity is set to zero, along with the storage pointer. In the second constructor, the call to inflate(initQuantity) increases quantity to the allocated size: //: C07:Stash3.cpp {O} // Function overloading #include "Stash3.h" #include " /require.h" #include <iostream> #include <cassert> 334 Thinking in C++ www.BruceEckel.com using namespace std; const int increment = 100; Stash::Stash(int sz) { size = sz; quantity = 0; next = 0; storage = 0; } Stash::Stash(int sz, int initQuantity) { size = sz; quantity = 0; next = 0; storage = 0; inflate(initQuantity); } Stash::~Stash() { if(storage != 0) { cout << "freeing storage" << endl; delete []storage; } } int Stash::add(void* element) { if(next >= quantity) // Enough space left? inflate(increment); // Copy element into storage, // starting at next empty space: int startBytes = next * size; unsigned char* e = (unsigned char*)element; for(int i = 0; i < size; i++) storage[startBytes + i] = e[i]; next++; return(next - 1); // Index number } void* Stash::fetch(int index) { require(0 <= index, "Stash::fetch (-)index"); if(index >= next) return 0; // To indicate the end // Produce pointer to desired element: return &(storage[index * size]); } 7: Function Overloading & Default Arguments 335 int Stash::count() { return next; // Number of elements in CStash } void Stash::inflate(int increase) { assert(increase >= 0); if(increase == 0) return; int newQuantity = quantity + increase; int newBytes = newQuantity * size; int oldBytes = quantity * size; unsigned char* b = new unsigned char[newBytes]; for(int i = 0; i < oldBytes; i++) b[i] = storage[i]; // Copy old to new delete [](storage); // Release old storage storage = b; // Point to new memory quantity = newQuantity; // Adjust the size } ///:~ When you use the first constructor no memory is allocated for storage . The allocation happens the first time you try to add( ) an object and any time the current block of memory is exceeded inside add( ) . Both constructors are exercised in the test program: //: C07:Stash3Test.cpp //{L} Stash3 // Function overloading #include "Stash3.h" #include " /require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main() { Stash intStash(sizeof(int)); for(int i = 0; i < 100; i++) intStash.add(&i); for(int j = 0; j < intStash.count(); j++) cout << "intStash.fetch(" << j << ") = " << *(int*)intStash.fetch(j) << endl; 336 Thinking in C++ www.BruceEckel.com const int bufsize = 80; Stash stringStash(sizeof(char) * bufsize, 100); ifstream in("Stash3Test.cpp"); assure(in, "Stash3Test.cpp"); string line; while(getline(in, line)) stringStash.add((char*)line.c_str()); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout << "stringStash.fetch(" << k << ") = " << cp << endl; } ///:~ The constructor call for stringStash uses a second argument; presumably you know something special about the specific problem you’re solving that allows you to choose an initial size for the Stash . unions As you’ve seen, the only difference between struct and class in C++ is that struct defaults to public and class defaults to private . A struct can also have constructors and destructors, as you might expect. But it turns out that a union can also have a constructor, destructor, member functions, and even access control. You can again see the use and benefit of overloading in the following example: //: C07:UnionClass.cpp // Unions with constructors and member functions #include<iostream> using namespace std; union U { private: // Access control too! int i; float f; public: U(int a); U(float b); 7: Function Overloading & Default Arguments 337 ~U(); int read_int(); float read_float(); }; U::U(int a) { i = a; } U::U(float b) { f = b;} U::~U() { cout << "U::~U()\n"; } int U::read_int() { return i; } float U::read_float() { return f; } int main() { U X(12), Y(1.9F); cout << X.read_int() << endl; cout << Y.read_float() << endl; } ///:~ You might think from the code above that the only difference between a union and a class is the way the data is stored (that is, the int and float are overlaid on the same piece of storage). However, a union cannot be used as a base class during inheritance, which is quite limiting from an object-oriented design standpoint (you’ll learn about inheritance in Chapter 14). Although the member functions civilize access to the union somewhat, there is still no way to prevent the client programmer from selecting the wrong element type once the union is initialized. In the example above, you could say X.read_float( ) even though it is inappropriate. However, a “safe” union can be encapsulated in a class. In the following example, notice how the enum clarifies the code, and how overloading comes in handy with the constructors: //: C07:SuperVar.cpp // A super-variable #include <iostream> using namespace std; class SuperVar { 338 Thinking in C++ www.BruceEckel.com enum { character, integer, floating_point } vartype; // Define one union { // Anonymous union char c; int i; float f; }; public: SuperVar(char ch); SuperVar(int ii); SuperVar(float ff); void print(); }; SuperVar::SuperVar(char ch) { vartype = character; c = ch; } SuperVar::SuperVar(int ii) { vartype = integer; i = ii; } SuperVar::SuperVar(float ff) { vartype = floating_point; f = ff; } void SuperVar::print() { switch (vartype) { case character: cout << "character: " << c << endl; break; case integer: cout << "integer: " << i << endl; break; case floating_point: cout << "float: " << f << endl; break; } } 7: Function Overloading & Default Arguments 339 int main() { SuperVar A('c'), B(12), C(1.44F); A.print(); B.print(); C.print(); } ///:~ In the code above, the enum has no type name (it is an untagged enumeration). This is acceptable if you are going to immediately define instances of the enum , as is done here. There is no need to refer to the enum’s type name in the future, so the type name is optional. The union has no type name and no variable name. This is called an anonymous union , and creates space for the union but doesn’t require accessing the union elements with a variable name and the dot operator. For instance, if your anonymous union is: //: C07:AnonymousUnion.cpp int main() { union { int i; float f; }; // Access members without using qualifiers: i = 12; f = 1.22; } ///:~ Note that you access members of an anonymous union just as if they were ordinary variables. The only difference is that both variables occupy the same space. If the anonymous union is at file scope (outside all functions and classes) then it must be declared static so it has internal linkage. Although SuperVar is now safe, its usefulness is a bit dubious because the reason for using a union in the first place is to save space, and the addition of vartype takes up quite a bit of space relative to the data in the union , so the savings are effectively 340 Thinking in C++ www.BruceEckel.com eliminated. There are a couple of alternatives to make this scheme workable. If the vartype controlled more than one union instance – if they were all the same type – then you’d only need one for the group and it wouldn’t take up more space. A more useful approach is to have #ifdef s around all the vartype code, which can then guarantee things are being used correctly during development and testing. For shipping code, the extra space and time overhead can be eliminated. Default arguments In Stash3.h , examine the two constructors for Stash( ) . They don’t seem all that different, do they? In fact, the first constructor seems to be a special case of the second one with the initial size set to zero. It’s a bit of a waste of effort to create and maintain two different versions of a similar function. C++ provides a remedy with default arguments . A default argument is a value given in the declaration that the compiler automatically inserts if you don’t provide a value in the function call. In the Stash example, we can replace the two functions: Stash(int size); // Zero quantity Stash(int size, int initQuantity); with the single function: Stash(int size, int initQuantity = 0); The Stash(int) definition is simply removed – all that is necessary is the single Stash(int, int) definition. Now, the two object definitions Stash A(100), B(100, 0); will produce exactly the same results. The identical constructor is called in both cases, but for A , the second argument is 7: Function Overloading & Default Arguments 341 automatically substituted by the compiler when it sees the first argument is an int and that there is no second argument. The compiler has seen the default argument, so it knows it can still make the function call if it substitutes this second argument, which is what you’ve told it to do by making it a default. Default arguments are a convenience, as function overloading is a convenience. Both features allow you to use a single function name in different situations. The difference is that with default arguments the compiler is substituting arguments when you don’t want to put them in yourself. The preceding example is a good place to use default arguments instead of function overloading; otherwise you end up with two or more functions that have similar signatures and similar behaviors. If the functions have very different behaviors, it doesn’t usually make sense to use default arguments (for that matter, you might want to question whether two functions with very different behaviors should have the same name). There are two rules you must be aware of when using default arguments. First, only trailing arguments may be defaulted. That is, you can’t have a default argument followed by a non-default argument. Second, once you start using default arguments in a particular function call, all the subsequent arguments in that function’s argument list must be defaulted (this follows from the first rule). Default arguments are only placed in the declaration of a function (typically placed in a header file). The compiler must see the default value before it can use it. Sometimes people will place the commented values of the default arguments in the function definition, for documentation purposes void fn(int x /* = 0 */) { // [...]... specification was still being finished Although the C committee then decided to include const in C, somehow it came to mean for them “an ordinary variable that cannot be changed.” In C, a const always occupies storage and its name is global The C compiler cannot treat a const as a compile-time constant In C, if you say 358 Thinking in C+ + www.BruceEckel.com const int bufsize = 10 0; char buf[bufsize]; you... consistent because the const is always placed to the right of what it modifies You’ll have to decide which is clearer for your particular coding style Here are the above lines in a compileable file: //: C0 8:ConstPointers.cpp const int* u; int const* v; int d = 1; int* const w = &d; const int* const x = &d; // (1) int const* const x2 = &d; // (2) int main() {} ///:~ 362 Thinking in C+ + www.BruceEckel.com Formatting... creating a simple “string” class: //: C0 7:MemTest.cpp // Testing the Mem class //{L} Mem #include "Mem.h" #include #include using namespace std; class MyString { Mem* buf; public: MyString(); MyString(char* str); ~MyString(); void concat(char* str); void print(ostream& os); }; MyString::MyString() { buf = 0; } MyString::MyString(char* str) { buf = new Mem(strlen(str) + 1) ; strcpy((char*)buf->pointer(),... MyString(char* str = ""); 346 Thinking in C+ + www.BruceEckel.com everything will work correctly, but you’ll lose the previous efficiency benefit since a Mem object will always be created To get the efficiency back, you must modify the constructor: MyString::MyString(char* str) { if(!*str) { // Pointing at an empty string buf = 0; return; } buf = new Mem(strlen(str) + 1) ; strcpy((char*)buf->pointer(),... thing to do Because bufsize occupies storage somewhere, the C compiler cannot know the value at compile time You can optionally say const int bufsize; in C, but not in C+ +, and the C compiler accepts it as a declaration indicating there is storage allocated elsewhere Because C defaults to external linkage for consts, this makes sense C+ + defaults to internal linkage for consts so if you want to accomplish... stuff"); s.print(cout); MyString s2; s2.concat("Using default constructor"); s2.print(cout); } ///:~ All you can do with this class is to create a MyString concatenate , text, and print to an ostream The class only contains a pointer to a Mem, but note the distinction between the default constructor, which sets the pointer to zero, and the second constructor, which creates a Mem and copies data into it... to make the block bigger (which is also the case when the block is of size zero after default construction), the new “extra” portion is set to zero using the Standard C library function memset( ) which was , introduced in Chapter 5 The subsequent function call is to the 344 Thinking in C+ + www.BruceEckel.com Standard C library function memcpy( ) which in this case copies , the existing bytes from mem... compiler will give you an error message if you accidentally try to change it Here’s an example: //: C0 8:Safecons.cpp // Using const for safety #include using namespace std; const int i = 10 0; // Typical constant const int j = i + 10 ; // Value from const expr long address = (long)&j; // Forces storage char buf[j + 10 ]; // Still a const expression int main() { cout . "Stash3.h" #include " /require.h" #include <iostream> #include <cassert> 334 Thinking in C+ + www.BruceEckel.com using namespace std; const int increment = 10 0; Stash::Stash(int. super-variable #include <iostream> using namespace std; class SuperVar { 338 Thinking in C+ + www.BruceEckel.com enum { character, integer, floating_point } vartype; // Define. Here’s the implementation of the class: //: C0 7:Mem.cpp {O} 344 Thinking in C+ + www.BruceEckel.com #include "Mem.h" #include <cstring> using namespace std; Mem::Mem() { mem