c++11: 智能指针 shared_ptr & unique_ptr

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_19923217/article/details/82188082

一、背景

1. 堆内存、栈内存、静态区内存

我们知道,静态内存用来保存局部 static 对象、类 static 数据成员以及定义在函数之外的变量。而栈内存用来保存定义在函数内的非 static 对象。

分配在静态区或栈内存中的对象由编译器自动创建和销毁,对于栈内存,仅在其定义的程序块运行时才存在,而 static 对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作 “堆”(heap)。程序用堆来存储动态分配的对象——即那些在运行时分配内存的对象。

动态对象(也就是在堆内存中的对象)不再使用时,代码必须显式的销毁它们

2. 动态内存

动态内存也被称为堆内存,在程序运行期间根据需求动态分配对象。动态对象的生存期由程序来决定,也就是说,当堆内存上的对象不在使用时,需要使用代码显式的销毁它们。

在 C++ 中,动态内存的管理是通过一组运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

使用时容易出现的问题
  1. 如果忘记释放动态对象内存,这种情况就会产生内存泄漏
  2. 如果在尚有指针引用内存的情况下就释放了它,这种情况会产生引用非法内存的指针

扩展阅读:为什么要使用动态内存分配?

3. 智能指针

智能指针的出现是为了更容易、更安全的使用动态内存,新的标准库提供了两种智能指针类型来管理动态对象。

智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。下面重点介绍两种类型智能指针的使用:

  1. shared_ptr 允许多个指针指向同一个对象;
  2. unique_ptr 独占所指向的对象

这两种类型智能指针都包含在 memory 头文件中。

二、shared_ptr 类

shared_ptr 内部有一个关联的计数器,记录指向对象的指针个数,通常称其为引用计数(reference count)。智能指针 shared_ptr 通过引用计数实现动态内存的自动管理,一旦一个 shared_ptr 的计数器变为0,它就会自动释放所管理对象的内存。

2.1 初始化

shared_ptr 同 vector 一样,也是模板,因此在创建一个智能指针时,必须给出额外的信息——指针可以指向的类型。

使用 new 返回的指针来初始化

std::shared_ptr<int> p(new int(10));

可以使用模板函数 make_shared 创建对象, make_shared 需指定类型(‘<>’中)及参数(‘()’内), 传递的参数必须与指定的类型的构造函数匹配:

std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<std::string> sp2 = std::make_shared<std::string>("Hello c++");

另外一种初始化方式是拷贝另一个 shared_ptr

std::shared_ptr<T> p(q)

其中,p 是 shared_ptr q 的拷贝,此操作会递增 q 中的计数器

2.2 成员函数列表

函数 说明
use_count 返回引用计数的个数
unique 返回是否独占使用权(引用计数为1)
swap 交换两个 shared_ptr 指针所指向的对象
reset(); 见下面说明
get 返回内部保存的指针,要小心使用,若智能指针释放了其对象,指针所指向的对象也就消失了
reset 函数说明
使用方式1:
p.reset();

使用方式2:
template< class Y > 
void reset( Y* ptr );

template< class Y, class Deleter > 
void reset( Y* ptr, Deleter d );

使用方式 1 不带任何参数,表示若 p 是唯一指向其对象的 shared_ptr,reset 函数会释放此对象。

使用方式二中,以 ptr 所指向的对象替换被管理对象。若还传递了参数 d,将会调用 d 函数而不是默认的 delete 操作来释放所管理的动态对象

2.3 用法说明

(1) 通常使用 auto 保存智能指针对象

我们通常用 auto 定义一个对象来保存 make_shared 的结果

// sp 指向一个动态分配的 string
auto sp = make_shared<string>();
(2) 智能指针初始化时定义 Deleter

当智能指针 shared_ptr 中的引用计数为 0 时,它会自动释放所管理对象的内存。默认使用 delete 表达式。

如果所管理对象释放动态内存时,需要其他一些自定义的操作,则可以在初始化时自定义 Deleter。

class CConnnect {
    void Disconnect() { PRINT_FUN(); }
};

void Deleter(CConnnect* obj) {
    obj->Disconnect(); // 做其它释放或断开连接等工作
    delete obj; // 删除对象指针
}

std::shared_ptr<CConnnect> sps(new CConnnect, Deleter);
(3) shared_ptr 的拷贝与赋值

当进行拷贝或者赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象。

无论何时,当我们拷贝一个 shared_ptr ,计数器都会递增,而当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁时(例如一个局部的 shared_ptr 离开其作用域),计数器就会递减。

