深入理解C++内存管理

这篇具有很好参考价值的文章主要介绍了深入理解C++内存管理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言

C++的高抽象层次,又兼具高性能,是其他语言所无法替代的,C++标准保持稳定发展,更加现代化,更加强大。但在各种活跃编程语言中,C++门槛依然很高,尤其C++的内存问题(内存泄露,内存溢出,内存宕机,堆栈破坏等问题),需要理解C++标准对象模型,C++标准库,标准C库,操作系统等内存设计,才能更加深入理解C++内存管理。

一、C++的内存布局

深入理解C++内存管理

如图,描述了C++程序的内存分布。

Code Segment(代码区):也称Text Segment,存放可执行程序的机器码。

Data Segment (数据区):存放已初始化的全局和静态变量, 常量数据(如字符串常量)。

BSS(Block started by symbol):存放未初始化的全局和静态变量。(默认设为0)

Heap(堆):从低地址向高地址增长。容量大于栈,程序中动态分配的内存在此区域。

Stack(栈):从高地址向低地址增长。由编译器自动管理分配。程序中的局部变量、函数参数值、返回变量等存在此区域。

 1.1 函数栈

如上图所示,可执行程序的文件包含BSS,Data Segment和Code Segment,当可执行程序载入内存后,系统会保留一些空间,即堆区和栈区。堆区主要是动态分配的内存(默认情况下),而栈区主要是函数以及局部变量等(包括main函数)。一般而言,栈的空间远远堆的空间。

当调用函数时,一块连续内存(堆栈帧)压入栈;函数返回时,堆栈帧弹出。

堆栈帧包含如下数据:

① 函数返回地址

② 局部变量/CPU寄存器数据备份

深入理解C++内存管理

1.2 全局变量

当全局/静态变量(如下代码中的x和y变量)未初始化的时候,它们记录在BSS段。

int x;
int z = 5;
void func()
{
     static int y;
}
int main()
{
    return 0;
}

处于BSS段的变量的值默认为0,考虑到这一点,BSS段内部无需存储大量的零值,而只需记录字节个数即可。

系统载入可执行程序后,将BSS段的数据载入数据段(Data Segment) ,并将内存初始化为0,再调用程序入口(main函数)。而对于已经初始化了的全局/静态变量而言,如以上代码中的z变量,则一直存储于数据段(Data Segment)。

1.2 内存对齐

对于基础类型,如float, double, int, char等,它们的大小和内存占用是一致的。而对于结构体而言,如果我们取得其sizeof的结果,会发现这个值有可能会大于结构体内所有成员大小的总和,这是由于结构体内部成员进行了内存对齐

1.2.1 为什么要内存对齐

① 内存对齐使数据读取更高效

在硬件设计上,数据读取的处理器只能从地址为k的倍数的内存处开始读取数据。这种读取方式相当于将内存分为了多个"块“,假设内存可以从任意位置开始存放的话,数据很可能会被分散到多个“块”中,处理分散在多个块中的数据需要移除首尾不需要的字节,再进行合并,非常耗时。

为了提高数据读取的效率,程序分配的内存并不是连续存储的,而是按首地址为k的倍数的方式存储;这样就可以一次性读取数据,而不需要额外的操作。

深入理解C++内存管理

② 在某些平台下,不进行内存对齐会崩溃 

1.2.2如何不使用内存对齐

当然有一些地方为了减少内存的开销就会可以避免内存对齐,比如SDS使用专门的编译优化来节省内存空间 :

struct __attribute__ ((__packed__)) sdshdr8

attribute ((packed))的作用就是告诉编译器,在编译 sdshdr8 结构时,不要使用字节对齐的方式,而是采用紧凑的方式分配内存。这是因为在默认情况下,编译器会按照 8 字节对齐的方式,给变量分配内存。也就是说,即使一个变量的大小不到 8 个字节,编译器也会给它分配 8 个字节。

为了节省内存,Redis 在这方面的设计上可以说是精打细算的。所以,Redis 采用了attribute ((packed))属性定义结构体,这样一来,结构体实际占用多少内存空间,编译器就分配多少空间。

