C++ 智能指针

本文详细介绍了C++中的智能指针,包括它们的特点和作用,如智能指针的RAII特性、所有权概念,以及四种类型的智能指针:auto_ptr(已废弃)、unique_ptr、shared_ptr和weak_ptr。此外,文章还讨论了原子操作的概念,强调其在多线程环境中的重要性,以及如何使用std::atomic来实现无锁编程。最后,文章提到了智能指针在避免内存泄漏和解决循环引用问题上的应用。
摘要由CSDN通过智能技术生成

一、智能指针特点

  • 智能指针封装了裸指针,内部还是裸指针的调用

  • 智能指针使用RAII特点,将对象生命周期使用栈来管理

  • 智能指针区分了所有权,因此使用责任更为清晰

  • 智能指针大量使用操作符重载和函数内联特点,调用成本和裸指针无差别

二、原子操作

1. 何谓"原子操作":

原子操作就是: 不可中断的一个或者一系列操作, 也就是不会被线程调度机制打断的操作, 运行期间不会有任何的上下文切换(context switch).

  • 互斥量:加锁一般针对一个代码段(几行代码)

  • 原子操作:针对的一般都是一个变量,而不是一个代码段

2. 原子操作的作用:

  1. 如果确定某个操作是原子的, 就不用为了去保护这个操作而加上会耗费昂贵性能开销的锁. - (巧妙的利用原子操作和实现无锁编程)

  1. 借助原子操作可以实现互斥锁(mutex). (linux中的mutex_lock_t)

  1. 借助互斥锁, 可以实现让更多的操作变成原子操作.

3、原子操作实例

std::atomic来代表原子操作,std::atomic是个类模板。std::atomic是用来封装某个类型的值的。

#include <iostream>
#include <vector> 
#include <thread>
#include <mutex>

class Test
{
public:
    Test() = default;

    void CThreadFunc()
    {
        for (int i = 0; i < 10000; ++i)
        {
            //std::lock_guard<std::mutex> lck(m_s_ivalue_mutex); //m_iValue需要加锁才可正常工作
            m_iValue++;
            m_atomic_value++;//不加锁,也可正常工作
        }
    }

    void Start()
    {
        std::vector<std::thread> threads;
        for (int i = 0; i < 10; ++i)
        {
            threads.push_back(std::thread(&Test::CThreadFunc, this));
        }

        for (auto& th : threads)
        {
            if (th.joinable())
            {
                th.join();
            }
        }
        std::cout << "m_iValue:" << m_iValue << ", m_atomic_value:" << m_atomic_value << std::endl;
    }

private:
    int m_iValue = 0;
    std::atomic<int> m_atomic_value = 0;//sta::atomic<T> 原子操作
    std::mutex m_s_ivalue_mutex;
};

int main()
{
    Test test;
    test.Start();

    getchar();
    return 0;
}

三、c++中的四个智能指针:auto_ptr,unique_ptr,shared_ptr,weak_ptr

智能指针出现的原因:智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

  • 1.auto_ptr(c++98的方案,c++11已经抛弃)原因是缺乏语言特性如 “针对构造和赋值” 的 std::move 语义,以及其他瑕疵。

  • 2.unique_ptr(替换auto_ptr):是 C++11 才开始提供的类型,是一种在异常时可以帮助避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。一旦拥有着被销毁或编程 empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。你可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。unique_ptr 用于取代 auto_ptr

  • 3.shared_ptr:shared_ptr实现共享式拥有概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。为了在结构较复杂的情景中执行上述工作,标准库提供 weak_ptr、bad_weak_ptr 和 enable_shared_from_this 等辅助类。多个智能指针可以共享同一个对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的所有资源。

  • 4.weak_ptr:weak_ptr 允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。因此,在 default 和 copy 构造函数之外,weak_ptr 只提供 “接受一个 shared_ptr” 的构造函数。可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题。

四、auto_ptr详解

1、auto_ptr的起源和弊端

