本文章主要讲解C++中指针相关问题,包括原始指针、野指针、空悬指针、智能指针(shared_ptr、unique_ptr、weak_ptr)、内存泄漏、循环引用、智能指针与多线程等知识。

原始指针

  1. 指针的概念

    int *p = new int(3);
    cout << "p:" << p << endl;    // 输出p所占内存空间所保存的值,即堆上的地址 
    cout << "&p:" << &p << endl;    // 输出p所占内存空间的地址
    cout << "*p:" << *p << endl;    // 输出p所指向的堆上的内存的值,即3
  2. 空悬指针

    当我们delete一个指针后,指针值就变为无效了。虽然指针值已经无效了,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了空悬指针(dangling pointer)。

    在delete指针之后,最好赋值为nullptr

    int *p = new int(3);
    delete p;
    p = nullptr;
  3. 野指针

    即未经初始化的指针。若使用该指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于访问一个本不存在的位置上的本不存在的对象。

  4. 释放问题

    • 重复delete某个指针两次,将会是未定义的行为(崩溃Abort),重复释放。
    • delete空指针不会有问题。所以再次印证了,释放某个指针之后,要把它赋值为nullptr。
  5. 内存耗尽

    • new的时候,如果内存耗尽,会抛出bad_alloc()异常,但是并不会返回空指针,所以以下代码是错误的

      int *pbigarr = new int[5147483647];
      if(pbigarr == nullptr)
      {
          cerr << "new failure!" << endl;
      }
    • 如果想要在new失败的时候返回空指针,需要使用定位new,修改为如下:

      int *pbigarr = new(nothrow) int[5147483647];
      if(pbigarr == nullptr)
      {
          cerr << "new failure!" << endl;
      }

智能指针

shared_ptr

  1. 构造

    1. 使用make_shared函数进行构造
    shared_ptr<int> p = make_shared<int>(42);
    shared_ptr<string> pstr = make_shared<string>(10, 'c');
    
    2. 与new结合使用来进行构造
    shared_ptr<int> p2(new int(43));
  2. 其它接口

    // 获取值
    cout << *p << endl;
    
    // 获取原始指针
    cout << p.get() << endl;
        
    // 判断p是否是唯一指向该共享对象的指针
    cout << std::boolalpha << p.unique() << endl;
    
    // 返回指向该共享对象的指针的数量
    cout << p.use_count() << endl;
    
    // 释放p所指向对象的内存,并令p重新指向p2
    p.reset(p2);
  3. 指定删除器:

    • 数组

      // c++17前不能传递数组类型作为shared_ptr的模板参数
      std::shared_ptr<int> sp1(new int[10](), std::default_delete<int[]>());
      
      // c++17后,支持数组,无需提供额外的删除器
      std::shared_ptr<int[]> sp1(new int[10]());
    • 其它类型(删除器为可调用对象即可)

      // 自定义删除器函数,释放int型内存
      void deleteIntPtr(int* p)
      {
          delete p;
          cout << "int 型内存被释放了...";
      }
      
      shared_ptr<int> ptr(new int(250), deleteIntPtr);
      
      // lambda表达式
      shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
      
      // C风格的网络库写法
      struct event_base *base = event_base_new();
      shared_ptr<event_base> p(base, event_base_free);

unique_ptr

  1. 构造

    unique_ptr<int> p2(new int(42));
    unique_ptr<string> p3(new string("liuguangxuan"));
    unique_ptr<string> p4 = make_unique<string>(10,'a');   // C++14新增
    
    cout << *p2 << endl;
    cout << *p3 << endl;
    cout << *p4 << endl;
  2. 其它接口

    - u = nullptr
        释放所指向的对象,并且将u置为空
    - u.release()
        u放弃对指针的控制,并返回指针,将u置为空
    - u.reset()
        释放u指向的对象,并将u置为空,等同于直接赋值为nullptr
    - u.reset(q)
        释放u指向的对象,并将u指向q
    - u.reset(nullptr)
        和直接赋值为nullptr一样
  3. 指定删除器

    • 数组

      // C++11支持数组类型,所以无需额外指定删除器
      std::unique_ptr<int[]> up1(new int[10]());
    • 其它类型

      // 必须指定类型,然后再指定删除器
      // decltype作用于函数的时候,返回的为函数类型,所以必须加上*
      // C风格的网络库写法
      struct event_base *base = event_base_new();
      unique_ptr<event_base, delctype(event_base_free)*> p(base, event_base_free);

