对象的拷贝、赋值、清理和移动
对象的赋值和拷贝
/*矩形类*/
class Rectangle {
public:
int length = 1; //长度
int width = 2; //宽度
public:
Rectangle() = default;
Rectangle(int L, int W) :length{ L }, width{ W } {};
void Print(void) const
{
cout << "length: " << length <<','
<< "width: " << width << endl;
}
};
默认情况下,对象可以拷贝。
特别是,一个类对象可以用同类的另一个对象的副本来进行初始化。
对象的赋值或拷贝只对数据成员赋值或拷贝,而不对成员函数赋值或拷贝。
Rectangle R1;
Rectangle R2{ 3,4 };
Rectangle R = R1; //对象的默认拷贝 R = 1,2
默认情况下,对象可以赋值。
Rectangle R1;
Rectangle R2{ 3,4 };
R1 = R2; //对象的赋值; R1 = 3,4
转换构造函数
只有一个参数的构造函数称为转换构造函数。
转换构造函数的作用是:将一个其它类型的数据转换成一个类的对象。
拷贝构造函数和移动构造函数也是转换构造函数。
例:将一个矩形对象转换成一个长方体对象。
/*长方体类*/
class Cuboid {
public:
int length = 1; //长度
int width = 1; //宽度
int height = 1; //高度
public:
Cuboid() = default;
Cuboid(int L, int W, int H): length{ L }, width{ W }, height{ H } {};
Cuboid(Rectangle& R) //转换构造函数
{
length = R.length;
width = R.width;
}
};
int main()
{
Rectangle R1; //无参构造
Rectangle R2{ 3,4 }; //有参构造
Cuboid C{ 1,2,3 };
Cuboid C1{ R1 }; //显示的类型转换,转换构造函数
C1 = R2; //隐式的类型转换,赋值
Cuboid C2 = R1; //隐式的类型转换,拷贝
}
抑制构造函数的隐式类型转换
如果不要求隐式类型转换,我们可以通过将构造函数声明为 explicit 加以阻止:
explicit Cuboid(Rectangle& R)
{
length = R.length;
width = R.width;
}
explicit 构造函数只能用于直接初始化。
Cuboid C1{ R1 }; //只能显示调用转换构造函数
拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0; /*字符串的存储空间*/
public:
MyString(const MyString& S); /*拷贝构造函数*/
};
/*拷贝构造函数*/
MyString::MyString(const MyString& S)
{
MaxSize = S.MaxSize;
str = new char[MaxSize] {};
for (unsigned int i = 0; i < MaxSize; i++)
{
str[i] = S.str[i];
}
}
拷贝构造函数的第一个参数必须是该对象的引用,而且通常是 const 引用。
拷贝初始化
拷贝初始化(使用等号或花括号),编译器将右侧运算对象拷贝到正在创建的对象中。
拷贝初始化不仅在使用等号定义变量时会发生,在下列情况也会发生:
- 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
拷贝赋值运算符重载
赋值运算符重载,必须定义为成员函数;其左侧运算对象就绑定到隐式的 this 参数。
赋值运算符右侧运算对象作为显式参数传递,通常为右侧运算对象的 const 引用。
赋值运算符重载通常应该返回一个指向其左侧运算对象的引用。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0; /*字符串的存储空间*/
public:
MyString& operator= (const MyString& S); /*拷贝赋值运算符重载*/
};
/*拷贝赋值运算符重载*/
MyString& MyString::operator= (const MyString& S)
{
delete[] str;
MaxSize = S.MaxSize;
str = new char[MaxSize] {};
for (unsigned int i = 0; i < MaxSize; i++)
{
str[i] = S.str[i];
}
return *this;
}
析构函数
析构函数的工作:释放对象使用的资源,并销毁对象的非 static 数据成员。
析构函数是类内的一个成员函数,名字由波浪号接类名构成。
它没有返回值,也不接受参数,更不能重载。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0; /*字符串的存储空间*/
public:
~MyString(); /*析构函数*/
};
/*析构函数*/
MyString::~MyString()
{
if (str != nullptr)
delete[] str;
}
析构函数完成的工作
析构函数有一个函数体和一个析构部分(析构部分是隐式的)。
当对象退出作用域或被 delete 释放时,析构函数会被隐式调用。
执行一个析构函数,首先执行函数体,然后销毁成员,最后成员按初始化顺序的逆序销毁。
销毁类类型的成员需要执行成员自己的析构函数。
内置类型被认为有一个不做任何事情的析构函数。
注意:隐式销毁一个内置指针类型的成员不会 delete 它所指向的对象。
何时调用析构函数
一个对象无论何时被销毁,都会自动调用其析构函数:
- 变量在离开其作用域时被销毁。
- 当一个对象被销毁时,其成员被销毁。
- 容器(无论标准库容器还是数组)被销毁时,其元素被销毁。
- 对于动态分配的对象,当对指向它的指针应用 delete 运算符时被销毁。
- 对于临时对象,当创建它的完整表达式结束时被销毁。
- 当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
调用构造函数和析构函数的顺序:先构造的后析构,后构造的先析构。
相当于,栈的先进后出原则。
合成的拷贝操作
合成拷贝构造函数
如果一个类没有定义自己的拷贝构造函数,编译器会我们定义一个合成的拷贝构造函数(synthessized copy constructor)。
编译器从给定对象中依次将每个非 static 成员拷贝到正在创建的对象中。
内置类型成员直接拷贝。
类类型的成员,会使用其拷贝构造函数来拷贝。
合成拷贝赋值运算符
如果一个类没有定义自己的拷贝赋值运算符,编译器会为它生成一个合成的拷贝赋值运算符。
它会将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员。
合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
合成析构函数的函数体为空。
class MyString{
public:
~MyString(){ } //等价于合成析构函数
}
在(空)析构函数体执行完后,成员被自动销毁。
成员是在析构函数体之后隐含的析构阶段中被销毁。
使用=default
我们可以通过= default
来显式地要求编译器生成合成的版本。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0;
public:
MyString() = default; /*默认构造函数*/
MyString(const MyString& S) = default; /*合成拷贝构造函数*/
MyString& operator= (const MyString& S) = default; /*合成拷贝赋值运算符*/
~MyString() = default; /*合成析构函数*/
};
当我们在类内用= default
修饰成员的声明时,合成的函数将隐式地声明为内联的。
如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用= default
。
阻止拷贝
在C++11标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为 private 的来阻止拷贝。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0;
public:
MyString() = default; /*默认构造函数*/
~MyString() = default; /*合成析构函数*/
private:
MyString(const MyString& S); /*禁止拷贝构造函数*/
MyString& operator= (const MyString& S); /*禁止拷贝赋值运算符*/
};
定义删除的函数
在C++11标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deletedfunction)来阻止拷贝。
删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。
在函数的参数列表后面加上= delete
来指出我们希望将它定义为删除的。
= delete
必须出现在函数第一次声明的时候。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0;
public:
MyString() = default; /*默认构造函数*/
MyString(const MyString& S) = delete; /*删除拷贝构造函数*/
MyString& operator= (const MyString& S) = delete; /*删除拷贝赋值运算符*/
~MyString() = default; /*合成析构函数*/
};
希望阻止拷贝的类应该使用= delete
来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们声明为 private 的。
合成的拷贝控制成员可能是删除的
对某些类来说,编译器将这些合成的成员定义为删除的函数:
- 如果类的某个成员的析构函数是删除的或不可访问的,则类的合成析构函数被定义为删除的。
- 如果类的某个成员的析构函数是删除的或不可访问的,则类合成的拷贝构造函数也被定义为删除的。
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的。
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个 const 的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。
- 如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用成员,它没有类内初始化器,或是类有一个 const 成员,它没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的。
本质上,这些规则的含义是:如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
移动构造函数
移动构造函数的第一个参数是该类类型的一个右值引用。
移动构造函数的任何额外的参数都必须有默认实参。
一旦资源完成移动,源对象必须不再指向被移动的资源;这些资源的所有权已经归属新创建的对象。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0; /*字符串的存储空间*/
public:
MyString( MyString && S) noexcept; /*移动构造函数*/
};
/*移动构造函数*/
MyString::MyString(MyString&& S) noexcept
{
delete[] str;
MaxSize = S.MaxSize;
str =S.str;
S.str = nullptr;
}
如果移动构造函数不抛出任何异常,我们就应该将它标记为 noexcept。
在一个构造函数中,noexcept 出现在参数列表和初始化列表开始的冒号之间。
移动赋值运算符重载
移动赋值运算符重载,必须定义为成员函数;其左侧运算对象就绑定到隐式的 this 参数。
赋值运算符右侧运算对象作为显式参数传递,通常为右值引用。
赋值运算符重载通常应该返回一个指向其左侧运算对象的引用。
class MyString
{
private:
char* str = nullptr;
unsigned int MaxSize = 0; /*字符串的存储空间*/
public:
MyString( MyString && S) noexcept; /*移动构造函数*/
};
/*移动赋值运算符重载*/
MyString& MyString::operator= (MyString&& S) noexcept
{
delete[] str;
MaxSize = S.MaxSize;
str = S.str;
S.str = nullptr;
return *this;
}
如果移动赋值运算符不抛出任何异常,我们就应该将它标记为 noexcept。
合成的移动操作
如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
编译器可以移动内置类型的成员。
如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。
移动操作永远不会隐式定义为删除的函数。
如果我们显式地要求编译器生成=default
的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
除了一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循下列原则:
- 移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
- 如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
- 如果有类成员是 const 的或是引用,则类的移动赋值运算符被定义为删除的。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作;否则,这些成员默认地被定义为删除的。