在C++98就已经有了一个智能指针auto_ptr,它可以算作是unique_ptr的前身,但是C++11之前没有移动的语义,导致这个C++98时代的智能指针使用起来问题很大,比如下面代码:

auto_ptr<Type> a = b;
  • 这个操作会把b置空,这就违反了软件工程的最小惊讶原则。

  • STL中的容器也普遍不支持auto_ptr,以vector为例,下面示例代码会编译报错:

auto_ptr<Type> a = xxx;vector<auto_ptr<Type>> v;v.push_back(a);

这是因为auto_ptr的拷贝构造函数和赋值构造函数都没用用const修饰,这是为了把源操作数置空做出的妥协,这也是C++11之前没有移动语义所采用的trick:

auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) { }
auto_ptr& operator=(auto_ptr& __a) throw() 
{  
    reset(__a.release());  
    return *this;
}

再看vector的push_back的原型:

void push_back(const value_type& __x) {......}

push_back要求传进来的必须是const引用,这样才能保证数据源的安全,这就导致push_back内部在拷贝构造auto_ptr对象时,无法将源操作数置空,于是编译出错。

  • auto_ptr 默认只会使用 delete 来进行删除操作,如果一个 AutoPointer 对象管理了一个数组,则会在离开其作用域时发生内存泄漏

所以auto_ptr的使用很受限,能用的地方用起来也很奇怪,C++11已经将其标记为deprecated,C++17直接将其标记为removed

五、unique_ptr 详解

1、简介

  • unique_ptr 是从 C++ 11 开始,定义在 <memory> 中的智能指针(smart pointer)。它持有对对象的独有权,即两个 unique_ptr 不能指向一个对象,不能进行复制操作只能进行移动操作

  • unique_ptr 之所以叫这个名字,是因为它只能指向一个对象,即当它指向其他对象时,之前所指向的对象会被摧毁。其次,当 unique_ptr 超出作用域时,指向的对象也会被自动摧毁,帮助程序员实现了自动释放的功能。

  • unique_ptr 也可能还未指向对象,这时的状态被称为 empty。

std::unique_ptr<int>p1(new int(5));
std::unique_ptr<int>p2=p1;// 编译会出错
std::unique_ptr<int>p3=std::move(p1);// 转移所有权, 现在那块内存归p3所有, p1成为无效的针.
p3.reset();//释放内存.
p1.reset();//无效

2、定义

unique_ptr 在 <memory> 中的定义如下:

// non-specialized 
template <classT, classD = default_delete<T>> class unique_ptr;
// array specialization   
template <classT, classD> classunique_ptr<T[],D>;

其中 T 指其管理的对象类型,D 指该对象销毁时所调用的释放方法,可以使用自定义的删除器,他也有一个默认的实现,即 detele 操作。

3、常用方法介绍

// unique_ptr constructor example
#include <iostream>
#include <memory>
 
//构造方法 std::unique_ptr::unique_ptr
int main () {
  std::default_delete<int> d;
  std::unique_ptr<int> u1;
  std::unique_ptr<int> u2 (nullptr);
  std::unique_ptr<int> u3 (new int);
  std::unique_ptr<int> u4 (new int, d);
  std::unique_ptr<int> u5 (new int, std::default_delete<int>());
  std::unique_ptr<int> u6 (std::move(u5));
  std::unique_ptr<int> u7 (std::move(u6));
  std::unique_ptr<int> u8 (std::auto_ptr<int>(new int));
 
  std::cout << "u1: " << (u1?"not null":"null") << '\n';
  std::cout << "u2: " << (u2?"not null":"null") << '\n';
  std::cout << "u3: " << (u3?"not null":"null") << '\n';
  std::cout << "u4: " << (u4?"not null":"null") << '\n';
  std::cout << "u5: " << (u5?"not null":"null") << '\n';
  std::cout << "u6: " << (u6?"not null":"null") << '\n';
  std::cout << "u7: " << (u7?"not null":"null") << '\n';
  std::cout << "u8: " << (u8?"not null":"null") << '\n';
 
  return 0;
}
执行结果:
u1: null
u2: null
u3: not null
u4: not null
u5: null
u6: null
u7: not null
u8: not null

