常见的【内存泄漏】姿势

关注公众号【高性能架构探索】,第一时间获取干货;回复【pdf】,免费获取计算机经典书籍

本文节选自文章:
内存泄漏-原因、避免以及定位

本文总结常见内存泄漏的几种方式,留意到这几点,可以避免95+%以上的内存泄漏

未释放

这种是很常见的,比如下面的代码:

int fun() {
    
    
    char * pBuffer = malloc(sizeof(char));
    
    /* Do some work */
    return 0;
}

上面代码是非常常见的内存泄漏场景(也可以使用new来进行分配),我们申请了一块内存,但是在fun函数结束时候没有调用free函数进行内存释放。

在C++开发中,还有一种内存泄漏,如下:

class Obj {
 public:
   Obj(int size) {
     buffer_ = new char;
   }
   ~Obj(){}
  private:
   char *buffer_;
};

int fun() {
  Object obj;
  // do sth
  return 0;
}

上面这段代码中,析构函数没有释放成员变量buffer_指向的内存,所以在编写析构函数的时候,一定要仔细分析成员变量有没有申请动态内存,如果有,则需要手动释放,我们重新编写了析构函数,如下:

~Object() {
  delete buffer_;
}

在C/C++中,对于普通函数,如果申请了堆资源,请跟进代码的具体场景调用free/delete进行资源释放;对于class,如果申请了堆资源,则需要在对应的析构函数中调用free/delete进行资源释放。

未匹配

在C++中,我们经常使用new操作符来进行内存分配,其内部主要做了两件事:

  1. 通过operator new从堆上申请内存(glibc下,operator new底层调用的是malloc)
  2. 调用构造函数(如果操作对象是一个class的话)

对应的,使用delete操作符来释放内存,其顺序正好与new相反:

  1. 调用对象的析构函数(如果操作对象是一个class的话)
  2. 通过operator delete释放内存
void* operator new(std::size_t size) {
    void* p = malloc(size);
    if (p == nullptr) {
        throw("new failed to allocate %zu bytes", size);
    }
    return p;
}
void* operator new[](std::size_t size) {
    void* p = malloc(size);
    if (p == nullptr) {
        throw("new[] failed to allocate %zu bytes", size);
    }
    return p;
}

void  operator delete(void* ptr) throw() {
    free(ptr);
}
void  operator delete[](void* ptr) throw() {
    free(ptr);
}

为了加深多这块的理解,我们举个例子:

class Test {
 public:
   Test() {
     std::cout << "in Test" << std::endl;
   }
   // other
   ~Test() {
     std::cout << "in ~Test" << std::endl;
   }
};

int main() {
  Test *t = new Test;
  // do sth
  delete t;
  return 0;
}

在上述main函数中,我们使用new 操作符创建一个Test类指针

  1. 通过operator new申请内存(底层malloc实现)
  2. 通过placement new在上述申请的内存块上调用构造函数
  3. 调用ptr->~Test()释放Test对象的成员变量
  4. 调用operator delete释放内存

上述过程,可以理解为如下:

// new
void *ptr = malloc(sizeof(Test));
t = new(ptr)Test
  
// delete
ptr->~Test();
free(ptr);

好了,上述内容,我们简单的讲解了C++中new和delete操作符的基本实现以及逻辑,那么,我们就简单总结下下产生内存泄漏的几种类型。

new 和 free

仍然以上面的Test对象为例,代码如下:

Test *t = new Test;
free(t)

此处会产生内存泄漏,在上面,我们已经分析过,new操作符会先通过operator new分配一块内存,然后在该块内存上调用placement new即调用Test的构造函数。而在上述代码中,只是通过free函数释放了内存,但是没有调用Test的析构函数以释放Test的成员变量,从而引起内存泄漏

new[] 和 delete

int main() {
  Test *t = new Test [10];
  // do sth
  delete t;
  return 0;
}

在上述代码中,我们通过new创建了一个Test类型的数组,然后通delete操作符删除该数组,编译并执行,输出如下:

in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in Test
in ~Test

从上面输出结果可以看出,调用了10次构造函数,但是只调用了一次析构函数,所以引起了内存泄漏。这是因为调用delete t释放了通过operator new[]申请的内存,即malloc申请的内存块,且只调用了t[0]对象的析构函数,t[1…9]对象的析构函数并没有被调用。

虚析构

记得08年面谷歌的时候,有一道题,面试官问,std::string能否被继承,为什么?

当时没回答上来,后来过了没多久,进行面试复盘的时候,偶然看到继承需要父类析构函数为virtual,才恍然大悟,原来考察点在这块。

下面我们看下std::string的析构函数定义:

~basic_string() { 
  _M_rep()->_M_dispose(this->get_allocator()); 
}

这块需要特别说明下,std::basic_string是一个模板,而std::string是该模板的一个特化,即std::basic_string

typedef std::basic_string<char> string;

