智能指针是一种在编程中用于管理动态分配内存的指针。其使用了资源获取即初始化(RAII)"的模式。它是一种封装了原始指针的对象,提供了额外的功能,例如:自动内存管理、所有权传递、线程安全等等。智能指针通过一定的规则来确保动态分配内存的正确使用,避免内存泄漏、重复释放、使用已经释放的内存等问题。C++ 语言中常见的智能指针包括 std::unique_ptr、std::shared_ptr、std::weak_ptr 等。
这时大家就会有个疑问了?既然已经有了指针,我们程序员自己就可以对内存资源的释放,为什么还要智能指针呢,这不是多此一举吗?
其实不然,有些情况简单对内存释放是并不能达到我们预期的结果的,举个栗子:
int *p = new int[10];
... // 功能操作
delete[] p;
乍一看,好像上述的代码没有什么问题,申请了内存,也手动释放了内存。但是这真的就是不会出问题吗?
并不是!如果在功能操作里面发生了异常,使代码的执行顺序跳过了delete[]释放的语句,这样就会导致内存泄漏。类似的例子是在函数体里return的返回,这都会导致程序并没有按照我们所愿的执行顺序执行下去,从而导致内存泄漏和线程安全。
所以使用智能指针就可以很大程度的减轻我们对内存资源的管理压力。
- 内存泄漏:当使用动态分配内存时,如果忘记释放内存,就会导致内存泄漏。而智能指针可以在对象被销毁时自动释放它所管理的内存,避免了手动释放内存的问题。
- 多线程安全:当多个线程同时访问同一个对象时,如果没有正确地使用同步机制,就会导致内存管理问题。而智能指针可以提供线程安全的内存访问,避免了多线程竞争的问题。
既然智能指针如此重要,我们现在就来学习一下他们的类型和作用。
智能指针
- auto_ptr运用了RAII思想,能够实现对资源的自动释放。
- 拥有与指针一样的使用功能
使用例子:
class Animal
{
public:
~Animal() { cout << "~Animal()" << endl; }
};
class Dog: public Animal
{
public:
~Dog() { cout << "~Dog()" << endl; }
};
void ptr()
{
auto_ptr<Dog> p(new Dog);
}
缺点:
- 尽可能不要将auto_ptr变量定义为全局变量或指针
- 除非自己知道后果,不要把auto_ptr智能指针赋值给同类型的另外一个智能指针
- auto_ptr不支持容器。
- auto_ptr的析构函数调用的是delete不是delete [],所以不能使用智能指针数组。
判断一个auto_ptr是否为空不能使用if(ptest == NULL),应该使用
if(ptest.get() == NULL)
auto_ptr的拷贝赋值就是资源所有权的转移,会出现意想不到的意外
前面说了,auto_ptr是存在隐患的,所以就c++11就出来了个unique_ptr,这个智能指针是安全的,并且具备auto_ptr的所有功能。
至于为什么安全呢?这里解释一下,因为他把拷贝构造函数,赋值运算符函数都禁止使用了
看看源码
unique_ptr是很安全了,但是美中不足的是他不可以进行赋值,我们都知道,原始指针是可以进行赋值,所以unique_ptr还是不够完美的,此时shared_ptr就闪亮登场了。
shared_ptr指针不仅安全,而且还可以进行赋值。
那么他是如何做到赋值的功能的呢?下面就是它的实现原理
“引用计数”,是一种内存管理技术,用于跟踪内存中对象的引用数量。每当有一个指针引用一个对象时,引用计数就会增加;相应地,当指针不再引用对象时,引用计数就会减少。当引用对象没有任何活动引用,可以安全地释放其占用的内存空间。
是的,shared_ptr里面有个计数器
shared_ptr看似十分完美,但是却暗藏杀机
是的,存在循环引用的绝杀
当使用shared_ptr存在循环引用时,对象的内存可能无法被正确释放,导致内存泄漏。以下是一个示例代码,展示了shared_ptr中循环引用的缺陷:
#include
class A {
public:
std::shared_ptr<A> B;
~A() {
std::cout << "Destructor called." << std::endl;
}
};
int main() {
std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2(new A());
a1->B = a2;
a2->B = a1;
return 0;
}
在上面的代码中,我们创建了两个A的对象,a1和a2。然后,我们将它们相互引用,形成了循环引用关系。a1拥有指向a2的智能指针,而a2也拥有指向a1的智能指针。
由于循环引用,当程序结束时,这两个对象的析构函数不会被调用,对象所占用的内存也无法被正确释放,造成了内存泄漏。通过输出语句std::cout << "Destructor called." << std::endl;
可以观察到析构函数没有被用。
为避免该问题,可以使用weak_ptr来打破循环引用,或者手动解除循环引用关系,例如通过调用reset()将智能指针重置为nullptr。
是弱引用,是资源的观察者,依托于shared_ptr得以发挥作用,主要用于检查资源是否存在,不影响资源的生命周期
template<class T>
shared_ptr<T> s = new int(10);
weak_ptr<T> w(s);
w.reset(); // 将w置空
w.use_count(); // 返回与w共享对象的shared_ptr的数量
w.expired(); // w.use_count()为0,则返回true,否则返回false
w.lock(); // w.expired()为真,返回一个指向w的shared_ptr对象;否则返回空shared_ptr
回到shared_ptr的循环引用的问题,weak_ptr的主要作用也就是解决循环引用问题
#include
class A {
public:
// std::shared_ptr B;
std::weak_ptr<A> B; // 换成弱引用即可解决问题
~A() {
std::cout << "Destructor called." << std::endl;
}
};
int main() {
std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2(new A());
a1->B = a2;
a2->B = a1;
return 0;
}
直接上代码
template<class T>
class unique_ptr
{
public:
unique_ptr() : ptr_(nullptr) {}
unique_ptr(T* up) : ptr_(up) {}
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
T& operator*() { return *ptr_; }
T* operator->() { return ptr_; }
~unique_ptr()
{
if(ptr_)
delete ptr_;
}
private:
T* ptr_;
};
template<class T>
class shared_ptr
{
public:
shared_ptr()
: ptr_(nullptr), count_(nullptr){}
shared_ptr(T* sp)
: ptr_(sp), count(new int(1)) {}
shared_ptr(const shared_ptr<T>& sp) : ptr_(sp), count_(sp.count_) { (*count_)++; }
T& operator*() { return *ptr_; }
T* operator->() { return ptr_; }
T* get() { return ptr_; }
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (sp.ptr_ != ptr_)
{
// 释放原有空间,防止内存泄漏
release();
_ptr = sp._ptr;
_count = sp._count;
(*_count)++;
}
return *this;
}
~shared_ptr()
{
release();
}
private:
void release()
{
if (-- * count == 0 && ptr_)
{
delete count_;
delete ptr_;
count_ = ptr_ = nullptr;
}
}
private:
T* ptr_;
int* count_;
};