智能指针
**问题:**在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:
- 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
- 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
- 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
**解决方法:**在 C++ 中,智能指针是一种用于管理动态分配的对象内存的工具,它可以帮助自动管理内存的分配和释放,从而减少内存泄漏的风险。C++ 标准库提供了几种不同类型的智能指针,其中最常用的有以下几种:
- std::shared_ptr:共享指针是一种允许多个智能指针共享同一个对象所有权的指针。当最后一个共享指针离开作用域时,对象会被自动销毁。
基本原理:就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向该对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。
std::shared_ptr<T> ptr;//ptr 的意义就相当于一个 NULL 指针。在 C++中,<T> 用于指定所持有对象的类型。
//make_shared辅助函数创建
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);//make_shared 是一个方便的函数,它会分配内存并返回一个指向新对象的 shared_ptr。
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
ptr1.reset(); // 原来所指的对象会被销毁
- std::unique_ptr:唯一指针是一种独占对象所有权的指针,不允许多个 unique_ptr 指向同一个对象。当 unique_ptr 离开作用域时,对象会被自动销毁。
std::unique_ptr<int> ptr = std::make_unique<int>(42);
- std::weak_ptr:弱指针不会增加对象的引用计数,因此不会阻止对象的销毁。它的作用是为了解决循环引用(circular reference)问题,弱指针可以从 shared_ptr 创建,从而避免内存泄漏。
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
这些智能指针在不同场景下具有不同的用途。std::shared_ptr
用于多个指针共享对象所有权的情况,std::unique_ptr
用于独占对象所有权的情况,std::weak_ptr
用于解决循环引用问题。选择适当的智能指针类型可以提高代码的安全性和可维护性。除了上述三种,还有其他的智能指针如 std::auto_ptr
(已在 C++11 中废弃)、std::scoped_ptr
等,但在现代 C++ 中推荐使用 std::shared_ptr
和 std::unique_ptr
。
针对std::shared_ptr
- make_shared辅助函数创建
std::shared_ptr<int> foo = std::make_shared<int> (10);
- 自定义所指堆内存的释放规则
当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。
对于申请的动态数组,释放规则可以
- 使用 C++11 标准中提供的
default_delete
模板类 - 可以自定义释放规则
//指定 default_delete 作为释放规则
//这段代码的作用是创建一个共享指针 p6,它将管理一个包含 10 个整数的数组,并在 p6 被销毁时,使用 std::default_delete<int[]>() 来释放数组内存。
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
delete[] p;//delete[] 操作符来释放该数组的内存,以防止内存泄漏。
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
- shared_ptr常用函数
get()函数,表示返回当前存储的指针(就是被shared_ptr所管理的指针) 。但是不建议使用get()函数获取 shared_ptr 关联的原始指针,因为如果在 shared_ptr 析构之前手动调用了delete函数,会导致错误。
shared_ptr<T> ptr(new T());
T *p = ptr.get(); // 获得传统 C 指针
use_count()函数,表示当前引用计数
shared_ptr<T> a(new T());
a.use_count(); //获取当前的引用计数
reset()函数,表示重置当前存储的指针。
shared_ptr<T> a(new T());
a.reset(); // 此后 a 原先所指的对象会被销毁,并且 a 会变成 NULL
operator*
:表示返回对存储指针指向的对象的引用。它相当于:* get()。
operator->
,表示返回指向存储指针所指向的对象的指针,以便访问其中一个成员。跟get函数一样的效果。
示例1:shared_ptr的基础应用:
#include <iostream>
#include <memory> // 共享指针必须要包含的头文件
using namespace std;
int main()
{
// 最好使用make_shared创建共享指针,
shared_ptr<int> p1 = make_shared<int>();//make_shared 创建空对象,
*p1 = 10;
cout << "p1 = " << *p1 << endl; // 输出10
// 打印引用个数:1
cout << "p1 count = " << p1.use_count() << endl;
// 第2个 shared_ptr 对象指向同一个指针
std::shared_ptr<int> p2(p1);
// 输出2
cout << "p2 count = " << p2.use_count() << sendl;
cout << "p1 count = " << p1.use_count() << endl;
// 比较智能指针,p1 等于 p2
if (p1 == p2) {
std::cout<< "p1 and p2 are pointing to same pointer\n";
}
p1.reset();// 无参数调用reset,无关联指针,引用个数为0
cout << "p1 Count = " << p1.use_count() << endl;
p1.reset(new int(11));// 带参数调用reset,引用个数为1
cout << "p1 Count = " << p1.use_count() << endl;
p1 = nullptr;// 把对象重置为NULL,引用计数为0
cout << "p1 Reference Count = " << p1.use_count() << endl;
if (!p1) {
cout << "p1 is NULL" << endl; // 输出
}
return 0;
}
示例2:shared_ptr作返回值
shared_ptr<string> factory(const char* p)
{
shared_ptr<string> p1 = make_shared<string>(p);
return p1;
}
void use_factory()
{
shared_ptr<string> p = factory("helloworld");
int num1 = p.use_count();
cout << *p << endl;//!离开作用域时,p引用的对象被销毁。
}
shared_ptr<string> return_share_ptr()
{
shared_ptr<string> p = factory("helloworld");
cout << *p << endl;
return p; //!返回p时,引用计数进行了递增操作。
} //!p离开了作用域,但他指向的内存不会被释放掉。
int main()
{
use_factory();
auto p = return_share_ptr();
cout << p.use_count() << endl;
system("pause");
return 0;
}
//可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。
//例如,当用一个shared_ptr去初始化另一个shared_ptr;当我们给shared_ptr赋予一个新的值或者是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。
//一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。
示例3:容器中的shared_ptr-记得用erease节省内存
对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,他就不会被释放掉。由于这个特性,保证shared_ptr在不用之后不再保留就非常重要了,通常这个过程能够自动执行而不需要人工干预,有一种例外就是我们将shared_ptr放在了容器中。所以永远不要忘记erease不用的shared_ptr。
#include <iostream>
using namespace std;
int main()
{
list<shared_ptr<string>>pstrList;
pstrList.push_back(make_shared<string>("1111"));
pstrList.push_back(make_shared<string>("2222"));
pstrList.push_back(make_shared<string>("3333"));
pstrList.push_back(make_shared<string>("4444"));
for(auto p:pstrList)
{
if(*p == "3333");
{
/*do some thing!*/
}
cout<<*p<<endl;
}
/*包含"3333"的数据我们已经使用完了!*/
for(list<shared_ptr<string>>::iterator itr = pstrList.begin();itr!=pstrList.end();++itr)
{
if(**itr == "3333"){
cout<<**itr<<endl;
pstrList.erase(itr);
}
}
cout<<"-------------after remove------------"<<endl;
for(auto p:pstrList)
{
cout<<*p<<endl;
}
while(1)
{
/*do somthing other works!*/
/*遍历 pstrList*/ //!这样不仅节约了大量内存,也为容器的使用增加了效率
}
}
示例4:shared_ptr:对象共享相同状态
使用shared_ptr在一个常见的原因是允许多个多个对象共享相同的状态,而非多个对象独立的拷贝!
#include <iostream>
using namespace std;
void copyCase()
{
list<string> v1({"1","b","d"});
list<string> v2 = v1; //!v1==v2占用两段内存
v1.push_back("cc"); //!v1!=v2
for(auto &p:v1){
cout<<p<<endl;
}
cout<<"--------void copyCase()---------"<<endl;
for(auto &p:v2){
cout<<p<<endl;
}
} //v1和v2分属两个不同的对象,一个改变不会影响的状态。
void shareCase()
{
shared_ptr<list<string>> v1 = make_shared<list<string>>(2,"bb");
shared_ptr<list<string>> v2 = v1;
(*v1).push_back("c2c");
for(auto &p:*v1){
cout<<p<<endl;
}
cout<<"----------shareCase()--------"<<endl;
for(auto &p:*v2){
cout<<p<<endl;
}
} //v1和v2属于一个对象的两个引用,有引用计数为证,其内容的改变是统一的。
int main()
{
copyCase();
cout<<"++++++++++++++++"<<endl;
shareCase();
}
示例5:shared_ptr管理动态数组
默认情况下,shared_ptr指向的动态的内存是使用delete来删除的。这和我们手动去调用delete然后调用对象内部的析构函数是一样的。与unique_ptr不同,shared_ptr不直接管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器来替代delete 。
#include <iostream>
using namespace std;
class DelTest
{
public:
DelTest(){
j= 0;
cout<<" DelTest()"<<":"<<i++<<endl;
}
~DelTest(){
i = 0;
cout<<"~ DelTest()"<<":"<<i++<<endl;
}
static int i,j;
};
int DelTest::i = 0;
int DelTest::j = 0;
void noDefine()
{
cout<<"no_define start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10]);
}
void slefDefine()
{
cout<<"slefDefine start running!"<<endl;
shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});
} //!传入lambada表达式代替delete操作。
int main()
{
noDefine(); //!构造10次,析构1次。内存泄漏。
cout<<"----------------------"<<endl;
slefDefine(); //!构造次数==析构次数 无内存泄漏
}
Warning 常见错误(注意以下代码全是错误代码)
1.不能使用原始指针初始化多个shared_ptr。
int* p11 = new int;
std::shared_ptr<int> p12(p11);
std::shared_ptr<int> p13(p11);
// 由于p1和p2是两个不同对象,但是管理的是同一个指针,这样容易造成空悬指针,
//比如p1已经将aa delete了,这时候p2里边的aa就是空悬指针了
2.不允许以暴露裸漏的指针进行赋值
//带有参数的 shared_ptr 构造函数是 explicit 类型的,所以不能像这样
std::shared_ptr<int> p1 = new int();//不能隐式转换,类型不匹配
隐式调用它构造函数
3.不要用栈中的指针构造 shared_ptr 对象
int x = 12;
std::shared_ptr<int> ptr(&x);
shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。
4.不要使用shared_ptr的get()初始化另一个shared_ptr
Base *a = new Base();
std::shared_ptr<Base> p1(a);
std::shared_ptr<Base> p2(p1.get());
//p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题
5.多线程中使用 shared_ptr
shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:
- 一个 shared_ptr 对象实体可被多个线程同时读取
- 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
- 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