C++拷贝控制之拷贝、赋值与销毁
标签:指针类型 合成 tor 生成 拷贝构造 tco out stp 自动
本文主要是《C++ Primer Ed5》第13章内容,希望能够对C++的拷贝控制了解的更为深入一些。
概述
C++中的拷贝控制操作主要涉及的几个拷贝控制函数为:
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
其中,
- 1和3定义了当用同类型的另一个对象【初始化】本对象时做什么,2和4定义了当用同类型的另一个对象【赋值】本对象时做什么,5定义了此类型对象销毁时做什么
- 如果一个类没有定义这些拷贝控制成员,编译器会自动为其定义缺失的操作
拷贝构造函数
- 关键点
- 即便用户定义了其他构造函数,编译器也会合成一个拷贝构造函数(但默认构造函数则是用户定义了构造函数,则编译器不会再提供合成构造函数)
- 拷贝构造函数传参必须是引用,如果传值,会陷入循环,一般是const的
- 一个类中如果有移动构造函数,则拷贝初始化是通过移动构造实现的
- 注意区分直接初始化和拷贝初始化的区别
- 前者相当于函数匹配的过程,后者是拷贝的过程,如vector中的push和emplace
- 调用拷贝构造初始化的几种情况
- 用等号
=
定义变量时
- 以非引用的形式给函数传参
- 函数返回一个非引用形式的对象作为返回值(现在gcc已经做优化了,编译时加上-fno-elide-constructors才会调用)
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
拷贝赋值运算符
- 重载赋值运算
- operator关键字后接入要定义的运算符,赋值运算符即函数
operator=
- 重载运算符的参数宝石运算符的运算对象,对于某些运算符,必须定义为类的成员函数,这样的话,运算符左侧的运算对象就能够绑定到隐式的this参数上
析构函数
- 关键点
- 一个类只能有唯一一个析构函数,且不能被重载
- 析构时按初始化的顺序逆序析构
- 隐式析构时内置的指针类型不会delete其指向的对象,需要手动释放资源
- 当指向一个对象的引用或指针离开作用域时,不会调用析构
- 析构函数本身并不是直接销毁成员,成员是在析构函数体之后隐含的析构阶段被销毁的,用户自定义的析构函数可以理解为不平凡的析构函数,可以用
std::is_trivial::value
查看,见代码中的TestPOD
,这部分暂不深入
- 执行析构函数的几种情况
- 变量离开作用域
- 对象被销毁,成员被销毁
- 容器(STL或数组)被销毁时,其元素被销毁
- 动态分配的对象,显示调用delete运算符时
- 临时对象,创建它的完整表达式结束时
三/五法则
- 一个类有三个基本的控制类的操作:拷贝构造函数、拷贝赋值运算符、析构函数,新标准下,还可以定义移动构造函数和移动赋值运算符
- 需要析构函数的类也需要拷贝和赋值操作
- 这里可以这样理解:对于需要显示声明析构函数的类(即具有不平凡析构函数),说明类中具有需要手动操作或释放的资源(特别是类中的指针成员),如果这里使用合成拷贝构造函数,那么合成拷贝构造函数默认的执行操作只是把指针再指向新的对象,这就会导致两个指针指向同一个对象,最后出现double free的问题
- 需要拷贝操作的类也需要赋值操作,反之亦然
控制各个拷贝函数
- default关键字
- 显示要求编译器生成合成版本的控制函数
- 如果在类内使用,编译器会将其隐式声明为inline,如果不希望是inline,只能在类外定义时加入default
- delete
- c++11标准可以在函数列表后添加
=delete
将函数定义为删除的,这样用户便不可再调用之
-
=delete
必须出现在函数第一次声明时,而=default
则不是
- 编译器在一开始编译时就需要知道该函数是否可删除,而default是编译器在生成代码时才需要
- 不能delete析构函数
- 析构函数被delete的类,可以被动态分配,但是不能delete,见代码
TestDefaultAndDelete
- private拷贝控制
- 还可以将拷贝构造函数等定义为private,以阻止用户使用,但并不能阻止类内其他成员和友元使用
- 如果为了不让其他成员和友元使用,可以声明为private,但是不定义
- 如果需要阻止拷贝,建议还是使用
delete
代码
#include
#define PRINT_INFO(str) std::cout id_);
}
FDatas(const FDatas& fd) {
this->id_ = fd.id_;
this->value_ = fd.value_;
PRINT_MEM_INFO("this is copy constructor. id_ = ", this->id_);
}
// 重载运算符,必须定义为成员函数,这样运算符左侧的则能绑定到隐式的this参数中
FDatas& operator=(const FDatas& fd) {
this->id_ = fd.id_;
this->value_ = fd.value_;
PRINT_MEM_INFO("this is copy operator, id_ = ", fd.id_);
return *this;
}
~FDatas() { PRINT_INFO("this is deconstructor"); }
public:
void Print() { PRINT_MEM_INFO("Print id_ = ", this->id_); }
private:
std::string id_;
int value_;
};
// gcc会做优化,返回临时对象时,不会构造临时对象了,加上-fno-elide-constructors才会
FDatas CopyData(FDatas fd) {
PRINT_INFO("test CopyData");
// 拷贝构造
FDatas tmp_fd = fd;
return tmp_fd;
}
void TestCopyConstructor() {
FDatas fd("abc", 13); // 直接初始化
FDatas fd1 = fd; // 拷贝初始化
FDatas fd1_1(fd); // 直接初始化
PRINT_INFO("test copy data");
// fd1拷贝给形参,调用拷贝构造函数
FDatas fd2 = CopyData(fd1);
FDatas fd3("ff", 12);
fd3 = fd2; // 拷贝赋值运算符
PRINT_INFO("sdf");
fd3.~FDatas(); // 即便显示调用了析构函数,最后还是会调用一次析构,因为这时候对象还是在内存中
fd3.Print();
PRINT_INFO("sdf111");
}
// POD
class A {};
class A1 {
A1(const A& a) {}
};
class B {
~B();
};
// 用std::is_trivial::value判断是否为平凡类型
void TestPOD() {
PRINT_MEM_INFO("is_trival FDatas: ", std::is_trivial::value);
PRINT_MEM_INFO("is_trival A: ", std::is_trivial::value); // 1
PRINT_MEM_INFO("is_trival A1: ", std::is_trivial::value); // 0, 有不平凡的构造函数
PRINT_MEM_INFO("is_trival B: ", std::is_trivial::value); // 0, 有不平凡的析构函数
}
class FDataNew {
public:
FDataNew() = default;
FDataNew(const FDataNew& fdn) = default;
FDataNew& operator=(const FDataNew& fdn);
~FDataNew() = default;
};
FDataNew& FDataNew::operator=(const FDataNew& fdn) = default;
struct FD {
FD() = default;
~FD() = delete;
};
void TestDefaultAndDelete() {
FDataNew fdn;
FDataNew fdn1(fdn);
FD* fd = new FD(); // Ok
// delete fd; // error, 析构函数被delete了
}
int main(int argc, char* argv[]) {
TestCopyConstructor();
TestPOD();
TestDefaultAndDelete();
return 0;
}
C++拷贝控制之拷贝、赋值与销毁
标签:指针类型 合成 tor 生成 拷贝构造 tco out stp 自动
原文地址:https://www.cnblogs.com/xiangs/p/12774360.html
评论