比如,我用attribute ((packed))属性定义结构体 s2,同样包含 char 和 int 两个类型的成员变量,代码如下所示:

  #include <stdio.h>
  int main() {
     struct __attribute__((packed)) s2{
        char a;
        int b;
     } ts2;
     printf("%lu\n", sizeof(ts2));
     return 0;
  }

当你运行这段代码时,你可以看到,打印的结果是 5,表示编译器用了紧凑型内存分配,s2 结构体只占用 5 个字节的空间。

总而言之,如果你在开发程序时,希望能节省数据结构的内存开销,就可以把attribute ((packed))这个编程方法用起来。

1.2.3 内存对齐的规则

定义有效对齐值(alignment)为结构体中 最宽成员 和 编译器/用户指定对齐值 中较小的那个。

(1) 结构体起始地址为有效对齐值的整数倍

(2) 结构体总大小为有效对齐值的整数倍

(3) 结构体第一个成员偏移值为0,之后成员的偏移值为 min(有效对齐值, 自身大小) 的整数倍

相当于每个成员要进行对齐,并且整个结构体也需要进行对齐。

struct A
{
    int i;
    char c1;
    char c2;
};

int main()
{
    cout << sizeof(A) << endl; // 有效对齐值为4, output : 8
    return 0;
}

1.3 内存碎片

程序的内存往往不是紧凑连续排布的,而是存在着许多碎片。我们根据碎片产生的原因把碎片分为内部碎片和外部碎片两种类型:

(1) 内部碎片:系统分配的内存大于实际所需的内存(由于对齐机制);

(2) 外部碎片:不断分配回收不同大小的内存,由于内存分布散乱,较大内存无法分配;

深入理解C++内存管理

1.4 继承类布局 

继承

如果一个类继承自另一个类,那么它自身的数据位于父类之后。

含虚函数的类

如果当前类包含虚函数,则会在类的最前端占用4个字节,用于存储虚表指针(vpointer),它指向一个虚函数表(vtable)。vtable中包含当前类的所有虚函数指针。

二、C++内存泄漏

2.1 什么是内存

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:

void MyFunction(int nSize)

{

 char* p= new char[nSize];

 if( !GetStringFrom( p, nSize ) ){

  MessageBox(“Error”);

  return;

 }

 …//using the string pointed by p;

 delete p;

}

当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。

广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。

2.2 内存泄漏的发生方式

以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回True,那么pOldBmp指向的HBITMAP对象总是发生泄漏。

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回True,那么pOldBmp指向的HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

       3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。另一个例子:

char* g_lpszFileName = NULL;

void SetFileName( const char* lpcszFileName )
{

 if( g_lpszFileName ){
  free( g_lpszFileName );
 }

 g_lpszFileName = strdup( lpcszFileName );
}

如果程序在结束的时候没有释放g_lpszFileName指向的字符串,那么,即使多次调用SetFileName(),总会有一块内存,而且仅有一块内存发生泄漏。

4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。举一个例子:

class Connection

{

 public:

  Connection( SOCKET s);
  ~Connection();
  …

 private:

  SOCKET _socket;
  …

};

class ConnectionManager

{

 public:

  ConnectionManager(){}

  ~ConnectionManager(){

   list::iterator it;
   for( it = _connlist.begin(); it != _connlist.end(); ++it ){
    delete (*it);
   }
   _connlist.clear();

  }

  void OnClientConnected( SOCKET s ){
   Connection* p = new Connection(s);
   _connlist.push_back(p);
  }

  void OnClientDisconnected( Connection* pconn ){
   _connlist.remove( pconn );
   delete pconn;
  }

 private:

  list _connlist;

};

假设在Client从Server端断开后,Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

2.3 检测内存泄漏

检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存。

如果要检测堆内存的泄漏,那么需要截获住malloc/realloc/free和new/delete就可以了(其实new/delete最终也是用malloc/free的,所以只要截获前面一组即可)。对于其他的泄漏,可以采用类似的方法,截获住相应的分配和释放函数。文章来源地址https://www.toymoban.com/news/detail-404762.html

