《C++Primer》第十三章——拷贝控制

第十三章:拷贝控制

一个类通过定义五种特殊的成员函数来控制此类型对象拷贝、移动、赋值和销毁时做什么
包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数
拷贝和移动构造函数定义了同类型的另一个对象初始化本对象时做什么
拷贝和移动赋值运算符定义了一个对象赋予同类型的另一个对象时做什么
析构函数定义了当此类型对象销毁时做什么
以上这些操作称为拷贝控制操作
如果我们不显示的定义这些操作,编译器会为我们定义,但编译器定义的版本的行为可能并非我们所想

13.1 拷贝、赋值与销毁

1.拷贝构造函数
如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数时拷贝构造函数

  • 拷贝构造函数的第一个参数必须是引用类型,且该参数几乎总是 const 的引用
    注:如果参数不是引用类型,则调用永远不会成功,因为为了调用拷贝构造函数则鼻血拷贝它的实参,但为了拷贝实参,我们又必须调用拷贝构造函数而进入无限循环
  • 拷贝构造函数可能会被隐式的使用,因此拷贝构造函数通常不应该是 explicit 的
    1)合成拷贝构造函数
  • 如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个合成拷贝构造函数
  • 一般情况,合成的拷贝构造函数会将其参数的成员逐个拷贝该类类型的对象(但对某些类来说,合成拷贝构造函数会组织我们拷贝该类类型的对象),编译器从给定对象中依次将每个非 static 成员拷贝到正在创建的对象中,对于类类型成员,会使用其拷贝构造函数来拷贝,内置类型的成员则直接拷贝
  • 对于数组,我们不能直接拷贝一个数组,但合成拷贝构造函数会逐个元素地拷贝一个数组类型的成员,若数组元素是类类型,则使用元素的拷贝构造函数来拷贝
    2)拷贝初始化
  • 直接初始化和拷贝初始化的差异
string dots(10, '.');				//直接初始化
string s(dots);						//直接初始化
string s2 = dots;					//拷贝初始化
string null_book = "999";			//拷贝初始化
string nines = string(100, '9');	//拷贝初始化

当使用直接初始化时,实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
当使用拷贝初始化时,要求编译器将右侧运算对象拷贝到正在创建的对象中,有时还需要进行类型转换
注:拷贝初始化一般使用拷贝构造函数来完成,但如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成

  • 拷贝初始化发生的情况(拷贝初始化是依靠拷贝构造函数和移动构造函数完成的)
    ①当我们用 = 定义变量时会发生
    ②将一个对象作为实参传递给一个非引用类型的形参
    ③从一个返回类型为非引用类型的函数返回一个对象
    ④用花括号列表初始化一个数组中的元素或一个聚合类中的成员
    ⑤某些类类型还会对它们所分配的对象使用拷贝初始化,eg:当我们初始化标准库容器或是调用其 insert 或 push 成员时,容器或对其元素进行拷贝初始化,与之相对,用 emplace 成员创建的元素都进行直接初始化
    2.拷贝赋值运算符
    类也可以控制其对象如何赋值,与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个
    1)重载赋值运算符
  • 重载运算符本质上是函数,其名字由 operator 关键字后接表示要定义的符号组成,因此赋值元素符是一个名为 operator= 的函数
  • 运算符函数也有一个返回类型和一个参数列表,重载运算符的参数表示运算符的运算对象
  • 为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用
    2)合成拷贝赋值运算符
  • 与处理拷贝构造函数一样,若一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符
  • 类似拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值
  • 合成拷贝构造运算符会将右侧运算对象的每个非 static 成员赋予左侧运算对象的对应成员,这一工作使用过成员类型的拷贝赋值运算符来完成的
  • 对于数组类型的成员,逐个赋值数组元素
  • 合成拷贝运算符返回一个指向其左侧运算对象的引用
//等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
   
	bookNo = rhs.bookNo;		//调用 string::operator=
	units_sold = rhs.units_sold;//使用内置的int赋值
	revenue = rhs.revenue;		//使用内置的double赋值
	return *this;				//返回一个此对象的引用
}

3.析构函数

  • 析构函数时类的一个成员函数名字由波浪号接类名构成,其没有返回值也不接受参数
  • 由于析构函数不接受参数,因此不能重载,对于一个给定类,只会有唯一一个析构函数
    1)析构函数完成什么操作
    回顾构造函数:构造函数有一个初始化部分和一个函数体,构造函数中成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化
  • 在析构函数中,也有一个函数体和一个析构部分,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁
  • 通常,析构函数释放对象在生存期内分配的所有资源
  • 在析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的,成员销毁时发生什么完全依赖于成员类型,销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数因此销毁内置类型什么也不需要做
  • 隐式销毁一个内置指针类型的成员不会 delete 其所指向的对象
    2)什么时候会调用析构函数
  • 变量在离开其作用域时被销毁
  • 当一个对象被销毁时,其成员被销毁
  • 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
  • 对于动态分配的对象,当对指向它的指针应用 delete 运算符时被销毁
  • 对于临时对象,当创建它的完整表达式结束时被销毁
    注:当指向一个对象的引用或指针离开作用域时,析构函数不会执行
    3)合成析构函数
  • 当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数
  • 类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁
  • 如果不是上述情况,则合成析构函数的函数体为空
    在一个析构函数中,首先执行析构函数函数体,然后销毁成员,因此析构函数体自身并不销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的,在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分进行的
    4.三/五法则
    1)需要析构函数的类也需要拷贝和赋值操作
    通常,若一个类需要析构函数,则代表其默认的析构函数不足以释放类所拥有的资源,其中最典型的就是指针成员(析构时需要手动去释放指针指向的内存)
    若存在自定义(且正确)的析构函数,但使用默认的拷贝构造函数,那么拷贝过去的只是指针,此时两个对象的指针变量指向同一块内存,指向同一块内存的后果很有可能是在两个对象中的析构函数中先后被释放两次。因此需要额外的拷贝控制函数去控制相应资源的拷贝
    所以这类例子的共同点就是:一个对象拥有额外的资源(指针指向的内存),但另一个对象使用默认的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后,析构函数释放了资源,这时另一个对象便失去了这块资源(但程序员还不知道)
