无人驾驶-CPP-共享指针

无人驾驶-CPP-共享指针

共享指针的用法

共享指针 (shared_ptr) 是现在的 Boost 库中提供的,并且应该是将来 C++1x 的标准库中提供的一个模板类。在此之前,ISO/IEC 14882:2003 标准库 中的“自动指针 (auto_ptr)”也有类似的功能。显然 shared_ptr 要比 auto_ptr 从功能上来说应该强大一些。这篇文章主要介绍 shared_ptr 的用法及注意事项。

1. shared_ptr 的功能

shared_ptr 主要的功能是,管理动态创建的对象的销毁。它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。

与普通指针相比,共享指针对象重载了 * 、-> 和==运算符, 所以你可以像通常的指针一样使用它。没有重载+、-、++、–、[ ]等运算法。

2. shared_ptr 的原理

它遵循共享所有权的概念,即不同的 shared_ptr 对象可以与相同的指针相关联,并在内部使用引用计数机制来实现这一点。

(1)每个 shared_ptr 对象在内部指向两个内存位置:
1、指向对象的指针。
2、用于控制引用计数数据的指针。(这里的引用计数为指向该内存块的共享指针个数)

(2)共享所有权如何在参考计数的帮助下工作:
1、当新的 shared_ptr 对象与原共享指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。
2、当任何与之相关的shared_ptr 对象被析构时,例如局部函数调用结束,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除堆中对应内存。

3. shared_ptr 所在库

3.1. 对于 Visual C++ 2010

目前,Visual C++ 2010 的 库里,已经包含了 shared_ptr 模板类,也就是说,你可以直接这样写:

  #include <memory>

3.2. 对于其它支持 ISO/IEC 14882:2003 标准的编译器

而 GNU G++ 的标准库中还没有支持(毕竟是将来的标准),如果在 G++ 中想使用 shared_ptr, 还是得用到 Boost 库,就是说,在 G++ 里,你得这样写:

 #include <boost/shared_ptr.hpp>

4. shared_ptr 对象的构造

保险起见,你应该仅从以下几种途径构造一个共享指针(以下例子中若没特殊说明,T 就代表共享指针所指向的对象的类型):

4.1. 使用空参数构造函数构造

也就是说,你可以直接定义一个 shared_ptr 而不指定构造函数的内容:

 std::shared_ptr<T> ptr;

这样做的话,ptr 的意义就相当于一个 NULL 指针。当你试图在一个空指针上做类似于 *ptr 或者 ptr->xx 之类的东西的时候,应该会收到异常的。

4.2. 直接从 new 操作符的返回值构造

std::shared_ptr<T> ptr(new T());

上面这行代码在堆里创建了两块内存:1.存储T。2.用于引用计数的内存,管理附加此内存的shared_ptr对象的计数,最初计数将为1.

因为带有参数的shared_ptr的复制构造函数(4.3)是explicit类型的,所以不能像这样 std::shared_ptr ptr = new T();隐式调用它构造函数。

但是链接里说构造新的shared_ptr对象的最佳方法是使std::make_shared类模板。因为 1)它一次性为T对象和用于引用计数的数据都分配了内存,而new操作符只是为T分配了内存(没理解,求高人解答)。2)它可以避免一些由堆指针或者new分配指针导致的错误。

 std::shared_ptr<T> p1=std::make_shared<T> ();

4.3. 使用复制构造函数(或等号重载),从其它 shared_ptr 的对象构造

std::shared_ptr<T> ptr1(new T()); // 本行与 4.2 中的构造方法是一样的,引用计数为1
std::shared_ptr<T> ptr2(ptr1);    // 这就是使用复制构造函数的方法,会让引用计数加 1

还有,shared_ptr 可以当作函数的参数传递,或者当作函数的返回值返回,这个时候其实也相当于使用复制构造函数。

   过程如下:作函数实参时,将指针执行复制构造函数传入函数体内,因此该内存块的引用计数+1;当作为函数返回值时,复制构造函数将内存地址传递给新指针,引用计数+1,然后,局部指针执行析构,引用计数-1。    