现在我们可以给出这个问题的答案:不能,因为std::string的析构函数不为virtual,这样会引起内存泄漏

仍然以一个例子来进行证明。

class Base {
 public:
  Base(){
    buffer_ = new char[10];
  }

  ~Base() {
    std::cout << "in Base::~Base" << std::endl;
    delete []buffer_;
  }
private:
  char *buffer_;

};

class Derived : public Base {
 public:
  Derived(){}

  ~Derived() {
    std::cout << "int Derived::~Derived" << std::endl;
  }
};

int main() {
  Base *base = new Derived;
  delete base;
  return 0;
}

上面代码输出如下:

in Base::~Base

可见,上述代码并没有调用派生类Derived的析构函数,如果派生类中在堆上申请了资源,那么就会产生内存泄漏

为了避免因为继承导致的内存泄漏,我们需要将父类的析构函数声明为virtual,代码如下(只列了部分修改代码,其他不变):

~Base() {
    std::cout << "in Base::~Base" << std::endl;
    delete []buffer_;
  }

然后重新执行代码,输出结果如下:

int Derived::~Derived
in Base::~Base

借助此文,我们再次总结下存在继承情况下,构造函数和析构函数的调用顺序。

派生类对象在创建时构造函数调用顺序:

  1. 调用父类的构造函数
  2. 调用父类成员变量的构造函数
  3. 调用派生类本身的构造函数

派生类对象在析构时的析构函数调用顺序:

  1. 执行派生类自身的析构函数
  2. 执行派生类成员变量的析构函数
  3. 执行父类的析构函数

为了避免存在继承关系时候的内存泄漏,请遵守一条规则:无论派生类有没有申请堆上的资源,请将父类的析构函数声明为virtual

循环引用

在C++开发中,为了尽可能的避免内存泄漏,自C++11起引入了smart pointer,常见的有shared_ptr、weak_ptr以及unique_ptr等(auto_ptr已经被废弃),其中weak_ptr是为了解决循环引用而存在,其往往与shared_ptr结合使用。

下面,我们看一段代码:

class Controller {
 public:
  Controller() = default;

  ~Controller() {
    std::cout << "in ~Controller" << std::endl;
  }

  class SubController {
   public:
    SubController() = default;

    ~SubController() {
      std::cout << "in ~SubController" << std::endl;
    }

    std::shared_ptr<Controller> controller_;
  };

  std::shared_ptr<SubController> sub_controller_;
};

int main() {
  auto controller = std::make_shared<Controller>();
  auto sub_controller = std::make_shared<Controller::SubController>();

  controller->sub_controller_ = sub_controller;
  sub_controller->controller_ = controller;
  return 0;
}

编译并执行上述代码,发现并没有调用Controller和SubController的析构函数,我们尝试着打印下引用计数,代码如下:

int main() {
  auto controller = std::make_shared<Controller>();
  auto sub_controller = std::make_shared<Controller::SubController>();

  controller->sub_controller_ = sub_controller;
  sub_controller->controller_ = controller;

  std::cout << "controller use_count: " << controller.use_count() << std::endl;
  std::cout << "sub_controller use_count: " << sub_controller.use_count() << std::endl;
  return 0;
}

编译并执行之后,输出如下:

controller use_count: 2
sub_controller use_count: 2

通过上面输出可以发现,因为引用计数都是2,所以在main函数结束的时候,不会调用controller和sub_controller的析构函数,所以就出现了内存泄漏

上面产生内存泄漏的原因,就是我们常说的循环引用

为了解决std::shared_ptr循环引用导致的内存泄漏,我们可以使用std::weak_ptr来单面去除上图中的循环。

class Controller {
 public:
  Controller() = default;

  ~Controller() {
    std::cout << "in ~Controller" << std::endl;
  }

  class SubController {
   public:
    SubController() = default;

    ~SubController() {
      std::cout << "in ~SubController" << std::endl;
    }

    std::weak_ptr<Controller> controller_;
  };

  std::shared_ptr<SubController> sub_controller_;
};

在上述代码中,我们将SubController类中controller_的类型从std::shared_ptr变成std::weak_ptr,重新编译执行,结果如下:

controller use_count: 1
sub_controller use_count: 2
in ~Controller
in ~SubController

从上面结果可以看出,controller和sub_controller均以释放,所以循环引用引起的内存泄漏问题,也得以解决。

可能有人会问,使用std::shared_ptr可以直接访问对应的成员函数,如果是std::weak_ptr的话,怎么访问呢?我们可以使用下面的方式:

std::shared_ptr controller = controller_.lock();

即在子类SubController中,如果要使用controller调用其对应的函数,就可以使用上面的方式。

关注公众号【高性能架构探索】,第一时间获取干货;回复【pdf】,免费获取计算机经典书籍

猜你喜欢

转载自blog.csdn.net/namelij/article/details/122537086
今日推荐