文章目录
前言
本文将介绍类如何控制该类型对象拷贝、赋值、移动或销毁时做什么。类通过一些特殊的成员函数控制这些操作,包括:拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符以及析构函数。
拷贝控制操作:
copy constructor, copy-assignment operator, move constructor, move-assignment operator, destructor
拷贝、赋值与销毁
拷贝构造函数
calss Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数, 必须引用类本身
};
直接初始化:要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中
string dots(10,'.'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string nb = "ljdf"; //拷贝初始化
string ni = string(100,'9'); //拷贝初始化
以下都是拷贝初始化:
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些标准库容器,当调用insert或push时,容器会对其元素进行拷贝初始化;相对的,用emplace成员创建的元素都进行直接初始化
编译器可以绕过拷贝构造函数
拷贝/移动构造函数必须时存在且可访问的(例如不能时private的)
拷贝赋值运算符
类控制其对象如何初始化、如何赋值
重载赋值运算符
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符,返回指向左侧运算对象的引用
};
合成拷贝赋值运算符synthesized copy-assignment operator
//等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs){
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this;
}
析构函数
析构函数与构造函数操作相反
class Foo{
public:
~Foo(); //析构函数,波浪号+类名,无返回值,不接受类名
};
由于析构函数不接受参数,因此它不能被重载。对于一个给定类,只会有唯一一个析构函数。
内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象
什么时候会调用析构函数
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建他的完整表达式结束时被销毁
{ //新作用域
//p和p2指向动态分配的的对象
Sales_data *p = new Sales_data; //p是一个内置指针
auto p2 = make_shared<Sales_data>(); //p2是一个shared_ptr
//这里不是直接初始化吗??? 为什么会用拷贝构造函数。因为item(*p)调用拷贝构造函数
// 从而直接初始化和拷贝初始化怎么分的?不能以有没有=区分吧?
Sales_data item(*p); //拷贝构造函数将*p拷贝到item中
vector<Sales_data> vec; //局部对象
vec.push_back(*p2); //拷贝p2指向的对象
delete p; //对p指向的对象执行析构函数
} //退出局部作用域;对item、p2和vec调用析构函数
//销毁p2会递减其引用计数;如果引用计数变为0,对象被释放
//销毁vec会销毁它的元素
synthesized destructor
class Sales_data{
//成员会被自动销毁,除此之外不需要做其他事情
~Sales_data(){}
//...
};
class X{
public:
X() {cout << "X()" << endl;}
X(int i):a(i){cout << "X(int i)" << endl;}
X(const X& ref) {
this->a = ref.a;
cout << "X(const X&)" << endl;
}
X& operator=(const X& ref){
this->a = ref.a;
cout << "X& operator=(const X&)" << endl;
return *this;
}
~X(){cout << "~X()" << endl;}
int print_val(){
return a;
}
private:
int a;
};
int main(){
{
X test1(9)/*调用普通构造函数*/, test2=test1, test3(test2)/*他俩都调用拷贝构造函数*/;
// X test2;
// test2 = test1;
cout << test2.print_val()<<endl;
}
return 0;}
三/五法则
如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数
class HasPtr{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { }
~HasPtr() { delete ps; }
// 错误:HasPtr需要一个 拷贝构造函数 和一个 拷贝赋值运算符
// 其他成员的定义
};
使用=default
class Sales_data{
public:
//拷贝控制成员;使用default
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data &);
~Sales_data() = default;
// other members defination, as usual
};
Sales_data& Sales_data::operator=(const Sales_data&) = default;
阻止拷贝
定义删除的函数
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的析构函数是删除的
NoDtor *p = new NoDtor(); //✔ 但是不能delete p
delete p; //❌ NoDtor的析构函数是删除的
本质上,当不可能拷贝、赋值或销毁类的成员时,类的合成拷贝控制成员就被定义为删除的
旧版本通过将拷贝控制成员声名(而不定义)在private下从而阻止其拷贝,应该弃用此方法,使用新的 =delete
拷贝控制和资源管理
行为像值的类
class HasPtr{
public:
HasPtr(const string &s = string()):ps(new string(s), i(0)) {}
// 对ps指向的string,每个HasPtr对象都有自己的拷贝
HasPtr(const HasPtr &p): ps(new string( *(p.ps) )), i(p.i) {}
HasPtr& operator=(const HasPtr &);
~HasPtr(){delete ps};
private:
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;
}
类值拷贝赋值运算符
定义行为像指针的类
引用计数 将引用计数器定义在动态内存中
class HasPtr{
public:
//构造函数分配新的string和新的计数器,将计数器置为1
HasPtr(const string &s = string()):
ps(new 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:
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; //将数据从rhs拷贝到本对象
i = rhs.i;
use = rhs.use;
return *this; //返回本对象
}
交换操作
编写自己的swap函数
class HasPtr{
friend void swap(HasPtr&, HasPtr&);
// 其他内容
};
inline void swap(HasPtr *lhs, HasPtr &rhs){
using std::swap; //如果HasPtr的数据成员没有swap则调用std的swap,否则调用的不是std的swap
//这样可以重载swap
swap(lhs.ps, rhs.ps); //交换指针,而不是string数据
swap(lhs.i, rhs.i) ; //交换int成员
}
与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。
swap函数应该调用swap,而不是std::swap
在赋值运算符中使用swap
拷贝并交换(copy and swap)
//注意rhs时按值传递的,意味着HasPtr的拷贝构造函数
//将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs){
//交换左侧运算对象和局部变量rhs的内容
swap(*this, rhs); //rhs现在指向本对象曾经使用的内存
return *this; //rhs被销毁,从而delete了rhs中的指针
}
拷贝控制示例
动态内存管理
简化本vector,StrVec类的设计
// 类vector类内存分配策略
class StrVec{
public:
StrVec(): //allocator成员进行默认初始化
elements(nullptr), first_free(nullptr), cap(nullptr){}
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
~StrVec();
void push_back(const std::string &);
size_t size() const
};
对象移动
右值引用
右值引用是将一个变量绑定到一个临时对象上,从而避免对象拷贝,右值引用本质上是改变对象的所有权
右值引用不能应用于左值,可以通过std::move()函数将“左值变为右值”
rvalue reference
可以将一个右值引用绑定到常量上(右值),但不能将一个右值引用直接绑定到一个左值上
int i = 42, &r=i; //✔
int &&rr = i; //❌
int &r2 = i * 42; //❌
const int &r3 = i*42; //正确
int &&rr2 = i*42; //✔
左值持久;右值短暂
变量是左值
可以看作只有一个运算对象而没有运算符的表达式
int &&rr1 = 42; //✔
int &&rr2 = rr1; //错误
标准库move函数
int &&rr3 = std::move(rr1); //OK 把rr1当成右值
移动构造函数和移动赋值运算符
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;
}
移动赋值运算符
StrVec &StrVec::operator=(StrVec &&rhs) noexcept /*:xxx 必须在类的声名和定义中都指定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;
}
合成的移动操作
右值引用和成员函数
两个版本
void push_back(const X&); //拷贝
void push_back(X&&); //移动
右值和左值引用成员函数
class Foo{
public:
Foo &operator=(const Foo&) &; 只能向可修改的左值赋值
};
Foo &Foo::operator=(const Foo &rhs) &{
//执行rhs赋予本对象所需的工作
return *this;
}
总结
拷贝、赋值、移动 控制成员
Foo(Foo&);
移动构造函数,移动赋值运算符
拷贝构造函数
Foo(Foo&);
拷贝赋值运算符
Foo& operator=(const Foo&);
析构函数
~Foo();