当前位置: 首页 > news >正文

【C++ Primer笔记】第十三章 拷贝控制

第十三章 拷贝控制
一个类通过定义五种特殊的成员函数来控制这些操作,包括:
拷贝构造函数(copy constructor).、
拷贝赋值运算符(copy-assignment operator)、
移动构造函数(move constructor)、
移动赋值运算符(move-assignment operator)和析构函数(destructor)。
如果一个类没有定义这些操作,编译器会自动合成缺失的操作

拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数定义了当此类型对象销毁时做什么。我们称这些操作为拷贝控制操作(copy control).
如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。因此,很多类会忽略这些拷贝控制操作。但是,对一些类来说,依赖这些操作的默认定义会导致灾难。通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。

拷贝构造函数
拷贝构造函数通常不应该是explicit的
//拷贝构造函数:第一个参数是自身类型的引用,额外参数都有默认值
class Foo {
public:
Foo() { cout << “默认构造!” << endl; };
Foo(const Foo&) { cout << “拷贝构造!” << endl; }; //拷贝构造函数
//…
};
Foo f(Foo f){ return f; };

合成拷贝构造函数
//Sales_data类的合成拷贝构造函数等价于:
class Sales_data{
public:
//其他成员与构造函数的定义,如前
//与合成的拷贝构造函数等价的拷贝构造函数的声明
Sales_data(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo),//使用string的拷贝构造函数
unitis_sold(orig.units_sold),
revenue(orig.revenue)
{ }

拷贝初始化
拷贝构造函数用来初始化非引用类类型 参数,所以自己的参数必须是引用类型
string dots(10, ‘.’); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string null_book = “9-999-99999-9”; //拷贝初始化
string nines = string(100, ‘9’); //拷贝初始化

拷贝初始化的限制
vector v1(10); //正确:直接初始化
vector v2 = 10; //错误:接受大小的构造含是explicit的
void f(vector); //f的参数进行拷贝初始化
f(10); //错误:不能用一个explicit的构造函数来拷贝一个实参
f(vector(10)); //正确:从一个int直接构造一个临时vector

编译可以绕过拷贝构造函数
string null_book =“9-999-99999-9”;//拷贝初始化
//改写为
string null_book(“9-999-99999-9”); //编译器略过了拷贝构造函数

拷贝赋值运算符
Sales_data trans, accum;
trans = accum; //使用Sales_data的拷贝赋值运算符
//拷贝赋值运算符接受一个与其所在类相同类型的参数
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
}

合成拷贝赋值运算符
//等价于合成拷贝赋值运算符
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; //返回一个此对象的引用
}

析构函数:不接收参数,不允许重载
//构造函数初始化对象的非static数据成员
//析构函数释放对象的使用资源,并销毁对象的非static数据成员
class Foo{ ~Foo();//析构函数
//… };

下面代码段定义了四个Sales_data对象:
{//新作用域
//p和p2指向动态分配的对象
Sales_data *p = new Sales_data;
//p是一个内置指针
auto p2 = make_shared<Sales_data>(); //p2是一个shared_ptr
Sales_data item(p);
//拷贝构造函数将
p拷贝到item中
vector<Sales_data> vec;
//局部对象
vec.push_back(*p2);
//拷贝p2指向的对象
delete p;
//对p指向的对象指向析构函数
}//退出局部作用域;对item、p2和vec调用析构函数
//销毁p2会递减其引用计数;如果引用计数变为0,对象被释放
//销毁vec会销毁它的元素

当指向一个对象的引用或指针离开作用域时,不会执行析构函数
合成析构函数
//等价于Sales_data的合成析构函数
class Sales_data{
public:
//成员会被自动销毁,除此之外不需要做其他事情
~Sales_data(){ }
//在析构函数体执行完毕后,成员会被自动销毁
//其他成语的定义,如前
};

需要自定义析构函数的类也需要拷贝和赋值操作
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0){ }
~HasPtr(){ delete ps; }
//错误:HasPtr需要一个拷贝构造函数和一个拷贝赋值运算符
//其他成员的定义省略…
};
HasPtr f(HasPtr hp) //HasPtr是传值参数,所以将被拷贝
{
HasPtr ret = hp; //拷贝给定的HasPtr
//处理ret
return ret; //ret和hp被销毁
}
HasPtr p(“some values”);
f§;//f结束时,p.ps指向的内存被释放
HasPtr q§; //现在p和q都指向无效内存

