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 = 42;
int &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右值引用和成员函数
总结
越往后越枯燥乏味,越抽象,坚持下去,今天也要加油鸭~