C++11解决内存泄露问题的智能指针:shared-创新互联

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

南华ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18980820575(备注:SSL证书合作)期待与您的合作!

目录

一、内存泄露

二、智能指针 

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#includestruct 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_ptrsp(p);

    std::shared_ptrsp1(new BigObj());
    std::shared_ptrsp2 = std::make_shared();
}

std::shared_ptrget_obj() {
    return std::make_shared();
}

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思想的代码如下:

templateclass 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_ptrptr1(new MyClass);
//or
std::shared_ptrptr = std::make_shared("Hello", 3.14);

  具体例子: 

#include#includeclass 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_ptrptr1(new MyClass);
    ptr1->DoSomething();

    std::shared_ptrptr2 = ptr1;
    ptr2->DoSomething();

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

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

接着,使用 std::shared_ptrptr2 = 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思想的实现:

templateclass 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_ptrptr1(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_ptrptr2(new int(20));
std::unique_ptrptr3 = std::move(ptr2);
2.3 weak_ptr 2.3.1 原理及简单实现
templateclass weak_ptr {
 public:
  weak_ptr() : ptr_(nullptr), count_(nullptr) {}
  weak_ptr(const shared_ptr& other) : ptr_(other.ptr_), count_(other.count_) {}
  
  T* get() const {
    if (count_ && *count_ >0) {
      return ptr_;
    }
    return nullptr;
  }

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

 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& other) : 
    ptr_(other.ptr_), count_(other.count_) {}

这段代码是 weak_ptr 类的构造函数,它接受一个 shared_ptr类型的参数 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_ptr;
};

class A {
  std::shared_ptr b_ptr;
};

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

2.3.3 具体使用方法
#include#includeclass A;

class B {
public:
    std::weak_ptr a_ptr;
};

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

int main() {
    std::shared_ptr a_ptr(new A);
    std::shared_ptr 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#includeint main() {
    std::shared_ptrshared_ptr_obj(new int);
    std::weak_ptrweak_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#includeint main() {
    std::shared_ptrshared_ptr_obj(new int);
    std::weak_ptrweak_ptr_obj(shared_ptr_obj);
    std::shared_ptrlocked_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是日常使用最多的。使用智能指针可以提高程序的安全性和可读性,减少内存泄露和空指针错误。但是,智能指针会带来一些额外的开销,对性能有一定的影响。

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

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


当前文章:C++11解决内存泄露问题的智能指针:shared-创新互联
浏览地址:http://pwwzsj.com/article/dojhoh.html