裸指针是什么?
裸指针(Raw Pointer) 是 C++ 中的一种基本指针类型,它直接存储内存地址,不附带任何额外的管理功能(如自动内存管理或生命周期管理)。裸指针是 C++ 中最原始的指针形式,通常用于直接操作内存。
没有自动内存管理功能,容易导致悬空指针、内存泄漏等问题。 需要手动释放
裸指针缺点:
1. 忘记释放资源,导致资源泄露(常发生内存泄漏问题)
2. 同一资源释放多次,导致释放野指针,程序崩溃
3. 明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间
return掉了,导致释放资源的代码未被执行到,懵
4. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到,
懵
智能指针,智能指针的智能二字,主要体现在用户可以不关注资源的释放,因为智能指针
会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异
常,资源在到期的情况下,一定会进行释放。
实现一个简单的 智能指针
#include
#include
using namespace std;
template
class CSmartPtr
{
public:
CSmartPtr(T* ptr = nullptr) : mptr(ptr) {}
~CSmartPtr() { delete mptr; }
T& operator* () { return *mptr; } // 必须引用, 否则不能修改
T* operator->() { return mptr; }
private:
T* mptr;
};
int main()
{
// int *p = new int;
CSmartPtr ptr1(new int);
*ptr1 = 20; // 虽然 ptr1 本身是普通对象, 但是里面是指针,想要访问指针, 除了写访问函数, 还能用 *重载
class Test
{
public:
void test() { cout << "test" << endl; }
};
CSmartPtr ptr2(new Test());
(*ptr2).test(); // 好好理解,为什么要用*,
ptr2->test();
return 0;
}
利用栈上的 对象, 出作用域 自动析构的特征, 做到资源的 自动释放!
上一节代码里, 加入做下面这个:
CSmartPtr ptr1(new int);
CSmartPtr ptr2(ptr1);
默认拷贝构造是一个浅拷贝
会导致 两次析构 释放同一个值, 造成野指针问题
野指针(Wild Pointer) 是指未初始化或指向已释放内存的指针。野指针的行为是未定义的,访问或修改野指针指向的内存可能导致程序崩溃、数据损坏或安全漏洞。
那些独占所有权的智能指针
禁止拷贝和赋值重载,auto_ptr例外
自动释放
它在 C++11 中被弃用,并在 C++17 中被移除。 why?
//c++库里
int main()
{
auto_ptr ptr1(new int);
auto_ptr ptr2(ptr1); // auto_ptr 源码显示, 在拷贝构造时, 会把原来的ptr1的指针置空
*ptr2 = 20;
cout << *ptr1 << endl; // 错误
return 0;
}
也就是说, 拷贝构造会使得 原来的 所有的 指针 都会 置空, 不能再访问这块内存
std::auto_ptr 采用独占所有权模型,但在拷贝或赋值时会发生所有权转移。
这意味着,当一个 auto_ptr 被拷贝或赋值给另一个 auto_ptr 时,原指针会失去所有权,变为 nullptr。
这种行为容易导致意外的所有权转移和悬空指针问题。
scoped_ptr
是 Boost 库中提供的一种智能指针,用于在特定作用域内管理动态分配的内存。
主要特点:禁止拷贝和赋值,从而避免所有权转移的问题
直接把 里面的 拷贝构造=delete; operator=() = delete
直接把 里面的 左值引用拷贝构造=delete; operator=() = delete
但是, 提供了 右值 引用 拷贝构造 和 右值引用 operator=, 因此 需要移动语义
虽然同样是 所有权, 但是 好处是, 可以清楚地 看出来, 是在转移 资源
int main()
{
unique_ptr ptr1(new int);
unique_ptr ptr2(move(ptr1)); // 需要移动语义, 好处, 清晰可见
*ptr2 = 20;
cout << *ptr1 << endl; // 也是错误的,
return 0;
}
好处: 多个指针 可以管理同一个资源
给每一个对象资源, 匹配一个引用计数
#include
// 引用计数类
class RefCnt {
public:
RefCnt(void* ptr = nullptr) : ptr_(ptr), count_(1) {}
// 增加引用计数
void addRef() {
++count_;
}
// 减少引用计数,如果计数为 0 则释放资源
void release() {
--count_;
if (count_ == 0) {
delete static_cast(ptr_); // 假设资源是 int 类型
delete this; // 释放引用计数对象本身
}
}
int getCount() const {
return count_;
}
private:
void* ptr_; // 指向资源的指针
int count_; // 引用计数
};
// 智能指针类
template
class CSmartPtr {
public:
// 构造函数
CSmartPtr(T* ptr = nullptr) : mptr(ptr) {
if (mptr) {
mpRefCnt = new RefCnt(mptr);
} else {
mpRefCnt = nullptr;
}
}
// 析构函数
~CSmartPtr() {
if (mpRefCnt) {
mpRefCnt->release();
}
}
// 拷贝构造函数
CSmartPtr(const CSmartPtr& src) : mptr(src.mptr), mpRefCnt(src.mpRefCnt) {
if (mpRefCnt) {
mpRefCnt->addRef();
}
}
// 拷贝赋值运算符
CSmartPtr& operator=(const CSmartPtr& src) {
if (this != &src) {
// 释放当前资源
if (mpRefCnt) {
mpRefCnt->release();
}
// 复制新资源
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
// 增加引用计数
if (mpRefCnt) {
mpRefCnt->addRef();
}
}
return *this;
}
// 解引用运算符
T& operator*() const {
return *mptr;
}
// 箭头运算符
T* operator->() const {
return mptr;
}
// 获取引用计数
int use_count() const {
return mpRefCnt ? mpRefCnt->getCount() : 0;
}
private:
T* mptr; // 指向资源的指针
RefCnt* mpRefCnt; // 指向引用计数对象的指针
};
// 测试代码
int main() {
CSmartPtr ptr1(new int(10));
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 1
{
CSmartPtr ptr2 = ptr1; // 拷贝构造
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 2
std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 输出 2
} // ptr2 离开作用域,引用计数减 1
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 1
return 0;
}
多线程下 是不安全的, 因为 引用计数的修改 不是原子操作
std::atomic count_; // 原子引用计数
使用 .use_count() 获得 指针计数
shared_ptr 是强智能指针, weak_ptr 是弱智能指针
强的 能改变资源的 引用计数, 弱的 不行
强智能指针, 交叉引用问题?
造成 new 出来的 资源 无法释放
#include
#include
using namespace std;
class B; // 前向声明
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
shared_ptr __ptrb; // 指向 B 的 shared_ptr
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
// 如果 B 也持有 A 的 shared_ptr,会导致循环引用
shared_ptr __ptra;
};
int main() {
shared_ptr pa(new A()) ;
shared_ptr pb(new B());
pa->__ptrb = pb; // 出作用域之前, 是2 ,达不到析构
pb->__ptra = pa;
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
初始状态(创建对象)
A
对象:
shared_ptr pa(new A());
A
对象的引用计数为 1(来自 pa
)。B
对象尚未创建。+-------------------+
| A |
|-------------------|
| shared_ptr __ptrb | -> nullptr
| ref_count: 1 |
+-------------------+
B
对象:
shared_ptr pb(new B());
B
对象的引用计数为 1(来自 pb
)。A
对象的引用计数仍为 1。+-------------------+ +-------------------+
| A | | B |
|-------------------| |-------------------|
| shared_ptr __ptrb | -> nullptr | shared_ptr __ptra | -> nullptr
| ref_count: 1 | | ref_count: 1 |
+-------------------+ +-------------------+
步骤 1:A
持有 B
(pa->__ptrb = pb
)
A
对象的 __ptrb
指向 B
对象。B
对象的引用计数从 1 变为 2(来自 pb
和 pa->__ptrb
)。A
对象的引用计数仍为 1。+-------------------+ +-------------------+
| A | | B |
|-------------------| |-------------------|
| shared_ptr __ptrb | ----> | shared_ptr __ptra | -> nullptr
| ref_count: 1 | | ref_count: 2 |
+-------------------+ +-------------------+
步骤 2:B
持有 A
(pb->__ptra = pa
)
B
对象的 __ptra
指向 A
对象。A
对象的引用计数从 1 变为 2(来自 pa
和 pb->__ptra
)。B
对象的引用计数仍为 2。+-------------------+ +-------------------+
| A | | B |
|-------------------| |-------------------|
| shared_ptr __ptrb | ----> | shared_ptr __ptra |
| ref_count: 2 | | ref_count: 2 |
+-------------------+ +-------------------+
^ |
| |
+-----------------------------+
最终状态(循环引用)
A
和 B
对象的引用计数均为 2。shared_ptr
,引用计数无法归零,导致内存泄漏。定义对象的地方 使用 强智能指针, 引用对象的 地方 使用 弱智能指针
#include
#include
using namespace std;
class B; // 前向声明
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr __ptrb; // 修改
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr __ptra; // 修改
};
int main() {
shared_ptr pa(new A()) ;
shared_ptr pb(new B());
pa->__ptrb = pb; // 出作用域之前, 是2 ,达不到析构
pb->__ptra = pa;
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
只观察资源, 不能使用资源
需要转换为 std::shared_ptr
才能访问对象
weak_ptr::lock
方法,可以将 std::weak_ptr
转换为 std::shared_ptr
,从而安全地访问对象。 shared_ptr ps = __ptra.lock();
.lock()lock
返回一个空的 std::shared_ptr
。#include
#include
using namespace std;
class B; // 前向声明
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void test() { cout << "test" << endl; }
weak_ptr __ptrb;
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
void func()
{
//__ptra->test(); // 弱智能指针不能 访问对象
shared_ptr ps = __ptra.lock();
ps->test();
}
weak_ptr __ptra;
};
int main() {
shared_ptr pa(new A()) ;
shared_ptr pb(new B());
pa->__ptrb = pb;
pb->__ptra = pa;
pb->func();
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
非常著名的 开源网络库 muduo 库
该源码中对于智能指针的应用非常优秀,其中借助shared_ptr和
weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:
线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又
要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问
该对象,就会发生不可预期的错误。
代码如下:
#include
#include
using namespace std;
class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(Test *p)
{
// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
*/
p->show();
}
int main()
{
// 在堆上定义共享对象
Test *p = new Test();
// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
std::thread t1(threadProc, p);
// 在main线程中析构Test共享对象
delete p;
// 等待子线程运行结束
t1.join();
return 0;
}
运行上面的代码,发现在main主线程已经delete析构Test对象以后,子线程threadProc再去访问Test对象的show方法,无法打印出*_ptr的值20。可以用shared_ptr和weak_ptr来解决多线程访问共享对象的线程安全问题,上面代码修改如下:
#include
#include
#include
using namespace std;
class Test
{
public:
// 构造Test对象,_ptr指向一块int堆内存,初始值是20
Test() :_ptr(new int(20))
{
cout << "Test()" << endl;
}
// 析构Test对象,释放_ptr指向的堆内存
~Test()
{
delete _ptr;
_ptr = nullptr;
cout << "~Test()" << endl;
}
// 该show会在另外一个线程中被执行
void show()
{
cout << *_ptr << endl;
}
private:
int *volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
{
// 睡眠两秒
std::this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
*/
shared_ptr<Test> ps = pw.lock();
if (ps != nullptr)
{
ps->show();
}
}
int main()
{
// 在堆上定义共享对象 , 智能释放
shared_ptr<Test> p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main线程中析构Test共享对象
// 阻塞等待子线程运行结束
t1.join();
return 0;
}
运行上面的代码,show方法可以打印出20,因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功,见上面代码示例。
如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败!main函数代码可以如下修改测试:
int main()
{
// 在堆上定义共享对象
shared_ptr<Test> p(new Test);
// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
std::thread t1(threadProc, weak_ptr<Test>(p));
// 在main线程中析构Test共享对象
// 设置子线程分离 主线程不等待子线程
t1.detach();
return 0;
}
该main函数运行后,最终的threadProc中,show方法不会被执行到。以上是在多线程中访问共享对象时,对shared_ptr和weak_ptr的一个典型应用。
我们经常用智能指针管理的资源是堆内存,当智能指针出作用域的时候,在其析构函数中会delete释放堆内存资源,但是除了堆内存资源,智能指针还可以管理其它资源,比如打开的文件,此时对于文件指针的关闭,就不能用delete了,这时我们需要自定义智能指针释放资源的方式,先看看unique_ptr智能指针的析构函数代码,如下:
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get()); // 这里获取底层的删除器,进行函数对象的调用
}
}
从unique_ptr的析构函数可以看到,如果要实现一个自定义的删除器,实际上就是定义一个函数对象而已,示例代码如下:
_EXPORT_STD template */>
class unique_ptr { // non-copyable pointer to an object
{}
这是unique的源码, 可以看到, 模板里的第二个参数, 使用 默认的 删除器, 这个是可选的, 因此 自定义删除类的 传入接口 就在这里
#include
#include
#include
using namespace std;
template
class MyDeletor
{
public:
void operator() (T* ptr) const
{
cout << "call MyDeletor.operator()" << endl;
delete[] ptr;
}
};
int main()
{
unique_ptr> ptr1(new int[100]);
return 0;
}
class FileDeleter
{
public:
// 删除器负责删除资源的函数
void operator()(FILE *pf)
{
fclose(pf);
}
};
int main()
{
// 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
return 0;
}
当然这种方式需要定义额外的函数对象类型,不推荐,
function下一节会讲到
自定义删除器 一般指出现在 指定语句中, 写类 就显得繁杂了
可以用C++11提供的函数对象function和lambda表达式更好的处理自定义删除器,代码如下:
#include
#include
#include // 包含 fopen 和 fclose 的头文件
#include
using namespace std;
int main()
{
// 自定义智能指针删除器,关闭文件资源
unique_ptr<FILE, function<void(FILE*)>>
filePtr(fopen("data.txt", "w"), [](FILE* pf)->void {cout << "call lambda release file" << endl;fclose(pf); });
// 自定义智能指针删除器,释放数组资源
unique_ptr<int, function<void(int*)>>
arrayPtr(new int[100], [](int* ptr)->void {cout << "call lambda release int []" << endl;delete[]ptr; });
return 0;
}
如果想进一步了解智能指针,可以查看智能指针的源码实现,或者看muduo网络库的源码。