type
status
date
slug
summary
tags
category
icon
password
Property
类通过一个或几个特殊的成员函数来控制其对象的初始化操作,这些函数被称作构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,类对象被创建的时候,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员的初始化工作。
构造函数的名字和类名相同,和其他函数不一样的是,构造函数没有返回类型,且不能被声明为
const
函数。当创建类的一个const
对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const
对象的构造过程中可以向其写值。合成的默认构造函数
类通过默认构造函数(default constructor) 来控制默认初始化过程,默认构造函数无须任何实参。
如果类没有显式地定义构造函数,则编译器会为类隐式地定义一个默认构造函数,该构造函数也被称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,合成的默认构造函数初始化数据成员的规则如下:
- 如果存在类内初始值,则用它来初始化成员
- 否则默认初始化该成员
某些类不能依赖于合成的默认构造函数
- 只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。一旦类定义了其他构造函数,那么除非再显式地定义一个默认的构造函数,否则类将没有默认构造函数。
- 如果类包含内置类型或者复合类型的成员,则只有当这些成员全部存在类内初始值时,这个类才适合使用合成的默认构造函数。否则用户在创建类的对象时就可能得到未定义的值。
- 编译器不能为某些类合成默认构造函数。例如类中包含一个其他类类型的成员,且该类型没有默认构造函数,那么编译器将无法初始化该成员。
对于
Sales_data
类,使用下面的参数定义4个不同的构造函数:- 一个空参数列表(即默认构造函数),既然已经定义了其他构造函数,那么也必须定义一个默认构造函数
- 一个
const string&
表示ISBN编号,编译器将赋予其他成员默认值
- 一个
const string&
表示ISBN编号,一个unsigned
表示售出的图书数量,一个double
表示图书的售出价格
- 一个
istream&
从中读取一条交易信息
=default
在
C++11
中,如果类需要默认的函数行为,可以通过在参数列表后面添加=default
来要求编译器生成构造函数。其中=default
既可以和函数声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default
在类的内部,则默认构造函数是内联的。构造函数初始值列表
负责为新创建对象的一个或几个数据成员赋初始值。形式是每个成员名字后面紧跟括号括起来的成员初始值,不同成员的初始值通过逗号分隔:
当某个数据成员被构造函数初始值列表忽略时,它会以与合成默认构造函数相同的方式隐式初始化:
构造函数不应该轻易覆盖掉类内初始值,除非新值与原值不同。如果编译器不支持类内初始值,则所有构造函数都应该显式初始化每个内置类型的成员。
在类的外部定义构造函数
与其他几个构造函数不同,以
istream
为参数的构造函数需要执行一些实际的操作。在它的函数体内,调用了read
函数给数据成员赋以初值:构造函数没有返回类型,所以上述定义从指定的函数名字开始。和其他成员函数一样,在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。因此,
Sales_data::Sales_data
的含义是定义 sales_data
类的成员,它的名字是Sales_data
。又因为该成员的名字和类名相同,所以它是一个构造函数。这个构造函数没有构造函数初始值列表,即构造函数初始值列表是空的。但是由于执行了构造函数体,所以对象的成员仍然能被初始化。
没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化。对于
Sales_data
来说,这意味着一旦函数开始执行,则bookNo
将被初始化成空string
对象,而units_sold
和 revenue
将是0构造函数初始值列表
我们定义变量时习惯于立即对其进行初始化,而非先定义、再赋值:
就对象的数据成员而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显示的初始化成员,则该成员将在构造函数体之前执行默认初始化:
构造函数的初始值有时必不可少
有时可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。如果成员是
const
或者是引用的话,必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化: 和其他常量对象或者引用一样,成员
ci
和ri
都必须被初始化。因此,如果没有为它们提供构造函数初始值的话将引发错误:随着构造函数体一开始执行,初始化就完成了。初始化
const
或者引用类型的数据成员的唯一机会就是通过构造函数初始值,因此该构造函数的正确形式应该是:- 如果没有在构造函数初始值列表中显式初始化成员,该成员会在构造函数体之前执行默认初始化
- 如果成员是
const
、引用,或者是某种未定义默认构造函数的类类型,必须通过构造函数初始值列表中将其初始化
建议:使用构造函数初始值
在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员,后者则先初始化再赋值。
除了效率问题外更重要的是,一些数据成员必须被初始化。最好养成使用构造函数初始值的习惯,避免某些意想不到的编译错误。
成员初始化的顺序
成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。
一般来说,初始化的顺序没什么特别要求。不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。
最好令构造函数初始值的顺序与成员声明的顺序一致,并且尽量避免使用某些成员初始化其他成员。
最好使用构造函数的参数作为成员的初始值,而尽量避免使用同一个对象的其他成员,这样可以不必考虑成员的初始化顺序:
默认实参和构造函数
Sales_data
默认构造函数的行为与只接受一个string
实参的构造函数差不多。唯一的区别是接受string
实参的构造函数使用这个实参初始化bookNo
,而默认构造函数(隐式地)使用string
的默认构造函数初始化bookNo
。可以把它们重写成一个使用默认实参的构造函数:当没有给定实参,或者给定了一个
string
实参时,两个版本的类创建了相同的对象。因为不提供实参也能调用上述的构造函数,所以该构造函数实际上为我们的类提供了默认构造函数。如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数
C++11
扩展了构造函数初始值功能,可以定义委托构造函数(delegating constructor)。委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或全部)职责委托给了其他构造函数。默认构造函数的作用
当对象被默认初始化或值初始化时会自动执行默认构造函数,默认初始化的发生情况:
- 在块作用域内不使用初始值定义非静态变量或数组
- 类本身含有类类型的成员且使用合成默认构造函数
- 类类型的成员没有在构造函数初始值列表中显式初始化
值初始化的发生情况:
- 数组初始化时提供的初始值数量少于数组大小
- 不使用初始值定义局部静态变量
- 通过
T()
形式(T
为类型)的表达式显式地请求值初始化
类必须包含一个默认构造函数以便在上述情况下使用,其中的大多数情况非常容易判断。不那么明显的一种情况是类的某些数据成员缺少默认构造函数:
在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
使用默认构造函数
下面的
obj
的声明可以正常编译通过:但当试图使用
obj
时,编译器将报错,提示不能对函数使用成员访问运算符。问题在于,尽管我们想声明一个默认初始化的对象,obj
实际的含义却是一个不接受任何参数的函数并且其返回值是Sales_data
类型的对象。如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名之后的空的括号对:
隐式的类类型转换
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数被称为转换构造函数(converting constructor)。
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
在
Sales_data
类中,接受string
的构造函数和接受istream
的构造函数分别定义了从这两种类型向Sales_data
隐式转换的规则。也就是说,在需要使用Sales_data
的地方,可以使用string
或者istream
作为替代:在这里用一个
string
实参调用了Sales_data
的combine
成员。该调用是合法的,编译器用给定的string
自动创建了一个Sales_data
对象。新生成的这个(临时)Sales_data
对象被传递给combine
。因为combine
的参数是一个常量引用,所以可以给该参数传递一个临时量。编译器只会自动执行一步类型转换:
类类型转换不是总有效
是否需要从
string
到Sales_data
的转换依赖于我们对用户使用该转换的看法:这段代码隐式地把
cin
转换成Sales_data
,这个转换执行了接受一个istream
的Sales_data
构造函数。该构造函数通过读取标准输入创建了一个(临时的)Sales_data
对象,随后将得到的对象传递给combine
。
Sales_data
对象是个临时量,一旦combine
完成就不能再访问它了。实际上,构建了一个对象,先将它的值加到item
中,随后将其丢弃。explicit
抑制构造函数定义的隐式转换
在要求隐式转换的程序上下文中,可以通过将构造函数声明为
explicit
的加以阻止。explicit
关键字只对接受一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为 explicit
的。只能在类内声明构造函数时使用 explicit
关键字,在类外定义时不能重复。explicit
构造函数只能用于直接初始化执行拷贝初始化时(使用
=
)会发生隐式转换,所以explicit
构造函数只能用于直接初始化:用
explicit
关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数。为转换显式地使用构造函数
尽管编译器不会将
explicit
的构造函数用于隐式转换过程,但是可以使用explicit
构造函数显式地强制转换类型。第一个调用直接使用
Sales_data
的构造函数,该调用通过接受string
的构造函数创建了一个临时的Sales_data
对象;第二个调用使用static_cast
执行了显式的而非隐式的转换。其中,static_cast
使用istream
构造函数创建了一个临时的Sales_data
对象标准库中含有显式构造函数的类
接受一个单参数的
const char*
的string
构造函数不是explicit
的。接受一个容量参数的
vector
构造函数是explicit
的。