C++ Primer第13章 拷贝控制

C++Premier 学习笔记(第十三章 拷贝控制(未完))


一、13.1 拷贝、赋值与销毁

13.1.1 拷贝构造函数

在这里插入图片描述
注意:拷贝构造函数的第一个参数必须是一个引用类型,拷贝构造函数通常不应该是explict的

合成拷贝构造函数
如果我们没有定义拷贝构造函数,编译器会给我们定义一个,与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。
在这里插入图片描述
拷贝初始化
在这里插入图片描述
拷贝初始化不仅仅在我们用=定义变量的时候发生,在下列情况也会发生
1.将一个对象作为实参传递给一个非引用类型的形参
2.从一个返回类型为非引用类型的函数返回一个对象
3.用花括号列表初始化一个数组中的元素或一个聚合类中的成员


13.1.2 拷贝复制运算符

重载赋值运算符

class Foo{
pubilc:
   Foo& oprator=(const Foo&);//赋值运算符  
   //。。。
};

合成拷贝赋值运算符
与处理拷贝构造函数一样,如果一个类没有定义自己的拷贝赋值运算符,编译器会为他生成一个合成拷贝赋值运算符。
在这里插入图片描述

13.1.3 析构函数

class Foo{
 public:
    ~Foo(); //析构函数
};

析构函数的工作:在一个析构函数中,首先执行函数体,然后销毁成员,成员按照初始化顺序进行逆序销毁,通常,析构函数释放对象在生存期分配的所有资源。


由于析构函数不接受任何参数,因此不能被重载,对于给定类,只会有唯一一个析构函数。
无论何时一个对象被销毁,就会自动调用其析构函数:
①变量在离开作用域时被销毁;
②当一个对象被销毁,其成员被销毁
③容器被销毁,元素被销毁
④对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
⑤对于临时对象,当创建它的完整表达式结束时被销毁。

如图

13.1.4 三/五法则

有三个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、析构函数,在新的标准下,一个类还可以定义一个移动构造函数和一个移动赋值运算符。
注意:
需要析构函数的类也需要拷贝和赋值操作
需要拷贝操作的类也需要赋值操作,反之亦然


13.1.5使用=default

我们可以通过拷贝控制成员定义为=default来显示要求编译器生成合成版本。

