构造函数的工作是保证每个对象的数据成员具有合适的初始值。
一、构造函数的定义
(1)构造函数可以被重载。
可以为一个类声明的构造函数的数量没有限制,只要每个构造函数的形参表是唯一的。
(2)实参决定使用哪个构造函数。
(3)构造函数自动执行。
只要创建该类型的一个对象,编译器就运行一个构造函数。
(4)构造函数不能声明为const。
创建类类型的const对象时,运行一个普通构造函数来初始化该const对象。构造函数的工作是初始化该对象,不管对象是否为const,都用一个构造函数来初始化该对象。
二、构造函数初始化式
构造函数可以包含一个初始化列表。
Sales_item::Sales_item(const string &book) : isbn(book), units_sold(0), revenue(0.0) {}
注意:构造函数可以定义在类的内部或外部。构造函数初始化列表只在构造函数的定义中而不是声明中指定。
构造函数初始化列表难以理解的一个原因在于,省略初始化列表并在构造函数的函数体内对数据成员赋值是合法的。
Sales_item::Sales_item(const string &book) { isbn = book; units_sold = 0; revenue = 0.0; }
上面构造函数给成员赋值,但没有进行显示初始化。
注意:
(1)不管是否有显式初始化式,在执行构造函数前,要初始化isbn成员。这个构造函数隐式使用string默认构造函数初始化isbn。执行构造函数的函数体时,isbn成员已经有值了,该值被构造函数函数体中的赋值所覆盖。
(2)构造函数分两个阶段执行:1)初始化阶段;2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。
(3)不管成员是否在构造函数初始化列表中显示初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始前。
(4)在构造函数初始化列表中没有显示提及的每个成员,将进行如下处理:如果是类类型,运行该类型默认构造函数来初始化该类型的成员;如果是内置类型,依赖于对象的作用域。全局作用域中它们被初始化为0,局部作用域中不被初始化。
1、有时需要构造函数初始化列表。
如果没有为类成员提供初始化式,则编译器会隐式地使用成员的默认构造函数初始化该成员。如果这个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化成员,必须提供初始化式。
注意:
(1)有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及const或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
(2)除了两种情况,对非类类型的数据成员进行赋值或使用初始化式在结果和性能上都是等价的。下面的构造函数是错误的。
class ConstRef { public: ConstRef(int ii); private: int i; const int ci; int &ri; }; ConstRef::ConstRef(int ii) { i = ii; //OK ci = ii; //不能对const赋值 ri = i; // }
可以初始化const对象或引用类型的对象,但不能对它们赋值。初始化const或引用类型数据成员的唯一机会是在构造函数初始化列表中。下面的构造函数是正确的。
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(ii) {}
注意:在许多类中,初始化和赋值严格来说都是低效率的:数据成员可能已经被直接初始化了,还要对它进行初始化和赋值。比效率问题更重要的是,某些数据成员必须要初始化。
2、成员初始化的次序。
每个成员在构造函数初始化列表中只能指定一次。构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。
注意:初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。
class X { int i; int j; public: X(int val) : j(val), i(j) {} };
这个初始化列表的效果是用尚未初始化的j值来初始化i。
注意:按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
3、初始化式可以是任意表达式。
一个初始化式可以是任意复杂的表达式。
Sales_item(const string &book, int cnt, double price) : isbn(book), units_sold(cnt), revenue(cnt * price) {}
4、类类型的数据成员的初始化式。
初始化类类型的成员时,要指定实参并传递给成员类型的一个构造函数。可以使用该类型的任意构造函数。
Sales_item() : isbn(10, '9'), units_sold(0), revenue(0.0) {}
三、默认实参与构造函数
使用默认实参可减少代码重复。
四、默认构造函数
只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。
1、合成的默认构造函数。
一个类哪怕只定义了一个构造函数,编译器也不会再生成默认构造函数。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。
注意:
(1)合成的默认构造函数使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数进行初始化。内置和复合类型的成员,只对定义在全局作用域中的对象才初始化。
(2)如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
2、类通常应定义一个默认构造函数。
在某些情况下,默认构造函数是由编译器隐式应用的。如果类没有默认构造函数,则该类就不能用在这些环境中。例如:NoDefault类没有默认构造函数,有一个接受string实参的构造函数。
(1)具有NoDefault成员的类的每个构造函数,必须通过传递一个初始的string值给NoDefault构造函数来显示初始化NoDefault成员。
(2)编译期不会为具有NoDefault类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式定义,并且默认构造函数必须显式初始化其NoDefault成员。
(3)NoDefault类型不能用作动态分配数组的元素类型。
(4)NoDefault类型的静态分配数组必须为每个元素提供一个显示的初始化。
(5)如果有一个保存NoDefault对象的容器,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
注意:通常如果定义了其他构造函数,则提供一个默认构造函数总是对的。在默认构造函数中给成员提供的初始值应该指出该对象是“空”的。
3、使用默认构造函数。
Sales_item myobj;
Sales_item myobj = Sales_item();
以上两种都是使用默认构造函数定义一个对象的正确方式。