常见C语言内存错误
前言
C语言强大的原因之一在于几乎能掌控所有的细节,包括对内存的处理,什么时候使用内存,使用了多少内存,什么时候该释放内存,这都在程序员的掌控之中。而不像Java中,程序员是不需要花太多精力去处理垃圾回收的事情,因为有JVM在背后做着这一切。但是同样地,能力越大,责任越大。不恰当地操作内存,经常会引起难以定位的灾难性问题。今天我们就来看看有哪些常见的内存问题。
初始化堆栈中的数据
对申请的内存或自动变量进行初始化是一个好习惯,例如:
int test()
{
int *a = (int*) malloc(10);
/*判断是否申请成功*/
if(NULL == a)
{
return -1;
}
/*将其初始化为0*/
memset(a,0,10);
/*do something*/
return 0;
}
我们经常需要在使用前将其初始化为0或使用calloc申请内存。关于初始化,在《C语言入坑指南-被遗忘的初始化》一文中,有更详细的阐述。
缓冲区溢出
缓冲区溢出通常指的是向缓冲区写入了超过缓冲区所能保存的最大数据量的数据。同样的,缓冲区溢出通常也伴随着难以定位的问题。例如下面的代码就存在缓冲区溢出的可能:
/*bad code*/
#include <stdio.h>
#include <string.h>
int main(void)
{
char buff[8] = {0};
char *p = "0123456789";
strcpy(buff,p);
printf("%s\n",buff);
return 0;
}
关于缓冲区溢出,可以通过《C语言入坑指南-缓冲区溢出》一文了解更多。
指针不等同于其指向的对象
我们可能常常错误性地认为指针对象的大小就是数据本身的大小,最常错误使用的就是下面的情况:
/*bad code*/
int test(int a[])
{
size_t len = sizeof(a)/sizeof(int);
/*do something*/
}
这里计算数组a的长度偶尔能够如愿,但实际上是错误的,因为数组名作为参数时,是指向该数组下标为0的元素的指针。因此sizeof(a)的值会是4或者8(取决于程序的位数)。
指针运算以指向对象大小为单位
对于下面的代码,ptr1 + 1之后,到底移动了多少个字节?ptr2 + 1呢?
int arr[] = {1,2,3};
int *ptr1 = arr;
char *ptr2 = (char*)arr;
实际上,它们移动的字节数,是以其指向对象大小为单位的。即ptr1 + 1会移动4字节(int类型),而ptr2 + 1 会移动1字节(char类型)。
下面的代码运行结果是什么?
#include<stdio.h>
int main(void)
{int a[5] = {1,2,3,4,5};
int *p = (int*)(&a+1);
printf("%d,%d",*(a+1),*(p-1));
return 0;
}
问题的答案也可在《C语言入坑指南-数组之谜》中找到。
不可引用已释放的内存
对于下面的代码:
/*bad code*/
char *getHelloString()
{
char string[] = "hello";
return string;
}
在其他地方调用getHelloString之后,如果再使用printf打印string,显然是不可取的。因为在调用返回之后,string所指向的内存已经释放了。有人可能会问了,为什么返回int类型就可以使用呢?比如:
int getInt()
{
int a = 10;
return a;
}
调用getInt显然能够得到a的值,这是为什么呢?因为你实际上返回的就是值10,而前面返回的是string的地址,这个值你也能获取,但是要获取这个地址值指向的内存,已经不可行了。
下面的情况也是应该避免的:
/*bad code*/
int *a = (int*)malloc(10);
/*do something*/
free(a);
a[0] = 10; //内存已经被释放,不可再引用
在这个例子中可能很容易发现问题,但是在大型程序中,这样的问题可能很难发现,一个建议就是在释放a的内存后,显式地将a置为NULL。即:
free(a);
a = NULL;
避免对NULL解引用
对于上面的例子,a置NULL之后还不够,我们需要经常对入参进行检查,避免对NULL解引用。这样就避免引用已经释放的内存。例如:
int calcSum(int *arr,int len)
{
/*入惨检查,避免引用空指针*/
if(NULL == arr || 0 == len)
{
return -1;
}
/*do something*/
return 0;
}
当然了,在C++中可以传引用,而避免这种重复的检查性代码。
下面的代码,同样也是有问题的:
char *str = NULL;
printf("%s",str);
这里str为NULL,却将其作为字符串打印,后果将是灾难性的。
申请的内存不使用时需要释放
使用malloc等申请的内存如果不使用free进行释放,将会引起内存泄露。长期运行将会导致可用内存越来越少,程序也将会变得越来越卡顿。
/*bad code*/
int doSomething(void *data,size_t len)
{
if(NULL == data)
{
return -1;
}
int *p = (int*)malloc(len);
/*do something*/
return 0;
}
在这里,doSomething中申请了内存却没有释放,多次调用之后,将导致内存泄露。也就是说,malloc或calloc与free经常是成对出现的。
总结
如果控制不当,强大的同时,也会造成更多的危害。上面所列出的仅仅是一些比较常见的内存相关问题,总结如下:
- 自动变量或申请的内存需要初始化
- 避免缓冲区溢出
- 指针不等同于指向的对象
- 指针运算以指向大小为单位
- 避免对NULL或已释放的内存进行引用
- 申请的内存不使用时及时释放
- 使用printf打印字符串时避免使用空指针
你踩过哪些坑?欢迎留言评论。
思考
下面的代码有什么问题?
int *arr = (int*)malloc(10);
/*do something*/
arr++;
free(arr);
公众号【编程珠玑】:专注但不限于分享计算机编程基础,Linux,C语言,C++,Python,数据库等编程相关[原创]技术文章,号内包含大量经典电子书和视频学习资源。欢迎一起交流学习,一起修炼计算机“内功”,知其然,更知其所以然。
常见C语言内存错误的更多相关文章
- 常见C语言编译错误解析【转】
C语言编译错误信息及说明1. 在函数 ‘transform’ 中:7: 错误:expected ‘;’ before ‘{’ token 解释:‘{’之前的某个语句缺少分号‘;’: 2. 在函数 ...
- 常见的C语言内存错误及对策(转)
http://see.xidian.edu.cn/cpp/html/483.html 一.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存.浅显的例子就 ...
- [转]C++常见内存错误汇总
在系统开发过程中出现的bug相对而言是比较好解决的,花费在这个上面的调试代价不是很大,但是在系统集成后的bug往往是难以定位的bug(最好方式是打桩,通过打桩可以初步锁定出错的位置,如:进入函数前打印 ...
- C语言内存管理(转)
伟大的Bill Gates 曾经失言: 640K ought to be enough for everybody — Bill Gates 1981 程序员们经常编写内存管理程序,往往提心吊胆.如果 ...
- C语言内存对齐详解
一.字节对齐基本概念 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型 ...
- C语言内存对齐原理
一.什么是字节对齐,为什么要对齐? 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这 ...
- C语言内存调试技巧—C语言最大难点揭秘
本文将带您了解一些良好的和内存相关的编码实践,以将内存错误保持在控制范围内.内存错误是 C 和 C++ 编程的祸根:它们很普遍,认识其严重性已有二十多年,但始终没有彻底解决,它们可能严重影响应用程序, ...
- 如何处理C++构造函数中的错误——兼谈不同语言的错误处理
用C++写代码的时候总是避免不了处理错误,一般来说有两种方式,通过函数的返回值或者抛出异常.C语言的错误处理一律是通过函数的返回值来判断的,一般是返回0.NULL或者-1表示错误,或者直接返回错误代码 ...
- mysql 常见的几个错误问题
Mysql常见的几个错误问题及解决方法: 1.问题: mysql DNS反解:skip-name-resolve 错误日志有类似警告: 点击(此处)折叠或打开 120119 16:26:04 [War ...
随机推荐
- HTTP状态码以及其含义大全
HTTP状态码(英语:HTTP Status Code)是用以表示网页服务器超文本传输协议响应状态的3位数字代码.我们在开发过程中比较常见的状态码有:200(请求成功).301(页面重定向).404( ...
- C# 插件热插拔
所谓热插拔就是插件可以 在主程序不重新启动的情况直接更新插件, 网上有很多方案: https://www.cnblogs.com/happyframework/p/3405811.html 如下: 但 ...
- Redis中的执行命令的过程
在redis.c的initServerConfig()方法中,通过调用dictCreate方法初始化server端的命令表.这个命令表是一个hashtable,可以通过key找到相关的命令: /* C ...
- 对于SQL的Join,在学习起来可能是比较乱的。我们知道,SQL的Join语法有很多inner的,有outer的,有left的,有时候,对于Select出来的结果集是什么样子有点不是很清楚。Coding Horror上有一篇文章,通过文氏图 Venn diagrams 解释了SQL的Join。我觉得清楚易懂,转过来。
对于SQL的Join,在学习起来可能是比较乱的.我们知道,SQL的Join语法有很多inner的,有outer的,有left的,有时候,对于Select出来的结果集是什么样子有点不是很清楚.Codi ...
- npm WARN enoent ENOENT: no such file or directory, open 'C:\Users\package.json'
在使用 npm 命令安装常用的 Node.js web框架模块 express时出现: 解决方法是 在命令行切换到安装nodejs文件下的nodejs\node_modules\npm 后执行npm ...
- 2.对于所有对象都通用的方法_EJ
第8条: 覆盖equals时请遵守通用约定 我们在覆盖equals方法时,必须遵守它的通用约定: 1.自反性.对于任何非null的引用值x,x.equals(x)必须返回true: 2.对称性.对于任 ...
- ES6中字符串扩展
ES6中字符串扩展 ① for...of 遍历字符串: 例如: for(let codePoint of 'string'){ console.log(codePoint) } 运行结果: ② in ...
- 前端入门8-JavaScript语法之数据类型和变量
声明 本系列文章内容全部梳理自以下几个来源: <JavaScript权威指南> MDN web docs Github:smyhvae/web Github:goddyZhao/Trans ...
- Deep Learning - 3 改进神经网络的学习方式
反向传播算法是大多数神经网络的基础,我们应该多花点时间掌握它. 还有一些技术能够帮助我们改进反向传播算法,从而改进神经网络的学习方式,包括: 选取更好的代价函数 正则化方法 初始化权重的方法 如何选择 ...
- 嵌套RecyclerView左右滑动替代自定义view
以前的左右滑动效果采用自定义scrollview或者linearlayout来实现,recyclerview可以很好的做这个功能,一般的需求就是要么一个独立的左右滑动效果,要么在一个列表里的中间部分一 ...