//析构方法 std::unique_ptr::~unique_ptr
int main () {
  // user-defined deleter
  auto deleter = [](int*p){
    delete p;
    std::cout << "[deleter called]\n";
  };
  std::unique_ptr<int,decltype(deleter)> foo (new int,deleter);
  std::cout << "foo " << (foo?"is not":"is") << " empty\n";
  return 0; // [deleter called]
}
执行结果:
foo is not empty
[deleter called]

//释放方法 std::unique_ptr::release  注意!注意!注意!这里的释放并不会销毁其指向的对象,而且将其指向的对象释放出去。
 
int main () {
  std::unique_ptr<int> auto_pointer (new int);
  int * manual_pointer;
  *auto_pointer=10;
  manual_pointer = auto_pointer.release();
  // (auto_pointer is now empty)
  std::cout << "manual_pointer points to " << *manual_pointer << '\n';
  delete manual_pointer;
  return 0;
}
执行结果:
manual_pointer points to 10

//重置方法 std::unique_ptr::reset
int main () {
  std::unique_ptr<int> up;  // empty
  up.reset (new int);       // takes ownership of pointer
  *up=5;
  std::cout << *up << '\n';
  up.reset (new int);       // deletes managed object, acquires new pointer
  *up=10;
  std::cout << *up << '\n';
  up.reset();               // deletes managed object,up is null
  return 0;
}

执行结果:
5
10

//交换方法 std::unique_ptr::swap
nt main () {
  std::unique_ptr<int> foo (new int(10));
  std::unique_ptr<int> bar (new int(20));
  foo.swap(bar);
  std::cout << "foo: " << *foo << '\n';
  std::cout << "bar: " << *bar << '\n';
  return 0;
}
执行结果:
20
10

4、自定义删除器详解

  • 为什么要使用自定义的删除器

当你的对象不能仅仅只是依靠 delete 删除时,那么你就需要依靠自定义的删除器了。例如对象中还有其它对象的数组等。

  • 怎么写

主要就是自己编写一个删除器,指定输入的参数,然后实现相应的释放操作,并将这个删除器传入 unique_ptr 中。

  • 实例分析

  • 开发中经常会使用到图片,假设有这么一个图片的结构体定义如下

/// 图像格式定义
typedefstructcv_image_t {
unsignedchar *data;            ///< 图像数据指针    
cv_pixel_format pixel_format;   ///< 像素格式
int width;                      ///< 宽度(以像素为单位)
int height;                     ///< 高度(以像素为单位)
int stride;                     ///< 跨度, 即每行所占的字节数
} cv_image_t;
  • 再写一个图片的释放方法

voidcv_image_release(cv_image_t* image)
{
    if(!image) 
    {
        return;    
    }
    delete[] 
    image->data;
    delete image;
    return;
}
  • 然后我们在代码中就可以使用一个如下的智能指针来智能的控制图片的释放操作了

cv_image_t image_input = 
{     
    (unsigned char*) image,    
    pixel_format,    
    image_width,    
    image_height,    
    image_stride
};
std::unique_ptr<cv_image_t, decltype(cv_image_release)*> image_guard(image_input , cv_image_release);
  • 优化写法

应用中很多地方都要使用图片,然后都需要这样一个图片的智能指针,不过每次都写这么一长串很麻烦,而且定义也都是一样的,那么我们就可以简单的封装一下,比如可以定义一个 cv_image_ptr 专门,如下:

structcv_image_destructor
{
    voidoperator()(cv_image_t* img)
    {
        cv_image_release(img);    
    }
};
typedef std::unique_ptr<cv_image_t, cv_image_destructor> cv_image_ptr;

这样我们再在代码中使用的时候就方便很多了,例如

cv_image_ptr image_guard;
image_guard.reset(image_input);

而且代码看上去也整洁了。

5、可以转换成shared_ptr

std::unique_ptr<std::string>foo()
{
    return std::make_unique<std::string>("foo");
}

