一、什么是智能指针?

  1. 使用new操作符手动分配内存时,就必须时刻警惕,在恰当的时机使用delete释放内存 ,一旦稍有差池,忘记释放内存,内存泄漏便会如幽灵般悄然出现,随着程序的持续运行,被泄漏的内存不断堆积,最终可能导致系统内存枯竭,程序无奈崩溃。
  2. 在内存已经释放后,指针仍在,且被错误使用,悬空指针便会现身,这同样可能引发程序崩溃或出现难以预测的错误行为。

二、为什么使用智能指针

在 C++ 的编程世界中,内存管理一直是让人又爱又恨的存在。手动管理内存赋予了开发者极高的控制权,能让程序在性能上达到极致;但稍有不慎,就会掉进内存泄漏、野指针等陷阱中,让程序变得极其不稳定。智能指针的出现,就像是一道光照进了这个略显混乱的内存管理世界。

        1.在 C++ 中,使用new分配内存后,如果忘记使用delete释放,内存泄漏就会悄然而至。比如在一个复杂的函数中:

void Myfunction()
{
    int* ptr = new int(10);
    //这里有大量的代码逻辑
    //很可能疏忽而忘记释放ptr

}

        2.野指针的问题同样棘手

void Myfunction()
{
    int* ptr = new int(10);
    delete ptr;
    //此时ptr成为了野指针
    //如果后续不小心再次使用ptr,就会导致未定义行为
    int value = *ptr;
}

        3.重复释放内存,对同一块内存进行多次释放,程序也会崩溃。

        为了解决这些问题,C++ 引入了智能指针。智能指针利用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)机制,将内存的管理和对象的生命周期绑定在一起。当智能指针对象创建时,它获取资源(即分配内存);当智能指针对象销毁时,它自动释放所指向的内存,无需手动调用delete。这就像是给内存分配和释放过程加上了一个可靠的 “管家”,极大地降低了内存管理出错的风险 。

三、智能指针的类型?

在 C++ 的智能指针家族中,包含了auto_ptr(已弃用)、unique_ptr、shared_ptr和weak_ptr ,每种智能指针都有其独特的设计目的和应用场景,接下来让我们深入了解一下它们。

1.auto_ptr(已弃用) 

        auto_ptr是 C++98 引入的智能指针,也是智能指针的 “鼻祖”,它采用所有权模式,当auto_ptr对象过期时,其析构函数会自动使用delete释放所指向的内存 ,这在一定程度上简化了内存管理,降低了内存泄漏的风险。然而,auto_ptr存在一些严重的缺陷,这也是它被弃用的原因。首先,auto_ptr在赋值和拷贝时会发生所有权转移,原指针会变为空指针。例如:以下程序会崩溃:

#include<iostream>
#include<memory>
using namespace std;
int main()
{
	auto_ptr<int> p1(new int(10));
	auto_ptr<int> p2 = p1;
	cout << "*p1=" << *p1 << endl;
	cout << "*p2=" << *p2 << endl;

	return 0;
	system("pause");
}

     结果:

      问题1:

       在上述代码中,p1赋值给p2后,p1的所有权转移给了p2,p1变为空指针,此时访问p1会导致未定义行为,这在实际编程中很容易引发难以调试的错误。

      问题2:

        auto_ptr不支持数组的管理,它的析构函数使用delete而不是delete[],如果用auto_ptr管理 数组,会导致内存释放错误。此外,auto_ptr不符合 STL 容器对元素拷贝语义的要求,无法在 STL 容器中使用 。由于这些问题,auto_ptr在 C++11 中被弃用,逐渐被更优秀的智能指针所取代。

2.unique_ptr

unique_ptr是 C++11 引入的智能指针,它实现了独占式拥有概念,保证同一时间内只有一个智能指针可以指向该对象,有效避免了资源泄露。unique_ptr不允许拷贝和赋值,这是为了确保资源的独占性 。例如:

#include<iostream>
#include<memory>
using namespace std;
int main()
{
	unique_ptr<int> p1(new int(10));
	//auto_ptr<int> p2 = p1;  //编译错误,不允许拷贝
	unique_ptr<int> p3 = move(p1);
	if (!p1)
	{
		cout << "p1 is nullptr" << endl;
	}
	cout << "p3=" << *p3<<endl;
	return 0;
	system("pause");
}

在这段代码中,尝试将p1赋值给p2会导致编译错误,而通过std::move将p1的所有权转移给p3后,p1变为空指针,p3拥有了资源的所有权 。

