找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2925

积分

0

好友

395

主题
发表于 5 天前 | 查看: 29| 回复: 0

对C程序员来说,管理和使用虚拟内存是个困难的、容易出错的任务。与内存有关的错误常常令人费解,因为它们在时间和空间上,经常在距错误源一段距离之后才表现出来。本文将带你梳理几种典型的C/C++开发中与内存相关的常见错误,帮助你更好地规避和排查。

以下是几种常见的与内存相关的错误:

  • 间接引用坏指针;
  • 读取未初始化的内存;
  • 栈缓冲区溢出;
  • 假设指针和它们指向对象的大小相同;
  • 错位错误;
  • 引用指针而不是它所指向的对象;
  • 误解指针运算;
  • 引用不存在的变量;
  • 引用空闲堆块中的数据;
  • 内存泄露。

Objective C按键特写

1、间接引用坏指针

间接引用坏指针的一个常见示例是scanf错误。假设要使用scanfstdin读一个整数到一个变量。正确的方法是传递给scanf一个变量的地址:

scanf函数正确与错误传参示例

但如果错误地传递了val的内容,而不是它的地址,scanf会将val的内容(一个随机整数值)解释为一个地址,并试图将一个字写到这个位置。在最好的情况下,程序会立即因访问非法地址而异常终止;但在最糟糕的情况下,val的内容恰好对应虚拟内存中某个合法的读/写区域,于是就会静默地覆盖那块内存,这种错误通常在程序运行相当长一段时间后,才会引发灾难性的、令人困惑的后果。

关键点:使用scanf时,务必确保传入的是变量的地址(使用&操作符)。

2、读取未初始化的内存

一个常见的误解是认为所有内存都会被自动初始化为零。虽然BSS段(存放未初始化的全局变量和静态变量)会被加载器初始化为零,但通过malloc等函数从堆上分配的内存则不会。错误地假设堆内存已初始化会导致程序行为不可预测。

看下面的示例,函数matvec旨在计算矩阵A与向量x的乘积y:

matvec函数代码示例,存在未初始化错误

这段代码错误地假设向量y已被初始化为零,导致y[i] += ...的计算结果完全错误。正确的做法是显式地将y[i]初始化为零,或者在分配时使用calloc函数,它会自动将内存初始化为零。

关键点calloc在动态分配内存后会自动初始化为零,而malloc不会,分配出的内存包含随机的垃圾数据。

3、栈缓冲区溢出

如果一个程序不检查输入字符串的大小就写入栈中的目标缓冲区,那么这个程序就存在缓冲区溢出漏洞。例如,下面的函数使用了不安全的gets函数:

bufoverflow函数,存在栈缓冲区溢出漏洞

gets函数会复制一个任意长度的字符串到缓冲区buf,如果输入超过63个字符(留一个给结尾的\0),就会覆盖栈帧中的其他数据(如返回地址),可能导致程序崩溃或被恶意利用。安全的做法是使用fgets函数,它可以限制读取的最大字符数。

关键点:永远不要使用gets,改用fgets或其它安全的输入函数,并始终校验输入长度。

4、假设指针和它们指向的对象大小相同

在不同的系统架构上,指针的大小可能与int等基础类型不同。常见的错误是在动态分配指针数组时,错误地使用了指向对象的大小而非指针本身的大小。

下面的函数makeArray1意图创建一个nm列的二维数组:

makeArray1函数,错误使用sizeof(int)而非sizeof(int*)

第5行中,sizeof(int)应改为sizeof(int *)。如果在一台int指针大小为8字节(如64位系统)而int为4字节的机器上运行,实际分配的内存只有预期的一半。随后的循环写入会越界,破坏相邻的内存区域。

以下是典型的数据类型大小对比:

32位与64位编译器下数据类型大小对比表

关键点:为指针数组分配内存时,务必使用sizeof(指针类型),例如sizeof(int *),而不是sizeof(int)

5、错位错误

错位错误(Off-by-one error)是另一类常见的边界错误,常导致数组访问越界。

还是看创建二维数组的例子,makeArray2函数:

makeArray2函数,循环条件错位导致越界

