C++ 拷贝控制简述

本文主要参考《C++ primer第五版(中文版)》第13章。若有理解错误,恳请指正。

C++ 11有五种特殊成员函数来控制对象的拷贝、移动和销毁:

如下表,去掉析构函数的四类拷贝控制函数可以按两个维度进行分类。本文分别从两个维度进行区分讲解。

初始化 赋值
拷贝(左值) 拷贝构造 拷贝赋值构造
移动(右值) 移动构造 移动赋值构造

1. 初始化和赋值

注意,以等号"="连接的不一定是赋值,也可能是初始化。这取决于是在变量声明时还是声明后使用“=”。

以“=”连接的赋值调用拷贝/移动赋值构造函数;以“=”连接的初始化调用拷贝/移动构造函数。例:

1.1. 初始化:直接初始化和拷贝初始化

相对赋值,初始化概念更加复杂。因为,对象赋值操作只对应于拷贝赋值或者移动赋值两种特殊成员函数;而对象初始化除了涉及拷贝构造、移动构造两种特殊成员函数,也涉及到各种被重载的“普通”构造函数。下面说明初始化时,什么样的构造函数会被匹配和调用。

首先初始化被分为两类:

直接初始化要求编译器使用普通的函数匹配选择构造函数,这些“普通的构造函数”并不在刚才提到的五种特殊拷贝控制成员函数之列。

拷贝初始化则要求编译器将右侧运算对象拷贝到正在创建的对象中,必要时还会进行类型转换。例:

注意:

  • ②和④虽然分别为直接初始化和拷贝初始化,但是都会调用拷贝构造函数。

  • 直接初始化会直接去尝试匹配所有构造函数,除非直接初始化匹配到我们的拷贝构造函数(②),与我们在本文所关注的五类特殊拷贝控制成员函数无关。

  • 拷贝初始化不一定调用拷贝构造函数,也可能调用移动构造函数。根据要拷贝对象的左右值,左值调用拷贝(④),右值调用移动(⑤⑥)。

  • ⑤中可能会被编译器优化为string null_book2("9-999-99999-9");来略过拷贝/移动构造函数,前提是拷贝/移动构造函数是可以访问的(不是private的)。

2. 拷贝和移动

前边的“注意”中也提到过,根据被拷贝/移动对象的左右值属性,左值将被拷贝,右值将被移动。

左值拷贝对应于拷贝/拷贝赋值构造函数;

右值移动对应于移动/移动赋值构造函数。例:

2.1. 拷贝:深/浅拷贝

对于存在类外资源的类(如所分配的大块内存buf等),深拷贝和浅拷贝的概念对应书中(chap. 13.2)定义的“行为像值的类”和“行为像指针的类”。通常我们需要自定义拷贝构造函数、拷贝赋值构造函数和析构函数让这些类正常工作。

行为像值的类(深拷贝) 需要拷贝对象真正的资源,而非成员指针,析构函数也需要释放资源。

行为像指针的类(浅拷贝) 拷贝时拷贝指针成员本身而非指针指向的资源。还要特别注意的是,只有在最后一个值被销毁时才可以真正销毁资源,这就需要在类中手动实现一种拷贝的“引用计数”,或者利用智能指针shared_ptr来管理。(shared_ptr会自己记录有多少用户共享指向的对象,当没有用户使用对象时,shared_ptr会负责资源你的释放。)

2.2. 移动

  • 当没有定义移动相关的函数时,右值也可能被拷贝。

  • 为把左值“变为”右值进行移动,可以对对象使用std::move()函数。例:

3. 总结

书中也提到,如果需要定义5个中的一个拷贝控制函数,最好将5个都定义全。C++拷贝控制的显式或隐式规则如此之复杂,这既给予了程序员足够的拷贝控制灵活度,也带来了类设计和资源管理的更大挑战,所谓权限越大,责任越大。本文只关注5种函数的区分以及一些相关概念之间的区分,没有详述函数定义默认函数合成、权限等很多细节,这些都可以从书中找到。我还可能在以后的博客中将C++与Rust、Python、Java等语言的内存控制进行对比。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注