前言

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. Docker在Linux上运行NetCore系列(一)配置运行DotNetCore控制台

    转发请注明此文章作者与路径,请尊重原著,违者必究. 系列文章:https://www.cnblogs.com/alunchen/p/10121379.html 本篇文章操作系统信息 Linux:ubu ...

  2. .Net EF6+Mysql 环境搭建

    由于一直使用的数据库是mysql,之前所用的orm都是轻量级的例如 dapper 这些的,然后想用ef配置一下mysql,总共时间花了差不多2天,才将坑填完,写个博客将流程记录一下 给后来者少掉点坑. ...

  3. sql server查询语句条件判断字段值是否为NULL

    判断字段是否为null select * from table where c is null    select * from table where c is not null 判断字段是否为空 ...

  4. .Net Core 项目中添加统一的XSS攻击防御过滤器

    一.前言 最近公司内部在对系统的安全进行培训,刚好目前手里的一个.net core 项目中需要增加预防xss的攻击,本文将大概介绍下何为XSS攻击以及在项目中如何统一的预防XSS攻击. 二.XSS简介 ...

  5. mysql写注释的几种方法

    MySQL的注释风格总的来说有三种.它们分别是 1.单行注释可以用"#" select 1 as cname; #this is a comment +-------+ | cna ...

  6. 【worker】js中的多线程

    因为下个项目中要用到一些倒计时的功能,所以就提前准备了一下,省的到时候出现一下界面不友好和一些其他的事情.正好趁着这个机会也加深一下html5中的多线程worker的用法和理解. Worker简介 J ...

  7. [日常] PHP库函数fgetss的BUG

    1. fgetss函数php官网的解释是: (PHP 4, PHP 5, PHP 7)  fgetss — 从文件指针中读取一行并过滤掉 HTML 标记 2. 测试后出现的问题是: 当文本中有一行数据 ...

  8. idea代码提示

    idea代码提示:Keymap-->Main menu-->Code-->Completion去掉Cyclic Expand Word的快捷键将Basic的快捷键更改为Alt+/

  9. 远程连接centos7 上的mysql报(ERROR 2003 (HY000): Can't connect to MySQL server on '168.x.x.x' (10060) )

    1.MySQL端口 因为上一篇文章我就已经给MySQL新建了一新用户,且赋予了远程连接数据库的所有权限(GRANT ALL PRIVILEGES ON *.* TO 'newuser' @ '%' I ...

  10. 浅谈JS中String()与 .toString()的区别

    我们知道String()与 .toString()都是可以转换为字符串类型,但是String()与 .toString()的还是有区别的 1..toString()可以将所有的的数据都转换为字符串,但 ...