【C++】C++的构造函数
构造函数是特殊的成员函数,只要创建类类型的对象,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。
class Sales_Item { public: //operations on Sales_item objects //default constructor needed to initialize members of built-in type Sales_item(): units_sold(0), revenue(0.0){} private: std::string isbn; unsigned units_solds; double revenue; };
构造函数的名字与类的名字相同,并且不能指定返回类型。像其他任何函数一样,它们可以没有形参,也可以定义多个形参。
构造函数的几个特性:
1. 构造函数可以重载
2. 构造函数不能声明为const
1 class Sales_item { 2 public: 3 Sales_item() const; //error 4 };
constg构造函数是不必要的。创建类类型的const对象时,运行一个普通构造函数来初始化该const对象。构造函数的工作是初始化对象。不管对象是否为const,都用一个构造函数初始化该对象。
构造函数初始化式:
1 Sales_item::Sales_item(const string &book): 2 isbn(book), units_sold(0), revenue(0.0) {}
构造函数初始化列表以一个冒号开始,接着是一个逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号里中的初始化式。这个构造函数将isbn成员初始化为book形参的值,将units_sold和revenue初始化为0。与任意的成员函数一样,构造函数可以定义在类的内部或外部。构造函数初始化式只在构造函数的定义中而不在声明中指定。
构造函数初始化列表难以理解的一个原因在于,省略初始化列表并在构造函数的函数体内对数据成员赋值是合法的。例如,可以将接受一个string的Sales_item构造函数编写为:
//legal but sloppier way to write constructor // no constructor initializer Sales_item::Sales_item(const string &book) { isbn = book; units_sold = 0; revenue = 0.0; }
这个构造函数给类Sales_item的成员赋值,但没有进行显式初始化。不管有没有显式的初始化式,在执行构造函数之前,要初始化化isbn成员。这个构造函数隐式使用默认的string构造函数初始化isbn。执行构造函数的函数体时,isbn已经有值了。该值被构造函数函数体中的赋值所覆盖。
从概念上讲,可以认为构造函数分两个阶段:(1)初始化阶段;(2)普通的计算阶段
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化(调用默认构造函数,若没有默认构造函数,则编译错误)。初始化发生在计算阶段开始前。
构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则进行初始化:
1)类类型:运行该类型的默认构造函数
2)内置或复合类型的成员的初始值依赖于对象的作用域;在局部作用域中这些成员不被初始化,而在全局作用域中它们初始化为0;
因为内置类型的成员不进行隐式初始化,所以对这些成员是进行初始化还是赋值似乎无关紧要。但对于类类型的数据成员若未在初始化列表当中显式初始化,而是在函数体里赋值,则相当于先调用类的默认构造函数初始化,再在函数体里赋值,故相比于直接利用初始化列表,效率较低。
如果没有为类提供初始化式,则编译器会隐式地使用成员类型的默认构造函数,如果那个类没有默认构造函数,则编译器尝试使用默认构造函数失败。
必须利用初始化列表的几种情况:
1)没有默认构造函数的类类型的成员;
2)const类型的成员;
3)引用类型的成员;
上述类型必须在初始化列表中进行初始化,在函数体中对他们赋值是不起作用的。
1 class ConstRef {
2 public:
3 ConstRef(int ii)'
4 private:
5 int i;
6 const int ci;
7 int &ri;
8 };
9 //no explicit constructor: error ri is uninitialized
10 ConstRef::Constef(int ii)
11 {
12 i = ii; //ok
13 ci = ii; //error:cannot assign to a const;
14 ri = i; //assignes to ri which was not bound to an objects
15 }
可以初始化const对象或引用类型的对象,但不能对它们赋值。在开始开始执行构造函数的函数体之前,要完成初始化。初始化const或引用类型数据成员唯一的机会是在构造函数初始化列表中。
成员初始化的次序:
每个成员在初始化列表中只能指定一次。
成员被初始化的顺序就是成员定义的次序,而与他们在构造函数的初始化列表中的顺序无关。
默认构造函数:
只要定义一个对象时没有提供初始化式,就使用默认构造函数。为所有形参提供默认实参的构造函数也定义了默认构造函数。
一个类哪怕只定义一个构造函数(包括复制构造函数),编译器也不会再生成默认构造函数。只有当一个类没有定义默认构造函数,编译器才自动生成一个。
如果类包含内置或复合类型(数组)的成员,则该类不依赖于合成的默认构造函数。它应该定义自己的默认构造函数。
合成的默认构造函数使用与变量初始化相同的规则来初始化成员。此外,每个构造函数应该为每个内置或复合类型的成员提供初始化式。没有初始化内置或复合类型成员的构造函数,将使那些成员处于未定义状态。除了作为赋值的目标外,以任何方式使用一个为定义的成员都是错误的。
使用默认构造函数:
1 Sales_item myobj();
编译myobj的声明没有问题,然而,使用myobj时会出错。原因是myobj的定义被编译器解释为一个函数的声明。使用默认构造函数定义一个对象的正确方式是去掉最后的空括号:
Sales_item myobj;
下面的代码也是正确的:
Sales_item myobj = Sales_item();
在这里,我们创建并初始化一个Sales_item对象,然后用它来按值初始化myobj。编译器通过运行Sales_item的默认构造函数来按值初始化一个Sales_item。
没有默认构造函数的后果:
假设有一个NoDefaultl,它没有定义自己的默认构造函数,却有一个接受一个string实参的构造函数。因为该类定义了一个构造函数,因此编译器将不会合成默认构造函数。NoDefault没有默认构造函数,一位置:
1)具有Nodefault成员的每个类的每个构造函数,必须传递一个初始的string值给Default构造函数来显式地初始化NoDefault成员。
2)编译器将不会为具有NoDefault类型成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,就必须显式地定义,并且默认构造函数必须显式地初始化其Nodefault成员。
3)NoDefault类型不能用作动态分配数组的元素。如果没有为类类型数组提供初始化式,则将用默认构造函数初始化每个元素。
4)NoDefault类型的静态分配数组必须为每个元素提供一个显式的初始化。
5)如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化的构造函数。编译器首先调用默认构造函数创建
一个临时值来初始化,然后使用复制构造函数复制到容器的每个元素。
即以下都是错误的:
NoDefault nd; NoDefault nd[10]; NoDefault *pa = new A[10]; std::vector<NoDefault> vec(10);
下面哪些陈述不正确:
a)类必须提供至少一个构造函数
不正确。因为类也可以不提供构造函数,这时使用由编译器合成的默认构造函数。
b)默认构造函数的形参列表中没有形参。
不正确。因为为所有形参都提供了默认实参的构造函数也定义了默认构造函数,而这样的构造函数形参列表中是有形参的。
c)如果一个类没有有意义的默认值,则该类不应该提供默认构造函数
不正确。因为如果一个类没有默认构造函数(指的是该类提供了构造函数,但没有提供自己的默认构造函数),则在编译器需要隐式使用默认构造函数的环境中,该类就不能使用,所以,一个类定义了其他构造函数,则通常也应该提供一个默认构造函数。
d)如果一个类没有定义默认构造函数,则编译器会自动生成一个,同时将每个数据成员初始化为相关类型的默认值。
不正确。因为编译器合成的默认构造函数,不是将每个数据成员初始化行为相关类型的默认值,而是使用与变量初始化相同的规则来初始化成员;类类型的成员执行各自的默认构造函数进行初始化;内置和复合类型的成员,只对定义在全局作用域中的对象才初始化。