概述
文章目录
- 13.1 拷贝、赋值与销毁
- 13.1.1 拷贝构造函数
- 合成拷贝构造函数
- 拷贝初始化
- 13.1.2 拷贝赋值运算符
- 重载赋值运算符
- 合成拷贝赋值运算符
- 13.1.3 析构函数
- 析构函数的作用和调用时机
- 合成析构函数
- 13.1.4 三/五法则
- 13.1.5 使用=default
- 13.1.6 阻止拷贝
- 13.2 拷贝控制和资源管理
- 13.2.1 行为像值的类
- 类值拷贝赋值运算符
- 13.2.2 行为像指针的类
- 13.3 交换操作
- 在赋值运算符中使用swap
- 13.6 对象移动
- 13.6.1 右值引用
- 13.6.2 移动构造函数和移动构造运算符
- 合成的移动操作
- 移动迭代器
- 13.6.3 右值引用和成员函数
- 总结
类的拷贝控制操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。这些操作定义了对此类的对象进行拷贝、移动、赋值和销毁时的行为。
如果一个类没有显式地定义这些拷贝控制成员,编译器会自动合成缺失的操作,可以用=delete
阻止编译器对某些操作进行合成。
13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
第一个参数是自身类类型的引用,且其它额外参数都有默认值
class Foo {
public:
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
}
为什么拷贝构造函数的参数必须是引用类型:如果参数不是引用类型,为了调用拷贝构造函数,就必须拷贝它的实参,但为了拷贝实参,又要调用拷贝构造函数…
合成拷贝构造函数
合成的拷贝构造函数的作用是将参数的每个非static成员拷贝到正在创建的对象中,每个成员的类型决定了它将被如何拷贝。
拷贝初始化
string s(10, '.'); // 直接初始化
string str("123"); // 直接初始化
string s1 = "999"; // 拷贝初始化
string s2 = str; // 拷贝初始化
拷贝初始化何时发生:
- 用 = 定义类的对象时
- 将一个对象作为实参传递给一个非引用类型的形参
- 用{}列表初始化一个数组中的元素或一个聚合类中的成员
- 初始化标准库容器,或调用insert、push成员时
13.1.2 拷贝赋值运算符
拷贝赋值运算符用来控制类的对象如何赋值。
重载赋值运算符
重载运算符,本质上是函数,名字由operator关键字+要定义的运算符的符号构成。赋值运算符就是一个名为 operator= 的函数。
class Foo {
public:
Foo& operator=(const Foo&);
}
标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
合成拷贝赋值运算符
struct NoCopy
{
NoCopy() = default;
NoCopy(const NoCopy&) = delete;
~NoCopy() = default;
}
13.1.3 析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,并可能做一些其它工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。
class Foo {
public:
~Foo(); // 析构函数
}
析构函数不接受参数,不能被重载,一个类只有唯一一个析构函数。
析构函数的作用和调用时机
构造函数:成员的初始化在函数体执行前完成,按照成员在类中出现的顺序初始化
析构函数:先执行函数体,然后按照初始化顺序的逆序销毁成员,成员是在析构函数体后隐含的析构阶段中被销毁的
对象被销毁的时机:
- 变量在离开作用域时被销毁
- 对象被销毁时,其成员也被销毁
- 容器(标准库容器或数组)被销毁时,其元素被销毁
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
合成析构函数
合成析构函数一般是一个空函数体,不需要额外的操作,析构部分是隐式的。在(空)析构函数体执行完毕后,成员会被自动销毁。
13.1.4 三/五法则
- 需要析构函数的类也需要拷贝和赋值操作
例如一个类需要在构造函数中分配动态内存,但合成析构函数不会delete指针数据成员,所以此类需要定义一个析构函数来释放构造函数分配的内存。而合成拷贝构造函数和合成拷贝赋值运算符只是简单地拷贝指针成员,可能使多个类对象指向相同的内存,此时析构函数的delete就会导致错误,可能使同一块内存被delete多次,行为是未定义的。
- 需要拷贝操作的类也需要赋值操作,反之亦然
例如一个类为每个对象分配一个唯一的编号,需要定义拷贝构造函数为每个新创建的对象生成一个唯一的编号,同时需要定义拷贝赋值运算符避免将编号赋予目的对象。
13.1.5 使用=default
可以通过 =default 显式要求编译器生成合成的拷贝控制成员。在类内使用 =default 时,合成的函数将隐式地声明为内联的(和在类中定义的成员函数一样)。
Foo() = default; // 在类内声明,将会隐式地声明为内联的
~Foo() = default;
13.1.6 阻止拷贝
对某些类来说,拷贝操作没有合理的意义,例如iostream类。通过将拷贝构造函数和拷贝赋值运算符定义为delete,来阻止拷贝。如果不定义为delete,编译器会为其生成合成的版本。
struct NoCopy {
NoCopy() = default;
NoCopy(const NoCopy&) = delete; // 阻止拷贝构造
NoCopy &operator=(const NoCopy&) = delete; // 阻止拷贝赋值
~NoCopy() = default;
}
如果某个类的析构函数=delete,则不能定义该类型的变量,或释放指向该类动态分配对象的指针。
当类存在成员不能默认构造、拷贝、赋值或销毁时(=delete或是private等),类的合成拷贝控制成员会被定义为delete。
如果类有const成员或引用成员,则类的合成拷贝赋值运算符=delete,如果此const成员没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数=delete。
13.2 拷贝控制和资源管理
类的拷贝行为可以像一个值,或者像一个指针。像值的类,意味着拷贝的副本和原对象是完全独立的,改变副本不会对原对象有任何影响,例如vector、string;像指针的类,意味着副本和原对象使用相同的底层数据,改变副本也会改变原对象,例如shared_ptr类。比较特殊的是IO类型和unique_ptr,它们不允许拷贝或赋值。
13.2.1 行为像值的类
类的每个成员都要有自己的拷贝
类值拷贝赋值运算符
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
// 先拷贝再释放旧内存,防止rhs和本对象是同一个对象
auto newp = new string(*rhs.ps);
delete ps;
ps = newp;
i = rhs.i;
return *this;
}
13.2.2 行为像指针的类
使用shared_ptr管理类的成员:拷贝(或赋值)一个shared_ptr会拷贝(赋值)它所指向的指针,shared_ptr类自己负责释放资源。或者使用引用计数直接管理资源。
引用计数作为类的成员:std::size_t *use
13.3 交换操作
应该用swap,而不是std::swap,如果某个成员存在特定的swap版本,其匹配程度会优于std中定义的版本。如果不存在特定的swap版本,则会使用std中的版本。
在赋值运算符中使用swap
HasPtr& HasPtr::operator=(HasPtr rhs)
{
swap(*this, rhs);
return *this;
}
离开作用域时,rhs被销毁,执行HasPtr的析构函数,delete rhs现在指向的内存,即释放左侧运算对象中原来的内存。
13.6 对象移动
13.6.1 右值引用
变量是左值
std::move的调用意味着,除了对被move的对象赋值或销毁它外,将不再使用它,对移后源对象的取值结果无法保证。
13.6.2 移动构造函数和移动构造运算符
移动构造相当于把被移动的对象“移为己用”,移动后对原对象的访问结果不能保证。
StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements), first_free(s.first_free), cap(s.cap)
{
// 使对s运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
由于移动构造函数不分配任何资源,通常不会抛出异常,在构造函数中指明noexcept,可以使标准库免除处理异常的一些额外工作。
合成的移动操作
如果类已经定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数,编译器就不会为它合成移动构造函数/赋值运算符了。
只有当类没有定义任何自己的拷贝控制成员,且类的每个非static数据成员都可移动时,编译器才会为它合成移动构造函数/赋值运算符。
如果类有拷贝构造函数,而没有移动构造函数,即使通过std::move调用,也会使用拷贝构造函数,把Foo&&转换为const Foo&。
移动迭代器
make_move_iterator
:接受一个迭代器参数,返回一个移动迭代器,解引用得到一个右值引用
13.6.3 右值引用和成员函数
引用限定符:限制赋值运算符的左侧运算对象的类型,用&(左值)、&&(右值)表示,在const限定符之后。
class Foo
{
public:
Foo &operator=(const Foo&) &; // 只能向左值赋值
}
总结
-
每个类都会通过拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数定义其对象拷贝、移动、赋值以及销毁时的操作。移动构造函数和移动赋值运算符接受一个(通常是非const的)右值引用,拷贝版本则接受一个(通常是const)的左值引用。
-
如果一个类未声明这些操作,编译器会自动合成。如果这些操作未定义成删除的,它们会逐成员初始化、移动、赋值或销毁对象(非static)。
-
拷贝构造函数:当向函数传递对象,或以传值方式从函数返回对象时,会隐式使用拷贝构造函数。
最后
以上就是快乐银耳汤为你收集整理的【C++ Primer读书笔记】第13章 拷贝控制的全部内容,希望文章能够帮你解决【C++ Primer读书笔记】第13章 拷贝控制所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复