std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = std::make_shared<int>(11);
auto sp3 = sp2; // 同 auto sp3(sp2);
printf("sp1.use_count = %d\n", sp1.use_count());  // 1
printf("sp2.use_count = %d\n", sp2.use_count());  // 2
printf("sp3.use_count = %d\n", sp3.use_count());  // 2
sp3 = sp1;
printf("sp1.use_count = %d\n", sp1.use_count());  // 2
printf("sp2.use_count = %d\n", sp2.use_count());  // 1
printf("sp3.use_count = %d\n", sp3.use_count());  // 2
(4) shared_ptr 自动释放相关联的内存

当动态对象不再被使用时,也就是引用计数为 0 时,shared_ptr 会自动释放动态对象,这个特征使得动态内存的使用变得非常容易、安全。

下面来看一个智能指针对比内置指针(new 生成)的例子:

前提:有一个 Foo 类型动态分配的对象,对象通过一个类型为 T 的参数进行初始化。

有一个大意的代码片段如下使用内置指针,就会造成内存泄漏

Foo* facroty(T arg)
{
    // 恰当处理 arg
    return new Foo(arg);
}

void use_factory(T arg)
{
    Foo *p = factory(arg);
    // 使用  p,但不 delete 它
    // p 离开了作用域,它指向的内存没有释放!
}

当使用内置指针管理动态内存时,一定要记得显式释放!

下面来看一下当使用智能指针 shared_ptr 时的情形:

shared_ptr<Foo> facroty(T arg)
{
    // 恰当处理 arg
    // shared_ptr 负责释放内存
    return make_shared<Foo>(arg);
}

void use_factory(T arg)
{
    shared_ptr<Foo> p = factory(arg);
    // 使用  p
    // p 离开了作用域,它指向的内存自动释放
}

由于 p 是 user_factory 函数的局部变量,在该函数结束时,p 被销毁。当 p 销毁时,将递减其引用计数并检查是否为 0,在例中,p 是唯一引用该动态对象的指针,所以销毁后引用计数为 0,p 指向的对象也会销毁,所占用的内存也会被释放。

三、unique_ptr 类

一个 unique_ptr 独占它所指向的对象。与 shared_ptr 不同,某个时刻只能有一个 unique_ptr 指向一个给定对象,当 unique_ptr 销毁时,它所指向的对象也被销毁。

3.1 初始化函数

c++ 11 标准库中没有类似 make_shared 的标准库函数返回一个 unique_ptr。

当我们定一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。

unique_ptr<int> p(new int(42))

与 shared_ptr 一样,unique_ptr 初始化时可以指定 deleter 函数,用来代替默认的 delete 表达式释放内存。

unique_ptr<T, D> p;
++++ 扩展:实现 make_unique 函数

std::make_unique 函数在 c++14 里才加入标准库,所以我们不能直接使用,但是我们可以自己写一个简单的实现版本:

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

3.3 unique_ptr 成员函数列表

函数 说明
u.release() u 放弃对指针的控制权,返回指针,并将 u 置为空
u.reset() 释放 u 所指向的对象
u.reset(q) 令 u 指向内置指针 q
u = nullptr 释放 u 所指向的对象,将 u 置为空

3.3 用法说明

(1) 通常使用 auto 保存智能指针对象

我们通常用 auto 定义一个对象来保存 make_unique 的结果

// 此代码在 c++14  标准库中才能编译通过

// sp 指向一个动态分配的 string
auto sp = make_unique<string>();
(2) unique_ptr 不支持普通拷贝或赋值操作

由于一个 unique_ptr 独占它所拥有的对象,因此 unique_ptr 不支持拷贝和赋值操作。

unique_ptr<string> p1(new string("hello"));
unique_ptr<string> p2(p1); // 错误:unique 不支持拷贝
unique_ptr<string> p3;
p3 = p2;                   // 错误:unique 不支持赋值
(3) 转移动态对象的控制权

unique_ptr 的 release 函数返回指向动态对象的指针,配合 reset 函数使用可以将指针的所有权从一个 unique_ptr 转移到另一个 unique_ptr

// 将所有权从 p1 转移给 p2
unique_ptr<string> p2(p1.release());

release 函数返回 unique_ptr 当前保存的指针并将其置为空,因此 p2 被初始化为 p1 原先保存的指针,而 p1 被置空。

猜你喜欢

转载自blog.csdn.net/qq_19923217/article/details/82188082