智能指针
智能指针
智能指针的发展历史
智能指针的发展历史可以追溯到C++语言对内存管理问题的不断探索和改进。以下是智能指针的主要发展阶段:
一、早期尝试与std::auto_ptr
时间背景:C++98/03标准时期。
特点:std::auto_ptr是C++98标准中定义的第一个智能指针模板。它管理通过new获得的地址,当对象过期时,其析构函数将使用delete来释放内存。
缺陷:
拷贝或赋值会改变资源的所有权
,这导致在STL容器中使用std::auto_ptr存在重大风险,因为容器内的元素必须支持可复制和可赋值。不支持数组对象的管理。
ps:所以并不建议使用此指针,指针悬空非常危险
二、std::unique_ptr的引入
时间背景:C++11标准。
特点:std::unique_ptr对其持有的堆内存具有唯一拥有权,即引用计数永远是1。std::unique_ptr对象销毁时会释放其持有的堆内存。
优势:
禁止复制语义
,只能通过移动构造来转移所有权,这避免了多个智能指针指向同一个资源的问题。提供了更安全、更明确的内存管理方式。
三、std::shared_ptr与std::weak_ptr的推出
时间背景:C++11标准。
std::shared_ptr特点:
- 实现了引用计数机制,可以自动追踪和管理指向堆上对象的引用计数。
- 当引用计数变为零时,智能指针会自动释放内存。
- 适合用来管理那些生命周期不确定的资源,比如在类中管理成员变量的生命周期,或者在函数间传递资源所有权。
std::weak_ptr特点:
- 为解决std::shared_ptr可能产生的循环引用问题而设计下面会解释此问题。
- 提供了一种不增加引用计数的引用方式,使得std::shared_ptr对象间可以相互查看,但不会阻止它们各自作用域结束时资源的释放。
四、智能指针的进一步发展与完善
C++14及以后:C++14标准中添加了std::make_unique方法,使得创建std::unique_ptr更加安全和便捷。
性能与优化:随着C++标准的发展,智能指针的性能也得到了不断优化,以更好地满足高性能应用的需求。
多线程支持:虽然std::shared_ptr本身不是线程安全的,但可以通过额外的同步机制(如锁)来实现线程安全的智能指针。
auto_ptr
一、基本特性
定义:auto_ptr是一种智能指针(虽然也没那么智能),它是“它所指向的对象”的拥有者。这种拥有具有唯一性,即一个对象只能有一个auto_ptr所指向它,严禁一物二主。
原理:auto_ptr的实现原理是RAII(Resource Acquisition Is Initialization),在构造的时候获取资源,在析构的时候释放资源。简单说就是构造的时候加入进去管理,智能指针销毁的时候一起销毁管理的内容
接口:auto_ptr的接口行为与普通指针相似,提供了operator*和operator->来访问其所指向的对象。但它没有定义任何的算术运算(包括++)。
二、使用方式
构造:auto_ptr的构造函数被声明为explicit,因此不能通过隐式转换来构造,只能显式调用构造函数。例如:
std::auto_ptr<int> pa(new int(10));
赋值:当auto_ptr以传值方式被作为参数传递给某函数时,这时对象的原来拥有者(实参)就放弃了对象的拥有权,把它转移到被调用函数中的参数(形参)上。赋值操作同样会转移拥有权。
这种情况下就会造成原指针悬空,不能再使用
例如:
std::auto_ptr<int> pa(new int(10));
std::auto_ptr<int> pb;
pb = pa; // 此时pb拥有对象,pa不再拥有
销毁:当auto_ptr指针被摧毁时,它所指向的对象也将被隐式销毁。即使程序中有异常发生,auto_ptr所指向的对象也将被销毁。
三、注意事项
-
不要共享所有权:不要让两个auto_ptr指向同一个对象,否则会导致重复释放或访问已被释放的内存。
-
不要指向数组:auto_ptr是通过delete而不是delete[]来释放其所拥有的资源的,因此不能指向数组。
-
不要作为容器元素:STL容器中的元素经常要支持拷贝和赋值等操作,而auto_ptr在这些过程中会传递所有权,导致source与sink元素之间不等价,因此不要将auto_ptr作为标准容器的元素。
-
避免异常安全问题:虽然auto_ptr可以在一定程度上解决内存泄漏问题,但在构造函数中申请多个资源时,如果其中一个资源申请失败,则可能导致已申请的资源无法释放。因此,在使用auto_ptr时需要谨慎处理异常安全问题。
ps:正因如此,不建议使用此老版本智能指针,实在是不太智能,很多公司明确规定不能使用,所以了解一下就好了
unique_ptr
概念
unique_ptr是C++11中引入的一种智能指针,用于管理动态分配的内存。它的主要特点是独占所有权,即每个unique_ptr实例拥有并管理所指向对象的唯一所有权。这种独占性确保了内存资源的安全释放,避免了内存泄漏和悬空指针问题。
定义
unique_ptr的定义在头文件中,它是一个模板类,可以管理任何类型的动态分配对象。其定义如下:
template< class T, class Deleter = std::default_delete<T> > class unique_ptr;
其中,T是unique_ptr所管理对象的类型,Deleter是一个删除器,用于在unique_ptr销毁时释放对象。默认情况下,使用std::default_delete作为删除器。
删除器是一个函数对象(可以是函数指针、函数对象或lambda表达式),它定义了当unique_ptr销毁时如何释放其所管理的资源。默认情况下,unique_ptr使用std::default_delete<T>作为其删除器,该删除器通过调用delete来释放对象。但在某些情况下,可能需要自定义删除器来执行特定的清理操作,例如释放动态分配的内存(free等)、关闭文件、释放锁等。
语法
unique_ptr的基本语法包括创建、访问、移动、释放等操作。以下是一些常用的语法示例:
创建unique_ptr:
std::unique_ptr<int> ptr(new int(10)); // 创建并管理一个int类型的动态分配对象
访问对象:
int value = *ptr; // 使用解引用操作符访问对象
移动unique_ptr:
std::unique_ptr<int> ptr2 = std::move(ptr); // 将ptr的所有权转移到ptr2,ptr变为空
释放对象:
ptr.reset(); // 释放ptr所管理的对象,使ptr变为空
自定义删除器:
void my_deleter(int* ptr) {
// 自定义删除逻辑
delete ptr;
}
std::unique_ptr<int, decltype(&my_deleter)> ptr_with_deleter(new int(20), &my_deleter); // 使用自定义删除器
使用案例
以下是一个使用unique_ptr管理动态分配内存并避免内存泄漏的示例:
#include <iostream>
#include <memory>
void process(std::unique_ptr<int> ptr) {
// 对ptr所指向的对象进行处理
std::cout << "Value: " << *ptr << std::endl;
// 当process函数结束时,ptr会自动销毁并释放所管理的内存
}
int main() {
std::unique_ptr<int> ptr(new int(10)); // 创建unique_ptr并管理动态分配的内存
process(std::move(ptr)); // 将ptr的所有权转移到process函数中
// 此时main函数中的ptr已经为空,不再拥有所管理的内存
return 0;
}
注意事项
-
独占所有权:unique_ptr的独占所有权意味着它不能被复制,只能被移动。因此,在传递unique_ptr时,需要使用std::move来转移所有权。
-
避免悬空指针:由于unique_ptr在销毁时会自动释放所管理的内存,因此可以避免悬空指针问题。但是,如果手动调用了reset方法或转移了所有权,需要确保不再访问已释放的内存。
-
自定义删除器:虽然unique_ptr提供了默认的删除器,但在某些情况下,可能需要使用自定义删除器来执行特定的清理操作。
-
不要与裸指针混用:为了避免内存泄漏和悬空指针问题,建议始终使用unique_ptr来管理动态分配的内存,而不是直接使用裸指针。
-
注意作用域:unique_ptr的作用域结束时,它会自动销毁所管理的对象。因此,在定义unique_ptr时,需要注意其作用域范围,以确保在适当的时候释放内存。
shared_ptr
概念
std::shared_ptr 是 C++ 标准库中定义的一种智能指针,它通过引用计数机制来管理动态分配的内存。多个 shared_ptr 可以指向同一个对象,当所有的 shared_ptr 都被销毁或者不再指向该对象时,该对象会自动释放。
语法结构
#include <memory>
std::shared_ptr<Type> ptr1(new Type()); // 使用原始指针
std::shared_ptr<Type> ptr2 = std::make_shared<Type>(); // 使用 make_shared 创建
常用函数和操作
- reset(): 重置 shared_ptr,使其不再指向原来的对象,可以选择性地将它指向一个新的对象。
- get(): 返回指向对象的原始指针。
- use_count(): 返回当前共享该对象的 shared_ptr 数量。
- unique(): 如果当前 shared_ptr 是唯一指向对象的指针,返回 true。
- swap(): 交换两个 shared_ptr。
- operator*() 和 operator->(): 用于访问 shared_ptr 所管理的对象。
删除器的使用
在 std::shared_ptr 中,删除器(deleter)是用于指定在智能指针销毁时要执行的自定义删除操作。shared_ptr 会在对象不再被任何 shared_ptr 所管理时自动调用删除器来释放资源。通常,shared_ptr 会调用 delete 来销毁对象,但有时你可能需要一个不同的销毁逻辑(比如使用自定义内存管理、释放额外的资源等),这时就可以使用删除器。
1. 使用删除器的基本方式
要在 shared_ptr 中使用删除器,你需要在创建智能指针时传入一个自定义删除器。这个删除器通常是一个函数、函数对象或 Lambda 表达式,用来替代默认的 delete 操作。
示例:使用自定义删除器
下面是一个简单的示例,展示如何在 shared_ptr 中使用自定义删除器:
#include <iostream>
#include <memory>
// 示例类
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
~MyClass() { std::cout << "MyClass destructor\n"; }
};
// 自定义删除器
void custom_deleter(MyClass* ptr) {
std::cout << "Custom deleter called\n";
delete ptr;
}
int main() {
// 创建 shared_ptr,并指定 custom_deleter 作为删除器
std::shared_ptr<MyClass> ptr(new MyClass(), custom_deleter);
// 离开作用域时,ptr 会调用 custom_deleter 来释放内存
return 0;
}
输出:
MyClass constructor
Custom deleter called
MyClass destructor
解释:
当创建 shared_ptr 时,你通过构造函数传递了一个原始指针 (new MyClass()) 和一个自定义删除器 custom_deleter。
当 shared_ptr 超出作用域时,它会自动调用删除器,而不是默认的 delete,因此调用 custom_deleter 函数。
在 custom_deleter 中,首先输出一条消息,然后使用 delete 删除对象。
2. 使用 Lambda 表达式作为删除器
除了定义普通的删除器函数,你还可以使用 Lambda 表达式来作为删除器,这种方式更加灵活,尤其是当删除逻辑简单或者依赖外部变量时。你可以在创建 shared_ptr 时直接嵌入删除器逻辑。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
~MyClass() { std::cout << "MyClass destructor\n"; }
};
int main() {
// 使用 Lambda 表达式作为删除器
std::shared_ptr<MyClass> ptr(new MyClass(), [](MyClass* ptr) {
std::cout << "Lambda deleter called\n";
delete ptr;
});
// 离开作用域时,ptr 会调用 Lambda 删除器
return 0;
}
输出:
MyClass constructor
Lambda deleter called
MyClass destructor
3. 使用函数对象作为删除器
你还可以使用自定义的函数对象(也叫做仿函数)作为删除器。它是一个类,该类实现了 operator(),使得实例可以像函数一样调用。
#include <iostream>
#include <memory>
// 自定义删除器函数对象
struct MyDeleter {
void operator()(MyClass* ptr) const {
std::cout << "MyDeleter called\n";
delete ptr;
}
};
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
~MyClass() { std::cout << "MyClass destructor\n"; }
};
int main() {
// 使用自定义的函数对象作为删除器
std::shared_ptr<MyClass> ptr(new MyClass(), MyDeleter());
// 离开作用域时,ptr 会调用 MyDeleter 来释放内存
return 0;
}
输出:
MyClass constructor
MyDeleter called
MyClass destructor
4. 通过删除器释放资源
删除器不仅限于释放内存,它还可以用于执行其他资源清理工作,比如释放文件句柄、网络连接、数据库连接等。比如:
#include <iostream>
#include <memory>
#include <fstream>
// 自定义删除器:关闭文件流
void file_deleter(std::ofstream* file) {
if (file->is_open()) {
std::cout << "Closing file\n";
file->close();
}
delete file;
}
int main() {
// 创建并管理文件流的 shared_ptr
std::shared_ptr<std::ofstream> file(new std::ofstream("example.txt"), file_deleter);
// 写入文件
*file << "Hello, World!" << std::endl;
// 离开作用域时,文件流会被删除器处理并关闭
return 0;
}
输出:
Closing file
5. 总结
- 删除器的作用:自定义删除器允许你在 shared_ptr 销毁时执行额外的清理操作,替代默认的 delete 操作。
- 用法:删除器可以是普通函数、函数对象或 Lambda 表达式。
- 应用场景:删除器不仅适用于内存释放,还可以用来管理其他资源的释放(如文件、网络连接等)。
使用案例
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass Constructor\n"; }
~MyClass() { std::cout << "MyClass Destructor\n"; }
void say_hello() { std::cout << "Hello from MyClass\n"; }
};
int main() {
// 使用 make_shared 创建 shared_ptr
std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>();
// 调用成员函数
p1->say_hello();
std::cout << "use_count: " << p1.use_count() << "\n"; // 输出引用计数
{
std::shared_ptr<MyClass> p2 = p1; // p2 和 p1 都指向同一个 MyClass 对象
std::cout << "use_count: " << p1.use_count() << "\n"; // 输出引用计数
} // p2 离开作用域,引用计数减少
std::cout << "use_count: " << p1.use_count() << "\n"; // 输出引用计数
// 当 p1 离开作用域时,MyClass 对象会被销毁
return 0;
}
输出:
MyClass Constructor
Hello from MyClass
use_count: 1
use_count: 2
use_count: 1
MyClass Destructor
注意事项(循环引用问题)
循环引用(Cyclic Reference)是指在程序中,两个或多个对象相互持有对方的引用,从而导致它们的生命周期永远无法结束,最终导致内存泄漏。在这种情况下,引用计数机制(如 std::shared_ptr)无法释放内存,因为每个对象的引用计数始终大于零。
循环引用的原因
在 C++ 中,std::shared_ptr 是通过引用计数来管理内存的。当多个 shared_ptr 实例指向同一个对象时,引用计数会增加,每当 shared_ptr 被销毁时,引用计数会减少。当最后一个 shared_ptr 被销毁时,对象的内存才会被释放。
然而,如果两个对象 A 和 B 使用 shared_ptr 相互持有对方的 shared_ptr,那么它们的引用计数就不会减少到零。具体原因是:
对象 A 持有 B 的 shared_ptr,而对象 B 持有 A 的 shared_ptr。
这会导致两个对象的引用计数始终大于 1,因为 A 对 B 的引用计数在 A 存在时永远不会为零,B 对 A 的引用计数也同样不会为零。
因为引用计数永远不会为零,std::shared_ptr 无法检测到这两个对象已经不再被需要,从而导致内存泄漏。
循环引用的示例
假设我们有两个类 A 和 B,它们互相持有对方的 shared_ptr:
#include <iostream>
#include <memory>
class B; // 前置声明
class A {
public:
std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> a_ptr; // B 持有 A 的 shared_ptr
~B() { std::cout << "B destroyed\n"; }
};
int main() {
// 创建 A 和 B 对象
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 互相引用
a->b_ptr = b;
b->a_ptr = a;
// 离开作用域时,a 和 b 的引用计数都不会归零
return 0;
}
输出:
(没有输出)—— 内存泄漏
在这个例子中,A 和 B 都互相持有对方的 shared_ptr,这导致它们的引用计数永远不会归零。所以当 main 函数结束时,它们的析构函数不会被调用,内存也不会被释放。
解决循环引用
为了避免循环引用,我们通常会使用 std::weak_ptr 来打破这种引用循环。
std::weak_ptr 是一种不增加引用计数的智能指针,它可以观察对象但不参与引用计数管理。通过 weak_ptr,我们可以避免循环引用带来的内存泄漏问题。
使用 std::weak_ptr 打破循环引用
在上面的例子中,我们可以用 std::weak_ptr 来打破循环引用,使得 B 不再持有 A 的强引用:
#include <iostream>
#include <memory>
class B; // 前置声明
class A {
public:
std::shared_ptr<B> b_ptr; // A 持有 B 的 shared_ptr
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // B 使用 weak_ptr 持有 A
~B() { std::cout << "B destroyed\n"; }
};
int main() {
// 创建 A 和 B 对象
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 互相引用,但 B 使用 weak_ptr
a->b_ptr = b;
b->a_ptr = a;
// 离开作用域时,A 和 B 的引用计数都能归零,内存可以被释放
return 0;
}
输出:
B destroyed
A destroyed
在这个修改后的版本中:
- A 仍然持有对 B 的 shared_ptr(即强引用)。
- B 通过 weak_ptr 持有对 A 的引用,这样 B 就不会增加 A 的引用计数。
- 当 main 函数结束时,A 和 B 都可以正常销毁,因为没有循环引用。
其它问题:
-
性能开销: 由于使用了引用计数,shared_ptr 会比原始指针或 unique_ptr 有更高的性能开销。在频繁创建和销毁的情况下,性能可能会受到影响。
-
线程安全性: shared_ptr 的引用计数操作是线程安全的,但管理的对象本身并不是线程安全的。如果多个线程同时访问相同的对象,必须额外小心同步问题。
-
避免使用原始指针: 创建 shared_ptr 时,最好避免直接使用原始指针。可以使用 std::make_shared 来创建对象,它不仅提供更安全的内存管理,还能提高性能,因为它在一次内存分配中分配对象和引用计数。
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(); // 推荐
自定义简易shared_ptr的实现
自定义的智能指针要注意赋值重载函数,要考虑自我赋值和同一个资源的智能指针无意义相互赋值,需要单独考虑处理,提高效率。
同时删除器是在定义构造函数时候再开了一个,而且删除器是用包装器打包的,方便适配仿函数,lambda函数等,默认的是用delete删除的
namespace share
{
template<class T>
class shared_ptr
{
shared_ptr() = delete;
void destroy()
{
_del(_ptr);
delete _count;
cout << "destroy" << endl;
}
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _count(new int(1))
{
cout << "参数构造" << endl;
}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _count(new int(1))
, _del(del)
{
cout << "模板参数构造" << endl;
}
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_count = sp._count;
_del = sp._del;
(*_count)++;
cout << "拷贝构造" << endl;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
{
return *this;
}
if (--(*_count) == 0)
{
destroy();
}
_ptr = sp._ptr;
_count = sp._count;
_del = sp._del;
++(*_count);
cout << "赋值重载" << endl;
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
~shared_ptr()
{
if (--(*_count) == 0)
destroy();
}
private:
T* _ptr;
int* _count;
function<void(T*)> _del = [](T* ptr) {
delete ptr;
};
};
}