【高质量代码】如何写出更高质量的C/C++代码(1):内存管理

简介: 内存的管理是C/C++开发程序过程中的一个比较麻烦的问题。对于经验不是足够丰富的程序员来说,开发比较复杂的程序的时候几乎肯定会遇到内存管理方面的bug。

内存的管理是C/C++开发程序过程中的一个比较麻烦的问题。对于经验不是足够丰富的程序员来说,开发比较复杂的程序的时候几乎肯定会遇到内存管理方面的bug。对C/C++语言以及编译机制深入的理解和养成良好的编程习惯可以尽量减少这类bug产生的几率。

1、C/C++程序运行时内存结构简介

一个典型的C/C++编译的进程所占用的内存空间通常分为5个部分,由低地址到高地址分别为:

  1. 代码段(Code/Text Segment):保存可执行程序运行的二进制代码段。
  2. 数据段(Data Segment):保存进程已经初始化的全局变量0。
  3. BSS段(BSS Segment):保存已经声明,但尚未初始化的全局变量,进程开始后这部分数据初始化为0;静态变量也视为全局变。
  4. 堆(Heap):保存进程动态分配的内存数据。C中使用malloc/calloc/free分配、释放,C++中使用new/delete分配、释放。如果不释放,进程会始终保存这些数据,直到进程退出。
  5. 栈(Stack):主要用于处理函数调用过程中的数据,主要有函数的参数、临时变量和返回地址等。在栈中保存的数据在生命周期结束后会自行释放。栈空间和堆空间按照实际情况确定大小,没有指定的数值。

2、内存的分配方式

通常在编程中常用到的内存分配方式有三种:

(1)从静态存储区分配,在程序编译的时候就已经确定。如全局变量、static变量等。

(2)从栈空间分布,如函数内部的局部变量。此类数据分配效率很高但容量有限。

(3)从堆空间动态分配,在程序运行时由程序员决定申请多少内存并负责释放。这类数据出问题的可能性最高。


3、内存管理中常见错误

实际编程中导致内存错误的情况通常发生于处理堆空间的数据时。主要有以下几种:

(1)使用了分配失败的内存空间。程序中申请内存可能会因为不同原因而失败,而使用申请空间失败的内存地址将会导致进程崩溃。通常,在使用内存空间之前判断指针是否为0可以避免类似问题。如果指向一段内存的指针作为函数的参数,那么可以在函数的入口处使用assert(p!=NULL) 处理,如果p指针为空则会返回错误。如果一段内存通过malloc/new获取,那么使用if(p==null)或if(p=!null)进行预防处理。

(2)分配成功,但是使用了未经过初始化的内存。此时进程可能不会崩溃,但是会导致数据引用错误。因此,创建数组等结构之后,应第一时间进行初始化,哪怕全部设为0。

(3)空间分配、初始化成功,但是读写越界。此类问题在使用for循环处理数组时经常出现,比如以下代码:

int *pArr = (int *)malloc(20);
memset(pArr, 0, 20);
for(int idx = 0;idx <= 20;idx++)
    *(pArr+idx) = idx;
问题经常就出现在for循环中究竟是<还是<=。在该使用<的地方使用了<=,就会导致最后一次循环时内存读写越界。
(4)内存泄露。在堆空间中手动分配的内存没有释放,这部分内存在进程退出之前就会一直存在,如果这部分的代码循环执行,那么很有可能出现系统的内存被耗尽的情况。3和4是新手程序员犯错误比较多的部分,只能在开发时多多留神。

(5)使用了释放的内存。通常会导致这种情况的有:①函数返回了指向栈内存的指针或引用到上层,这会导致上层使用该函数返回的指针/引用时发现指针无效,因为我们想要的数据已经随着函数调用结束而销毁了;②对于一些全局指针,在free或delete之后,没有设为null,产生了“野指针”。在后面继续使用该指针时,通过if(p==null)判断的方法便失效了。解决方法是注意返回指针或引用时,应返回指向全局数据的指针和引用,在释放内存之后第一时间将指针设为null


4、指针和数组的对比

在C/C++中,经常可以使用指针和数组达到相同的目的,因此时常会产生这样的疑惑:即二者是否是等价的?实际上二者由其区别,主要在于:数组在栈空间或者静态存储区创建,数组名对应某一块指定的内存区且不可以指向其他内存区,一旦创建之后,数组的地址和容量在生命周期内固定,只可能改变数组的内容;而指针则没有此限制,可以指向任意类型的地址,因此经常用指针来操作堆空间的动态内存。下面的程序可以反映二者的区别:

char a[] = "hello";
a[0] = 'X';
cout << a << endl;
char *p = " world";
p[0] = 'X';  
cout << p << endl;
在该段程序中,a表示一个数组,这个数组有独立的内存空间并初始化为"hello",因此其内部元素的值可以改变;而指针p指向的是保存在常量文本区的字符串本身,并没有进行一次数据拷贝,因此试图修改常量文本的操作会在运行时导致程序崩溃。

二者另一个区别在于使用sizeof运算符计算内存大小时的结果。对于一个初始化过的数组,使用sizeof运算符得到的是数组的大小;而对于一个指针变量,使用sizeof运算符得到的是指针变量的大小(一般为4),与指向的内存数据大小无关。如以下程序所示:

char a[] = "Hello world";
char *p = a;
cout << sizeof(a) <<endl;//返回12
cout << sizeof(p) <<endl;//返回4
需要注意的一点是,当数组名作为函数的参数传递时,当做指针变量处理。