unique_ptr支持管理数组,它会使用delete[]来释放数组内存 。同时,unique_ptr还可以作为函数返回值,高效地将资源的所有权转移给调用者。例如:

#include<iostream>
#include<memory>
using namespace std;

unique_ptr<int> createint()
{
	return unique_ptr<int>(new int(20));
}


int main()
{
	unique_ptr<int>ptr = createint();
	cout << *ptr << endl;
	return 0;
	system("pause");
}

在上述代码中,createInt函数返回一个unique_ptr<int>,将资源的所有权转移给main函数中的ptr 。

3.shared_ptr

shared_ptr实现了共享式拥有概念,允许多个智能指针指向相同对象,通过引用计数机制来管理对象的生命周期 。当一个shared_ptr指向对象时,引用计数加 1;当shared_ptr离开作用域或被重新赋值时,引用计数减 1,当引用计数为 0 时,对象会被自动释放 。例如:

#include<iostream>
#include<memory>
using namespace std;

int main()
{
	shared_ptr<int> s1(new int(10));
	shared_ptr<int> s2 = s1;
	cout << "s1 use_count:" << s1.use_count() << endl;
	cout << "s2 use_count:" << s2.use_count() << endl;
	s1.reset();
	cout << "s2 use_count:" << s2.use_count() << endl;
	return 0;
	system("pause");
}

注意:reset() 函数的默认行为是将 shared_ptr 持有的对象释放,并将 shared_ptr 重置为空指针。如果 reset() 不带参数,它将释放当前 shared_ptr 持有的对象,并将其重置为 nullptr。

在这段代码中,s1和s2共享同一个对象,引用计数为 2,当s1调用reset函数后,引用计数减 1,此时只有s2指向对象,引用计数变为 1 。

shared_ptr在多线程环境中使用时,需要注意线程安全问题,因为引用计数的修改不是原子操作,可能会导致数据竞争 。为了解决这个问题,可以使用std::atomic来实现原子操作,或者使用互斥锁来保护引用计数的修改 。此外,shared_ptr还支持自定义删除器,可以在对象被释放时执行一些额外的清理操作 。

4.weak_ptr

weak_ptr是一种不控制对象生命周期的智能指针,它指向一个由shared_ptr管理的对象,主要用于协助shared_ptr解决循环引用问题 。weak_ptr不会增加对象的引用计数,它的构造和析构不会影响对象的生命周期 。例如:

#include<iostream>
#include<memory>
using namespace std;

class B;
class A
{
public:
	shared_ptr<B> pb1;
	~A() {
		cout << "A delete" << endl;
	}
};

class B
{
public:
	shared_ptr<A> pa1;
	~B() {
		cout << "B delete" << endl;
	}
};

void func()
{
	shared_ptr<A> pa(new A());
	shared_ptr<B> pb(new B());
	pa->pb = pb;
	pb->pa = pa;
	cout << "pa use_count: " << pa.use_count() << endl;
	cout << "pb use_count: " << pb.use_count() << endl;

}
int main()
{
	func();
	return 0;
	system("pause");
}

结果:

在上述代码中,A和B相互引用,形成了循环引用,导致pa和pb的引用计数永远不会为 0,对象无法被释放(A,B的对象都创建在堆上,两者互相依赖) 。如果将A中的std::shared_ptr<B> pb;改为std::weak_ptr<B> pb;,就可以打破循环引用,使对象能够正常释放 。

例如:

#include<iostream>
#include<memory>
using namespace std;

class B;
class A
{
public:
	weak_ptr<B> pb1;
	~A() {
		cout << "A delete" << endl;
	}
};

class B
{
public:
	shared_ptr<A> pa1;
	~B() {
		cout << "B delete" << endl;
	}
};

void func()
{
	shared_ptr<A> pa(new A());
	shared_ptr<B> pb(new B());
	pa->pb = pb;
	pb->pa = pa;
	cout << "pa use_count: " << pa.use_count() << endl;
	cout << "pb use_count: " << pb.use_count() << endl;

}
int main()
{
	func();
	return 0;
	system("pause");
}

结果:

使用weak_ptr(弱引用)不增加B对象的引用计数。因此,B对象的引用计数为1,A对象的引用计数为2,若此时代码执行结束,栈空间上的pa指针先进行释放,A对象的引用计数减1为1,后释放pb指针,B对象的引用计数减1后为0,B对象释放内存空间,因此pa1成员函数也得到释放,A对象引用计数减1后为0,A对象也得到释放。因此不会产生内存泄漏。
 

