C++11 解决内存泄露问题的智能指针:shared

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

        我们经常听到内存泄漏但是对这个抽象的概念一直没有什么理解比如产生内存泄漏又将如何我平时写程序从来不考虑这个等等。这篇的目的第一给大家实验实验内存泄露带来的问题让大家直观感受内存泄露。第二介绍C++11新特性智能指针shared_ptr、unique_ptr、weak_ptr。

目录

一、内存泄露

二、智能指针 

2.1 shared_ptr

2.1.1 实现原理及代码

2.1.2 shared_ptr的使用

2.2 unique_ptr

2.2.1 unique_ptr原理及代码实现

2.2.2 unique_ptr使用方式

2.3 weak_ptr

2.3.1 原理及简单实现

2.3.2 weak_ptr的作用何在

2.3.3 具体使用方法

三、总结


一、内存泄露

        申请的内存未得到释放而引发的内存泄露指的是程序在使用指针动态分配内存时由于程序设计问题没有释放已经不再使用的内存造成程序的内存使用量逐渐增加。这会带来以下结果

        1. 如果内存泄露这个过程累积会占用大量内存直至内存不足程序崩溃、系统假死。

        2. 系统的性能会受到影响系统会自动回收这些内存从而导致系统不流畅了。

        3. 占用内存是实际的物理内存会影响其他程序的运行因为虚拟内存映射到物理内存是需要实际物理内存的如果物理内存被占太多就会导致其他程序无内存可用。

        一个具体的例子

        下面是一段 C 语言代码它用于动态分配内存来存储一个字符串

char* myString = (char*) malloc(100);
strcpy(myString, "Hello, World!");
printf("%s\n", myString);

        如果我们不释放 myString 指向的内存那么这段代码就会导致内存泄露。为了避免内存泄露我们应该在不再使用 myString 指向的内存后调用 free 函数释放这段内存。

char* myString = (char*) malloc(100);
strcpy(myString, "Hello, World!");
printf("%s\n", myString);
free(myString);

        这样就能避免内存泄露了。

        问题内存泄露在程序执行完后的内存会得到释放吗

        回答内存泄露在程序执行完后的内存并不会得到释放。当程序结束时系统会释放程序所占用的所有资源包括内存但是这些内存中包含的未释放的空间并不会得到释放而是被系统回收。

        如果程序存在大量的内存泄露那么系统在程序结束时就需要释放大量的内存这会导致系统性能下降。而且这些内存可能被其它程序或系统进程重新利用这样会占用一些系统的资源导致系统性能下降。

        为了避免这种情况的发生应该尽早地发现和修复程序中的内存泄露问题让程序能够及时释放不再使用的内存。

        问题内存泄露的问题很严重吗可以避免吗有什么方法

        回答内存泄露问题是一种常见的程序设计问题在大型程序中特别容易发生。如果程序存在大量的内存泄露那么它可能导致系统性能下降甚至导致程序崩溃或系统假死。

        

尽管内存泄露问题是一种常见问题但是可以通过一些方法来避免它

  • 明确程序的内存使用计划确保程序及时释放不再使用的内存。

  • 使用自动内存管理工具如 C++ 的智能指针来管理内存。

  • 使用内存检测工具如 valgrind在程序运行时自动检测内存泄露。

  • 在代码审查和测试过程中特别关注内存管理问题。

        这四条建议貌似只有第二条是有实际作用的其他的就像喊口号一样食之无味弃之可惜。

二、智能指针 

        智能指针实现的原理很简单通过使用对象去管理资源并使用引用计数规则来进行统计对象使用次数当使用次数减小至0时自动调用析构释放之前分配的内存从而避免内存泄露。

        我们来看一个简单的示例

#include <iostream>
#include <memory>

struct BigObj {
    BigObj() {
        std::cout << "big object has been constructed" << std::endl;
    }
    ~BigObj() {
        std::cout << "big object has been destructed" << std::endl;
    }
};