5、指针作为函数的参数

一个很重要的原则是:不要使用作为参数的指针去申请内存。因为在函数执行时,形参会被重新分配一个与原来不同的指针变量,如果使用这个指针去申请内存,那么不但调用上层不能得到内存空间,函数内部申请到的内存也会因为地址指针丢失无法释放而造成内存泄露。解决此类问题可以通过传递“指向指针的指针”或者将内存地址通过return返回的方法。需要注意的一点是,不要return栈内存空间,因别这部分空间在函数返回时将消亡。


6、“野指针”导致的问题

free和delete将释放参数所指向的内存地址,但是并没有将指向该地址的指针置0,内存被释放后,标识该内存的地址指针变量的值并未改变。此时这个指针值是合法的,但是指向的内容却是非法的,这就造成了“野指针”的产生。如果这个指针变量再次被使用,那么if(p==null)的合法性判断将会失效。

造成“野指针”产生还有两种可能:刚刚定义的指针变量没有被初始化,局部指针变量在刚刚创建时不会设为NULL而是一个随机值;指针变量的生命周期合法,但是指向的对象已消亡,这是指向该对象的指针也将成为野指针。为了杜绝这类情况,需要注意遵守以下原则:

  1. 每一个内存区域被释放后,第一时间将指向该区域的指针赋值为NULL。
  2. 定义一个指针变量是,或者赋给初值NULL,或者直接令其指向一段合法申请到的内存区。
  3. 尽量将指针变量定义在与目标对象/内存一直的声明周期,让其“同生共死”。


7、指针变量同动态内存的关系

C/C++语言其实并不聪明,很多时候并不能理解我们编程时的想法。比如,函数的局部变量在函数结束时消亡,但是在内部申请的内存却不会因为指针变量的消亡而被释放。还有,我们把一段内存区释放,那指向该内存的指针变量依然保持原有值,变成了“野指针”。因此,实际上,这二者并没有直接的联系,编程时一定要分别处理。


8、malloc/free与new/delete

malloc和free是C语言的标准库函数,new和delete是C++的运算符,二者都能实现内存的动态申请和释放。而二者的区别也正反映了两种语言的设计差异:C是更加面向过程的语言,C++则是面向对象的语言,new和delete除了释放内存之外,更多地考虑了面向对象的一些特性。

C++与C的最本质区别之一在于C++定义了类这一概念,并且对对象的产生和消亡定义了构造函数和析构函数,因此new/delete在生成和释放对象时,对调用对象的构造和析构函数进行一些该类的个性化的操作,这是malloc/free力所不能及的。

因此,需要遵照的原则是:为了一个C++对象申请动态内存是,一定要使用new,释放是也一定要用delete,否则会因为构造和析构函数没被调用而产生错误。

对于free函数,如果p为NULL,那么可以多次调用free(p),但是如果p不是NULL,那么连续两次进行free就会使得程序崩溃。这也给了我们另一个理由在释放内存之后马上将指针设为NULL。


9、内存申请失败

通常内存申请失败时,将会返回NULL给指针变量,此时应判断指针是否为NULL,如果的确申请失败,则应该返回错误,或者直接使用exit(n)来结束进程。

目录
相关文章
|
5月前
|
安全 Java 应用服务中间件
Spring Boot + Java 21:内存减少 60%,启动速度提高 30% — 零代码
通过调整三个JVM和Spring Boot配置开关,无需重写代码即可显著优化Java应用性能:内存减少60%,启动速度提升30%。适用于所有在JVM上运行API的生产团队,低成本实现高效能。
685 3
|
5月前
|
存储 大数据 Unix
Python生成器 vs 迭代器:从内存到代码的深度解析
在Python中,处理大数据或无限序列时,迭代器与生成器可避免内存溢出。迭代器通过`__iter__`和`__next__`手动实现,控制灵活;生成器用`yield`自动实现,代码简洁、内存高效。生成器适合大文件读取、惰性计算等场景,是性能优化的关键工具。
327 2
|
5月前
|
C++ Windows
应用程序无法正常启动(0xc0000005)?C++报错0xC0000005如何解决?使命召唤17频频出现闪退,错误代码0xC0000005(0x0)
简介: 本文介绍了Windows应用程序出现错误代码0xc0000005的解决方法,该错误多由C++运行库配置不一致或内存访问越界引起。提供包括统一运行库配置、调试排查及安装Visual C++运行库等解决方案,并附有修复工具下载链接。
1611 1
|
7月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
256 26
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
8月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
162 1
|
7月前
|
API 数据安全/隐私保护 C++
永久修改机器码工具, exe一机一码破解工具,软件机器码一键修改工具【c++代码】
程序实现了完整的机器码修改功能,包含进程查找、内存扫描、模式匹配和修改操作。代码使用
|
8月前
|
C++
爱心代码 C++
这段C++代码使用EasyX图形库生成动态爱心图案。程序通过数学公式绘制爱心形状,并以帧动画形式呈现渐变效果。运行时需安装EasyX库,教程链接:http://【EasyX图形库的安装和使用】https://www.bilibili.com/video/BV1Xv4y1p7z1。代码中定义了屏幕尺寸、颜色数组等参数,利用随机数与数学函数生成动态点位,模拟爱心扩散与收缩动画,最终实现流畅的视觉效果。
1086 0
|
11月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。

热门文章

最新文章