前言

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语言内存错误的更多相关文章

  1. 常见C语言编译错误解析【转】

    C语言编译错误信息及说明1. 在函数 ‘transform’ 中:7: 错误:expected ‘;’ before ‘{’ token    解释:‘{’之前的某个语句缺少分号‘;’: 2. 在函数 ...

  2. 常见的C语言内存错误及对策(转)

    http://see.xidian.edu.cn/cpp/html/483.html 一.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内存.浅显的例子就 ...

  3. [转]C++常见内存错误汇总

    在系统开发过程中出现的bug相对而言是比较好解决的,花费在这个上面的调试代价不是很大,但是在系统集成后的bug往往是难以定位的bug(最好方式是打桩,通过打桩可以初步锁定出错的位置,如:进入函数前打印 ...

  4. C语言内存管理(转)

    伟大的Bill Gates 曾经失言: 640K ought to be enough for everybody — Bill Gates 1981 程序员们经常编写内存管理程序,往往提心吊胆.如果 ...

  5. C语言内存对齐详解

    一.字节对齐基本概念 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型 ...

  6. C语言内存对齐原理

    一.什么是字节对齐,为什么要对齐? 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这 ...

  7. C语言内存调试技巧—C语言最大难点揭秘

    本文将带您了解一些良好的和内存相关的编码实践,以将内存错误保持在控制范围内.内存错误是 C 和 C++ 编程的祸根:它们很普遍,认识其严重性已有二十多年,但始终没有彻底解决,它们可能严重影响应用程序, ...

  8. 如何处理C++构造函数中的错误——兼谈不同语言的错误处理

    用C++写代码的时候总是避免不了处理错误,一般来说有两种方式,通过函数的返回值或者抛出异常.C语言的错误处理一律是通过函数的返回值来判断的,一般是返回0.NULL或者-1表示错误,或者直接返回错误代码 ...

  9. mysql 常见的几个错误问题

    Mysql常见的几个错误问题及解决方法: 1.问题: mysql DNS反解:skip-name-resolve 错误日志有类似警告: 点击(此处)折叠或打开 120119 16:26:04 [War ...

随机推荐

  1. JavaScript 系列博客(四)

    JavaScript 系列博客之(四) 前言 本篇介绍 JavaScript 中的对象.在第一篇博客中已经说到 JavaScript 是一种''对象模型''语言.所以可以这样说,对象是 JavaScr ...

  2. 本人开源项目 Lu-Rpc

    Lu-Rpc 是个专为学习者准备的 RPC 框架, 初始架构非常简单, 可供初学者扩展和学习. Lu 可以认为是中文世界的撸, 即撸 Rpc--- 造个 Rpc 轮子. Lu-Rpc 架构图如下: L ...

  3. shell编程基础(七): 处理文件命令sed与awk

    一.sed(以行为单位处理文件) sed意为流编辑器(Stream Editor),在Shell脚本和Makefile中作为过滤器使用非常普遍,也就是把前一个程序的输出引入sed的输入,经过一系列编辑 ...

  4. 【原】KMeans与深度学习自编码AutoEncoder结合提高聚类效果

    这几天在做用户画像,特征是用户的消费商品的消费金额,原始数据(部分)是这样的: id goods_name goods_amount 男士手袋 1882.0 淑女装 2491.0 女士手袋 345.0 ...

  5. 玩儿虫那些事(四)—— 使用curl

    目录 一.爬一个简单的网站 二.模拟登录新浪 三.各种请求的发送 四.使用curl 五.模拟登录QQ空间 六.selenium的使用 七.phantomjs的使用 八.开源框架webmagic 九.开 ...

  6. 【转】JAVA解压.TAR.Z及.ZIP文件

     解压.ZIP文件 package app.qdupr.Method; import java.io.File; import java.io.FileOutputStream; import jav ...

  7. [转]MySQL忘记root密码解决方法

    本文转自:https://www.cnblogs.com/wxdblog/p/6864475.html 今天重新装了一遍MySQL,因为用的是免安装的,所以需要重新设置密码,然后我一通,结果搞得自己也 ...

  8. 【问题】vs IIS破除文件上传限制最全版

    今天在测试一下上传文件的时候发现iis和配置存在上传文件大小限制(IIS默认大小30M,最大运行为2g:2147483647),百度了一部分资料有些发布到IIS好使,但是在VS调试中不好使.于是自己不 ...

  9. [linux] tcpdump抓包案例

    1.常见参数 tcpdump -i eth0 -nn -s0 -v port 80 -i 选择监控的网卡 -nn 不解析主机名和端口号,捕获大量数据,名称解析会降低解析速度 -s0 捕获长度无限制 - ...

  10. [PHP]算法-队列结构的PHP实现

    题目描述 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 思路: 1.php数组完全就能实现 2.array_push 从尾部往里压入元素 3.array_shi ...