C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄漏。解决这个问题最有效的方式是使用智能指针。
智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动销毁动态分配的对象,防止内存泄漏。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次,内部的引用计数减1,减为0时,删除所指的堆内存。
C++提供了三种智能指针,使用前需要引用头文件<memory>:
std::shared_ptr:共享的智能指针
std::shared_ptr:独占的智能指针
std::weak_ptr:弱引用的智能指针,不共享指针,不能操作资源,是用来监视shared_ptr的
shared_ptr
计数函数
共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数、std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:long use_count() const noexcept;
使用方式如下:
#include <iostream>
#include <memory>int main() {std::shared_ptr<int> sp1 = std::make_shared<int>(10);std::shared_ptr<int> sp2 = sp1;std::shared_ptr<int> sp3 = sp1;std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;std::cout << "sp2 use_count: " << sp2.use_count() << std::endl;std::cout << "sp3 use_count: " << sp3.use_count() << std::endl;return 0;
}
首先创建了一个 shared_ptr
对象 sp1
,并通过 make_shared
初始化它指向一个值为 10
的 int
对象。然后 sp2
和 sp3
都赋值为 sp1
,此时这三个 shared_ptr
都指向同一个int
对象。
当调用 sp1.use_count()
、sp2.use_count()
和 sp3.use_count()
时,都会返回 3
,因为有三个shared_ptr
指向同一个对象。
初始化方式
使用构造函数初始化:std::shared_ptr<T> 智能指针名字(创建堆内存);
使用方式如下:
#include <iostream>
#include <memory>
using namespace std;int main()
{// 使用智能指针管理一块 int 型的堆内存shared_ptr<int> ptr1(new int(520));cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//输出1// 使用智能指针管理一块字符数组对应的堆内存shared_ptr<char> ptr2(new char[12]);cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//输出1// 创建智能指针对象, 不管理任何内存shared_ptr<int> ptr3;cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//输出0// 创建智能指针对象, 初始化为空shared_ptr<int> ptr4(nullptr);cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//输出0return 0;
}
如果智能指针被初始化了一块有效内存,那么这块内存的引用计数+1,如果没有被初始化或者被初始化为nullptr空指针,那么引用计数不会+1。此外,不要使用一个原始指针初始化多个shared_ptr。比如说:
int *p = new int;shared_ptr<int> p1(p);shared_ptr<int> p2(p);//error!会报错
使用拷贝和移动构造函数初始化:
#include <iostream>
#include <memory>
using namespace std;int main()
{// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1shared_ptr<int> ptr1(new int(520));cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//输出1//调用拷贝构造函数shared_ptr<int> ptr2(ptr1);cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//输出2shared_ptr<int> ptr3 = ptr1;cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//输出3//调用移动构造函数shared_ptr<int> ptr4(std::move(ptr1));cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//输出3std::shared_ptr<int> ptr5 = std::move(ptr2);cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;//输出3return 0;
}
如果使用拷贝的方式初始化共享智能指针对象,这两个对象会同时管理同一块堆内存,堆内存对应的引用计数也会增加;如果使用移动的方式初始智能指针对象,只是转让了内存的所有权,管理内存的对象并不会增加,因此内存的引用计数不会变化。
通过make_shared初始化:shared_ptr<T> make_shared( Args&&... args)
它可以在一次内存分配中同时完成对象的创建和 shared_ptr
的初始化,提高效率并减少内存泄漏风险。
#include <iostream>
#include <string>
#include <memory>
using namespace std;class Test
{
public:Test() {cout << "construct Test..." << endl;}Test(int x) {cout << "construct Test, x = " << x << endl;}Test(string str) {cout << "construct Test, str = " << str << endl;}~Test(){cout << "destruct Test ..." << endl;}
};int main()
{// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1shared_ptr<int> ptr1 = make_shared<int>(520);cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;shared_ptr<Test> ptr2 = make_shared<Test>();cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;shared_ptr<Test> ptr3 = make_shared<Test>(520);cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;shared_ptr<Test> ptr4 = make_shared<Test>("我是要成为海贼王的男人!!!");cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;return 0;
}
使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。
通过reset方法初始化
共享智能指针提供std::shared_ptr::reset方法函数原型如下:
ptr:指向要取得所有权的对象的指针
d:指向要取得所有权的对象的指针
aloc:内部存储所用的分配器
#include <iostream>
#include <string>
#include <memory>
using namespace std;int main()
{// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1shared_ptr<int> ptr1 = make_shared<int>(520);shared_ptr<int> ptr2 = ptr1;shared_ptr<int> ptr3 = ptr1;shared_ptr<int> ptr4 = ptr1;cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//4cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//4cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//4cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//4ptr4.reset();cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;//3cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;//3cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;//3cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;//0shared_ptr<int> ptr5;ptr5.reset(new int(250));cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;//1return 0;
}
std::shared_ptr
的 reset
函数用于改变 shared_ptr
所管理的对象,它有以下几种作用:
一、释放当前管理的对象(如果有)并接管新对象
- 当不带参数调用
reset
函数时(如ptr4.reset();
):- 它会将当前
shared_ptr
(这里是ptr4
)所管理的对象的引用计数减1
。如果引用计数变为0
,则释放该对象所占用的内存。在这个例子中,ptr4
原本和ptr1
、ptr2
、ptr3
指向同一个对象(值为520
的int
),调用reset
后,该对象的引用计数从4
变为3
,因为ptr4
不再指向它。 - 同时,
ptr4
被设置为一个空的shared_ptr
,即它不再管理任何对象,其内部的指针被设置为nullptr
。
- 它会将当前
二、接管新分配的对象
- 当带参数调用
reset
函数时(如ptr5.reset(new int(250));
):- 首先,它会释放
ptr5
当前可能管理的任何对象(在这个例子中,ptr5
最初是空的,所以没有对象需要释放)。 - 然后,它会使用传入的指针(这里是通过
new int(250)
创建的新的int
对象的指针)来初始化ptr5
,使ptr5
管理这个新的对象。此时,新对象的引用计数被初始化为1
,因为只有ptr5
指向它。
- 首先,它会释放
三、注意事项
- 内存管理:
- 使用
reset
函数时要小心,特别是在传递指针给reset
时。确保传递的指针是通过new
动态分配的内存,或者是与shared_ptr
兼容的内存分配方式(例如,使用std::allocate_shared
分配的内存)。如果传递了一个不适当的指针,可能会导致内存泄漏或其他未定义行为。
- 使用
- 引用计数的变化:
- 每次调用
reset
都会影响引用计数,并且可能导致对象的释放。在多线程环境中,如果多个shared_ptr
同时访问和修改同一个对象,需要注意对reset
函数的调用顺序和并发访问的安全性,以避免意外的引用计数变化和对象释放。虽然shared_ptr
的引用计数操作在一定程度上是线程安全的,但复杂的操作序列可能仍然需要额外的同步措施来确保正确性。
- 每次调用
- 与其他智能指针操作的结合:
reset
函数通常与shared_ptr
的其他操作(如赋值、拷贝构造等)结合使用。例如,在函数返回shared_ptr
时,可以使用reset
来确保返回的shared_ptr
正确管理对象的生命周期。同时,在对象的所有权转移场景中,reset
可以用于明确地释放当前的所有权并转移到新的shared_ptr
上。但要注意在这些操作中保持逻辑的正确性,避免出现悬空指针、内存泄漏或对象被过早释放等问题。
获取原始指针
通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。
当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的 get() 方法,其函数原型如下:
T* get() const noexcept;
使用如下:
#include <iostream>
#include <string>
#include <memory>
using namespace std;int main()
{int len = 128;shared_ptr<char> ptr(new char[len]);// 得到指针的原始地址char* add = ptr.get();memset(add, 0, len);strcpy(add, "我是要成为海贼王的男人!!!");cout << "string: " << add << endl;shared_ptr<int> p(new int);*p = 100;cout << *p.get() << " " << *p << endl;return 0;
}
memset(add, 0, len);
将add
指向的内存区域(即ptr
管理的字符数组)的前len
个字节设置为0
,这是一种初始化内存的操作,确保数组中的内容在复制字符串之前是清空的。strcpy(add, "我是要成为海贼王的男人!!!");
将字符串常量复制到add
指向的字符数组中。这里使用了strcpy
函数,它会将源字符串的字符逐个复制到目标数组中,直到遇到字符串结束符'\0'
。需要注意的是,strcpy
函数在复制字符串时不会检查目标数组的大小,如果源字符串长度超过目标数组长度,会导致缓冲区溢出错误。在实际应用中,更安全的做法是使用strncpy
函数,并确保正确处理字符串长度和数组大小的关系。shared_ptr<int> p(new int);
创建了一个shared_ptr
对象p
,它管理一个动态分配的int
对象。*p = 100;
通过解引用p
来访问它所管理的int
对象,并将其值设置为100
。cout << *p.get() << " " << *p << endl;
分别通过p.get()
获取原始指针并解引用和直接解引用p
来输出int
对象的值。这两种方式都可以正确获取p
所管理的int
对象的值,并且输出结果相同,都是100
。这里展示了两种访问shared_ptr
所管理对象的方式,在实际使用中可以根据具体情况选择。
指定删除器
当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
#include <iostream>
#include <memory>
using namespace std;// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p)
{delete p;cout << "int 型内存被释放了...";
}int main()
{shared_ptr<int> ptr(new int(250), deleteIntPtr);return 0;
}
删除器也可以是lambda表达式,因此也可以这么写:
shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
在上面的代码中,lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。
在C++11中使用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象,具体的处理代码如下:
shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete<T>()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:
shared_ptr<int> ptr(new int[10], default_delete<int[]>());
另外,我们还可以自己封装一个make_shared_array方法来让shared_ptr支持数组,代码如下:
#include <iostream>
#include <memory>
using namespace std;template <typename T>
shared_ptr<T> make_share_array(size_t size)
{// 返回匿名对象return shared_ptr<T>(new T[size], default_delete<T[]>());
}int main()
{shared_ptr<int> ptr1 = make_share_array<int>(10);cout << ptr1.use_count() << endl;shared_ptr<char> ptr2 = make_share_array<char>(128);cout << ptr2.use_count() << endl;return 0;
}