热度 13
2011-6-16 13:53
9996 次阅读|
0 个评论
In C++, a constructor is a special class member function that gives guaranteed initialization for objects of its class type. In my last few articles, I've been explaining what constructors are and what kind of code compilers generate on their behalf. 1, 2 Last month, I started to explain the behavior of constructors for classes with members that have constructors of their own. 3 This month, I'll pick up where I left off. Although C doesn't support constructors, C programs can provide functions that mimic constructors. Well-written C programs do. As in the past, I'll explain the behavior of C++ constructors by presenting C code that exhibits much the same behavior. This should help you see not only what C++ is doing behind the scenes, but also show how you can emulate constructors in C. Assignment vs. initialization For my example, I've been using a class for entries in a symbol table, where each entry stores a name and an associated ID and a value. The name is a character string, the ID is an unsigned integer value, and the value is a sequence of one or more signed integer values. The entry class definition looks in part like: class entry { entry(string const n, int v); ~~~ private: static unsigned counter; string name; unsigned id; sequence value; }; Here, string might be the Standard C++ string class, or something similar. The sequence class might actually be a typedef alias for a standard container class template specialization, such as: typedef vector sequence; or it might be a custom-built class. A class can have more than one constructor. At the moment, the entry class has just one, declared as: entry(string const n, int v); This constructor initializes an entry so that its name is n and its value is v . The constructor also uses static member counter to generate a unique ID for each entry . The constructor definition that I presented last time looked like: entry::entry(string const n, int v) { name = n; value.push_back(v); id = ++counter; } Strictly speaking, the first statement in the constructor body is not an initialization. It's an assignment that replaces name 's value, assuming the string has already been initialized. Calling push_back is not an initialization, either. It assumes that value already has an initial value, and appends one more value to whatever's already there. Remember, entry 's members name and value have class types. Those classes have constructors, which provide guaranteed initialization. C++ preserves the guarantee by inserting default constructor calls for entry 's members into the entry constructor itself. (A default constructor is a constructor that can be called with an empty argument list.) The compiler generates a call that applies the default string constructor to entry 's member name , and another call that applies the default sequence constructor to member value . A C function that performs the same work as the entry constructor might look like: void entry_construct_nv (entry *_this, string const *n, int v) { string_construct(_this-name); sequence_construct(_this-value); string_copy(_this-name, n); sequence_push_back(_this-value, v); _this-id = ++counter; } As the C code indicates, the entry constructor initializes name to be an empty string, only to replace the empty string with a copy of n . Similarly, it initializes value to be an empty sequence, only to append the value of v . The constructor code would be shorter and faster if the constructor simply initialized name with a copy of n and value as a sequence containing just v . C++ provides member initializers to eliminate such unnecessary default initialization. Member initializers Again, the entry class has two members of class type, name and value . C++ upholds the initialization guarantee by applying the default constructors to name and value as part of the entry constructor. If you'd like the entry constructor to apply different constructors to its members, it must use member initializers. A constructor definition may include a list of member initializers. Each member initializer specifies the initial value for some class member. For example, in: entry::entry(string const n, int v) : name (n), value (1, v) { id = ++counter; } the member initializer name (n) specifies that this entry constructor will initialize its member name using the string copy constructor with n as its argument. The member initializer value (1, v) specifies that the entry constructor will initialize value using a sequence constructor that accepts 1 and v as its arguments. This will initialize the sequence to contain one element whose value is v . Using member initializers often streamlines the work of a constructor. In this case, it eliminates statements from the constructor body. A C function that performs the same work as this entry constructor has fewer function calls than it did before: void entry_construct_nv (entry *_this, string const *n, int v) { string_construct_copy(_this-name, n); sequence_construct_cv(_this-value, 1, v); _this-id = ++counter; } Member initializers can appear only in constructors, not in any other functions. If present, the member initializer list must appear after the closing parenthesis of the constructor's parameter list and before the opening brace of the function body. If a constructor has any member initializers, a colon (":") must appear before the first one. Different programmers have different styles for formatting member initializer lists. I like to place the colon immediately after the parameter list and place the member initializers on the line that follows. Each member initializer has the general form: member-name ( expression-list ) where member-name is the name of a class member and expression-list is a list of zero or more expressions separated by commas. If the initializer names a member of class type, the expression list can be any sequence that's acceptable as the argument list to one of the constructors for that member. Whenever possible, C++ strives to treat objects of class and non-class types according to uniform rules. Thus, you can use member initializers to initialize members of non-class type. Typically when a member initializer has the form m (v) and m has a non-class type, the member initializer generates the same code as the assignment: m = v; appearing in the constructor body. For example, you can write the previous constructor definition as: entry::entry(string const n, int v): name (n), id (++counter) , value (1, v) { } and it generates the same code as before. It's not uncommon for a C++ constructor to have an empty function body and do all the work in the member initializers. When a member has a const-qualified type, you must use a member initializer to initialize that member. For example, if you declare the id member in the entry class to be const , as in: class entry { ~~~ unsigned const id; sequence value; }; then any statement that tries to modify id won't compile, even in a constructor: entry::entry(string const n, int v): name (n), value (1, v) { id = ++counter; // error if id is const } The only way to initialize such a const member is with a member initializer. The same is true for members of a reference type. Less to be surprised about One of the easiest ways to misuse an object in C is to fail to initialize it properly. In C++, you can use constructors to guarantee initialization, thus getting the compiler to do automatically what you might forget to do yourself. Unfortunately, fear about guaranteed initialization leads some programmers to shy away from C++, claiming that C++ does too much behind the scenes. Using member initializers offers more control over what constructors do, and helps eliminate unnecessary default initialization. Endnotes: 1. Saks, Dan, "Demystifying constructors," Eetasia.com , April 2011. http://forum.eetasia.com/BLOG_ARTICLE_7112.HTM 2. Saks, Dan, "Constructors and object definitions," Eetasia.com , April 2011. http://forum.eetasia.com/BLOG_ARTICLE_7322.HTM 3. Saks, Dan, " Thoughts on member initialization," Eetasia.com , May 2011. http://forum.eetasia.com/BLOG_ARTICLE_7817.HTM