C++内存管理
RAII(Resource Acquisition Is Initialization)即资源获取即初始化。资源的获取(比如申请内存、打开文件)和对象的初始化绑定。资源的释放(比如释放内存、关闭文件)和对象的析构绑定。当对象的生命周期结束,析构函数会自动调用,资源也就被自动释放,无需手动管理。智能指针就是按照RAII的思想设计的。
一、内存划分
在C++中内存可被划分为:栈区、堆区、数据段、代码段等。其中数据段可分为全局/静态区和常量区。
- 栈区:又叫堆栈,存储非静态局部变量、函数参数、返回值等。栈是向下增长的。
- 堆区:用于程序运行时动态内存分配。堆是向上增长的。
- 数据段:存储全局变量、静态变量、字符串常量和const修饰的全局变量。
- 代码段:存储可执行代码和只读常量。
注意:很多编译器会把常量区和代码段合并(因为两者都是只读的),像字符串常量和const修饰的全局变量就可能被放到代码段。
二、new和delete
2.1 动态内存申请释放
在C语言中用于申请释放空间的是malloc和free,而在C++中依然可以使用这两个函数,不过C++引入了新的操作符new和delete。
int main()
{
int* p = new int;
*p = 1;
//int* p = new int(1);
std::cout << *p;
delete p;
return 0;
}
如果想申请多个空间则可以这样
int main()
{
int* p = new int[10];
delete[] p;
return 0;
}
2.2 底层原理
new和delete用于动态内存申请释放。new通过operator new全局函数来申请空间,delete通过operator delete全局函数来释放空间。在operator new中是调用malloc来申请空间,operator delete是调用free来释放空间。
- new:先调用operator new来开辟空间,然后在申请的空间上执行构造函数。
- delete:先在将要释放的空间上执行析构函数,然后调用operator delete来释放空间。
- new[]:在operator new[]会先调用operator new函数来完成N个对象空间的申请,然后在申请的空间上执行N次构造函数。
- delete[]:会先在将要释放的空间上进行N次执行析构函数,然后在operator delete[]中实际调用operator delete来释放整块空间。
注意:new[]会在数组头部多存一个size_t类型的数,记录数组元素个数,方便后续析构多少次。new和delete一定要匹配使用!详见关于new和delete的匹配问题-CSDN博客
三、智能指针
3.1 介绍
RAII(Resource Acquisition Is Initialization)即资源获取即初始化。
- 资源的获取(比如申请内存、打开文件)和对象的初始化绑定。
- 资源的释放(比如释放内存、关闭文件)和对象的析构绑定。
- 当对象的生命周期结束,析构函数会自动调用,资源也就被自动释放,无需手动管理。
智能指针就是按照RAII的思想设计的。
3.2 C++标准库中智能指针的使用
下面的指针都在<memory>头文件里。
3.2.1 auto_ptr
这是C++98设计出来的智能指针,功能是拷贝时把被拷贝的对象的资源管理权移交给拷贝对象,这时被拷贝的对象会悬空,这是一个很糟糕的问题。

像上图并不会出现悬空,因为p是以值传递的方式传给ap的构造函数,所以外部p就不受影响。但是如果ap的生命周期提前结束的话,p就会变为悬空指针。如果是auto_ptr就会发生变为悬空。

3.2.2 unique_ptr
这是C++11设计出来的智能指针,特点是不支持拷贝,支持移动,在不需要拷贝的场景就可以优先考虑使用它。如果拷贝就会报错:

在移动时被移动的对象会悬空。

3.2.3 shared_ptr
这是C++11设计出来的智能指针,特点是支持拷贝,支持移动,在需要拷贝的场景就可以优先考虑使用它。它的底层是靠引用计数的方式实现。

3.2.4 weak_ptr
这是C++11设计出来的智能指针,它不支持RAII,也意味着不能管理资源,它只要是解决shared_ptr的循环引用的内存泄漏问题。
3.2.5 删除器
智能指针析构默认是delete释放资源,也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。所以智能指针在构造时支持给一个删除器(本质上就是可调用对象),当你给定后,析构时就会调用删除器。像new[]经常使用所以unique_ptr和shared_ptr都特化了一个版本,使用时 unique_ptr<T[]>、shared_ptr<T[]> 在析构时就会delete[]。
unique_ptr
//仿函数
template <class T>
class Delete
{
public:
void operator()(T* p)//这里的p是unique_ptr内部管理的A*类型指针
{
cout << "删除器" << endl;
delete p;
}
};
//函数指针
template <class T>
void delete_func(T* p)
{
cout << "删除器" << endl;
delete p;
}
int main()
{
//仿函数
unique_ptr<A, Delete<A>> ap1(new A(1));
//函数指针
unique_ptr<A, void (*)(A*)> ap2(new A(1), delete_func<A>);
//lambda表达式
auto del = [](A* p)
{
cout << "删除器" << endl;
delete p;
};
unique_ptr<A, decltype(del)> ap3(new A(1), del);
return 0;
}
shared_ptr
int main()
{
//仿函数
shared_ptr<A> ap1(new A(1), Delete<A>());
//函数指针
shared_ptr<A> ap2(new A(1), delete_func<A>);
//lambda表达式
auto del = [](A* p)
{
cout << "删除器" << endl;
delete p;
};
shared_ptr<A> ap3(new A(1), del);
return 0;
}
shared_ptr和unique_ptr的构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。
关于shared_ptr对象本身来说它会在栈/堆/全局区,而它里面的控制块(引用计数分为强引用和弱引用)、实际资源则在堆上。
3.3 shared_ptr的循环引用问题
假设现在有下面的代码:
struct Node {
Node(int val)
:_val(val)
,_next(nullptr)
{}
int _val;
shared_ptr<Node> _next;
};
int main()
{
shared_ptr<Node> n1 = make_shared<Node>(1);
shared_ptr<Node> n2 = make_shared<Node>(2);
n1->_next = n2;
n2->_next = n1;
return 0;
}
这就是典型的循环引用。当创建完智能指针之后,n1和n2的强引用计数为1。然后执行下面的两行代码这时n1和n2的强引用计数为2,当程序结束后栈上的n1和n2的shared_ptr被销毁,这时n1和n2的强引用计数变为1。
此时,n1的强引用计数为 1,这个计数由n2->_next持有。n2对象的强引用计数为 1,这个计数由n1->_next持有。由于两个对象的强引用计数都不为 0,它们的内存和控制块都不会被释放,造成了内存泄漏。
这时候就要使用weak_ptr,它只是观测资源,不拥有它,因此它的存在与否不影响资源的释放。使用它之后弱引用加一,只要强引用变为0就会立即销毁shared_ptr对象。
struct Node {
Node(int val)
:_val(val)
{}
int _val;
weak_ptr<Node> _next;
};
int main()
{
shared_ptr<Node> n1 = make_shared<Node>(1);
shared_ptr<Node> n2 = make_shared<Node>(2);
n1->_next = n2;
n2->_next = n1;
return 0;
}
weak_ptr和shraed_ptr是可以相互转换的。
3.4 shared_ptr的线程安全问题
如果shared_ptr的对象在多个线程中,对shared_ptr进行拷贝析构时会修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或原子操作保证线程安全。
3.5 补充
当想定义智能指针时不要这样写
shared_ptr<Node> n();
编译器会把上面的代码解析为函数声明。可以用下面的几种写法
shared_ptr<Node> n;
shared_ptr<Node> n{};
shared_ptr<Node> n = make_shared<Node>();
更多推荐


所有评论(0)