C语言中常见内存错误

前言

C语言强大的原因之一在于几乎能掌控所有的细节,包括对内存的处理,什么时候使用内存,使用了多少内存,什么时候该释放内存,这都在程序员的掌控之中。而不像Java中,程序员是不需要花太多精力去处理垃圾回收的事情,因为有JVM在背后做着这一切。但是同样地,能力越大,责任越大。不恰当地操作内存,经常会引起难以定位的灾难性问题。今天我们就来看看有哪些常见的内存问题。

初始化堆栈中的数据

对申请的内存或自动变量进行初始化是一个好习惯,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
int test()
{
int *a = (int*) malloc(10);
/*判断是否申请成功*/
if(NULL == a)
{
return -1;
}
/*将其初始化为0*/
memset(a,0,10);
/*do something*/
return 0
}

我们经常需要在使用前将其初始化为0或使用calloc申请内存。关于初始化,在《C语言入坑指南-被遗忘的初始化》一文中,有更详细的阐述。

缓冲区溢出

缓冲区溢出通常指的是向缓冲区写入了超过缓冲区所能保存的最大数据量的数据。同样的,缓冲区溢出通常也伴随着难以定位的问题。例如下面的代码就存在缓冲区溢出的可能:

1
2
3
4
5
6
7
8
9
10
11
/*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语言入坑指南-缓冲区溢出》一文了解更多。

指针不等同于其指向的对象

我们可能常常错误性地认为指针对象的大小就是数据本身的大小,最常错误使用的就是下面的情况:

1
2
3
4
5
6
/*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呢?

1
2
3
int arr[] = {1,2,3};
int *ptr1 = arr;
char *ptr2 = (char*)arr;

实际上,它们移动的字节数,是以其指向对象大小为单位的。即ptr1 + 1会移动4字节(int类型),而ptr2 + 1 会移动1字节(char类型)。
下面的代码运行结果是什么?

1
2
3
4
5
6
7
8
9
#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语言入坑指南-数组之谜》中找到。

不可引用已释放的内存

对于下面的代码:

1
2
3
4
5
6
/*bad code*/
char *getHelloString()
{
char string[] = "hello";
return string;
}

在其他地方调用getHelloString之后,如果再使用printf打印string,显然是不可取的。因为在调用返回之后,string所指向的内存已经释放了。有人可能会问了,为什么返回int类型就可以使用呢?比如:

1
2
3
4
5
int getInt()
{
int a = 10;
return a;
}

调用getInt显然能够得到a的值,这是为什么呢?因为你实际上返回的就是值10,而前面返回的是string的地址,这个值你也能获取,但是要获取这个地址值指向的内存,已经不可行了。

下面的情况也是应该避免的:

1
2
3
4
5
/*bad code*/
int *a = (int*)malloc(10);
/*do something*/
free(a);
a[0] = 10; //内存已经被释放,不可再引用

在这个例子中可能很容易发现问题,但是在大型程序中,这样的问题可能很难发现,一个建议就是在释放a的内存后,显式地将a置为NULL。即:

1
2
free(a);
a = NULL;

避免对NULL解引用

对于上面的例子,a置NULL之后还不够,我们需要经常对入参进行检查,避免对NULL解引用。这样就避免引用已经释放的内存。例如:

1
2
3
4
5
6
7
8
9
10
int calcSum(int *arr,int len)
{
/*入惨检查,避免引用空指针*/
if(NULL == arr || 0 == len)
{
return -1;
}
/*do something*/
return 0;
}

当然了,在C++中可以传引用,而避免这种重复的检查性代码。
下面的代码,同样也是有问题的:

1
2
char *str = NULL;
printf("%s",str);

这里str为NULL,却将其作为字符串打印,后果将是灾难性的。

申请的内存不使用时需要释放

使用malloc等申请的内存如果不使用free进行释放,将会引起内存泄露。长期运行将会导致可用内存越来越少,程序也将会变得越来越卡顿。

1
2
3
4
5
6
7
8
9
10
11
/*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打印字符串时避免使用空指针

你踩过哪些坑?欢迎留言评论。

思考

下面的代码有什么问题?

1
2
3
4
int *arr = (int*)malloc(10);
/*do something*/
arr++;
free(arr);

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!