到了这里,关于深入理解C++内存管理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入理解C&C++内存管理

    我们先来看一下下面的代码,分析一下对于每个变量的存储位置: 【说明】 栈 又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。 内存映射段 是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。

    2023年04月26日
    浏览(26)
  • 深入理解Linux内核——内存管理(3)

    提要:本系列文章主要参考 MIT 6.828课程 以及两本书籍 《深入理解Linux内核》 《深入Linux内核架构》 对Linux内核内容进行总结。 内存管理的实现覆盖了多个领域: 内存中的物理内存页的管理 分配大块内存的伙伴系统 分配较小内存的slab、slub、slob分配器 分配非连续内存块的

    2024年02月13日
    浏览(33)
  • 深入理解Linux虚拟内存管理(六)

    Linux 内核设计与实现 深入理解 Linux 内核 Linux 设备驱动程序 Linux设备驱动开发详解 深入理解Linux虚拟内存管理(一) 深入理解Linux虚拟内存管理(二) 深入理解Linux虚拟内存管理(三) 深入理解Linux虚拟内存管理(四) 深入理解Linux虚拟内存管理(五) 深入理解Linux虚拟内存

    2024年02月08日
    浏览(37)
  • 深入理解Linux内核——内存管理(1)

    提要:本系列文章主要参考 MIT 6.828课程 以及两本书籍 《深入理解Linux内核》 《深入Linux内核架构》 对Linux内核内容进行总结。 内存管理的实现覆盖了多个领域: 内存中的物理内存页的管理 分配大块内存的伙伴系统 分配较小内存的slab、slub、slob分配器 分配非连续内存块的

    2024年02月13日
    浏览(31)
  • 深入理解 Spark(四)Spark 内存管理模型

    Executor 进程作为一个 JVM 进程,其内存管理建立在 JVM 的内存管理之上,整个大致包含两种方式:堆内内存和堆外内存。 一个 Executor 当中的所有 Task 是共享堆内内存的。一个 Work 中的多个 Executor 中的多个 Task 是共享堆外内存的。 堆内内存和堆外内存 大数据领域两个比较常见

    2024年01月24日
    浏览(34)
  • 深入理解Linux内核——内存管理(4)——伙伴系统(1)

    提要:本系列文章主要参考 MIT 6.828课程 以及两本书籍 《深入理解Linux内核》 《深入Linux内核架构》 对Linux内核内容进行总结。 内存管理的实现覆盖了多个领域: 内存中的物理内存页的管理 分配大块内存的伙伴系统 分配较小内存的slab、slub、slob分配器 分配非连续内存块的

    2024年02月10日
    浏览(37)
  • Python内存管理与垃圾回收机制:深入理解与优化【第138篇—RESTful API】

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 在Python编程中,内存管理与垃圾回收机制是至关重要的主题。了解Python如何管理内存和处理垃圾回收对于编写高效、稳定的程序至关重要。本

    2024年03月18日
    浏览(41)
  • 【c/c++】深入探秘:C++内存管理的机制

    🔥个人主页 : Quitecoder 🔥 专栏 : c++笔记仓 朋友们大家好,本篇文章我们详细讲解c++中的动态内存管理 我们来看内存区域划分 数据段就是我们所说的 全局变量 ,代码段是我们所说的 常量区 ,我们需要重点关注的是 堆区 ,这部分是由我们自己控制的 我们来依次讨论:

    2024年04月16日
    浏览(24)
  • 深入理解计算机系统(13)_存储器层次结构

    第一章 计算机的基本组成 1. 内容概述 2. 计算机基本组成 第二章 计算机的指令和运算 3. 计算机指令 4. 程序的机器级表示 5. 计算机运算 6. 信息表示与处理 第三章 处理器设计 7. CPU 8. 处理器体系结构 9. 优化程序性能 10. 其他处理器 第四章 存储器和IO系统 11. 存储器的层次结构

    2024年02月16日
    浏览(23)
  • 【C++干货基地】深度理解C++中的高效内存管理方式 new & delete

    🎬 鸽芷咕 :个人主页  🔥 个人专栏 : 《C++干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活!   哈喽各位铁汁们好啊,我是博主鸽芷咕《C++干货基地》是由我的襄阳家乡零食基地有感而发,不知道各位的城市有没有这种实惠又全面的零食基地呢?C++ 本身作

    2024年04月26日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包