4.4. 从 shared_ptr 提供的类型转换 (cast) 函数的返回值构造

shared_ptr 也可以类型转换,有关类型转换的详情参见下面的 6. 此处假设 B 是 A 的子类,那么,根据C++继承与派生中的知识,B 的指针当然是可以转换成 A 的指针的。在共享指针里,应该这样做:

std::shared_ptr<B> ptrb(new B());
std::shared_ptr<A> ptra( dynamic_pointer_cast<A>(ptrb) );  //本质还是复制构造

5. shared_ptr 的“赋值”

shared_ptr 也可以直接赋值,但是必须是赋给相同类型的 shared_ptr 对象,而不能是普通的 C 指针或 new 运算符的返回值。当共享指针 a 被赋值成 b 的时候,如果 a 原来是 NULL, 那么直接让 a 等于 b 并且让它们指向的东西的引用计数加 1; 如果 a 原来也指向某些东西的时候,如果 a 被赋值成 b, 那么原来 a 指向的东西的引用计数被减 1, 而新指向的对象的引用计数加 1. 就是说以下代码是允许的:

std::shared_ptr<T> a(new T());
std::shared_ptr<T> b(new T());
a = b; // 此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1

shared_ptr 的对象在构造之后,可以被赋予空值,此时使用的应该是 reset() 函数或者nullptr,如:

std::shared_ptr<T> a(new T());
a.reset();        // 此后 a 原先所指的对象的引用计数-1,并且 a 会变成 NULL。这里内存会被销毁
a=nullptr;        //同上。推荐多用

6. shared_ptr 的类型转换

shared_ptr 有两种类型转换的函数,一个是 static_pointer_cast, 一个是 dynamic_pointer_cast. 其实用法真的和 C++ 提供的 static_cast 和 dynamic_cast 很像,再结合 4.4. 的代码和以下类似的代码,几乎没什么好讲的:

std::shared_ptr<A> ptra;
std::shared_ptr<B> ptrb(new B());
ptra = dynamic_pointer_cast<A>(ptrb);

7. 从 shared_ptr 的对象获得传统 C 指针

std::shared_ptr<T> ptr(new T());
T *p = ptr.get(); // 获得传统 C 指针

建议少用get()函数,因为如果在shared_ptr析构之前手动调用了delete函数,同样会导致类似的错误。

8. shared_ptr 的错误用法

一定要注意,本节所述所有方法,都是错误的!

8.1. 在“中途”使用传统 C 指针构造共享指针

所谓在中途,指的就是不从 new 的返回值直接构造共享指针,比如从 this 指针构造自己的共享指针等。

8.2. 从一个对象的传统 C 指针,构造出两个或以上的共享指针

其实这种和 8.1. 也是类似的,或者说,这种情况是 8.1. 的一种具体情况,比如,下面的代码是错误的:

T *a = new T();
shared_ptr<T> ptr1(a);
shared_ptr<T> ptr2(a);

这样的话,ptr1 和 ptr2 的引用计数是单独算的,它们任意一个对象在析构的时候,都会销毁 a 所指的对象,a就为悬空指针,所以,这个对象会被“销毁两次”。因此报错。(make_shared类模板可以避免)

8.3 不要用栈中的指针构造shared_ptr对象

shared_ptr默认的构造函数中使用的是delete来删除关联的内存,所以构造的时候也必须使用new出来的堆空间的指针。如果是栈中的指针,当shared_ptr对象超出作用域调用析构函数delete内存时,会报错。(make_shared类模板可以避免)

9. 智能指针的使用

基于Boost库, C++11 加入了shared_ptr和weak_ptr. 它们最早在TR1中就被引入, 但在C++11中, 在Boost的基础上又加入了新的功能.

std::shared_ptr使用引用计数. 每一个shared_ptr的拷贝都指向相同的内存. 在最后一个shared_ptr析构的时候, 内存才会被释放.

std::shared_ptr<int> p1(new int(5));
std::shared_ptr<int> p2 = p1; // 都指向同一内存.
 
