构造函数
目的
- 在构造函数中可以进行各种初始化操作
构造函数的任务就是初始化该类的一个对象。一般而言,初始化操作必须建立一个类不变式,所谓不变式就是从成员函数(从类外)被调用时必须保持的某些东西。比如:
class Vector{
public:
Vector(int s);
private:
double *elem; // elem执行一个数组,保存sz个double
int sz; //sz为负
};
上面,我们用注释陈述不变式:“elem执行一个数组,保存sz个double"以及"sz为负”。构造函数必须保证这两点为真:
Vector::Vector(int s){
if(s < 0) throw Bad_size{s};
sz = s;
elem = new double(s);
}
此构造函数尝试建立不变式,如果失败就抛出一个异常。如果构造函数无法建立不变式,则不应该创建对象而且必须保证没有资源泄漏。需要获取并在用完后显式或者隐式的归还的任何东西都是资源,比如内存、锁、文件句柄、线程句柄。
为什么应该定义一个不变式呢?
- 聚焦于类的设计工作上
- 理清类的行为(比如错误状态下的行为)
- 简化成员函数的定义
- 理清类的资源管理
- 简化类的文档
构造函数可以建立不变式并获取资源。一般而言,构造函数是通过初始化类成员和基类来完成这些工作的
优点
- 无序考虑类是否被初始化
- 经过构造函数完全初始化后的对象可以为
const
类型,也能更方便的被标准容器或算法使用
缺点
定义
- 构造函数是类的一种特殊的非静态成员函数,其特殊之处有三点:
- 构造函数的函数名必须与类名相同
- 构造函数无返回值
- 当我们创建类对象时构造函数会被自动调用,而不需要我们主动调用
- 类名不可以用于此类内的普通成员函数,数据成员等等:
struct S{
S(); //OK
void S(int); //error:不能为构造函数指定返回类型
int S; //error:类名只能表示构造函数
enum S{foo, bar}; //error:类名只能表示构造函数
};
- 构造函数用于初始化该类类型的对象
- 在类的构造函数定义中,成员初始化器列表指定各个直接和虚基类和各个非静态数据成员的初始化器。 (请勿与
std::initializer_list
混淆)- 成员初始化器列表,其语法是冒号字符 : 后随一或多个 成员初始化器 的逗号分隔列表
- 成员初始化器列表
- 构造函数必须不是
协程
(C++20 起)
非静态成员函数:非静态成员函数是声明于类的成员说明中,不带 static 或 friend 说明符的函数。
显式初始化成员是一个好主意,注意,一个”隐式初始化“的内置成员其实是未初始化的
基类初始化器
- 一个构造函数可以初始化自己的成员和基类,但不能初始化基类的基类:
struct B{B(int);};
struct BB: B{};
struct BBB : BB{
BBB(int i) : B(i){}; //错误,尝试初始化基类的基类
};
- 派生类的基类的初始化方式和非数据成员相同。即,如果基类要求一个初始化器,我们就必须再构造函数中提供相应的基类初始化器。如果我们希望进行默认构造,可以显式指出:
class B1{B1();}; //具有默认构造函数
class B2{B2(int);}; //无默认构造函数
struct D1 : B1, B2{
D1(int i) : B1{}, B2{i}{}
};
struct D2: B1, B2{
D1(int i) : B2{i}{} // 隐式使用B1{}
};
struct D3: B1, B2{
D1(int i){}; //error:B2要求一个int初始化器
}
与成员初始化类似,基类按照声明顺序进行初始化,建议按此顺序指定基类的初始化器。基类的初始化在成员之前,销毁在成员之后。
语法
类名 ( 形参列表(可选) ) 异常说明(可选) attr(可选)
类名
必须指明当前类(或者类模板的当前实例化) ,或当在命名空间作用域或在友元声明中声明时,它必须是有限定的类名。
成员初始化器列表,其语法是冒号字符 : 后随一或多个 成员初始化器 的逗号分隔列表,每项均具有下列语法
- 用直接初始化,或者当表达式列表为空时用值初始化,初始化类或者标识符所指的基类或成员
类或标识符 ( 表达式列表(可选) )
- 用列表初始化(如果列表为空则为值初始化,而在初始化聚合体时为聚合初始化),初始化类或者标识符所指的基类或成员
类或标识符 花括号初始化器列表
- 用包展开初始化多个基类
- 类或标识符 - 任何指明非静态数据成员的标识符,或任何指明该类自身 (对于委托构造函数)或直接或虚基类的类型名。
- 表达式列表 - 可为空的,传递给基类或成员的参数的逗号分隔列表
- 花括号初始化器列表 - 花括号包围的初始化器和嵌套的花括号初始化器列表的列表
- 形参包 - 变参模板形参包的名字
形参包 ...
解释
- 构造函数没有名字且无法被直接调用。
- 构造函数在发生初始化时调用,而且它们按照初始化的规则进行选择
- 无
explicit
说明符的构造函数是转换构造函数 - 有
constexpr
说明符的构造函数令其类型成为字面类型,具体请参见constexpr构造函数(https://blog.csdn.net/zhizhengguan/article/details/114990126) - 可以接收同类型的另一对象为实参的构造函数是复制构造函数和移动构造函数
- 无
- 在开始执行组成构造函数体的复合语句之前,所有直接基类,虚基类,及非静态数据成员的初始化均已结束。成员初始化器列表是能指定这些对象的非默认初始化之处。
- 对于不能默认初始化的基类和非静态数据成员,例如引用和const限定的类型的成员,必须指定成员初始化器
- 对没有成员初始化器的匿名联合体或者变体成员不进行初始化
类或标识符
指明虚基类的初始化器,在并非所构造对象的最终派生类的构造期间被忽略- 出现于
表达式列表
或花括号初始化器列表
中的名字在构造函数的作用域中求值:
class X {
int a, b, i, j;
public:
const int& r;
X(int i)
: r(a) // 初始化 X::r 为指代 X::a
, b{i} // 初始化 X::b 为形参 i 的值
, i(i) // 初始化 X::i 为形参 i 的值
, j(this->i) // 初始化 X::j 为 X::i 的值
{ }
};
- 成员初始化器所抛出的异常可被
函数try块
处理 - 成员函数(包括虚成员函数)可从成员初始化器调用,但如果在该点所有基类尚未被全部初始化,则行为未定义
- 对于虚调用(如果在该点时已初始化直接基类),适用与从构造函数与析构函数中进行虚函数调用相同的规则:虚成员函数表现如同 *this 的动态类型是正在构造的类的静态类型(动态派发不在继承层级下传),而对纯虚成员函数的虚调用(但非静态调用)是未定义行为。
- 若非静态数据成员具有默认成员初始化器且同时出现在成员初始化器列表中,则使用成员初始化器而非默认成员初始化器
struct S {
int n = 42; // 默认成员初始化器
S() : n(7) {} // 将设置 n 为 7,而非 42
};
- 引用成员不能绑定到成员初始化器列表中的临时量
struct A {
A() : v(42) { } // 错误
const int& v;
};
变量的初始化在构造时提供其初值
复合类的构造函数
包含类类型成员的类称为复合类。 创建复合类的类类型成员时,调用类自己的构造函数之前,先调用构造函数。 当包含的类没有默认构造函数是,必须使用复合类构造函数中的初始化列表。
#include <iostream>
using namespace std;
class Label{
public:
Label(string label, string address) :m_label(label),m_address(address) {};
string m_label;
string m_address;
};
class Box {
public:
Box(int width, int length, int height){
m_width = width;
m_length = length;
m_height = height;
}
private:
int m_width;
int m_length;
int m_height;
};
class StorageBox : public Box{
public:
StorageBox(int width, int length, int height, Label label)
:Box(width, length, height) , m_label(label){}
private:
Label m_label;
};
int main() {
//通过命名对象
Label label1{ "some_name", "some_address" };
StorageBox sb1(1, 2, 3, label1);
// 通过临时对象
StorageBox sb2(3, 4, 5, Label{ "another name", "another address" });
// 通过临时对象作为初始化列表
StorageBox sb3(1, 2, 3, {"myname", "myaddress"});
}
例子
struct S {
int n;
S(int); // 构造函数声明
S() : n(7) {} // 构造函数定义。
// ": n(7)" 为初始化器列表
// ": n(7) {}" 为函数体
};
S::S(int x) : n{x} {} // 构造函数定义。": n{x}" 为初始化器列表
int main()
{
S s; // 调用 S::S()
S s2(10); // 调用 S::S(int)
}
调用构造函数时发生了什么?
当我们定义一个对象:
T object
时,实际上会发生什么事情呢?如果T有一个构造函数(不管是用户提供的还是编译器合成的),它会被调用。那么调用构造函数时做了什么呢?
构造函数可能内带大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视类T的继承体系而定。一般编译器所做的扩充操作如下:
- 记录在成员初始化列表中的
data members
初始化操作会被放进构造函数的函数本身,并以members
的声明顺序为顺序 - 如果有一个
member
并没有出现在成员初始化列表之中,但它有一个默认构造函数
,那么该默认构造函数必须被调用 - 在那之前,如果类对象有
vptr
,它们必须被设定初值,指向适当的虚函数表 - 在那之前,所有上一层的
base class constructors
必须被调用,以基类的声明顺序为顺序(与成员初始化列表中的顺序没有关联)- 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去
- 如果基类没有列于成员初始化列表中,而它有默认构造函数(或者default memberwise copy constructor),那么就调用它
- 如果基类是多重继承下的第二或后继的基类,那么this指针必须调整
- 在那一种,所有的
virtual base class constructors
必须被调用,从左到右,从最深到最浅- 如果类列于成员初始化列表中,那么任何明确指定的参数都应该传递过去
- 如果类没有列于成员初始化列表中,而它有默认构造函数,那么就调用它
- 如果类中的每一个
virtual base class subobject
的偏移量必须在执行期可被存取 - 如果类对象是最底层(most-derived)的类,其构造函数可能被调用,某些用以支持这个行为的机制必须被放进来
看个例子:
class Point{
public:
Point(float x = 0.0, float y = 0.0)
: _x(x), _y(y){}
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
virtual float z() {return 0.0;};
protected:
float _x, _y;
};
class Line{
Point _begin, _end;
public:
Line(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
Line(const Point&, const Point&);
draw();
};
对于Line,每一个explicit constructor都会被扩充以调用其两个成员类对象的constrors,如果我们定义constructor如下:
Line::Line(const Point&begin, const Point&end)
: _begin(begin), _end(end) {}
它会被编译器扩充并转换为:
Line* Line::Line(Line* this, const Point&begin, const Point&end){
this->_beigin.Point::Point(begin);
this->_end.Point::Point(end);
return this;
}
由于Point声明了一个 copy constructor、一个copy operator,以及一个destructor (virtual),所以此时的implicit copy constructor、copy operator、destructor都将有实际功能(nontrival)
当定义一个类对象:
Line a;
时, implicit line destructor会被合成出来。
- 如果Line派生自Point,那么合成出来的destructor将会是vritual
- 但是这里Line只是内带Point object,所以被合成出来的destructors只是nontrivila而已
在其中,它的member class object的destructor会被调用(尤其构造的相反顺序):
// 合成出来的(implicit) line destructor
inline void Line::~Line(Line *this){
this->_end.Point::~Point();
this->_begin.Point:~Point();
}
当然,如果Point destructor是inline函数,那么每一个调用操作会在调用地点被扩展开来。注意,虽然Point destructor是virtual ,但其调用操作(在Line析构函数中)会被静态的决议出来。
同样的,当:
Line b = a;
时,implicit Line copy constructor会被合成出来,成为一个inline public member、
当:
a = b;
时,implicit copy assignment operator会被合成出来,成为一个inline public member。