class Sales_data{
public:
      //拷贝控制成员;使用defaul
    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;

当我们在类内使用=default 修饰成员声明时,合成的函数隐式地声明为内联函数;如果我们不希望合成的成员是内联函数,那么应该对成员的类外定义使用=default,就像拷贝赋值运算符所做的那样


13.1.6阻止拷贝

对于某些类来说,拷贝操作并没有意义,则定义的时候必须采用某种机制去阻止拷贝或者赋值
定义删除的函数
在新的标准下,可以定义删除的函数去阻止拷贝

struct NoCopy{
   NoCopy()=default; //使用合成的默认构造函数
   NoCopy(const NoCopy &)=delete; //阻止拷贝
   NoCopy& operator=(const NoCopy&)=delete; //阻止赋值
   ~NoCopy()=default;//使用合成的析构函数
   //其他成员
};

=delete 通知编译器,我们不希望定义这些成员,与=default不同,=delete必须出现在函数第一次生命的时候;另一个不同之处是我们可以对任何函数指定=delete(我们只能对编译器可以合成的默认构造函数或者拷贝控制成员=default)


析构函数不能是删除的成员
我们不能删除析构函数,如果析构函数被删除了,那就无法销毁此类型的对象了

在这里插入图片描述
在这里插入图片描述
合成的拷贝控制成员有可能是删除的
C++premier 第五版 P450 几种情况


private拷贝控制
在这里插入图片描述
由于拷贝构造函数和拷贝赋值运算符是私有的,用户代码不能拷贝对象,但是,友元函数和成员函数仍然可以进行拷贝对象,为了阻止其拷贝,我们可以将这些成员声明为private,但是并不定义,则访问的时候会导致一个链接时错误。

二、13.2 拷贝控制和资源管理

13.2.1行为像值的类

为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝

在这里插入图片描述

类值拷贝赋值运算符
在本例中,先拷贝右侧对象,在处理自赋值情况,并能保证在异常发生时,代码也是安全的,完成拷贝之后,我们释放左侧运算对象的资源,并更新指针指向新的string。
在这里插入图片描述

13.2.2行为像指针的类

对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。


引用计数
将计数器放在动态内存中,当创建一个对象的时候,我们非陪一个新的计数器,当拷贝或者赋值的时候,我们拷贝指向计数器的指针,使用这种方法,副本和原来对象都会指向相同的计数器。


定义一个计数的类
在这里插入图片描述
类指针的拷贝成员"篡改"引用计数
当拷贝或者赋值给一个HasPtr对象时,我们希望副本和元对象指向相同的string,拷贝指针本身,同时递增计数器。
同时需要注意的是,析构条件不能无条件delete ps;可能还会有其他对象指向这块内存,因此应该递减,当计数器的值变为0时,释放内存

HasPtr::~Hasptr(){
   if(--*use == 0){
        delete ps;
        delete use;
   }
}

在这里插入图片描述

三、13.3 交换操作

除了定义拷贝控制的成员,管理资源的类通常还定义一个名字为swap的函数。
如果一个类定义了自己的swap,那么算法将使用类的自定义版本,否则,则使用标准库里的swap,为了交换两个对象,我们需要进行一次拷贝,两次赋值,如下:

HasPtr temp = v1;
v1 = v2;
2 = temp;

理论上,我们更希望swap交换的是指针,而不是分配string的新副本,如下:

string *temp = v1.ps;
v1.ps = v2.ps;
v2.ps = temp;

编写我们自己的swap函数
我们可以在类上定义一个自己版本的swap来重载swap的默认行为,如下:
在这里插入图片描述
swap函数应该调用swap,而不是std::swap
在上述例子中,数据成员是内置类型,而内置类型没有特定版本的swap,所以对swap的调用会调用,标准库std::swap;
但是,如果有一个类的成员,有自己的swap函数,调用std::swap就是错误,

void swap(Foo &lhs, Foo &rhs){
   //Foo类对象中成员h是HasPtr类型
   std::swap(lhs.h, rhs.h);
   //错误,这个函数使用了标准库版本的swap,而不是HasPtr版本的
}

但是,代码会编译通过,且整成运行,使用此版本与简单默认版本swap并没有任何性能差异,问题在于我们显式调用了标准库的swap,我们并不希望用std中版本的,则要进行改正:

void swap(Foo &lhs, Foo &rhs){
   using std::swap;
   swap(lhs.h,rhs.h);  //使用HasPtr版本的swap
   //交换类型Foo的其他成员
}

每一个swap调用都是未加限定的,每一个调用都应该是swap,而并不是std::swap,如果存在类型特定的swap函数,那么其匹配程度会优先于std中定义的版本,如果不存在类型特定的版本,则调用std中的。
在赋值运算符中使用swap
在这里插入图片描述

四、13.4 拷贝控制实例

五、13.5 动态内存管理类

在此,我们实现一个StrVec类,即vector类的一个简化版本,只适用于string。
StrVec类的设计

在这里插入图片描述
elements,指向分配内存的首位置
first_free,指向最后一个实际元素之后的位置
cap,指向分配的内存末尾之后的位置
四个具体函数:
alloc_n_copy会分配内存,并拷贝一个给定范围中的元素。
free会销毁构造的元素并释放内存。
chk_n_alloc保证SrtVec至少有容纳一个新元素的空间。如果没有空间添加新元素,则会调用reallocate来分配更多的内存。
reallocate在内存用完时为StrVec分配新内存。


SreVec类定义

class StrVec {
private:
	static allocator<string> alloc;//分配元素
	//被添加元素的函数所使用
	void chk_n_alloc() {
		if (size() == capacity())
			reallocate();
	}
	//工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
	pair<string*, string*>alloc_n_copy(const string*, const string*);
	void free();   //销毁元素并释放空间
	void reallocate(); //获取更多内存并且拷贝已有元素
	string *elements;  //指向数组首元素指针
	string *first_free;  //指向数组第一个空闲元素
	string *cap;    //指向数组尾后位置的指针
public:
	StrVec() ://allocator成员进行默认初始化
		elements(nullptr), first_free(nullptr), cap(nullptr) {}
	StrVec(const StrVec &); //拷贝构造函数
	StrVec &operator=(const StrVec &); //拷贝赋值运算符
	~StrVec(); //析构函数
	void push_back(const string&); //拷贝元素
	size_t size() const { return first_free - elements; }
	size_t capacity() const { return cap - elements; }
	string* begin() const { return elements; }
	string* end() const { return first_free; }
};

类定义了多个成员:
1.默认构造函数默认初始化alloc并将指针初始化为空,表示并没有元素
2.size成员返回当前真正在使用的元素的数目,等于firs_free-elements
3.capacity成员返回可以保存的元素的数量,等于cap-elemens
4.当没有空间容纳新元素的时候,即cap==first_free,chk_n_alloc会为StrVec重新分配内存
5.begin和end会分别返回指向首元素和最后一个构造的元素的之后的位置的指针


使用construct
在这里插入图片描述
alloc_n_copy
在这里插入图片描述
alloc_n_copy用尾指针减去首指针,来计算需要多少空间,在分配内存后,它必须在此空间中构造给定元素的副本

free
首先destroy元素,然后释放所分配的内存空间。
在这里插入图片描述
拷贝控制成员
基于alloc_n_copy和free之后,实现拷贝控制比较简单
在这里插入图片描述
构造函数会调用free:
StrVec::~StrVec(){ free(); }

处理拷贝赋值运算符
在这里插入图片描述
在重新分配内存的过程中移动而不是拷贝元素
移动构造函数和std::move,移动构造函数通常是将资源从给定对象“移动”而不是拷贝正在创建的对象,而我们知道标准库保证“移后源”string 仍然保证是一个有效的、可析构的状态。
我们使用第二个机制是move标准函数,定义在utility头文件中。

reallocate成员
在这里插入图片描述

13.6对象移动

13.6.1右值引用
所谓右值引用就是绑定到右值的引用,我们通过&&而不是&来获得右值引用,右值引用有一个重要的特点——只能绑定到一个将要销毁的对象。
类似于任何引用,一个右值引用也不过是某个对象的另一个名字而已,如我们所知,对于常规引用(我们称之为左值引用),我们不能将其绑定到要求转换的表达式、字面常量或者返回右值表达式上,但是不能将一个右值引用直接绑定到一个左值上。

int i = 42int &r = i;         //正确 r引用i
int &&r = i;        //错误 不能将一个右值引用绑定到一个左值上
int &r2 = i * 42;   //错误 i*42是一个右值
const int &r3 = i * 42;   //正确 我们可以将一个const引用绑定到一个右值上
int &&rr2 = i * 42//正确 将rr2绑定到乘法结果中

左值持久;右值短暂
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
由于右值引用只能绑定到临时对象,我们得知:
1.所引用的对象将要被销毁
2.该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源

变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似于任何表达式,变量表达式也具有左值/右值属性。变量表达式都是左值,带来的结果就是,我们不能讲一个右值引用绑定到一个右值引用类型的变量上。

int &&rr1 = 42;  //正确:字面常量是右值
int &&rr2 = rr1; //错误:表达式rr1是左值!

标准库move函数
虽然不能将一个右值引用直接绑定在一个左值上,但是我们可以显式将一个左值转换为右值,我们可以通过调用一个名字为move的新标准库函数来获得绑定到左值上的右值引用。

int &&rr3 = std::move(rr1);  //OK

move告诉编译器,我们有一个左值,但我们希望像一个右值一样处理它。
13.6.2 移动沟道函数和移动赋值运算符
类似于拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用,与拷贝构造函数一样,任何额外的参数必须有默认实参。
除了完成移动资源,移动构造函数还必须确保移动之后的源对象处于一个这样的状态——销毁它是无害的,特别的,一旦资源完成移动,源对象必须不再指向被移动的资源。
在这里插入图片描述
移动赋值运算符
在这里插入图片描述 13.6.3右值引用和成员函数

总结

越往后越枯燥乏味,越抽象,坚持下去,今天也要加油鸭~

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值