四、智能指针间的区别

在所有权模型上,unique_ptr、shared_ptr 和 weak_ptr 有着本质的不同。unique_ptr 就像是一位独占资源的 “霸道总裁”,它对所指向的对象拥有独占所有权 ,在其生命周期内,不允许其他 unique_ptr 同时指向同一个对象。这一特性使得 unique_ptr 在管理需要明确单一所有权的资源时,表现出色,例如一个函数内部动态分配的临时对象,通过 unique_ptr 管理,能够确保在函数结束时,对象被正确释放,避免内存泄漏 。

shared_ptr 则像是一个资源共享的 “社交达人”,允许多个 shared_ptr 指向同一个对象,通过引用计数机制来共享对象的所有权 。每一个指向该对象的 shared_ptr 都会增加引用计数,当 shared_ptr 离开作用域或被重新赋值时,引用计数减 1,只有当引用计数降为 0 时,对象才会被销毁 。这种所有权模型适用于多个组件需要共享同一资源的场景,比如在一个多模块协作的程序中,多个模块可能需要访问同一个配置文件对象,就可以使用 shared_ptr 来管理这个配置文件对象的所有权 。

weak_ptr 更像是一个对对象的 “观察者”,它不拥有对象的所有权,只是对 shared_ptr 所管理的对象进行弱引用 。weak_ptr 的存在不会影响对象的引用计数,它主要用于解决 shared_ptr 之间可能出现的循环引用问题 。例如,在一个双向链表的实现中,如果节点之间使用 shared_ptr 相互引用,就会形成循环引用,导致节点无法被正确释放,而使用 weak_ptr 来表示其中一个方向的引用,就可以打破循环引用,使节点能够正常释放 。

内存管理方式

在内存管理方式上,auto_ptr(已弃用)、unique_ptr 和 shared_ptr 各有特点 。auto_ptr 在过期时,会自动使用 delete 释放所指向的内存,但由于其在赋值和拷贝时会发生所有权转移,容易引发悬空指针等问题,所以在 C++11 中已被弃用 。

unique_ptr 采用独占式的内存管理方式,当 unique_ptr 对象被销毁时,它所指向的内存会被立即释放 。这是因为 unique_ptr 不允许拷贝和赋值,确保了资源的独占性,从而保证了内存释放的及时性和正确性 。例如,在管理一个文件句柄时,可以使用 unique_ptr 来确保在不再需要文件句柄时,文件资源能够被及时关闭和释放 。

shared_ptr 通过引用计数来管理内存,只有当引用计数为 0 时,对象所占用的内存才会被释放 。这种内存管理方式使得 shared_ptr 在需要共享资源的场景中非常灵活,但也带来了一定的性能开销,因为每次引用计数的增减都需要进行额外的操作 。此外,在多线程环境下,由于引用计数的修改不是原子操作,需要额外的同步机制来保证线程安全 。

weak_ptr 本身并不直接管理内存,它主要用于协助 shared_ptr 解决循环引用问题 。当 weak_ptr 指向的对象被销毁后,weak_ptr 并不会自动变为空指针,而是需要通过 lock 函数来检查对象是否仍然存在 。如果对象已被销毁,lock 函数会返回一个空的 shared_ptr,从而避免了对已释放内存的访问 。

应用场景不同

不同的智能指针适用于不同的应用场景 。unique_ptr 适用于需要明确单一所有权的场景,例如在函数返回值中传递动态分配的对象,或者在类中管理成员对象的生命周期 。在一个工厂函数中,当创建一个新的对象并返回时,可以使用 unique_ptr 来确保对象的所有权能够安全地转移给调用者,并且在调用者不再需要该对象时,对象能够被正确释放 。

shared_ptr 适用于需要多个对象共享同一资源的场景,比如在实现一个全局的资源池时,多个模块可能需要从资源池中获取和使用资源,使用 shared_ptr 可以方便地管理资源的生命周期,确保在所有模块都不再使用资源时,资源才会被释放 。此外,在实现一些复杂的数据结构,如哈希表、链表等,当节点需要被多个地方引用时,也可以使用 shared_ptr 来管理节点的所有权 。

weak_ptr 主要用于解决 shared_ptr 之间的循环引用问题,以及在需要观察对象状态但不影响对象生命周期的场景中使用 。在一个观察者模式的实现中,观察者对象可能需要观察被观察对象的状态变化,但观察者对象的生命周期不应该影响被观察对象的生命周期,这时就可以使用 weak_ptr 来实现观察者对象对被观察对象的引用 。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