需要拷贝操作的类也需要赋值操作,反之亦然:
• 考虑一个类为每个对象分配一个唯一的ID
• 需要自定义拷贝构造函数,和赋值运算操作符
• 不需要自定义一个析构函函数

使用=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;//非内联
//我们只能对具有合成版本的成员函数使用=default
阻止拷贝:例如,iostream类阻止拷贝,以免多个对象写入或读取相同的IO缓冲
//=delete通知编译器,我们不希望定义这些成员
//删除的函数(deleted function)
struct NoCopy {
NoCopy() = default; //使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator = (const NoCopy&) = delete; //阻止拷贝
~NoCopy() = default; //使用合成的析构函数
//其他成员
};
与=default不同,=delete必须出现在函数第一次声明的时候
对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针:
struct NoDtor {
NoDtor() = default; //使用合成默认构造函数
~NoDtor() = delete; //我们不能销毁NoDTor类型的对象
};
NoDtor nd; //错误
NoDtor *P = new NoDtor(); //正确
delete p; //错误
本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
在新标准发布之前,阻止拷贝时通过声明为private来实现的:
class PrivateCopy{
//无访问说明符;接下来的成员默认为private的;
//拷贝控制成员是private的,因此普通用户代码无法访问
PrivateCopy(const PrivateCopy&);
PrivateCopy &operator = (const PrivateCopy&);
//其他成员
public:
PrivateCopy() = default; //使用合成的默认构造函数
~PrivateCopy(); //用户可以定义此类型的对象,但无法拷贝他们
};
友元和成员函数仍旧可以拷贝对象

拷贝控制和资源管理
两种选择:定义拷贝操作,使类的行为看起来像一个值或者像一个指针行为像值的类
对于管理的资源,每个对象都应该拥有一份自己的拷贝
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0){ }
//对ps指向的string,每个HasPtr对象都有自己的拷贝
HasPtr(const HasPtr &p):ps( new std::string(*p.ps) ),i(p.i){ }
HasPtr& operator=(const HasPtr &); ~HasPtr(){ delete ps; }
private:
std::string *ps;
int i;
};
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps);//拷贝底层string
delete ps; //释放就内存
ps = newp; //从右侧运算对象拷贝数据到本对象
i = rhs.i;
return *this; //返回本对象
}
• 如果将一个对象赋予它自己,赋值运算符必须能正确工作
• 大多数赋值运算符组合了析构函数和拷贝构造函数的工作

下面的赋值运算符是错误的!
HasPtr& HasPtr::operator=(const HasPtr &rhs){
delete ps; //释放对象指向的string
//如果rhs和*this是同一个对象,我们就将从已释放的内存中拷贝数据!
ps = new string(*rhs.ps);
i = rhs.i;
return *this;
}
定义行为像指针的类
我们这里不使用shared_ptr,而是设计自己的引用计数
class HasPtr{
public:
//构造函数分配新的string和新的计数器,将计数器置为1
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0),use(new std::size_t(1)){ }
//拷贝构造函数拷贝所有三个数据成员,并递增计数器
HasPtr(const HasPtr &p):
ps(p.ps),i(p.i),use(p.use) {++*use;}
HasPtr& operator=(const HasPtr&); ~HasPtr();
private:
std::string *ps;
int i;
std::size_t use; //用来记录有多少个对象共享ps的成员
};
HasPtr::~HasPtr()
{
if(–*use==0){ //如果引用计数变为0
delete ps; //释放string内存
delete use; //释放计数器内存
}
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use; //递增右侧运算对象的引用计数
if(–*use == 0){ //然后递减本对象的引用计数
delete ps;
delete use;
}
ps = rhs.ps;
i = rhs.i;
use = rhs.use;
return * this;
}

交换操作
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)),i(0){ }
//对ps指向的string,每个HasPtr对象都有自己的拷贝
HasPtr(const HasPtr &p):ps( new std::string(*p.ps) ),i(p.i){ }
HasPtr& operator=(const HasPtr &); ~HasPtr(){ delete ps; }
private:
std::string *ps;
int i;
};