int main()
{
    std::shared_ptr<std::string>sp1=foo();
    auto up=std::make_unique<std::string>("Hello World");
    std::shared_ptr<std::string>sp2=std::move(up);
    //std::shared_ptr<std::string> sp3 = up; 错误,编译报错
}

六、shared_ptr

1. shared_ptr的定义

  • shared_ptr是一种 智能指针( smart pointer)。shared_ptr的作用有如内 指针,但会记录有多少个tr1::shared_ptrs共同指向一个对象。这便是所谓的 引用计数(reference counting)。一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。这在非环形数据结构中防止资源泄露很有帮助。

2. shared_ptr的数据结构及示例

  • shared_ptr是引用计数型智能指针,几乎所有的实现都采用在堆上放个计数器。

  • 示例1

class object
{
private:
    int value;
public:
    object(int x = 0) :value(x) {}
    ~object() {}
    int& Value() { return value; }
    const int& Value( ) const { return value; }
 };

int main()
{
    shared_ptr<Object> apa(new object(10));
    shared_ptr<Object> apb = apa;
    return 0;
}
  • 示例2

有3个shared_ptr对象apa、gx、apb。

shared_ptr<Object> gx(new object(1));//线程之间共享的shared_ptr对象
shared_ptr<Object> apa;//线程A的局部变量
shared_ptr<Object> apb(new object(2));//线程B的局部变量

一开始,各安其事:

线程A执行 apa = gx;(即read gx),以下完成了步骤1,还没来及执行步骤⒉。这时切换到了B线程。

同时编程B执行gx = apb;(即write gx),两个步骤一起完成了。先是步骤1:

再是步骤2:

这时Object 1 对象已经销毁,apa.mPtr成了空悬指针!

最后回到线程A,完成步骤2:

  • 总结:多线程无保护地读写gx,造成了“apa是空悬指针”的后果。这正是多线程读写同一个shared_ptr对象必须加锁的原因。

3. shared_ptr的线程安全性

  • (shared_ptr)的引用计数本身是线程安全(引用计数是原子操作)。

  • 多个线程同时读同一个shared_ptr对象是线程安全的。

  • 如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。

  • 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。

4. 其他注意事项

  • shared_ptr作为unordered_map的key

  • 如果把 shared_ptr放到unordered_set中,或者用于unordered_map 的 key,那么要小心hash table退化为链表。但是其hash_yalue是shared_ptr隐式转换为 bool的结果。也就是说,如果不自定义hash函数,那么unordered_{set/map}会退化为链表。

  • 为什么要尽量使用make_shared()?申请被管理对象以及引用计数的内存;调用适当的构造函数初始化对象;返回一个shared_ptr。

  • 为了节省一次内存分配,原来shared_ptr x(new Object(10)))需要为Object对象和RefCnt各分配一次内存,现在用make_shared()的话,可以一次分配一块足够大的内存,供Object和RefCnt对象容身。不过Object的构造函数所需的参数要传给make_shared(),后者再传给Object: : Object(),这只有在C++11里通过perfect forwarding(完美转发)才能完美解决。

七、weak_ptr基本用法

  • 循环引用问题

  • 虽然shared_ptr是用来避免内存泄漏,可以自动释放内存。但是shared_ptr在使用中可能存在循环引用,使引用计数失效,从而导致内存泄漏的情况。如下代码所示:

#include <iostream>
#include <memory>
using namespace std;
class A {
public:
    std::shared_ptr<B> bptr;
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {//设定一个作用域
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构
    return 0;
}
  • 循环引用导致ap和bp的引用计数都为2,在离开作用域之后,ap和bp的引用计数只减为1,而没有减为0,导致两个指针都不会被析构,产生内存泄漏。

  • weak_ptr定义

  • weak_ptr叫弱引用指针。是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。weak_ptr 设计的目的是为了协助shared_ptr而引入的一种智能指针,它可以解决shared_ptr循环引用的问题。weak_ptr只可以从一个shared_ptr或另一个 weak_ptr 对象来构造, 它的构造和析构不会引起引用记数的增加或减少。