void test_shared_ptr() {
    BigObj *p = new BigObj();
    std::shared_ptr<BigObj> sp(p);

    std::shared_ptr<BigObj> sp1(new BigObj());
    std::shared_ptr<BigObj> sp2 = std::make_shared<BigObj>();
}

std::shared_ptr<BigObj> get_obj() {
    return std::make_shared<BigObj>();
}

int main() {

    test_shared_ptr();
    auto p = get_obj();

}

输出

big object has been constructed
big object has been constructed
big object has been constructed
big object has been destructed
big object has been destructed
big object has been destructed
big object has been constructed
big object has been destructed

        可见我们动态创建的4个BigObj指针对象都被释放了。

        智能指针一共分为三种shared_ptr、unique_ptr、weak_ptr。

2.1 shared_ptr

2.1.1 实现原理及代码

        C++引入RAII特性RAII可以保证任何情况下使用对象先构造后析构。shared_ptr使用了计数器来统计对象被引用的次数可表示shared_ptr思想的代码如下

template<typename T>
class shared_ptr {
public:
    shared_ptr(T* p = nullptr) : ptr(p), count(new int(1)) {}
    ~shared_ptr() {
        if (--*count == 0) {
            delete ptr;
            delete count;
        }
    }
    shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
        ++*count;
    }
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            if (--*count == 0) {
                delete ptr;
                delete count;
            }
            ptr = other.ptr;
            count = other.count;
            ++*count;
        }
        return *this;
    }
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }

private:
    T* ptr;
    int* count;
};

        每个 shared_ptr 对象都维护了一个指向该指针引用计数的指针当拷贝一个 shared_ptr 对象时引用计数就会加一每当销毁一个 shared_ptr 对象时引用计数就会减一。当引用计数为0时就会自动释放动态分配的内存避免内存泄露。

 shared_ptr(const shared_ptr& other) : ptr(other.ptr), count(other.count) {
        ++*count;
    }

        上面的代码 shared_ptr的拷贝构造当一个 shared_ptr 对象被拷贝时它会将指针和引用计数指针指向原来的 shared_ptr 对象。这样原来的 shared_ptr 对象和新拷贝的 shared_ptr 对象都指向了同一个对象并且共享了同一个引用计数。这样就可以保证在所有 shared_ptr共享一份计数。

    ~shared_ptr() {
        if (--*count == 0) {
            delete ptr;
            delete count;
        }
    }

        当 shared_ptr 对象被销毁时它会先减少引用计数如果引用计数变为0则表示没有其它 shared_ptr 对象指向该对象了那么就可以释放动态分配的内存并释放引用计数所占用的内存。

    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            if (--*count == 0) {
                delete ptr;
                delete count;
            }
            ptr = other.ptr;
            count = other.count;
            ++*count;
        }
        return *this;
    }

        这段代码实现了 shared_ptr 的赋值操作符重载。它首先判断左操作数和右操作数是否相同如果不同则将左操作数的引用计数减一如果引用计数为0就释放内存。然后将左操作数的指针和引用计数指针指向右操作数并将右操作数的引用计数加1。这样左操作数和右操作数就共享了同一个指针和引用计数这就是 shared_ptr 的赋值操作的基本思路。

2.1.2 shared_ptr的使用

        

        声明shared_ptr指针的方式

std::shared_ptr<MyClass> ptr1(new MyClass);
//or
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>("Hello", 3.14);

        具体例子 

#include <iostream>
#include <memory>

class MyClass {
 public:
  MyClass() { std::cout << "MyClass constructed." << std::endl; }
  ~MyClass() { std::cout << "MyClass destructed." << std::endl; }
  void DoSomething() { std::cout << "MyClass is doing something." << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr1(new MyClass);
    ptr1->DoSomething();

    std::shared_ptr<MyClass> ptr2 = ptr1;
    ptr2->DoSomething();

    ptr1.reset();
    ptr2.reset();
    return 0;
}

        定义了一个 MyClass 类并在 main 函数中使用 shared_ptr 来管理该类的对象。首先在 main 函数中使用 std::shared_ptr<MyClass> ptr1(new MyClass); 创建了一个 shared_ptr 对象 ptr1并使用 new 关键字动态分配了一个 MyClass 对象。然后使用 ptr1->DoSomething(); 调用了 MyClass 的一个成员函数。