weak_ptr

weak_ptr为一种伴随类,和shared_ptr搭配使用,解决循环引用的问题。

循环引用

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

class Son;
class Father
{
public:
    Father()
    {
        cout << __FUNCTION__ << endl;
    }
    ~Father()
    {
        cout << __FUNCTION__ << endl;
    }

public:
    shared_ptr<Son> son_;
};

class Son
{
public:
    Son()
    {
        cout << __FUNCTION__ << endl;
    }
    ~Son()
    {
        cout << __FUNCTION__ << endl;
    }

public:
    shared_ptr<Father> father_;        // 会造成循环引用,无法调用析构
    //weak_ptr<Father> father_;        // 正确
    
};

int main(int argc, char *argv[])
{
    shared_ptr<Father> f(new Father());
    shared_ptr<Son> s(new Son());

    f->son_ = s;
    s->father_ = f;

    cout << "father count:" << f.use_count() << endl;
    cout << "son count:" << s.use_count() << endl;

    return 0;
}

上面的输出为:

// 可见没有执行析构函数,lsan会报内存泄漏
Father
Son
father count:2
son count:2

=================================================================
==2055250==ERROR: LeakSanitizer: detected memory leaks

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
    #1 0x560aa1484cd1 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Son*>(Son*) /usr/include/c++/10/bits/shared_ptr_base.h:628
    #2 0x560aa1484ba8 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Son*>(Son*, std::integral_constant<bool, false>) /usr/include/c++/10/bits/shared_ptr_base.h:639
    #3 0x560aa1484a25 in std::__shared_ptr<Son, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<Son, void>(Son*) /usr/include/c++/10/bits/shared_ptr_base.h:1128
    #4 0x560aa1484752 in std::shared_ptr<Son>::shared_ptr<Son, void>(Son*) /usr/include/c++/10/bits/shared_ptr.h:159
    #5 0x560aa1484255 in main /root/workspace/test/main.cpp:52
    #6 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
    #1 0x560aa1484c4b in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Father*>(Father*) /usr/include/c++/10/bits/shared_ptr_base.h:628
    #2 0x560aa1484b72 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<Father*>(Father*, std::integral_constant<bool, false>) /usr/include/c++/10/bits/shared_ptr_base.h:639
    #3 0x560aa14849dd in std::__shared_ptr<Father, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<Father, void>(Father*) /usr/include/c++/10/bits/shared_ptr_base.h:1128
    #4 0x560aa148472c in std::shared_ptr<Father>::shared_ptr<Father, void>(Father*) /usr/include/c++/10/bits/shared_ptr.h:159
    #5 0x560aa148422c in main /root/workspace/test/main.cpp:51
    #6 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58

Indirect leak of 16 byte(s) in 1 object(s) allocated from:
    #0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
    #1 0x560aa1484236 in main /root/workspace/test/main.cpp:52
    #2 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58

Indirect leak of 16 byte(s) in 1 object(s) allocated from:
    #0 0x7fabaf806647 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:99
    #1 0x560aa148420d in main /root/workspace/test/main.cpp:51
    #2 0x7fabaf39c209 in __libc_start_call_main ../sysdeps/x86/libc-start.c:58

SUMMARY: AddressSanitizer: 80 byte(s) leaked in 4 allocation(s).

将shared_ptr替换成weak_ptr之后的输出为:
(只需要将其中一个类的shared_ptr修改成weak_ptr即可,如果都修改成了weak_ptr,但是weak_ptr又不增加引用计数,容易引起对象提前被销毁)

// 没有内存泄漏
Father
Son
father count:1
son count:2
~Father
~Son

构造

shared_ptr<string> p3(new string("liuguangxuan"));
    
weak_ptr<string> ws(p3);
weak_ptr<string> ws = p3;

其它接口

w.reset()
    // 将w置为空
w.use_count()
    // 与w共享对象的shared_ptr的个数
w.expired()
    // 若w.use_count()为0,返回true,否则返回false
