Team LiB
Previous Section Next Section

12.4. Constructors

12.4. 构造函数

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:

构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。第 7.7.3 节展示了如何定义构造函数:

     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.

这个构造函数使用构造函数初始化列表来初始化 units_soldrevenue 成员。isbn 成员由 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:

例如,逻辑上可以通过提供两个额外的构造函数来扩展 Sales_item 类:一个允许用户提供 isbn 的初始值,另一个允许用户通过读取 istream 对象来初始化对象:

     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.

用于初始化一个对象的实参类型决定使用哪个构造函数。在 empty 的定义中,没有初始化式,所以运行默认构造函数。接受一个 string 实参的构造函数用于初始化 Primer_3rd_ed;接受一个 istream 引用的构造函数初始化 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.

第一种情况下,运行接受一个 string 实参的构造函数,来初始化变量 Primer_2nd_ed。第二种情况下,动态分配一个新的 Sales_item 对象。假定分配成功,则通过运行默认构造函数初始化该对象。

Constructors for const Objects

用于 const 对象的构造函数

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

构造函数不能声明为 const 第 7.7.1 节

     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.

const 构造函数是不必要的。创建类类型的 const 对象时,运行一个普通构造函数来初始化该 const 对象。构造函数的工作是初始化对象。不管对象是否为 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

12.4.1. 构造函数初始化式

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.

构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。这个构造函数将 isbn 成员初始化为 book 形参的值,将 units_soldrevenue 初始化为 0。与任意的成员函数一样,构造函数可以定义在类的内部或外部。构造函数初始化只在构造函数的定义中而不是声明中指定。

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

构造函数初始化列表是许多相当有经验的 C++ 程序员都没有掌握的一个特性。



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

构造函数初始化列表难以理解的一个原因在于,省略初始化列表在构造函数的函数体内对数据成员赋值是合法的。例如,可以将接受一个 stringSales_item 构造函数编写为:

     // 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.

这个构造函数给类 Sales_item 的成员赋值,但没有进行显式初始化。不管是否有显式的初始化式,在执行构造函数之前,要初始化 isbn 成员。这个构造函数隐式使用默认的 string 构造函数来初始化 isbn。执行构造函数的函数体时,isbn 成员已经有值了。该值被构造函数函数体中的赋值所覆盖。

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.

从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。

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.

在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 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.

在本节中编写的两个 Sales_item 构造函数版本具有同样的效果:无论是在构造函数初始化列表中初始化成员,还是在构造函数函数体中对它们赋值,最终结果是相同的。构造函数执行结束后,三个数据成员保存同样的值。不同之外在于,使用构造函数初始化列表的版本初始化数据成员,没有定义初始化列表的构造函数版本在构造函数函数体中对数据成员赋值。这个区别的重要性取决于数据成员的类型。

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.

有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。



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

记住,可以初始化 const 对象或引用类型的对象,但不能对它们赋值。在开始执行构造函数的函数体之前,要完成初始化。初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中。编写该构造函数的正确方式为

     // 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.

必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。



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!

在这种情况下,构造函数初始化列表看起来似乎是用val 初始化 j,然后再用 j 来初始化 i。然而,i 首先被初始化。这个初始化列表的效果是用尚未初始化的 j 值来初始化 i

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 编写构造函数可能更好:

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

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

在这个版本中,ij 初始化的次序就是无关紧要的。

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 类一个新的构造函数,该构造函数接受一个 string 表示 isbn,一个 usigned 表示售出书的数目,一个 double 表示每本书的售出价格:

     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.

revenue 的初始化式使用表示价格和售出数目的形参来计算对象的 revenue 成员。

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:

初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。例如,Sales_item 类可以使用任意一个 string 构造函数来初始化 isbn第 9.6.1 节)。也可以用 ISBN 取值的极限值来表示 isbn 的默认值,而不是用空字符串。可以将 isbn 初始化为由 10 个 9 构成的串:

     // 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.

这个初始化式使用 string 构造函数,接受一个计数值和一个字符,并生成一个 string,来保存重复指定次数的字符。

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.

使用构造函数初始化列表编写类的默认构造函数,该类包含如下成员:一个 const string,一个 int,一个 double* 和一个 ifstream&。初始化 string 来保存类的名字。

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.

假定有个命名为 NoDefault 的类,该类有一个接受一个 int 的构造函数,但没有默认构造函数。定义有一个 NoDefault 类型成员的类 C。为类 C 定义默认构造函数。


12.4.2. Default Arguments and Constructors

12.4.2. 默认实参与构造函数

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

再来看看默认构造函数和接受一个 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.

这两个构造函数几乎是相同的:唯一的区别在于,接受一个 string 形参的构造函数使用该形参来初始化 isbn。默认构造函数(隐式地)使用 string 的默认构造函数来初始化 isbn

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

可以通过为 string 初始化式提供一个默认实参将这些构造函数组合起来:

     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:

在这里,我们只定义了两个构造函数,其中一个为其形参提供一个默认实参。对于下面的任一定义,将执行为其 string 形参接受默认实参的那个构造函数:

     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.

empty 的情况下,使用默认实参,而 Primer_3rd_ed 提供了一个显式实参。

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.

类的两个版本提供同一接口:给定一个 string 或不给定初始化式,它们都将一个 Sales_item 初始化为相同的值。

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

我们更喜欢使用默认实参,因为它减少代码重复。



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 定义了两个构造函数,其中之一有一个默认实参对应其单个 string 形参。使用该 Sales_item 版本,确定用哪个构造函数来初始化下述的每个变量,并列出每个对象中数据成员的值:

     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.

逻辑上讲,我们可能希望将 cin 作为默认实参提供给接受一个 istream& 形参的构造函数。编写使用 cin 作为默认实参的构造函数声明。

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?