        接着使用 std::shared_ptr<MyClass> ptr2 = ptr1; 将 ptr1 的指针和引用计数拷贝给 ptr2此时 ptr1 和 ptr2 共享了同一个 MyClass 对象。

        最后在程序结束前使用 ptr1.reset(); 和 ptr2.reset(); 来释放动态分配的 MyClass 对象并销毁 ptr1 和 ptr2。

        运行结果 

MyClass constructed.
MyClass is doing something.
MyClass is doing something.
MyClass destructed.

2.2 unique_ptr

2.2.1 unique_ptr原理及代码实现

        unique_ptr思想的实现

template<typename T>
class unique_ptr {
 public:
  explicit unique_ptr(T* ptr = nullptr) : ptr_(ptr) {}
  ~unique_ptr() { delete ptr_; }

  T* release() {
    T* tmp = ptr_;
    ptr_ = nullptr;
    return tmp;
  }

  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  T* get() const { return ptr_; }

 private:
  T* ptr_;
};

        unique_ptr 是一种独占所有权的智能指针它只能有一个指针指向一个特定的对象。

        unique_ptr 的构造函数接受一个指向 T 类型对象的指针并将其赋值给类的成员变量 ptr_。         ~unique_ptr() 析构函数会自动调用 delete 释放动态分配的对象。

        release() 函数可以释放 unique_ptr 对象所持有的指针并将该指针返回。

        operator* 和 operator-> 函数可以像普通指针一样使用 unique_ptr 对象get() 函数可以获取 unique_ptr 对象所持有的指针。

2.2.2 unique_ptr使用方式

        1. 创建unique_ptr并为其分配内存

std::unique_ptr<int> ptr1(new int(10));

        2.  访问 unique_ptr 对象所指向的内存

int x = *ptr1;
std::cout << x << std::endl;  // Output: 10

        3.  释放 unique_ptr 对象所持有的指针

int* raw_ptr = ptr1.release();

        4.  移动语义

std::unique_ptr<int> ptr2(new int(20));
std::unique_ptr<int> ptr3 = std::move(ptr2);

2.3 weak_ptr

2.3.1 原理及简单实现

template<typename T>
class weak_ptr {
 public:
  weak_ptr() : ptr_(nullptr), count_(nullptr) {}
  weak_ptr(const shared_ptr<T>& other) : ptr_(other.ptr_), count_(other.count_) {}
  
  T* get() const {
    if (count_ && *count_ > 0) {
      return ptr_;
    }
    return nullptr;
  }

  shared_ptr<T> lock() const {
    if (count_ && *count_ > 0) {
      return shared_ptr<T>(ptr_, count_);
    }
    return shared_ptr<T>();
  }

 private:
  T* ptr_;
  int* count_;
};

        weak_ptr 类包含了一个指向 T 类型对象的指针 ptr_ 和一个指向引用计数的指针 count_。

        get() 函数返回所指向的对象的指针但是会检查引用计数是否大于0.

        lock()函数可以返回一个 shared_ptr 对象, 如果引用计数大于0返回一个新的 shared_ptr 对象否则返回一个空的 shared_ptr 对象。

weak_ptr(const shared_ptr<T>& other) : 
    ptr_(other.ptr_), count_(other.count_) {}

        这段代码是 weak_ptr 类的构造函数它接受一个 shared_ptr<T> 类型的参数 other。通过 other 对象获取其管理的对象的指针 ptr_ 和引用计数指针 count_。然后将它们分别赋值给 weak_ptr 类的成员变量 ptr_ 和 count_。

2.3.2 weak_ptr的作用何在

        为解决循环引用问题。

        循环引用是指两个或多个对象之间相互引用而这些对象都不能被释放因为它们都被另一个对象所引用。这样的话这些对象就会成为内存泄露导致程序无法正常工作。

        而 weak_ptr 就可以在这种情况下解决这个问题。它可以访问一个 shared_ptr 所管理的对象但不会增加该对象的引用计数。这样就可以解除循环引用使得这些对象能够正常被释放。