假设需要交换两个HasPtr对象,v1和v2
HasPtr temp = v1; //创v1的值的一个零时副本
v1 = v2;
v2 = temp;

我们希望交换指针,而不是分配string的新副本
string *temp = v1.ps; //为v1.ps中的指针创建一个副本
v1.ps = v2.ps;
v2.ps = temp;

编写我们自己的swap函数
class HasPtr{
friend void swap(HasPtr&,HasPtr&);
//其他成员定义… };
inline void swap(HasPtr &lhs,HasPtr &rhs)
{
using std::swap;
swap(lhs.ps,rhs.ps); //交换指针,而不是string数据
swap(lhs.i,rihs.i); //交换int成员
}

//假定类Foo有HasPtr的成员h
//下面的代码能够正常运行,但性能… void swap(Foo &lhs, Foo &rhs)
{
//错误:这个函数使用了标准库版本的swap,而不是HasPtr版本
std::swap(lhs.h,rhs.h);
//交换类型Foo的其他成员
}
//正确的swap函数
void swap(Foo &lhs, Foo &rhs)
{
using std::swap;
swap(lhs.h,rhs.h); //使用HasPtr版本的swap
//交换类型Foo的其他成员
}

在赋值运算符中使用swap
//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数
//将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
//交换左侧运算对象和局部变量rhs的内容
swap(*this,rhs); //rhs现在指向本对象曾经使用的内存
return *this; //rhs被销毁,从而delete了rhs中的指针
}

对象移动
• 标准库容器、string和shared_ptr类既支持移动也支持拷贝
• IO类和unique_ptr类可以移动但不能拷贝

右值引用
• 通过&&而不是&来获得右值引用
• 只能绑定到一个将要销毁的对象

左值持久:对象的身份
右值短暂:对象的值

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

变量是左值,即使这个变量是右值引用
int &&rr1 = 42; //正确:字面常量时右值
int &&rr2 = rr1; //错误:表达式rr1是左值

标准库move函数
//move告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它
int &&rr3 = std::move(rr1); //ok

移动构造函数和移动赋值运算符
类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源

不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
//移动构造函数
StrVec::StrVec(strVec &&s) noexcept //移动操作不应抛出任何异常
//成员初始化器接管s中的资源
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//令s进入这样的状态–对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}

移动构造函数不分配新内存
最终,移后源对象会被销毁,如果我们忘记了改变s.first_free,则销毁移后源对象就会释放掉我们刚刚移动的内存