w.lock()
    // 如果expired()为true,则返回一个空shared_ptr,否则返回指向w的对象的shared_ptr

auto_ptr

auto_ptr在C++17中被移除,建议使用shared_ptr和unique_ptr来代替。

模拟智能指针

template <typename T>
class Shared_Ptr
{
public:
    Shared_Ptr(T *t = nullptr)                      // 构造
    {
        count_ = new int(1);
        resource_ = t;
    }

    Shared_Ptr(const Shared_Ptr *rhs)                  // 拷贝 构造
    {
        count_ = rhs->count_;
        resource_ = rhs->resource_;
        ++*count_;
    }

    Shared_Ptr& operator=(const Shared_Ptr *rhs)      // 拷贝赋值
    {
        -- *count_;                    // 先释放自身的

        if(*count_ == 0)
        {
            delete count_;
            delete resource_;
        }

        count_ = rhs->count_;                // 再拷贝rhs的到自身
        resource_ = rhs->resource_;

        ++*count_;
    }

    ~Shared_Ptr()                                   // 析构
    {
        --*count_;

        if(*count_ == 0)
        {
            delete count_;
            delete resource_;
        }
    }

    int use_count()
    {
        return *count_;
    }

    int *count_;
    T *resource_;

};

智能指针与多线程

此问题不能一概而论,分3种情况。

  • 引用计数:引用计数为线程安全的。
  • shared_ptr对象自身:非线程安全的。线程安全级别同内置类型、标准库容器、std::string一样。

    • 一个shared_ptr对象实体可被多个线程同时读取
    • 两个shared_ptr对象实体可被两个线程同时写入
    • 如果要从多个线程读写同一shared_ptr对象,那么需要加锁
  • shared_ptr所管理的对象:非线程安全的。

智能指针性能

先说结论:

raw : unique_ptr : shared_ptr的效率约为:1 : 1.3 : 2.5

即原始指针最快,其次unique_ptr的效率会比原始指针慢一点,最后shared_ptr最慢。但是考虑到智能指针的便利性,所以在没有共享需求的场景下,最好使用unique_ptr,其次是shared_ptr。

测试步骤:

分别执行1亿次的分配int内存并释放,统计耗时。

  1. 原始(raw)指针

    int main(int argc, char *argv[])
    {
        const int count = 100000000;    
        for(int i = 0;i < count;++i)
        {
            int *p = new int(30);
            delete p;
        }
        return 0;
    }

    执行3次分别耗时:

    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m22.031s
    user    0m21.619s
    sys    0m0.392s
    
    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m22.830s
    user    0m22.444s
    sys    0m0.384s
    
    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m21.997s
    user    0m21.612s
    sys    0m0.384s

    平均每次耗时约为:22.286秒

  2. unique_ptr

    int main(int argc, char *argv[])
    {
        const int count = 100000000;    
        for(int i = 0;i < count;++i)
        {
            unique_ptr<int> u(new int(30));
        }
        return 0;
    }

    执行3次分别耗时:

    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m30.154s
    user    0m29.839s
    sys    0m0.313s
    
    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m28.627s
    user    0m28.241s
    sys    0m0.384s
    
    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m29.240s
    user    0m28.935s
    sys    0m0.304s

    平均每次耗时约为:29.340秒

  3. shared_ptr

    int main(int argc, char *argv[])
    {
        const int count = 100000000;    
        for(int i = 0;i < count;++i)
        {
            shared_ptr<int> p(new int(30));
        }
        return 0;
    }

    执行3次分别耗时:

    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m55.556s
    user    0m55.414s
    sys    0m0.139s
    
    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m56.702s
    user    0m56.511s
    sys    0m0.180s
    
    root@debian:~/workspace/cpp11/build# time ./main 
    real    0m53.917s
    user    0m53.731s
    sys    0m0.176s

    平均每次耗时约为:55.392秒

检测内存泄漏

  1. 安装Sanitizer

    apt-get install libasan8
  2. CMake配置

    // 检测内存泄漏
    set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=leak")
    
    // 检测其它指针问题(释放后使用、堆溢出、栈溢出等)
    // 此配置也能检测部分内存泄漏,但是循环引用的内存泄漏得需要上面那句话来检测
    // set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")

参考

标签: C++, 指针

添加新评论