C++:构造函数与拷贝控制
什么是构造函数
构造函数
(constructor)是类的一种特殊的成员函数,它被用于控制类的初始化过程、初始化对象的数据成员。无论何时只要类的对象被创建,都会执行构造函数,不同的初始化方法会调用不同的构造函数。
构造函数的特点
-
构造函数的名字和类名相同。
-
构造函数没有返回类型。
-
构造函数有一个(可能为空的)参数列表和一个(可能为空的)函数体。
-
构造函数不能被声明成
const
属性。类对象在构造函数完成之后才可以设置常量性,所以构造函数无法被声明成
const
的,并且构造函数在const
对象的构造过程中可以向其写值。 -
构造函数也支持重载,不同的构造函数之间必须在参数数量或者参数类型上有所区别。
构造函数的种类
默认构造函数
类通过一个特殊的构造函数来控制默认初始化的过程,这个函数叫做默认构造函数
(default constructor)。默认构造函数无须任何实参。
class Foo {
public:
Foo() {}
};
当类没有显式地定义(任何一种)构造函数时,编译器会隐式地定义一个默认构造函数,又称合成默认构造函数
(synthesized default constructor)。
在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。
默认初始化和值初始化都可以调用默认构造函数:
- 块作用域内不使用任何初始值定义一个非静态变量或者数组时(默认初始化)。
- 当一个类本身含有类类型的成员且使用合成的默认构造函数时(默认初始化)。
- 当类类型的成员没有在构造函数初始值列表中显示地初始化时(默认初始化)。
- 在数组初始化的过程中如果没有提供的初始值数量少于数组的大小时(值初始化)。
- 当不使用初始值定义一个局部静态变量时(值初始化)。
- 当通过书写形如
T()
的表达式显式地请求值初始化时,其中T
是类型名(值初始化)。
Foo foo(); // 错误,声明了一个函数非对象。
Foo foo; // 正确,foo 是一个对象非函数,且调用默认构造函数,默认初始化。
new Foo; // 默认初始化。
Foo foo{}; // 值初始化。
Foo(); // 值初始化。
Foo{}; // 值初始化。
new Foo(); // 值初始化。
new Foo{}; // 值初始化。
值初始化和默认初始化的区别:值初始化多了一对括号。
参数构造函数
当使用参数进行类对象初始化时,调用的是类的参数构造函数。根据形参的个数与种类,调用不同的参数构造函数。
class Foo {
public:
Foo(int i): _i(i) {}
Foo(int i, float j): _i(i), _j(j) {}
private:
int _i;
float _j;
};
Foo f0(1); // 调用第一个构造函数
Foo f1(1, 0.1); // 调用第二个构造函数
Foo f2 = {1}; // 调用第一个构造函数
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都是默认值,则此构造函数是拷贝构造函数
(copy constructor)。拷贝构造函数的第一个参数必须是引用类型,虽然可以是非 const
的,但是通常总是一个 const
的引用。拷贝构造函数不应该是 explicit 的。
class Foo {
public:
Foo(const Foo &other) {} // 拷贝构造函数
Foo(const Foo &other, int i=1): _i(i) {} // 拷贝构造函数
private:
int _i;
};
如果没有显示的定义一个拷贝构造函数,即使存在其他构造函数,编译器也会默认生成一个合成拷贝构造函数
(synthesized copy constructor)。
当发生拷贝初始化时,类的拷贝构造函数会被调用:
- 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
Foo f0; // 默认构造
Foo f1(f0); // 拷贝构造
Foo f2 = f1; // 拷贝构造
移动构造函数
类似拷贝构造函数,移动构造函数
(move constructor)的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数必须有默认实参。移动构造函数不会分配任何新的内存,它会接管旧对象的内存,并将旧对象的指针置为 nullptr
,从而完成对象的移动操作。
class Foo {
public:
Foo(Foo &&other) {}
};
Foo f1;
Foo f2 = std::move(f1); // 调用移动构造函数
转换构造函数
如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时也把这种构造函数称为转换构造函数
(converting constructor)。
class Foo {
public:
Foo(int i) {}
};
Foo f1 = 1; // 隐式调用参数构造函数
// 等于 Foo temp(1); Foo f1(temp) 多执行一次拷贝构造,效率低。
- 编译器只会自动地执行一步类型转换。
- 可以使用
explicit
将构造函数声明为禁止隐式转换。 explicit
构造函数只能使用直接初始化。
委托构造函数
委托构造函数
(delegating constructor)使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些职责委托给了其他构造函数。被委托的构造函数也称为目标构造函数。
class Foo {
public:
Foo(int i, float f, const string &s): _i(i), _f(f), _s(s) {} // 目标构造函数
Foo(int i): Foo(i, 0.0, "") {} // 委托构造函数
Foo(float f): Foo(0, f, "") {} // 委托构造函数
Foo(const string &s): Foo(0, 0.0, s) {} // 委托构造函数
};
在构造函数中,除了使用初始值列表的方式,尽量不要调用其他构造函数。
拷贝控制
类通过拷贝构造函数
(copy constructor)、拷贝赋值运算符
(copy-assignment operator)、移动构造函数
(move constructor)、移动赋值运算符
(move-assignment operator)和析构函数
(destructor)来控制类对象的拷贝
、移动
、赋值
和销毁
。这些行为也称为拷贝控制操作
(copy control)。
拷贝赋值运算符
重载运算符本质上是函数,其名字由 operator
关键字后接表示要定义的运算符的符号组成。赋值运算符是一个名为 operator=
的函数,通常应该返回一个指向其左侧运算对象的引用。
class Foo {
public:
Foo &operator=(const Foo &rhs) {}
};
如果没有显示定义,编译器会默认生成一个合成拷贝赋值运算符。重载运算符的参数表示运算符的运算对象,如果一个运算符是一个成员函数,其左侧运算对象就是绑定到隐式的 this
参数。
某些运算符,包括赋值运算符,必须定义为成员函数。
当对象以及初始化完成,通过赋值操作进行赋值时,调用赋值运算符:
Foo f1;
Foo f2;
f1 = f2; // 调用赋值运算符
移动赋值运算符
与移动构造函数与赋值运算符操作相同,当对象完成初始化操作后,使用移动语义对其执行赋值操作时,调用移动赋值操作符
(move-assignment operator)。
class Foo {
public:
Foo &operator=(Foo &&rhs) {}
};
Foo f1;
Foo f2;
f1 = std:move(f2); // 调用移动赋值运算符
析构函数
析构函数
(destructor)与构造函数相反,构造函数初始化对象的数据成员,析构函数则释放对象使用的资源,销毁对象的非 static 数据成员。
class Foo {
public:
~Foo() {}
};
析构函数不接受参数,析构函数不能重载。一个类只能有一个析构函数。通常在以下情况时执行析构函数:
- 变量在离开其作用域时被注销。
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,其元素被销毁。
- 对于动态分配的对象,当指向它的指针被 delete 销毁时销毁。
- 对于临时创建的对象,当创建它的完整表达式结束时被销毁。
=default
可以使用 =default 显式的要求编译器生成合成的构造函数。
class Foo {
Foo() =default;
};
=delete
使用 =delete 阻止编译器生成合成的构造函数,但是析构函数不能是 =delete 函数。
class Foo {
Foo() =delete;
};