class HasPtr{
   
public:
	HasPtr(const std::string &s = std::string()):
		ps(new std::string(s)), i(0) {
    }
	~HasPtr(){
    delete ps; }
	//错误,HasPtr需要一个拷贝构造函数和一个拷贝赋值运算符
private:
	std::string *ps;
	int i;
}

HasPtr f(HasPtr hp)		//HasPtr 是传值参数,所以将被拷贝
{
   
	HasPtr ret = hp;	//拷贝给定的 HasPtr
	//处理 ret
	return ret;			//ret 和 hp 被销毁
}

HasPtr p("some values");
f(p);			//当 f 结束时,p.ps 指向的内存被释放
HasPtr q(p);	//现在 p 和 q 都指向无校内存

2)需要拷贝操作的类也需要赋值操作,反之亦然
需要拷贝操作代表这个类在拷贝时需要进行一些额外的操作。 赋值操作相当于先析构+拷贝,所以拷贝需要的赋值也需要,反之亦然
3) 析构函数不能是删除的
如果类的析构函数是删除的,那么成员便无法销毁,所以在程序中不能定义这个类的对象,可以动态分配该对象并获得其指针,但无法销毁这个动态分配的对象(delete 失效)
4)如果一个类有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的
如果没有这条规则,可能会创造出无法被删除的对象。 理论上来说,当析构函数不能被访问时,任何静态定义的对象都不能通过编译器的编译,所以这种情况只会出现在与动态分配有关的拷贝/默认构造函数身上
5) 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作
原因很简单,const或引用成员只能在初始化时被赋值一次,而合成的拷贝赋值操作会对所有成员都进行赋值。显然,它不能赋值const和引用成员,所以合成的拷贝构造函数不能被使用,即会被定义为删除的
6)所有五个拷贝控制成员应该看作一个整体
一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作
某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作,这些类通常拥有一个资源,而拷贝成员必须拷贝此资源
而拷贝一个资源会导致一些额外的开销,在这种拷贝并非必要的前提下,定义移动构造函数和移动赋值运算符的类可以避免这种开销

5.使用=default

  • 我们可以通过将拷贝控制成员定义为=default来显示的要求编译器生成合成的版本
  • 我们指定能对具有合成版本的成员函数使用 =default,即默认构造函数或拷贝控制成员
  • 当我们在类内使用=default修饰成员的声明时,合成的函数将隐式地声明为内联的,如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default
class Sales_data{
   
public:
	//拷贝控制成员,使用 default
	Sales_data() = default;
	Sales_data(const Sales_data&) = default;
	Sales_data& operator=(const Sales_data &);
	~Sales_data() = default;
}
Sales_data& Sales_data::operator=(const Sales_data&) = default;

6.阻止拷贝
对于某些类来说,拷贝构造函数和拷贝赋值运算符没有合理的意义,在此情况下,定义类时必须采用某种机制组织拷贝或赋值
为了阻止拷贝,看起来应该 不定义拷贝控制成员,但这种策略是无效的,因为如果我们的类未定义这些操作,编译器将为它生成合成的版本
1)定义删除的函数
在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝
删除的函数:我们虽然定义了它们,但不能以任何方式使用,在函数的参数列表后面加上 =delete 来指出我们希望将它定义为删除的
=delete 与 =default 的不同

  • =delete 必须出现在函数第一次声明的时候
  • 我们可以对任何函数指定 =delete,我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用 =default,虽然删除函数的主要用途是禁止拷贝控制成员,但我们希望引导函数匹配过程时,删除函数有时也是有用的
    2)析构函数不能是删除的成员
    3)合成的拷贝控制成员可能是删除的
    如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的
    具体例子见 p 450
    4)private拷贝控制
  • 通过将拷贝控制成员声明为 private 并且不定义它们,从而阻止用户代码、友元和成员函数都无法进行拷贝
  • 声明但不定义一个成员函数是合法的,试图访问一个未定义的成员将导致一个链接时错误
  • 通过声明但不定义 private 的拷贝构造函数,我们可以预先阻止任何拷贝该类型的企图,试图拷贝对象的用户代码将在编译阶段被标记为错误;成员函数或友元函数中的拷贝操作将导致链接时的错误
  • 新标准希望阻止拷贝的类都应使用 =delete 来定义自己的拷贝构造函数和拷贝赋值运算符,而不应将他们声明为 private 的

13.2 拷贝控制和资源管理

管理类外资源的类必须定义拷贝控制成员,这种类需要通过析构函数来释放对象所分配的资源,一旦一个类需要析构函数,则几乎肯定需要一个拷贝构造函数和一个拷贝赋值运算符
为了定义这些成员,首先需要确定此类型对象的语义,一般来说有两种选择:可以定义拷贝操作,使类的行为看起来像一个值或一个指针

  • 类的行为像一个值,意味着它应该也有自己的状态,当拷贝一个像值的对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响,反之亦然 eg: string 类的行为像一个值
  • 类的行为像一个指针,这种类共享状态,当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据,改变副本也会改变原对象,反之亦然 egÿ
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值