拷贝构造,移动构造,析构函数,深拷贝,浅拷贝
一、深拷贝浅拷贝
1.浅拷贝:
多个数据公用一段内存空间,如果一个数据释放了该空间,其他数据就找不到该数据,从而引发错误。
浅拷贝就像两个人共用一个衣柜,他们分别拥有相同的钥匙,但是某一天如果他们其中某个人偷偷的把锁换了,那么另外一个人去开柜子的时候就会遇到麻烦。这时候如果给两个人分配不同的衣柜(深拷贝),那么这个问题也就不存在了。
2.深拷贝
每一个数据都有各自的内存空间,一个数据释放内存空间不会影响其他数据。
3.示例代码
点击查看代码
#include <iostream>
class Object
{
public:
Object() :pValue(new int(0)) { std::cout << "construct\n"; }
//Object(const Object& obj) :pValue(obj.pValue) {std::cout << "Shallow copy\n"; } // 浅拷贝,类如果含有指针,调用析构函数会崩溃
Object(const Object& obj) :pValue(new int(*obj.pValue)) { std::cout << "Deep copy\n"; } // 深拷贝
~Object() {
std::cout << "destruct\n";
if (pValue)
{
delete pValue;
pValue = nullptr;
}
}
private:
int* pValue;
};
int main()
{
{
Object obj1;
Object obj2(obj1);
}
return 0;
}
二、构造函数、拷贝构造函数、移动构造函数、重载赋值、重载移动赋值示例代码
点击查看代码
#include <iostream>
class A
{
public:
int* p = nullptr;
public:
A()
{
std::cout << "construction\n";
p = new int(1);
}
A(const A& a) noexcept
{
std::cout << "deep copy construction\n";
this->p = new int(*a.p);
}
A(A&& a) noexcept
{
std::cout << "move construction\n";
this->p = new int(*a.p);
a.p = nullptr;
}
const A& operator=(const A& a) noexcept
{ // 返回常引用,因为不可能将返回值赋值给一个const A 例如{A a1;const A a2; a2 = a1 //编译报错}
// {A a1; auto a2 = a1; //调用拷贝构造函数}
std::cout << "assignment\n";
this->p = new int(*a.p);
return *this;
}
const A& operator=(A&& a) noexcept
{
std::cout << "move assignment\n";
this->p = new int(*a.p);
a.p = nullptr;
return *this;
}
~A()
{
std::cout << "destruction\n";
delete p;
p = nullptr;
}
};
int main()
{
{
A a; //普通构造函数
A b(a); //拷贝构造
A c(std::move(a)); //移动构造,没有移动构造函数则调用拷贝构造
A aa, bb, cc; //普通构造(3次)
bb = std::move(aa); //移动赋值,没有移动赋值运算符则调用普通赋值
cc = bb; //普通赋值
A dd = std::move(cc); //移动构造,没有移动构造函数则调用拷贝构造
A ee = dd; //拷贝构造,注意和 {A a;a = b;}的区别
}
return 0;
}
三、析构函数
先执行函数体,然后按照初始化成员的逆序销毁成员。在这里需要注意的是销毁成员的并不是函数体,而是析构部分。
调用析构函数的时刻:
-
变量在离开其作用域时被销毁;
-
当一个对象被销毁时,其成员被销毁。
-
容器被销毁时,成员被销毁。
-
对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
-
对于临时对象,当创建它的赛事表达式结束时被销毁。
注意点:
类里面有动态分配内存,一定要自己来定义析构函数(如果没有,一般情况下默认的析构函数就足够了)。
如果一个类需要定义析构函数,那么几乎可以肯定它也需要一个拷贝构造函数和一个赋值操作,类中有指针类型的成员,我们必须防止浅拷贝问题,所以,一定需要拷贝构造函数和赋值操作符,这两个函数是防止浅拷贝问题所必须的。需要拷贝操作的类也需要赋值操作,反之亦然。(三法则)
本质上,当不可能拷贝、赋值、或销毁类的所有成员时,类的合成拷贝控制函数就被定义成删除的了。
三、拷贝构造函数
移动构造函数和移动赋值运算符通常需要被显式声明为 “无异常抛出” ,拷贝函数不建议使用explicit
关键字
拷贝构造函数形参必须是自身类的引用:
函数实参与形参之间的值传递,是通过拷贝完成的。那么当我们将该类的对象传递给一个函数的形参时,会调用该类的拷贝构造函数,而拷贝构造函数本身也是一个函数,因为是值传递而不是引用,在调用它的时候也需要调用类的拷贝构造函数(它自身),这样无限循环下去,无法完成。
阻止拷贝:
阻止拷贝的例子:文件、网络通信连接等,iostream类阻止拷贝,以避免多个对象写入或读取相同的IO缓冲,多线程编程中,thread调用函数的参数如果为IO必须使用std::move语义
定义删除的函数,可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的:
struct NoCopy
{
NoCopy()=default; //使用合成的默认构造函数
NoCopy(const NoCopy&)=delete; //阻止拷贝
//....
};
NonCopyable实现
class NonCopyable
{
protected:
NonCopyable() = default;
~NonCopyable() = default;
private:
NonCopyable(const NonCopyable&) = delete;
const NonCopyable& operator=( const NonCopyable& ) = delete;
};
构造和析构函数设置为protocted权限,这样就不能直接创建NonCopyable对象,只能由子类构造和析构函数调用它们。拷贝构造函数和拷贝赋值函数声明为private,子类不能进行拷贝操作
四、函数返回对象时,构造函数调用次数
Linux下 g++使用以下参数可以关掉编译器优化-fno-elide-constructors
例如g++ test.cpp -o out -fno-elide-constructors
点击查看代码
#include <iostream>
template <typename T>
class MyVector
{
public:
MyVector() :data(nullptr) { std::cout << "default\n"; } //无参构造
explicit MyVector(T t) :data(new T(t)), size(1) { std::cout << "only one\n"; } //有参构造
MyVector(MyVector<T>& t) {
std::cout << "copy\n";
if (t.size <= 0) {
size = 0;
data = nullptr;
}
else if (t.size == 1) {
data = new T(*t.data);
size = 1;
}
else {
data = new T[t.size]();
data = t.data;
size = t.size;
}
}
MyVector(MyVector<T>&& t) { // 移动构造
std::cout << "move\n";
if (t.size <= 0) {
size = 0;
data = nullptr;
}
else if (t.size == 1) {
data = t.data;
size = 1;
delete t.data;
t.data = nullptr;
}
else {
data = t.data;
size = t.size;
delete[] t.data;
t.data = nullptr;
}
}
MyVector(std::initializer_list<T> init_list) { // 初始化列表构造
std::cout << "init list\n";
data = new T[init_list.size()]();
size = init_list.size();
size_t index = 0;
for (auto elem : init_list)
{
data[index++] = elem;
if (index >= size)
break;
}
}
const MyVector<T>& operator=(const MyVector<T>& t) { //拷贝赋值
std::cout << "assign\n";
if (t.size <= 0) {
size = 0;
data = nullptr;
}
else if (t.size == 1) {
data = new T(*t.data);
size = 1;
}
else {
data = new T[t.size]();
data = t.data;
size = t.size;
}
return *this;
}
const MyVector<T>& operator=(MyVector<T>&& t) { // 移动赋值
std::cout << "move assign\n";
if (t.size <= 0) {
size = 0;
data = nullptr;
}
else if (t.size == 1) {
data = t.data;
size = 1;
delete t.data;
t.data = nullptr;
}
else {
data = t.data;
size = t.size;
delete[] t.data;
t.data = nullptr;
}
return *this;
}
~MyVector() {
std::cout << "destruct\n";
if (data)
{
if (size > 1)
delete[] data;
else
delete data;
data = nullptr;
}
}
private:
T* data{ nullptr };
size_t size{ 0 };
};
MyVector<int> getVec1()
{
return {}; //default
}
MyVector<int> getVec2()
{
return MyVector<int>();
//default MyVector<int>()
//move return
//destruct 析构MyVector<int>()产生的匿名对象
}
MyVector<int> getVec3()
{
return std::move(MyVector<int>());
//destruct
//move
//destruct
}
int main()
{
{
MyVector<int> vec1; // 无参构造
MyVector<int> vec2{ 1,2,3,4 }; //初始化列表构造
MyVector<int> vec3(2); //有参构造
MyVector<int> vec4 = MyVector<int>(); //无参构造 移动构造 析构
MyVector<int> vec5(vec4); //拷贝构造
MyVector<int> vec6 = std::move(vec5); //移动构造
vec1 = std::move(vec6); //移动赋值
std::cout << "11111111\n";
}
std::cout << "========\n";
{
auto ret1 = getVec1(); //移动 析构 -> 将get返回的对象移动到ret1,然后析构get返回的匿名对象
std::cout << "22222222\n";
auto ret2 = getVec2(); //移动 析构
std::cout << "33333333\n";
auto ret3 = getVec3(); //移动 析构
std::cout << "44444444\n";
}
return 0;
}