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