  • weak_ptr解决循环引用

  • shared_ptr智能指针的循环引用导致的内存泄漏问题,可以通过weak_ptr解决。只需要将A或B的任意一个成员变量改为weak_ptr:

#include <iostream>
#include <memory>
using namespace std;
class A {
public:
    std::weak_ptr<B> bptr; // 修改为weak_ptr
    ~A() {
        cout << "A is deleted" << endl;
    }
};
class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
int main()
{
    {//设定一个作用域
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    cout<< "main leave" << endl; 
    return 0;
}

上面代码中在对B的成员赋值时,即执行ap->bptr=bp时,由于bptr是weak_ptr,它并不会增加引用计数,所以bp的引用计数仍然会是1,在离开作用域之后,bp的引用计数为减为0,A指针会被析构,析构后其内部的aptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。

  • weak_ptr函数

  1. use_count():获取当前观察资源的引用计数:

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; //输出1
  1. expired():判断所观察资源是否已经释放:

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
cout << "weak_ptr无效,资源已释放";
else
cout << "weak_ptr有效";
  1. lock():lock()函数返回一个指向共享对象的shared_ptr,如果对象被释放,则返回一个空shared_ptr:

std::weak_ptr<int> gw;
void f()
{
    auto spt = gw.lock();
    if(gw.expired()) {
        cout << "gw无效,资源已释放";
    }
    else {
        cout << "gw有效, *spt = " << *spt << endl;
    }
}
int main()
{
    {
        auto sp = std::make_shared<int>(42);
        gw = sp;
        f();
    }
f();
return 0;
}
  • 使用场景:weak_ptr不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况。

这时就不能使用weak_ptr直接访问对象。那么可以用上面的代码首先判断weak_ptr指向对象是否存在?

  • weak_ptr返回this指针

通过shared_ptr返回this指针

  • 不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针。因此,返回this可能会导致重复析构:

#include <iostream>
#include <memory>
using namespace std;
class A
        {
        public:
            shared_ptr<A> GetSelf()
            {
                return shared_ptr<A>(this); // 不要这么做
            }
            ~A()
            {
                cout << "Destructor A" << endl;
            }
        };
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf();
    return 0;
}

运行结果:

  • 由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。

  • shared_from_this()

  • 正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回this的shared_ptr:

#include <iostream>
#include <memory>
using namespace std;
class A: public std::enable_shared_from_this<A>
        {
        public:
            shared_ptr<A>GetSelf()
            {
                return shared_from_this(); //
            }
            ~A()
            {
                cout << "Destructor A" << endl;
            }
        };
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf(); // ok
    return 0;
}
  • std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回。

  • 在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,sp1的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。

  • 需要注意的是,获取自身智能指针的函数应在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

  • weak_ptr使用注意事项

  • weak_ptr在使用前需要检查合法性。

weak_ptr<int> wp;
{
    shared_ptr<int> sp(new int(1)); //sp.use_count()==1
    wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
    shared_ptr<int> sp_ok = wp.lock(); //
}
shared_ptr<int> sp_null = wp.lock(); //sp_null.use_count()==0;
  • 因为上述代码中sp和sp_ok离开了作用域,所以其容纳的对象已经被释放了。

  • 所以最后得到了一个容纳NULL指针的sp_null对象。故在使用wp前需要调用wp.expired()函数判断一下。

  • 如果shared_ptr sp_ok和weak_ptr wp属于同一个作用域:

#include <iostream>
#include <memory>
using namespace std;
int main()
{
    weak_ptr<int> wp;
    shared_ptr<int> sp_ok;
    {
        shared_ptr<int> sp(new int(1)); //sp.use_count()==1
        wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
        sp_ok = wp.lock();
    }
    if(wp.expired()) {
        cout << "shared_ptr is destroy" << endl;
    } else {
        cout << "shared_ptr no destroy" << endl;
    }
}

运行结果:

这时wp不为空。

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值