        举个例子假设有一个类A,它有一个指针成员变量指向另一个类B, 类B也有一个指针成员变量指向类A这样就出现了循环引用。 而使用 weak_ptr 就可以解除这种循环引用使得两个类都能够正常被释放。

class A;
class B {
  std::weak_ptr<A> a_ptr;
};

class A {
  std::shared_ptr<B> b_ptr;
};

        在这个例子中类A和类B互相持有对方的指针但是使用了 weak_ptr 就可以解除循环引用。当类A的对象被销毁时类B的 weak_ptr 对象不会增加A的引用计数这样就不会出现循环引用。反之亦然。

2.3.3 具体使用方法

#include <iostream>
#include <memory>

class A;

class B {
public:
    std::weak_ptr<A> a_ptr;
};

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() {std::cout << "A is deleted" << std::endl;}
};

int main() {
    std::shared_ptr<A> a_ptr(new A);
    std::shared_ptr<B> b_ptr(new B);
    a_ptr->b_ptr = b_ptr;
    b_ptr->a_ptr = a_ptr;
    return 0;
}

        类A和类B互相持有对方的指针但是使用了 weak_ptr 就可以解除循环引用。当类A的对象被销毁时类B的 weak_ptr 对象不会增加A的引用计数这样就不会出现循环引用从而可以避免内存泄漏。

        运行结果为: A is deleted

        在 weak_ptr 对象转换为 shared_ptr 对象之前应该先使用 expired() 或 lock() 函数检查原先的 shared_ptr 对象是否已经销毁。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> shared_ptr_obj(new int);
    std::weak_ptr<int> weak_ptr_obj(shared_ptr_obj);
    if (weak_ptr_obj.expired()) {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    } else {
        std::cout << "shared_ptr object is still alive" << std::endl;
    }
    shared_ptr_obj.reset();
    if (weak_ptr_obj.expired()) {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    } else {
        std::cout << "shared_ptr object is still alive" << std::endl;
    }
    return 0;
}

        结果

shared_ptr object is still alive
shared_ptr object has been deleted

        

        lock() 函数返回一个 shared_ptr 对象如果引用的对象已经被销毁则返回一个空的 shared_ptr 对象。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> shared_ptr_obj(new int);
    std::weak_ptr<int> weak_ptr_obj(shared_ptr_obj);
    std::shared_ptr<int> locked_ptr = weak_ptr_obj.lock();
    if (locked_ptr) {
        std::cout << "shared_ptr object is still alive" << std::endl;
    } else {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    }
    shared_ptr_obj.reset();
    locked_ptr = weak_ptr_obj.lock();
    if (locked_ptr) {
        std::cout << "shared_ptr object is still alive" << std::endl;
    } else {
        std::cout << "shared_ptr object has been deleted" << std::endl;
    }
    return 0;
}

shared_ptr object is still alive
shared_ptr object has been deleted

        这两个函数都可以用来检查原先的 shared_ptr 对象是否已经销毁但是在使用时应根据具体情况来选择。如果只是需要知道原先的 shared_ptr 对象是否已经销毁那么可以使用 expired() 函数。如果需要在原先的 shared_ptr 对象未销毁时访问其所管理的对象那么可以使用 lock() 函数。

三、总结

        智能指针的出现类似于加入了垃圾回收机制一般来讲shared_ptr是日常使用最多的。使用智能指针可以提高程序的安全性和可读性减少内存泄露和空指针错误。但是智能指针会带来一些额外的开销对性能有一定的影响。

        首先智能指针会增加对象的大小因为需要存储引用计数和其他元数据。其次使用智能指针会带来额外的内存分配和释放操作当创建和销毁智能指针对象时会引起额外的内存开销。最后使用智能指针会增加锁的使用导致线程同步时产生额外的开销。总的来说使用智能指针会带来一定的性能开销但是能够带来的好处远大于开销。如果性能是关键的话可以考虑使用自己实现的智能指针或者在高性能代码中使用原始指针。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++