p1.reset(); // 因为p2还在,所以内存没有释放.
p2.reset(); // 释放内存, 因为没有shared_ptr指向那块内存了.
 
std::shared_ptr 使用引用计数, 所以有循环计数的问题. 为了打破循环,可以使用std::weak_ptr. 顾名思义, weak_ptr是一个弱引用, 只引用, 不计数. 如果一块内存被shared_ptr和weak_ptr同时引用, 当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存, 内存也会被释放. 所以weak_ptr不保证它指向的内存一定是有效的, 在使用之前需要检查.
 
std::shared_ptr<int> p1(new int(5));
std::weak_ptr<int> wp1 = p1; // 还是只有p1有所有权.
 
{
  std::shared_ptr<int> p2 = wp1.lock(); // p1和p2都有所有权
  if(p2) // 使用前需要检查
  { 
    // 使用p2
  }
} // p2析构了, 现在只有p1有所有权.
 
p1.reset(); // 内存被释放.
 
std::shared_ptr<int> p3 = wp1.lock(); // 因为内存已经被释放了, 所以得到的是空指针.
if(p3)
{
  // 不会执行到这.
}

在多个线程中同时对一个shared_ptr循环执行两遍swap。 shared_ptr的swap函数的作用就是和另外一个shared_ptr交换引用对象和引用计数,是写操作。执行两遍swap之后, shared_ptr引用的对象的值应该不变。

#include <stdio.h>
#include <tr1/memory>
#include <pthread.h>
 
using std::tr1::shared_ptr;
 
shared_ptr<int> gp(new int(2000));
 
shared_ptr<int>  CostaSwapSharedPtr1(shared_ptr<int> & p)
{
    shared_ptr<int> p1(p);
    shared_ptr<int> p2(new int(1000));
    p1.swap(p2);
    p2.swap(p1);
    return p1;
}
 
shared_ptr<int>  CostaSwapSharedPtr2(shared_ptr<int> & p)
{
    shared_ptr<int> p2(new int(1000));
    p.swap(p2);
    p2.swap(p);
    return p;
}
 
 
void* thread_start(void * arg)
{
    int i =0;
    for(;i<100000;i++)
    {
        shared_ptr<int> p= CostaSwapSharedPtr2(gp);
        if(*p!=2000)
        {
            printf("Thread error. *gp=%d \n", *gp);
            break;
        }
    }
    printf("Thread quit \n");
    return 0;
}
 
 
 
int main()
{
    pthread_t thread;
    int thread_num = 10, i=0;
    pthread_t* threads = new pthread_t[thread_num];
    for(;i<thread_num;i++)
        pthread_create(&threads[i], 0 , thread_start , &i);
    for(i=0;i<thread_num;i++)
        pthread_join(threads[i],0);
    delete[] threads;
    return 0;
}

这个程序中我启了10个线程。每个线程调用10万次 CostaSwapSharedPtr2函数。 在CostaSwapSharePtr2函数中,对同一个share_ptr全局变量gp进行两次swap(写操作), 在函数返回之后检查gp的值是否被修改。如果gp值被修改,则证明多线程对同一个share_ptr执行写操作是不安全的。

程序运行的结果如下:

Thread error. *gp=1000 
Thread error. *gp=1000 
Thread quit 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread error. *gp=1000 
Thread quit 
Thread quit

10个线程有9个出错。证明多线程对同一个share_ptr执行写操作是不安全的。

我们在程序中,如果不运行CostaSwapSharedPtr2, 改成运行CostaSwapSharedPtr1呢?

CostaSwapSharedPtr1和CostaSwapSharedPtr2的区别在于, 它不是直接对全局变量gp进行写操作,而是将gp拷贝出来一份再进行写操作。运行的结果如下:

Thread quit 
Thread quit 
Thread quit 
Thread quit 
Thread quit 
Thread quit 
Thread quit 
Thread quit 
Thread quit 
Thread quit

参考链接

  1. https://blog.csdn.net/yusiguyuan/article/details/22037833
  2. https://blog.csdn.net/aishuirenjia/article/details/91986961
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值