Section 12.4. Constructors


12.4. Constructors

Constructors (Section 2.3.3, p. 49) are special member functions that are executed whenever we create new objects of a class type. The job of a constructor is to ensure that the data members of each object start out with sensible initial values. Section 7.7.3 (p. 262) showed how we define a constructor:

      class Sales_item {      public:          // operations on Sales_itemobjects          // default constructor needed to initialize members of built-in type          Sales_item(): units_sold(0), revenue(0.0) { }      private:          std::string isbn;          unsigned units_sold;          double revenue;      }; 

This constructor uses the constructor initializer list to initialize the units_sold and revenue members. The isbn member is implicitly initialized by the string default constructor as an empty string.

Constructors have the same name as the name of the class and may not specify a return type. Like any other function, they may define zero or more parameters.

Constructors May Be Overloaded

There is no constraint on the number of constructors we may declare for a class, provided that the parameter list of each constructor is unique. How can we know which or how many constructors to define? Ordinarily, constructors differ in ways that allow the user to specify differing ways to initialize the data members.

For example, we might logically extend our Sales_item class by providing two additional constructors: one that would let users provide an initial value for the isbn and another that would let them initialize the object by reading an istream object:

      class Sales_item;      // other members as before      public:          // added constructors to initialize from a string or an istream          Sales_item(const std::string&);          Sales_item(std::istream&);          Sales_item();      }; 

Arguments Determine Which Constructor to Use

Our class now defines three constructors. We could use any of these constructors when defining new objects:

      // uses the default constructor:      // isbn is the empty string; units_soldand revenue are 0      Sales_item empty;      // specifies an explicit isbn; units_soldand revenue are 0      Sales_item Primer_3rd_Ed("0-201-82470-1");      // reads values from the standard input into isbn, units_sold, and revenue      Sales_item Primer_4th_ed(cin); 

The argument type(s) used to initialize an object determines which constructor is used. In the definition of empty, there is no initializer, so the default constructor is run. The constructor that takes a single string argument is used to initialize Primer_3rd_ed; the one that takes a reference to an istream initializes Primer_4th_ed.

Constructors Are Executed Automatically

The compiler runs a constructor whenever an object of the type is created:

      // constructor that takes a string used to create and initialize variable      Sales_item Primer_2nd_ed("0-201-54848-8");      // default constructor used to initialize unnamed object on the heap      Sales_item *p = new Sales_item(); 

In the first case, the constructor that takes a string is run to initialize the variable named Primer_2nd_ed. In the second case, a new Sales_item object is allocated dynamically. Assuming that the allocation succeeds, then the object is initialized by running the default constructor.

Constructors for const Objects

A constructor may not be declared as const (Section 7.7.1, p. 260):

      class Sales_item {      public:          Sales_item() const;    // error      }; 

There is no need for a const constructor. When we create a const object of a class type, an ordinary constructor is run to initialize the const object. The job of the constructor is to initialize an object. A constructor is used to initialize an object regardless of whether the object is const.

Exercises Section 12.4

Exercise 12.19:

Provide one or more constructors that allows the user of this class to specify initial values for none or all of the data elements of this class:

      class NoName {      public:          // constructor(s) go here ...      private:          std::string *pstring;          int         ival;          double      dval;      }; 

Explain how you decided how many constructors were needed and what parameters they should take.

Exercise 12.20:

Choose one of the following abstractions (or an abstraction of your own choosing). Determine what data is needed in the class. Provide an appropriate set of constructors. Explain your decisions.

      (a) Book        (b) Date      (c) Employee      (d) Vehicle    (e) Object    (f) Tree 


12.4.1. The Constructor Initializer

Like any other function, a constructor has a name, a parameter list, and a function body. Unlike other functions, a constructor may also contain a constructor initializer list:

      // recommended way to write constructors using a constructor initializer      Sales_item::Sales_item(const string &book):           isbn(book), units_sold(0), revenue(0.0) { } 

The constructor initializer starts with a colon, which is followed by a comma-separated list of data members each of which is followed by an initializer inside parentheses. This constructor initializes the isbn member to the value of its book parameter and initializes units_sold and revenue to 0. As with any member function, constructors can be defined inside or outside of the class. The constructor initializer is specified only on the constructor definition, not its declaration.

The constructor initializer is a feature that many reasonably experienced C++ programmers have not mastered.



One reason constructor initializers are hard to understand is that it is usually legal to omit the initializer list and assign values to the data members inside the constructor body. For example, we could write the Sales_item constructor that takes a string as

      // legal but sloppier way to write the constructor:      // no constructor initializer      Sales_item::Sales_item(const string &book)      {          isbn = book;          units_sold = 0;          revenue = 0.0;      } 

This constructor assigns, but does not explicitly initialize, the members of class Sales_item. Regardless of the lack of an explicit initializer, the isbn member is initialized before the constructor is executed. This constructor implicitly uses the default string constructor to initialize isbn. When the body of the constructor is executed, the isbn member already has a value. That value is overwritten by the assignment inside the constructor body.

Conceptually, we can think of a constructor as executing in two phases: (1) the initialization phase and (2) a general computation phase. The computation phase consists of all the statements within the body of the constructor.

Data members of class type are always initialized in the initialization phase, regardless of whether the member is initialized explicitly in the constructor initializer list. Initialization happens before the computation phase begins.



Each member that is not explicitly mentioned in the constructor initializer is initialized using the same rules as those used to initialize variables (Section 2.3.4, p. 50). Data members of class type are initialized by running the type's default constructor. The initial value of members of built-in or compound type depend on the scope of the object: At local scope those members are uninitialized, at global scope they are initialized to 0.

The two versions of the Sales_item constructor that we wrote in this section have the same effect: Whether we initialized the members in the constructor initializer list or assigned to them inside the constructor body, the end result is the same. After the constructor completes, the three data members hold the same values. The difference is that the version that uses the constructor initializer initializes its data members. The version that does not define a constructor initializer assigns values to the data members in the body of the constructor. How significant this distinction is depends on the type of the data member.

Constructor Initializers Are Sometimes Required

If an initializer is not provided for a class member, then the compiler implicitly uses the default constructor for the member's type. If that class does not have a default constructor, then the attempt by the compiler to use it will fail. In such cases, an initializer must be provided in order to initialize the data member.

Some members must be initialized in the constructor initializer. For such members, assigning to them in the constructor body doesn't work. Members of a class type that do not have a default constructor and members that are const or reference types must be initialized in the constructor initializer regardless of type.



Because members of built-in type are not implicitly initialized, it may seem that it doesn't matter whether these members are initialized or assigned. With two exceptions, using an initializer is equivalent to assigning to a nonclass data member both in result and in performance.

For example, the following constructor is in error:

      class ConstRef {      public:          ConstRef(int ii);      private:          int i;          const int ci;          int &ri;      };      // no explicit constructor initializer: error ri is uninitialized      ConstRef::ConstRef(int ii)      {              // assignments:           i = ii;   // ok           ci = ii;  // error: cannot assign to a const           ri = i;   // assigns to ri which was not bound to an object      } 

Remember that we can initialize but not assign to const objects or objects of reference type. By the time the body of the constructor begins executing, initialization is complete. Our only chance to initialize const or reference data members is in the constructor initializer. The correct way to write the constructor is

      // ok: explicitly initialize reference and const members      ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { } 

Advice: Use Constructor Initializers

In many classes, the distinction between initialization and assignment is strictly a matter of low-level efficiency: A data member is initialized and assigned when it could have been initialized directly. More important than the efficiency issue is the fact that some data members must be initialized.

We must use an initializer for any const or reference member or for any member of a class type that does not have a default constructor.



By routinely using constructor initializers, we can avoid being surprised by compile-time errors when we have a class with a member that requires a constructor initializer.


Order of Member Initialization

Not surprisingly, each member may be named only once in the constructor initializer. After all, what might it mean to give a member two initial values? What may be more surprising is that the constructor initializer list specifies only the values used to initialize the members, not the order in which those initializations are performed. The order in which members are initialized is the order in which the members are defined. The first member is initialized first, then the next, and so on.

The order of initialization often doesn't matter. However, if one member is initialized in terms of another, then the order in which members are initialized is crucially important.



Consider the following class:

      class X {          int i;          int j;      public:          // run-time error: i is initialized before j          X(int val): j(val), i(j) { }      }; 

In this case, the constructor initializer is written to make it appear as if j is initialized with val and then j is used to initialize i. However, i is initialized first. The effect of this initializer is to initialize i with the as yet uninitialized value of j!

Some compilers are kind enough to generate a warning if the data members are listed in the constructor initializer in a different order from the order in which the members are declared.

It is a good idea to write constructor initializers in the same order as the members are declared. Moreover, when possible, avoid using members to initialize other members.



It is often the case that we can avoid any problems due to order of execution for initializers by (re)using the constructor's parameters rather than using the object's data members. For example, it would be better to write the constructor for X as

      X(int val): i(val), j(val) { } 

In this version, the order in which i and j are initialized doesn't matter.

Initializers May Be Any Expression

An initializer may be an arbitrarily complex expression. As an example, we could give our Sales_item class a new constructor that takes a string representing the isbn, an unsigned representing the number of books sold, and a double representing the price at which each of these books was sold:

      Sales_item(const std::string &book, int cnt, double price):          isbn(book), units_sold(cnt), revenue(cnt * price) { } 

This initializer for revenue uses the parameters representing price and number sold to calculate the object's revenue member.

Initializers for Data Members of Class Type

When we initialize a member of class type, we are specifying arguments to be passed to one of the constructors of that member's type. We can use any of that type's constructors. For example, our Sales_item class could initialize isbn using any of the string constructors (Section 9.6.1, p. 338). Instead of using the empty string, we might decide that the default value for isbn should be a value that represents an impossibly high value for an ISBN. We could initialize isbn to a string of ten 9s:

      // alternative definition for Sales_item default constructor      Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {} 

This initializer uses the string constructor that takes a count and a character and generates a string holding that character repeated that number of times.

Exercises Section 12.4.1

Exercise 12.21:

Write the default constructor using a constructor initializer for class that contains the following members: a const string, an int, a double*, and an ifstream&. Initialize the string to hold the name of the class.

Exercise 12.22:

The following initializer is in error. Identify and fix the problem.

      struct X {          X (int i, int j): base(i), rem(base % j) { }          int rem, base;      }; 

Exercise 12.23:

Assume we have a class named NoDefault that has a constructor that takes an int but no default constructor. Define a class C that has a member of type NoDefault. Define the default constructor for C.


12.4.2. Default Arguments and Constructors

Let's look again at our definitions for the default constructor and the constructor that takes a string:

      Sales_item(const std::string &book):                isbn(book), units_sold(0), revenue(0.0) { }      Sales_item(): units_sold(0), revenue(0.0) { } 

These constructors are almost the same: The only difference is that the constructor that takes a string parameter uses the parameter to initialize isbn. The default constructor (implicitly) uses the string default constructor to initialize isbn.

We can combine these constructors by supplying a default argument for the string initializer:

      class Sales_item {      public:          // default argument for book is the empty string          Sales_item(const std::string &book = ""):                    isbn(book), units_sold(0), revenue(0.0) { }          Sales_item(std::istream &is);          // as before      }; 

Here we define only two constructors, one of which provides a default argument for its parameter. The constructor that takes a default argument for its single string parameter will be run for either of these definitions:

      Sales_item empty;      Sales_item Primer_3rd_Ed("0-201-82470-1"); 

In the case of empty, the default argument is used, whereas Primer_3rd_ed supplies an explicit argument.

Each version of our class provides the same interface: They both initialize a Sales_item to the same values given a string or given no initializer.

We prefer to use a default argument because it reduces code duplication.



12.4.3. The Default Constructor

The default constructor is used whenever we define an object but do not supply an initializer. A constructor that supplies default arguments for all its parameters also defines the default constructor.

The Synthesized Default Constructor

If a class defines even one constructor, then the compiler will not generate the default constructor. The basis for this rule is that if a class requires control to initialize an object in one case, then the class is likely to require control in all cases.

Exercises Section 12.4.2

Exercise 12.24:

Using the version of Sales_item from page 458 that defined two constructors, one of which has a default argument for its single string parameter, determine which constructor is used to initialize each of the following variables and list the values of the data members in each object:

      Sales_item first_item(cin);      int main() {          Sales_item next;          Sales_item last("9-999-99999-9");      } 

Exercise 12.25:

Logically, we might want to supply cin as a default argument to the constructor that takes an istream&. Write the constructor declaration that uses cin as a default argument.

Exercise 12.26:

Would it be legal for both the constructor that takes a string and the one that takes an istream& to have default arguments? If not, why not?


The compiler generates a default constructor automatically only if a class defines no constructors.



The synthesized default constructor initializes members using the same rules as those that apply for how variables are initialized. Members that are of class type are initialized by running each member's own default constructor. Members of built-in or compound type, such as pointers and arrays, are initialized only for objects that are defined at global scope. When objects are defined at local scope, then members of built-in or compound type are uninitialized.

If a class contains data members of built-in or compound type, then the class should not rely on the synthesized default constructor. It should define its own constructor to initialize these members.



Moreover, every constructor should provide initializers for members of built-in or compound type. A constructor that does not initialize a member of built-in or compound type leaves that member in an undefined state. Using an undefined member in any way other than as the target of an assignment is an error. If every constructor sets every member to an explicit, known state, then member functions can distinguish between an empty object and one that has actual values.

Classes Should Usually Define a Default Constructor

In certain cases, the default constructor is applied implicitly by the compiler. If the class has no default constructor, then the class may not be used in these contexts. To illustrate the cases where a default constructor is required, assume we have a class named NoDefault that does not define its own default constructor but does have a constructor that takes a string argument. Because the class defines a constructor, the compiler will not synthesize the default constructor. The fact that NoDefault has no default constructor means:

  1. Every constructor for every class that has a NoDefault member must explicitly initialize the NoDefault member by passing an initial string value to the NoDefault constructor.

  2. The compiler will not synthesize the default constructor for classes that have members of type NoDefault. If such classes want to provide a default, they must define one explicitly, and that constructor must explicitly initialize their NoDefault member.

  3. The NoDefault type may not be used as the element type for a dynamically allocated array.

  4. Statically allocated arrays of type NoDefault must provide an explicit initializer for each element.

  5. If we have a container such as vector that holds NoDefault objects, we cannot use the constructor that takes a size without also supplying an element initializer.

In practice, it is almost always right to provide a default constructor if other constructors are being defined. Ordinarily the initial values given to the members in the default constructor should indicate that the object is "empty."



Using the Default Constructor

A common mistake among programmers new to C++ is to declare an object initialized with the default constructor as follows:


      // oops! declares a function, not an object      Sales_item myobj(); 


The declaration of myobj compiles without complaint. However, when we try to use myobj

      Sales_item myobj();   // ok: but defines a function, not an object      if (myobj.same_isbn(Primer_3rd_ed))   // error: myobj is a function 

the compiler complains that we cannot apply member access notation to a function! The problem is that our definition of myobj is interpreted by the compiler as a declaration of a function taking no parameters and returning an object of type Sales_itemhardly what we intended! The correct way to define an object using the default constructor is to leave off the trailing, empty parentheses:

      // ok: defines a class object ...      Sales_item myobj; 

On the other hand, this code is fine:

      // ok: create an unnamed, empty Sales_itemand use to initialize myobj      Sales_item myobj = Sales_item(); 

Here we create and value-initialize a Sales_item object and to use it to initialize myobj. The compiler value-initializes a Sales_item by running its default constructor.

Exercises Section 12.4.3

Exercise 12.27:

Which, if any, of the following statements are untrue? Why?

  1. A class must provide at least one constructor.

  2. A default constructor is a constructor with no parameters for its parameter list.

  3. If there are no meaningful default values for a class, the class should not provide a default constructor.

  4. If a class does not define a default constructor, the compiler generates one automatically, initializing each data member to the default value of its associated type.


12.4.4. Implicit Class-Type Conversions

As we saw in Section 5.12 (p. 178), the language defines several automatic conversions among the built-in types. We can also define how to implicitly convert an object from another type to our class type or to convert from our class type to another type. We'll see in Section 14.9 (p. 535) how to define conversions from a class type to another type. To define an implicit conversion to a class type, we need to define an appropriate constructor.

A constructor that can be called with a single argument defines an implicit conversion from the parameter type to the class type.



Let's look again at the version of Sales_item that defined two constructors:

      class Sales_item {      public:          // default argument for book is the empty string          Sales_item(const std::string &book = ""):                    isbn(book), units_sold(0), revenue(0.0) { }          Sales_item(std::istream &is);          // as before       }; 

Each of these constructors defines an implicit conversion. Accordingly, we can use a string or an istream where an object of type Sales_item is expected:

      string null_book = "9-999-99999-9";      // ok: builds a Sales_itemwith 0 units_soldand revenue from      // and isbn equal to null_book      item.same_isbn(null_book); 

This program uses an object of type string as the argument to the Sales_item same_isbn function. That function expects a Sales_item object as its argument. The compiler uses the Sales_item constructor that takes a string to generate a new Sales_item object from null_book. That newly generated (temporary) Sales_item is passed to same_isbn.

Whether this behavior is desired depends on how we think our users will use the conversion. In this case, it might be a good idea. The string in book probably represents a nonexistent ISBN, and the call to same_isbn can detect whether the Sales_item in item represents a null Sales_item. On the other hand, our user might have mistakenly called same_isbn on null_book.

More problematic is the conversion from istream to Sales_item:

      // ok: uses the Sales_item istream constructor to build an object       // to pass to same_isbn      item.same_isbn(cin); 

This code implicitly converts cin to a Sales_item. This conversion executes the Sales_item constructor that takes an istream. That constructor creates a (temporary) Sales_item object by reading the standard input. That object is then passed to same_isbn.

This Sales_item object is a temporary (Section 7.3.2, p. 247). We have no access to it once same_isbn finishes. Effectively, we have constructed an object that is discarded after the test is complete. This behavior is almost surely a mistake.

Supressing Implicit Conversions Defined by Constructors

We can prevent the use of a constructor in a context that requries an implicit conversion by declaring the constructor explicit:

      class Sales_item {      public:          // default argument for book is the empty string          explicit Sales_item(const std::string &book = ""):                    isbn(book), units_sold(0), revenue(0.0) { }          explicit Sales_item(std::istream &is);          // as before      }; 

The explicit keyword is used only on the constructor declaration inside the class. It is not repeated on a definition made outside the class body:

      // error: explicit allowed only on constructor declaration in class header      explicit Sales_item::Sales_item(istream& is)      {          is >> *this; // uses Sales_iteminput operator to read the members      } 

Now, neither constructor can be used to implicitly create a Sales_item object. Neither of our previous uses will compile:

      item.same_isbn(null_book); // error: string constructor is explicit      item.same_isbn(cin);       // error: istream constructor is explicit 

When a constructor is declared explicit, the compiler will not use it as a conversion operator.



Explicitly Using Constructors for Conversions

An explicit constructor can be used to generate a conversion as long as we do so explicitly:

      string null_book = "9-999-99999-9";      // ok: builds a Sales_itemwith 0 units_soldand revenue from      // and isbn equal to null_book      item.same_isbn(Sales_item(null_book)); 

In this code, we create a Sales_item from null_book. Even though the constructor is explicit, this usage is allowed. Making a constructor explicit turns off only the use of the constructor implicitly. Any constructor can be used to explicitly create a temporary object.

Ordinarily, single-parameter constructors should be explicit unless there is an obvious reason to want to define an implicit conversion. Making constructors explicit may avoid mistakes, and a user can explicitly construct an object when a conversion is useful.



Exercises Section 12.4.4

Exercise 12.28:

Explain whether the Sales_item constructor that takes a string should be explicit. What would be the benefits of making the constructor explicit? What would be the drawbacks?

Exercise 12.29:

Explain what operations happen during the following definitions:

      string null_isbn = "9-999-99999-9";      Sales_item null1(null_isbn);      Sales_item null("9-999-99999-9"); 

Exercise 12.30:

Compile the following code:

      f(const vector<int>&);      int main() {          vector<int> v2;          f(v2);  // should be ok          f(42);  // should be an error          return 0;      } 

What can we infer about the vector constructors based on the error on the second call to f? If the call succeeded, then what would you conclude?


12.4.5. Explicit Initialization of Class Members

Although most objects are initialized by running an appropriate constructor, it is possible to initialize the data members of simple nonabstract classes directly. Members of classes that define no constructors and all of whose data members are public may be initialized in the same way that we initialize array elements:

      struct Data {          int ival;          char *ptr;      };      // val1.ival = 0; val1.ptr = 0      Data val1 = { 0, 0 };      // val2.ival = 1024;      // val2.ptr = "Anna Livia Plurabelle"      Data val2 = { 1024, "Anna Livia Plurabelle" }; 

The initializers are used in the declaration order of the data members. The following, for example, is an error because ival is declared before ptr:

      // error: can't use "Anna Livia Plurabelle" to initialize the int ival      Data val2 = { "Anna Livia Plurabelle" , 1024 }; 

This form of initialization is inherited from C and is supported for compatibility with C programs. There are three significant drawbacks to explicitly initializing the members of an object of class type:

  1. It requires that all the data members of the class be public.

  2. It puts the burden on the programmer to initialize every member of every object. Such initialization is tedious and error-prone because it is easy to forget an initializer or to supply an inappropriate initializer.

  3. If a member is added or removed, all initializations have to be found and updated correctly.

It is almost always better to define and use constructors. When we provide a default constructor for the types we define, we allow the compiler to automatically run that constructor, ensuring that every class object is properly initialized prior to the first use of that object.



Exercises Section 12.4.5

Exercise 12.31:

The data members of pair are public, yet this code doesn't compile. Why?

      pair<int, int> p2 = {0, 42}; // doesn't compile, why? 




C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net