移动赋值运算符
StrVec &StrVec::operator = (StrVec &&rhs) noexcept{
//直接检测自赋值
if(this != &rhs){
free(); //释放已有元素
elements = rhs.elements; //从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}

合成的移动操作
只有当一个类没有定义任何自己版本的拷贝控制成员,且所有数据成员都能移动构造或移动赋值时,编译器才会合成移动构造函数或移动赋值运算符

//编译器会为X和hasX合成移动操作
struct X{
int i; //内置类型可以移动
std::string s; //string定义了自己的移动操作
};
struct hasX{
X mem; //X有合成的移动操作
};
X x, x2 = std::move(x); //使用合成的移动构造函数
hasX hx, hx2 = std::move(hx); //使用合成的移动构造函数

假定Y是一个类,定义了拷贝构造函数但未定义移动构造函数
struct hasY{
hasY() = default;
hasY(hasY&&) = default;
Y mem; //hasY将有一个删除的移动构造函数
};
hasY hy, hy2 = std::move(hy); //错误:移动构造函数是删除的

移动右值,拷贝左值……
//移动构造函数、拷贝构造函数同时存在,编译器使用普通的函数匹配规则
//赋值操作的情况也类似
StrVec v1, v2;
v1 = v2; //v2是左值;使用拷贝赋值
StrVec getVec(istream &); //getVec返回一个右值
v2 = getVec(cin); //getVec(cin)是一个右值;使用移动赋值

如果没有移动构造函数,右值也被拷贝
class Foo{
public:
Foo() = default;
Foo(const Foo&); //拷贝构造函数
//其他成员定义,但Foo未定义移动构造函数
};
Foo x;
Foo y(x); //拷贝构造函数;x是一个左值
Foo z(std::move(x)); //拷贝构造函数,因为未定义移动构造函数

拷贝并交换赋值运算符和移动操作
class HasPtr{
public:
//添加的移动构造函数
HasPtr(HasPtr &&p) noexcept: ps(p.ps),i(p.i){p.ps = 0;}
//赋值运算符即是移动赋值运算符,也是拷贝赋值运算符
HasPtr& operator=(HasPtr rhs)
{ swap(*this,rhs); return *this;}
//其他成员函数的定义…
}
//假定hp和hp2都是HasPtr对象
hp = hp2; //hp2是一个左值;hp2通过拷贝构造函数来拷贝
hp = std::move(hp2); //移动构造函数移动hp2

右值引用和成员函数
class StrVec{
public:
void push_back(const std::string&); //拷贝
void push_back(std::string&&); //移动
//其他成员定义,如前
};
void StrVec::push_back(const string&s)
{
chk_n_alloc(); //确保有空间容纳新元素
//在first_free指向的元素中构造s的一个副本
alloc.construct(first_free++,s);
}
void StrVec::push_back(string &&s)
{
chk_n_alloc();//如果需要的话为StrVec重新分配内存
alloc.construct(first_free++,std::move(s));
}
StrVec vec; //空Strvec
string s =
“some string or another”;
vec.push_back(s); //拷贝
vec.push_back(“done”); //移动

右值和左值引用成员函数,引用限定符可以是&或&&
string s1 =
“a value”
, s2 =
“another”;
auto n = (s1 + s2).find(‘a’);
s1 + s2 =
“wow!”;
//阻止对一个右值进行赋值
//引用限定符
class Foo {
public:
Foo &operator=(const Foo&) &; //只能向可修改的左值赋值
//…
};
Foo &Foo::operator=(const Foo &rhs) &{
//…
return *this;
}

Foo &retFoo(); //返回一个引用;retFoo调用是一个左值
Foo retVal(); //返回一个值;retVal调用是一个右值
Foo i,j; //i和j是左值
i=j; //正确
retFoo() = j; //正确
retVal() = j; //错误
i = retVal(); //正确
//与const一起使用必须在const限定符之后
class Foo{
public:
Foo someMem() & const; //错误:const限定符必须在前
Foo anotherMem() const &; //正确
};

重载和引用函数
class Foo{
public:
Foo sorted() &&;
Foo sorted() const &;
//Foo的其他成员的定义
private:
vector data;
};
//本对象为右值,因此可以原址排序
Foo Foo::sorted() &&
{
sort(data.begin(),data.end());
return this;
}
//本对象是const或是一个左值,不能对其进行原址排序
Foo Foo::sorted() const &{
Foo ret(this); //拷贝一个副本
sort(ret.data.begin(),ret.data.end()); //排序副本
return ret;
}
retVal().sorted(); //&&
retFool().sorted(); //&
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符
class Foo{
public:
Foo sorted() &&;
Foo sorted() const; //错误:必须加上引用限定符
using Comp = bool(const int&,const int&);
Foo sorted(Comp
); //正确:不同的参数列表
Foo sorted(Comp
) const; //正确:两个版本都没有引用限定符
};

相关文章:

  • MySQL使用索引的最佳指南
  • Linux扫描第3次笔记
  • HBuilder X 快捷键,多行代码右移动,左移动
  • 计算机基础——无处不网络(2)
  • 数据结构进阶 unordered系列的效率对比
  • VScode远程连接Linux
  • QTreeView ui相关
  • [贪心]376. 摆动序列 53. 最大子序和 122.买卖股票的最佳时机II 55. 跳跃游戏 45. 跳跃游戏II
  • 【SpringBoot3】SpringBoot中实现全局统一异常处理
  • 分支语句(选择结构)——“C”
  • 【寒假每日一题】洛谷 P6421 [COCI2008-2009#2] RESETO
  • aws codesuit 在codebuild和codepipeline中集成jenkins
  • DPU网络开发SDK—DPDK(八)
  • DW 2023年1月Free Excel 第五次打卡 文本函数
  • 第四层:友元与函数成员别样定义
  • SpringCloud(11):Hystrix请求合并
  • SpringBoot的自动配置
  • C语言萌新如何使用scanf函数?
  • TryHackMe-NahamStore(常见web漏洞 大杂烩)
  • 【Java入门】Java数据类型