前言:
在C 编程中,内存管理是至关重要的一个环节。传统的手动内存管理方式容易导致内存泄漏、悬挂指针等问题。为了解决这些问题,C 引入了智能指针。本文将详细讲解C 中智能指针的概念、种类、使用方法以及注意事项。
一、引言
在正式讲解智能指针之前,我们先来了解一下为什么会诞生智能指针:
在C 中,指针是用于访问内存地址的一种特殊变量。传统的指针管理需要程序员手动分配和释放内存,这容易导致以下问题:
- 内存泄漏:当程序员忘记释放内存时,会导致内存泄漏,最终耗尽系统资源。
- 悬挂指针:当指针指向的内存被释放后,如果指针没有被设置为NULL,那么它就变成了悬挂指针,访问悬挂指针可能会导致未定义行为。
- 双重释放:当指针被错误地释放两次时,会引发程序崩溃。
为了解决这些问题,C 引入了智能指针,它是一种特殊的对象,能够自动管理指针指向的内存。
下面是一个内存泄漏的例子:
代码语言:javascript复制void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数如果抛异常就会导致 delete[] p3未执行,p3没被释放.
delete[]p3;
}
二、智能指针的原理及目的
了解使用智能指针之前,我们要先来了解RAII
2.1 智能指针的原理
2.1.1 RAII
RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处: · 不需要显式地释放资源。 · 采用这种方式,对象所需的资源在其生命期内始终保持有效
智能指针是一种能够自动管理指针指向内存的类模板。它通过重载解引用运算符(*)和箭头运算符(->)来模拟指针的行为,同时内部使用某种机制(RAII的原理)来自动释放内存。
总结一下智能指针的原理:
1. RAII特性 2. 重载operator*和opertaor->,具有像指针一样的行为。
2.2 智能指针的目的
智能指针的主要目的是:
1、自动释放内存:当智能指针超出作用域或被销毁时,它会自动释放所管理的内存。 2、防止内存泄漏和悬挂指针:智能指针确保内存被正确释放,从而避免内存泄漏和悬挂指针。
三、智能指针的种类
C 标准库提供了三种主要的智能指针:
std::unique_ptr
:独占智能指针,表示指针指向的内存只能由一个智能指针拥有。std::shared_ptr
:共享智能指针,表示多个智能指针可以共享同一块内存。std::weak_ptr
:弱指针,用于解决共享指针可能导致的循环引用问题。
在标准库出来之前,还有一个auto_ptr,下面我们会对这几个进行逐一讲解
3.1 std::auto_ptr
auto_ptr是在C 98版本中就给出的,它的实现原理是:管理权转移,只有一个对象能够管理资源
下面是auto_ptr的简单实现:
代码语言:javascript复制template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值(这一步很重要,不然容易发生双重释放)
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
3.2 std::unique_ptr
std::unique_ptr
是独占智能指针,它确保了指针指向的内存只能由一个智能指针拥有。当std::unique_ptr
被销毁或赋值给另一个std::unique_ptr
时,它所指向的内存会被自动释放。
下面我们来看一下库中它的声明方式:
代码语言:javascript复制#include <memory>
int main() {
std::unique_ptr<int> ptr(new int(10));
// 使用ptr
// ...
return 0;
}
我们来简单的模拟一下它的实现(简单粗暴,防止拷贝,这样就能确保管理权不会被转移):
代码语言:javascript复制template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//将它的赋值和拷贝都禁止,就可以保证管理权无法转移
//delete关键字:声明函数时用到这个可以使这个函数无意义
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
3.3 std::shared_ptr
std::shared_ptr
是共享智能指针,它允许多个智能指针共享同一块内存。std::shared_ptr
内部使用引用计数来管理内存,当引用计数为0时,内存会被自动释放。
下面我们来看一下库中它的声明方式:
代码语言:javascript复制#include <memory>
int main() {
std::shared_ptr<int> ptr1(new int(10));
std::shared_ptr<int> ptr2 = ptr1;
// 使用ptr1和ptr2
// ...
return 0;
}
我们来简单的模拟一下它的实现:
代码语言:javascript复制 //引用计数
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
~shared_ptr()
{
if(--(*_pcount)==0)
{
cout << "~shared_ptr()" << endl;
delete _ptr;
delete _pcount;
}
}
int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if(_ptr!=sp._ptr)
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
(*sp._pcount) ;
_pcount = sp._pcount;
return *this;
}
}
private:
T* _ptr;
int* _pcount;
};
这里其实是涉及到线程锁的一些知识,这些内容比较靠后,等我们后面学到之后再回来讲
3.4 std::weak_ptr
std::weak_ptr
是弱指针,它用于解决共享指针可能导致的循环引用问题。弱指针不会增加引用计数,因此不会阻止内存的释放。
那么什么是共享指针的循环引用呢?我们下面详细讲解一下
先看一下下面这个共享指针(就是shared_ptr)的例子
(这个样例是建立在我们上面的模拟实现上的)
代码语言:javascript复制//循环引用
struct ListNode
{
int val;
zda::shared_ptr<ListNode> prev;
zda::shared_ptr<ListNode> next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr3()
{
zda::shared_ptr<ListNode> n1 = new ListNode;
zda::shared_ptr<ListNode> n2 = new ListNode;
//循环引用(下面这两个同时放开的时候会发生循环引用引发崩溃)
//n1->next = n2;
n2->prev = n1;
}
所以说shared_ptr在有些情况下会有循环引用的问题存在,比如链表,而weak_ptr就是专门来解决shared_ptr这个问题的,所以weak_ptr的模拟实现上与shared_ptr十分相似,可以直接借用
代码语言:javascript复制 //weak_ptr
//不支持RAII
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.~shared_ptr;
return *this;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
四、智能指针的使用方法
使用智能指针时,需要注意以下几点:
- 初始化:智能指针必须通过
new
操作符或构造函数进行初始化。 - 赋值:智能指针之间可以相互赋值,但
std::unique_ptr
不能赋值给std::shared_ptr
。 - 解引用:使用解引用运算符(*)和箭头运算符(->)来访问智能指针指向的内存。
- 重置:使用
reset
方法来重置智能指针,释放当前指向的内存,并可以重新指向新的内存。 - 比较:智能指针之间可以使用比较运算符进行比较。
五、注意事项
- 避免循环引用:在使用共享智能指针时,要注意避免循环引用,否则可能导致内存无法释放。
- 不要使用原始指针:尽量避免使用原始指针来管理内存,使用智能指针可以简化代码并提高安全性。
- 了解智能指针的行为:在使用智能指针之前,要了解它们的行为,以避免潜在的问题。
六、总结
以上就是C 智能指针的知识点总结,有些涉及线程安全的问题等到后期学到之后再进行补充
感谢各位大佬观看,创作不易,还请各位大佬点赞支持!!!