五种语义概述
1、析构函数
2、拷贝构造函数
3、拷贝赋值运算符
4、移动构造函数
5、移动赋值运算符
析构函数
析够函数是一个类的生命周期结束时候,自动调用的终结函数,主要用于资源的释放等。
析构函数demo
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass()
{
NumCount = new int(10);
printf("this is %s class constructor\n", __FUNCTION__);
}
~MyClass()
{
delete NumCount;
NumCount = nullptr;
printf("this is %s class destructor\n", __FUNCTION__);
}
private:
int *NumCount = nullptr;
};
int main()
{
MyClass obj;
return 0;
}
运行效果
this is MyClass class constructor
this is ~MyClass class destructor
如果我们修改以下代码
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass()
{
NumCount = new int(3);
NumCount[0] = 0;
NumCount[1] = 1;
NumCount[2] = 2;
printf("this is %s class constructor\n", __FUNCTION__);
}
// ~MyClass()
// {
// delete NumCount;
// NumCount = nullptr;
// printf("this is %s class destructor\n", __FUNCTION__);
// }
public:
int *GetPtr()
{
return NumCount;
}
private:
int *NumCount = nullptr;
};
int main()
{
int * ptr = nullptr;
{
MyClass obj;
ptr = obj.GetPtr();
}
printf("ptr[0] : %d\n", ptr[0]);
printf("ptr[1] : %d\n", ptr[1]);
printf("ptr[2] : %d\n", ptr[2]);
return 0;
}
运行结果
this is MyClass class constructor
ptr[0] : 0
ptr[1] : 1
ptr[2] : 2
可以看到在注释掉MyClass的析构函数后,MyClass的生命周期已经结束了,但是NumCount的资源未被释放掉。这种资源泄漏的危害是十分巨大的。
当我们注释掉MyClass的析构函数,编译器会自动生成默认的析够函数,其语义如下:
~MyClass() = default;
默认的析构函数只能释放栈上面的资源,对于手动分配的堆上的资源是无法释放的。因此在写一个类的时候,一定要显式声明出其对应的析构函数。
拷贝构造函数
拷贝构造函数即用一个已有对象初始化另一个同类型对象。
MyClass a; // 普通构造
MyClass b = a; // 调用拷贝构造
MyClass c(a); // 同样调用拷贝构造
拷贝构造demo
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) //拷贝构造函数
{
printf("this is %s class copy construction \n", __FUNCTION__);
}
~MyClass() //析构函数
{
printf("this is %s class destructor\n", __FUNCTION__);
}
};
int main()
{
MyClass obj1;
MyClass obj2 = obj1;
MyClass obj3(obj1);
return 0;
}
运行效果
this is MyClass class constructor
this is MyClass class copy construction
this is MyClass class copy construction
this is ~MyClass class destructor
this is ~MyClass class destructor
this is ~MyClass class destructor
可以看到obj2和obj3都是调用拷贝构造去创建的。现在我们将显式的拷贝构造变成默认的。即不写拷贝构造,或者将拷贝构造写成default。
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
a = 1;
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = default;// 默认构造函数显式声明,等同将这句代码删掉
~MyClass() //析构函数
{
printf("this is %s class destructor\n", __FUNCTION__);
}
public:
int GetData()
{
return a;
}
void SetData(const int &data)
{
a = data;
}
private:
int a = 0;
};
int main()
{
MyClass obj1; //构造函数
printf("obj1 a : %d\n", obj1.GetData());
MyClass obj2 = obj1; //拷贝构造
printf("obj2 a : %d\n", obj2.GetData());
obj1.SetData(3);
MyClass obj3(obj1); //拷贝构造
printf("obj3 a : %d\n", obj3.GetData());
return 0;
}
运行结果
this is MyClass class constructor
obj1 a : 1
obj2 a : 1
obj3 a : 3
this is ~MyClass class destructor
this is ~MyClass class destructor
this is ~MyClass class destructor
obj1调用构造函数将a初始化为1
obj2调用拷贝构造,将obj1中的a数值拷贝给了obj2中的a,所以obj2中的a = 1
obj1通过SetData接口将a设置为3
obj3调用拷贝构造,将obj1中的a数值拷贝给了obj3中的a,所以obj3中的a = 3
现在我们修改一下代码:
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
NumCount = new int(3);
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = default;
~MyClass() //析构函数
{
delete NumCount;
NumCount = nullptr;
printf("this is %s class destructor\n", __FUNCTION__);
}
int * GetPtr()
{
return NumCount;
}
private:
int *NumCount = nullptr;
};
int main()
{
MyClass obj1; //构造函数
printf("obj1 NumCount : %p\n", obj1.GetPtr());
MyClass obj2 = obj1; //拷贝构造
printf("obj2 NumCount : %p\n", obj2.GetPtr());
MyClass obj3(obj1); //拷贝构造
printf("obj3 NumCount : %p\n", obj3.GetPtr());
return 0;
}
运行结果
this is MyClass class constructor
obj1 NumCount : 0x953eb0
obj2 NumCount : 0x953eb0
obj3 NumCount : 0x953eb0
this is ~MyClass class destructor
free(): double free detected in tcache 2
已放弃 (核心已转储)
可以看到三个对象中的NumCount地址是一样的,到在通过默认拷贝构造中,对于分配在堆上面的成员变量即NumCount发生的是浅拷贝,obj2,obj3只是将obj1中的NumCount地址拷贝了,并没有分配其新的空间。导致在析构时候对同一个地址进行**多次释放(又称 Double Free)**从而使得程序挂掉。
我们重新实现拷贝构造函数,实现深拷贝。
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
NumCount = new int(size);
NumCount[0] = 0;
NumCount[1] = 1;
NumCount[2] = 2;
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &other)
{
if (NumCount == nullptr) {
NumCount = new int(other.size);
for (int i = 0; i < size; i++) {
NumCount[i] = other.NumCount[i];
}
}
}
~MyClass() //析构函数
{
delete[] NumCount;
NumCount = nullptr;
printf("this is %s class destructor\n", __FUNCTION__);
}
int * GetPtr()
{
return NumCount;
}
private:
int size = 3;
int *NumCount = nullptr;
};
int main()
{
MyClass obj1; //构造函数
printf("obj1 NumCount : %p\n", obj1.GetPtr());
printf("obj1 NumCount[0] = %d\n", obj1.GetPtr()[0]);
printf("obj1 NumCount[1] = %d\n", obj1.GetPtr()[1]);
printf("obj1 NumCount[2] = %d\n", obj1.GetPtr()[2]);
MyClass obj2 = obj1; //拷贝构造
printf("obj2 NumCount : %p\n", obj2.GetPtr());
printf("obj2 NumCount[0] = %d\n", obj2.GetPtr()[0]);
printf("obj2 NumCount[1] = %d\n", obj2.GetPtr()[1]);
printf("obj2 NumCount[2] = %d\n", obj2.GetPtr()[2]);
MyClass obj3(obj1); //拷贝构造
printf("obj3 NumCount : %p\n", obj3.GetPtr());
printf("obj3 NumCount[0] = %d\n", obj3.GetPtr()[0]);
printf("obj3 NumCount[1] = %d\n", obj3.GetPtr()[1]);
printf("obj3 NumCount[2] = %d\n", obj3.GetPtr()[2]);
return 0;
}
运行结果
this is MyClass class constructor
obj1 NumCount : 0x17a3eb0
obj1 NumCount[0] = 0
obj1 NumCount[1] = 1
obj1 NumCount[2] = 2
obj2 NumCount : 0x17a42e0
obj2 NumCount[0] = 0
obj2 NumCount[1] = 1
obj2 NumCount[2] = 2
obj3 NumCount : 0x17a4300
obj3 NumCount[0] = 0
obj3 NumCount[1] = 1
obj3 NumCount[2] = 2
this is ~MyClass class destructor
this is ~MyClass class destructor
this is ~MyClass class destructor
可以看到三个对象中的NumCount指针都不一样,NumCount中的数据完全拷贝过来了,程序也没有异常。因此在写构造函数的时候需要注意:
1、如果对象都是在栈上分配的可以不写拷贝构造函数。或者显式的默认声明出来
MyClass(const MyClass &) = default;
2、如果这个类不会使用拷贝构造
MyClass obj2 = obj1; //拷贝构造
MyClass obj3(obj1); //拷贝构造
可设置为delete
MyClass(const MyClass &) = delete;
这样在编译时候既可以检查出报错
code1.cpp: In function ‘int main()’:
code1.cpp:39:20: error: use of deleted function ‘MyClass::MyClass(const MyClass&)’
MyClass obj2 = obj1; //拷贝构造
^
code1.cpp:16:5: note: declared here
MyClass(const MyClass &) = delete;
^
code1.cpp:40:22: error: use of deleted function ‘MyClass::MyClass(const MyClass&)’
MyClass obj3(obj1); //拷贝构造
^
code1.cpp:16:5: note: declared here
MyClass(const MyClass &) = delete;
拷贝赋值运算符
拷贝赋值运算符是 C++ 中用于把一个对象的值赋给另一个已经存在的对象的运算符函数。
上面我们讲了拷贝构造如下:
MyClass obj2 = obj1; //拷贝构造
MyClass obj3(obj1); //拷贝构造
那么拷贝赋值运算符是下述方式的构造
MyClass obj2; 拷贝构造
MyClass obj3;
obj3 = obj2; 拷贝赋值运算符!!!!!!
拷贝赋值运算符demo
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() // 构造函数
{
Count = 1;
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = delete; // 禁用拷贝构造
MyClass& operator=(const MyClass &) = default; // 使用默认拷贝赋值运算符
~MyClass() // 析构函数
{
printf("this is %s class destructor\n", __FUNCTION__);
}
public:
void SetCount(const int &num)
{
Count = num;
}
int GetCount()
{
return Count;
}
private:
int Count = 0;
};
int main()
{
MyClass obj1; //构造函数
MyClass obj2; //拷贝构造
printf("obj1 Count : %d\n", obj1.GetCount());
printf("obj2 Count : %d\n", obj2.GetCount());
obj1.SetCount(6);
obj2 = obj1;
printf("obj2 Count : %d\n", obj2.GetCount());
return 0;
}
运行结果
this is MyClass class constructor
this is MyClass class constructor
obj1 Count : 1
obj2 Count : 1
obj2 Count : 6
this is ~MyClass class destructor
this is ~MyClass class destructor
现在我们修改一下Count的类型
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
if (Count == nullptr) {
Count = new int(3);
}
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = delete; //拷贝构造
MyClass& operator=(const MyClass &other) = default;
~MyClass() //析构函数
{
delete Count;
Count = nullptr;
printf("this is %s class destructor\n", __FUNCTION__);
}
public:
void SetCount(const int &num)
{
*Count = num;
}
int *GetCount()
{
return Count;
}
private:
int * Count = nullptr;
};
int main()
{
MyClass obj1; //构造函数
MyClass obj2; //构造函数
printf("obj1 Count : %d, addr : %p\n", *obj1.GetCount(), obj1.GetCount());
printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());
obj1.SetCount(6);
obj2 = obj1;
printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());
return 0;
}
运行结果
this is MyClass class constructor
this is MyClass class constructor
obj1 Count : 3, addr : 0x893eb0
obj2 Count : 3, addr : 0x8942e0
obj2 Count : 6, addr : 0x893eb0
this is ~MyClass class destructor
free(): double free detected in tcache 2
已放弃 (核心已转储)
在创建obj1和obj2的时候,可以看到 obj1->Count = 0x893eb0, obj2->Count = 0x8942e0;在执行完拷贝赋值运算后,obj2->Count的地址变成了obj1->Count。也就是说执行完拷贝赋值运算obj2obj1中的成员执行了浅拷贝。只是将obj1->Count中的地址拷贝过来。obj1和obj2访问的是同一块空间,此时同时obj2中的Count空间无指针指向,出现了内存泄漏。同时在这两个对象生命周期结束时,执行析构函数会出现,出现“double free” 导致程序直接挂掉。
因此在涉及到分配在堆上的内存时,拷贝赋值运算符要格外注意。下面是修正版本
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
if (Count == nullptr) {
Count = new int(3);
}
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = delete; //拷贝构造
MyClass& operator=(const MyClass &other) //拷贝赋值运算符
{
printf("this is %s class copy assignment operator \n", __FUNCTION__);
if (this == &other) { // 自赋值保护
return *this;
}
delete Count; //释放构造函数分配的资源
Count = nullptr;
Count = new int(*other.Count);
return *this; //注意返回的是当前对象不是Count !!!!!!!
}
~MyClass() //析构函数
{
delete Count;
Count = nullptr;
printf("this is %s class destructor\n", __FUNCTION__);
}
public:
void SetCount(const int &num)
{
*Count = num;
}
int *GetCount()
{
return Count;
}
private:
int * Count = nullptr;
};
int main()
{
MyClass obj1; //构造函数
MyClass obj2; //构造函数
printf("obj1 Count : %d\n", *obj1.GetCount());
printf("obj2 Count : %d\n", *obj2.GetCount());
obj1.SetCount(6);
obj2 = obj1;
printf("obj2 Count : %d\n", *obj2.GetCount());
return 0;
}
运行结果
this is MyClass class constructor
this is MyClass class constructor
obj1 Count : 3
obj2 Count : 3
this is operator= class copy assignment operator
obj2 Count : 6
this is ~MyClass class destructor
this is ~MyClass class destructor
移动构造函数
移动构造函数是 C++11 引入的一个特殊构造函数,用来从另一个对象“窃取”资源,而不是拷贝资源,从而提升性能。
上诉的拷贝构造和拷贝赋值运算符中都讲述了double free问题移动构造函数中也和上述一样,再使用默认移动构造函数时候,对于分配在堆上的数据,只会使用浅拷贝,导致出现double free。
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
if (Count == nullptr) {
Count = new int(3);
}
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = delete; //拷贝构造
MyClass& operator=(const MyClass &other) = delete; //拷贝赋值运算符
MyClass(MyClass &&other) = default; //移动构造函数
~MyClass() //析构函数
{
delete Count;
Count = nullptr;
printf("this is %s class destructor\n", __FUNCTION__);
}
public:
void SetCount(const int &num)
{
*Count = num;
}
int *GetCount()
{
return Count;
}
private:
int * Count = nullptr;
};
int main()
{
MyClass obj1; //构造函数
printf("obj1 Count : %d, addr : %p\n", *obj1.GetCount(), obj1.GetCount());
obj1.SetCount(6);
MyClass obj2 = std::move(obj1); //构造函数
printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());
return 0;
}
运行结果
this is MyClass class constructor
obj1 Count : 3, addr : 0xc0feb0
obj2 Count : 6, addr : 0xc0feb0
this is ~MyClass class destructor
free(): double free detected in tcache 2
已放弃 (核心已转储)
可以看到obj1->Count和obj2->Count的指针完全一样,在析构的时候出现double free。导致程序挂掉。
在类中如果有使用裸指针的话,一定要注意移动构造函数。下面我们修正上述的问题
#include <iostream>
#include <cstdio>
class MyClass
{
public:
MyClass() //构造函数
{
if (Count == nullptr) {
Count = new int(3);
}
printf("this is %s class constructor\n", __FUNCTION__);
}
MyClass(const MyClass &) = delete; //拷贝构造
MyClass& operator=(const MyClass &other) = delete; //拷贝赋值运算符
MyClass(MyClass &&other)
{
Count = other.Count;
other.Count = nullptr;
}
~MyClass() //析构函数
{
//C++ 标准规定:delete nullptr; 是安全的,不会出错。 可以不判断是否为nullptr
if(Count == nullptr)
{
delete Count;
Count = nullptr;
}
printf("this is %s class destructor\n", __FUNCTION__);
}
public:
void SetCount(const int &num)
{
*Count = num;
}
int *GetCount()
{
return Count;
}
private:
int * Count = nullptr;
};
int main()
{
MyClass obj1; //构造函数
printf("obj1 Count : %d, addr : %p\n", *obj1.GetCount(), obj1.GetCount());
obj1.SetCount(6);
MyClass obj2 = std::move(obj1);
printf("obj1 addr : %p\n", obj1.GetCount());
printf("obj2 Count : %d, addr : %p\n", *obj2.GetCount(), obj2.GetCount());
return 0;
}
运行结果
this is MyClass class constructor
obj1 Count : 3, addr : 0x51ceb0
obj1 addr : (nil)
obj2 Count : 6, addr : 0x51ceb0
this is ~MyClass class destructor
this is ~MyClass class destructor
可以看到obj1->Count和obj2->Count的指针完全一样,在move obj2对象后obj2->Count被设置为了nullptr,避免了double free。
穿插一个小知识点
struct MyStringHolder {
std::string s;
// 方式1:初始化列表
MyStringHolder(MyStringHolder&& other)
: s(std::move(other.s)) {} // 直接调用string的移动构造
// 方式2:构造体内赋值
MyStringHolder(MyStringHolder&& other) {
s = std::move(other.s); // 先默认构造s,再移动赋值
}
};
struct Demo {
std::string s = "world"; // 默认初始值
// 情况1:用初始化列表 → 忽略默认初始值,直接构造为 "Hello"
Demo() : s("Hello") {}
// 情况2:没用初始化列表 → 用默认初始值 "world"
// Demo() {}
// 情况3:没默认初始值也没初始化列表 → s 默认构造为空串
// std::string s; Demo() {}
};
因此在初始化类的成员变量时候,尽量使用初始化成员列表方式
移动赋值运算符
移动赋值运算符是 C++ 中用于把 一个对象的资源搬过来而不是拷贝一份 的运算符函数。
上面我们讲了移动构造如下:
MyClass obj1;
MyClass obj2 = std::move(obj1);
那么移动赋值运算符是下述方式
MyClass obj1;
MyClass obj2;
obj2 = std::move(obj3);
移动赋值运算:参考上面举的例子这里就不举double free 这种例子了。
移动赋值运算符运算符demo
#include <iostream>
#include <cstring>
class MyClass
{
private:
char *data;
public:
MyClass(const char *str = "")
{
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~MyClass()
{
delete[] data;
}
// 拷贝构造
MyClass(const MyClass &other) = delete;
// 移动构造
MyClass(MyClass &&other) = delete;
// 拷贝赋值
MyClass &operator=(const MyClass &other) = delete;
// 移动赋值
MyClass &operator=(MyClass &&other) noexcept
{
if (this != &other)
{
delete[] data;
data = other.data;
other.data = nullptr;
}
printf("Move assignment\n");
return *this;
}
void print()
{
printf("data : %s\n", (data ? data : "null"));
};
};
int main()
{
MyClass a("Hello");
MyClass b("World");
b = std::move(a); // 调用移动赋值
a.print();
b.print();
}
运行结果
Move assignment
data : null
data : Hello
总结
| 类型 | 范式 | 使用场景 |
|---|---|---|
| 析构 | ~MyClass() | 对象生命周期结束 |
| 拷贝构造 | MyClass(const MyClass &) | MyClass a; MyClass b = a; MyClass c(a); |
| 拷贝赋值运算符 | MyClass& operator=(const MyClass &other) | MyClass obj2; MyClass obj3; obj3 = obj2; |
| 移动构造 | MyClass(MyClass &&other) | MyClass obj1; MyClass obj2 = std::move(obj1); |
| 移动赋值运算符 | MyClass &operator=(MyClass &&other) | MyClass a; MyClass b; b = std::move(a); |
–结语–
感谢您的阅读,如有问题可以私信或评论区交流。
^ _ ^
1229

被折叠的 条评论
为什么被折叠?