在第7行,循环条件写成了i <= n,这将导致循环执行n+1次。在第8行,会尝试初始化并访问不存在的A[n],从而覆盖A数组之后的内存,引发不可预知的行为。

关键点:时刻牢记C语言中数组下标从0开始,到n-1结束。仔细检查所有循环的边界条件。

6、引用指针而不是它所指向的对象

如果不注意C语言操作符的优先级和结合性,可能会误操作指针本身而非指针指向的值。看下面这个从二叉堆中删除元素的函数:

binheapDelete函数,运算符优先级错误

第6行代码*size--;的本意是减少size指针所指向的整数值。然而,后缀递减运算符--和间接引用运算符*优先级相同,且从右向左结合。因此,这行代码实际执行的是*(size--);,即先减少指针size自身的值(使其指向错误的内存位置),然后再解引用。正确的写法是使用括号明确意图:(*size)--;

关键点:当对操作符的优先级和结合性不确定时,果断使用括号来明确计算顺序。

7、误解指针运算

指针算术运算的步长是其指向类型的大小,而非固定的1字节。这是一个常见的疏忽。

下面这个search函数旨在扫描一个int数组,寻找值val首次出现的位置:

search函数,指针运算步长错误

第4行p += sizeof(int);是错误的。因为pint *类型,p++p += 1本身就会让p前进sizeof(int)个字节。这里又额外加上了sizeof(int),导致每次循环跳过4个整数,无法正确遍历数组。应直接写作p++;

8、引用不存在的变量(返回局部变量地址)

新手程序员有时会不理解栈帧的生命周期,返回指向局部变量的指针。

stackref函数,返回局部变量的地址

函数stackref返回了局部变量val的地址。一旦函数返回,其栈帧就被释放,尽管返回的指针p仍指向一个合法的内存地址,但该地址对应的val变量已不复存在。后续对该地址的读写行为是未定义的,可能导致程序崩溃或数据混乱。

关键点:永远不要返回指向栈上局部变量(非静态)的指针或引用。如需返回,请使用动态分配(堆内存)或静态/全局变量。

9、引用空闲堆块中的数据(Use After Free)

与上一条类似,引用已经通过free释放的堆内存是严重的错误。

heapref函数,使用已释放的内存

heapref函数中,第10行释放了x指向的内存块。然而,在第14行又通过x[i]访问了这块已释放的内存。此时,该内存可能已被重新分配用于其他用途,内容被覆盖,或者被内存管理器标记为不可用,导致程序崩溃。

关键点:在释放一块内存后,应立即将所有指向它的指针置为NULL(良好习惯),并确保后续不再访问它。

10、内存泄露

内存泄露是程序缓慢失血的“隐形杀手”。当分配了堆内存(malloc, calloc等)却忘记释放(free)时,就会发生内存泄露。

leak函数,分配内存后未释放

leak函数分配了内存,但在返回前没有释放它。指针x是局部变量,函数返回后,指向该内存块的唯一指针丢失,导致这块内存无法再被程序访问或释放。随着函数被反复调用,泄露的内存会不断累积,最终可能耗尽所有可用内存。

关键点:对于C标准库的malloc/free,以及C++的new/delete,必须确保成对使用。对于复杂的资源管理,可以考虑使用RAII(资源获取即初始化)思想或智能指针(C++)。

总结

内存管理是C/C++编程中的基石,也是难点所在。本文列举的十类错误——从坏指针引用到内存泄露——覆盖了开发中常见的大部分陷阱。理解这些错误的本质,并养成谨慎的编程习惯(如检查边界、初始化变量、配对管理资源、善用工具如Valgrind进行内存检查),是写出健壮、可靠程序的关键。希望这份指南能帮助你在计算机基础和底层编程实践中更游刃有余。如果你想与更多开发者交流此类问题,欢迎访问云栈社区一起探讨。




上一篇:从内存操作到函数指针:C语言指针全面解析与应用指南
下一篇:Redis缓存与数据库一致性解决方案,详解高并发系统下的四大核心策略
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-7 18:37 , Processed in 0.943437 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表