对于分别接受一个 string 和接受一个 istream& 的构造函数,具有默认实参都是合法的吗?如果不是,为什么?


12.4.3. The Default Constructor

12.4.3. 默认构造函数

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.

一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。这条规则的根据是,如果一个类在某种情况下需要控制对象初始化,则该类很可能在所有情况下都需要控制。

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.

合成的默认构造函数(synthesized default constructor)使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和复合类型的成员,如指针和数组,只对定义在全局作用域中的对象才初始化。当对象定义在局部作用域中时,内置或复合类型的成员不进行初始化。

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:

在某些情况下,默认构造函数是由编译器隐式应用的。如果类没有默认构造函数,则该类就不能用在这些环境中。为了例示需要默认构造函数的情况,假定有一个 NoDefault 类,它没有定义自己的默认构造函数,却有一个接受一个 string 实参的构造函数。因为该类定义了一个构造函数,因此编译器将不合成默认构造函数。NoDefault 没有默认构造函数,意味着:

  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.

    具有 NoDefault 成员的每个类的每个构造函数,必须通过传递一个初始的 string 值给 NoDefault 构造函数来显式地初始化 NoDefault 成员。

  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.

    编译器将不会为具有 NoDefault 类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其 NoDefault 成员。

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

    NoDefault 类型不能用作动态分配数组的元素类型。

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

    NoDefault 类型的静态分配数组必须为每个元素提供一个显式的初始化式。

  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.

    如果有一个保存 NoDefault 对象的容器,例如 vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。

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:

初级 C++ 程序员常犯的一个错误是,采用以下方式声明一个用默认构造函数初始化的对象:


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


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

编译 myobj 的声明没有问题。然而,当我们试图使用 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:

编译器会指出不能将成员访问符号用于一个函数!问题在于 myobj 的定义被编译器解释为一个函数的声明,该函数不接受参数并返回一个 Sales_item 类型的对象——与我们的意图大相径庭!使用默认构造函数定义一个对象的正确方式是去掉最后的空括号:

     // 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.

在这里,我们创建并初始化一个 Sales_item 对象,然后用它来按值初始化 myobj。编译器通过运行 Sales_item 的默认构造函数来按值初始化一个 Sales_item

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

12.4.4. 隐式类类型转换

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.

第 5.12 节介绍过,C++ 语言定义了内置类型之间的几个自动转换。也可以定义如何将其他类型的对象隐式转换为我们的类类型,或将我们的类类型的对象隐式转换为其他类型。在第 14.9 节将会看到如何定义从类类型到其他类型的转换。为了定义到类类型的隐式转换,需要定义合适的构造函数。

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:

让我们再看看定义了两个构造函数的 Sales_item 版本:

     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:

这里的每个构造函数都定义了一个隐式转换。因此,在期待一个 Sales_item 类型对象的地方,可以使用一个 string 或一个 istream

     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.

这段程序使用一个 string 类型对象作为实参传给 Sales_itemsame_isbn 函数。该函数期待一个 Sales_item 对象作为实参。编译器使用接受一个 stringSales_item 构造函数从 null_book 生成一个新的 Sales_item 对象。新生成的(临时的)Sales_item 被传递给 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.

这个行为是否我们想要的,依赖于我们认为用户将如何使用这个转换。在这种情况下,它可能是一个好主意。book 中的 string 可能代表一个不存在的 ISBN,对 same_isbn 的调用可以检测 item 中的 Sales_item 是否表示一个空的 Sales_item。另一方面,用户也许在 null_book 上错误地调用了 same_isbn

More problematic is the conversion from istream to Sales_item:

更成问题的是从 istreamSales_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.

这段代码将 cin 隐式转换为 Sales_item。这个转换执行接受一个 istreamSales_item 构造函数。该构造函数通过读标准输入来创建一个(临时的)Sales_item 对象。然后该对象被传递给 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.

这个 Sales_item 对象是一个临时对象(第 7.3.2 节)。一旦 same_isbn 结束,就不能再访问它。实际上,我们构造了一个在测试完成后被丢弃的对象。这个行为几乎肯定是一个错误。

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:

可以通过将构造函数声明为 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:

explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它:

     // 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.

当构造函数被声明 explicit 时,编译器将使用它作为转换操作符。



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.

在这段代码中,从 null_book 创建一个 Sales_item。尽管构造函数为显式的,但这个用法是允许的。显式使用构造函数只是中止了隐式地使用构造函数。任何构造函数都可以用来显式地创建临时对象。

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.

通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象。



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?

解释一下接受一个 stringSales_item 构造函数是否应该为 explicit。将构造函数设置为 explicit 的好处是什么?缺点是什么?

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?

基于对 f 的第二个调用中出现的错误,我们可以对 vector 构造函数作出什么推断?如果该调用成功了,那么你能得出什么结论?


12.4.5. Explicit Initialization of Class Members

12.4.5. 类成员的显式初始化

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:

尽管大多数对象可以通过运行适当的构造函数进行初始化,但是直接初始化简单的非抽象类的数据成员仍是可能的。对于没有定义构造函数并且其全体数据成员均为 public 的类,可以采用与初始化数组元素相同的方式初始化其成员:

     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:

根据数据成员的声明次序来使用初始化式。例如,因为 ivalptr 之前声明,所以下面的用法是错误的:

     // 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:

这种形式的初始化从 C 继承而来,支持与 C 程序兼容。显式初始化类类型对象的成员有三个重大的缺点。

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

    要求类的全体数据成员都是 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 的数据成员为 public,然而下面这段代码却不能编译,为什么?

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


Team LiB
Previous Section Next Section