一. 析构函数
1.1 简介
析构函数执行与构造函数相反的操作:构造函数初始化对象的非 static 数据成员,还可能做一些其它工作。析构函数释放对象使用的资源,并销毁对象的非 static 数据成员。
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。对一个给定类,只会有唯一一个析构函数。
#include <cstdio>
class Person{
private:
int age;
public:
Person() = default;
Person(int age)
:age(age){}
~Person(){
printf("I' destructor.\n");
}
};
int main(){
Person you(5);
return 0;
}
1.2 析构函数什么时候调用
无论何时一个对象被销毁,就会自动调用其析构函数
- 变量离开其作用域时被销毁
- 当一个对象销毁时,其成员被销毁
- 动态分配的对象,delete 时被销毁
- 临时对象销毁时
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
二. RALL 惯用法
2.1 简介
在有垃圾回收的编程语言里,如 Java、Python、Go,不需要程序员随时注意内存是否泄漏了,而 C++ 则需要程序员细心认真的处理内存,避免内存泄漏,即任何申请的内存都被释放。
那么如何实现呢?C++的解决方法是 Rall 惯用法和构建在其上的智能指针
Rall 全称 Resource acquisition is initialization. 意为资源获取要通过构造函数初始化,然后析构函数负责释放资源
int main(){
int *pointer = new int{7};
dosomething(pointer);
delete pointer;
return 0;
}
看似没有内存泄漏,但实际这样的代码是很脆弱的
- malloc 和 free 或 new 和 delete 可能相距很远,难以看出对应关系
- 中间如果有任何异常,如 dosomething 发生了错误,程序提前结束,delete 根本不会执行。
- 如果将这段代码复用,搬到其它地方,容易漏掉 free
所以只靠程序员的细致认真,远远不够 !!!
int *pointer = createLinkList();
pointer 这个指针我们到底要不要释放,如果 createLinkList 这个函数是我们写的,我们肯定知道,但是如果不是呢?
因此 Rall 惯用法对于 C++ 程序员来说至关重要,必须熟练掌握
2.2 原理
Rall 惯用法的原理很简单,即利用栈上的对象在离开作用域时,会自动调用析构函数
#include <cstdio>
class MyInt{
private:
int *int_pointer;
public:
MyInt(int value){
this->int_pointer = new int{value};
}
~MyInt(){
delete this->int_pointer;
}
public:
int getValue(void) const{
return *(this->int_pointer);
}
};
int main(){
MyInt my_int(7);
printf("Value: %d\n", my_int.getValue());
return 0;
}
三. 三五法则
- 如果一个类定义了析构函数,那么必须同时定义或删除拷贝构造函数和拷贝赋值运算符,否则出错
- 如果一个类定义了拷贝构造函数,那么必须定义或删除拷贝赋值运算符,否则出错,删除将导致低效
- 如果一个类定义了移动构造函数,那么必须定义或删除移动赋值运算符,否则出错,删除将导致低效
- 如果一个类定义了拷贝构造函数或拷贝赋值运算符,那么最好同时定义移动构造函数或移动赋值运算符,否则低效
三五法则是前人总结的,避免犯错的经验
四. 拷贝控制
4.1 删除拷贝构造函数和拷贝赋值运算符
#include <stdio.h>
class MyInt{
private:
int *int_pointer;
public:
MyInt(int value){
this->int_pointer = new int{value};
}
MyInt(const MyInt &) = delete;
MyInt &operator=(const MyInt &) = delete;
~MyInt(){
delete this->int_pointer;
}
public:
int getValue(void) const{
return *(this->int_pointer);
}
};
int main(){
MyInt my_int(7);
printf("Value: %d\n", my_int.getValue());
return 0;
}
同时定义拷贝构造函数和拷贝赋值运算符,这两个函数的写法不一样,类的行为也不一样,具体分为行为像值的类和行为像指针的类
4.2 行为像值的类
对于行为像值的类,每个对象都应该拥有一份自己的资源
class MyInt{
private:
int *int_pointer;
public:
MyInt(int value){
this->int_pointer = new int{value};
}
MyInt(const MyInt &my_int)
:int_pointer(new int{*my_int.int_pointer}){}
MyInt &operator=(const MyInt &my_int){
int *new_int_pointer = new int{*my_int.int_pointer};
delete this->int_pointer;
this->int_pointer = new_int_pointer;
return *this;
}
~MyInt(){
delete this->int_pointer;
}
public:
int getValue(void) const{
return *(this->int_pointer);
}
};
int main(){
MyInt my_int(7);
MyInt she_int(1);
MyInt you_int(she_int);
printf("Value: %d\n", you_int.getValue());
you_int = my_int;
printf("Value: %d\n", you_int.getValue());
return 0;
}
编写赋值运算符时,一定要注意两点
- 如果将一个类赋予它自身,赋值运算符必须能正常工作
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
当你编写一个赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部对象中。当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了。一旦左侧运算对象的资源被销毁,就只剩下将数据从局部对象拷贝到左侧运算对象的成员中了
MyInt &operator=(const MyInt &my_int){
delete this->int_pointer;
this->int_pointer = new int{*my_int.int_pointer};
return *this;
}
如果 my_int 和 int_pointer 是同一个对象,delete 会释放掉 my_int 和 int_pointer 指向的 int, 接下来就会访问一个指向无效内存的指针,其行为和结果是未定义的
4.3 行为像指针的类
对于行为像指针的类,每个对象应该共享一份资源。此时,需要使用引用计数
引用计数的工作方式如下:
- 除了初始化对象,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享资源。当我们创建一个对象时,只有一个对象共享资源,此时将计数器初始化为 1
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出当前对象的资源又被一个新对象共享。
- 析构函数递减计数器,指出共享资源的对象少了一个。如果计数器变为 0,则析构函数释放资源
- 拷贝赋值运算符递增右侧对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为 0,意味着它的共享资源没有对象了,拷贝赋值运算符就必须销毁对象
计数器应该放在哪里?
可以将计数器放在动态内存中,当创建一个对象时,分配一个新的计数器;当拷贝或赋值对象时,我们拷贝指向计数器的指针,这样前后两个对象都会指向相同的计数器。
class MyInt{
private:
int *int_pointer;
int *use_count;
public:
MyInt(int value)
:int_pointer(new int{value}), use_count(new int{1}){}
MyInt(const MyInt &my_int)
:int_pointer(my_int.int_pointer), use_count(my_int.use_count){
++(*this->use_count);
}
MyInt &operator=(const MyInt &my_int){
++(*my_int.use_count);
--(*this->use_count);
if(*this->use_count == 0){
delete this->int_pointer;
delete this->use_count;
}
this->int_pointer = my_int.int_pointer;
this->use_count = my_int.use_count;
return *this;
}
~MyInt(){
--(*this->use_count);
if(*this->use_count == 0){
delete this->int_pointer;
delete this->use_count;
}
}
public:
int getValue(void) const{
return *this->int_pointer;
}
int getUseCount(void) const{
return *this->use_count;
}
};
int main(){
MyInt my_int(7);
MyInt she_int(1);
MyInt you_int(my_int);
you_int = she_int;
return 0;
}
作业
给你两个二进制字符串 a
和 b
,以二进制字符串的形式返回它们的和。
示例 1:
输入:a = “11”, b = “1”
输出:“100”
示例 2:
输入:a = “1010”, b = “1011”
输出:“10101”