指针的奇技淫巧二__C的动态内存管理

指针的强大很大程度上源于它能追踪动态分配的内存,内存管理对所有的程序来说都是相当重要的,有程序运行时内存的隐式管理,也有我们下面讲的自己把数据推到内存堆上管理。PHP对于这一点额。。。

#动态内存分配
在C中动态分配内存的基本步骤有:

  1. 用malloc类的函数分配内存;
  2. 用这些内存支持应用程序;
  3. 用free函数释放内存。
1
2
3
4
int *pi = (int*) malloc(sizeof(int));
*pi = 5;
printf("*pi: %d\n",*pi);
free(pi)

解引操作常见的错误

1
2
int *pi;
*pi = (int*) malloc(sizeof(int));

问题出在赋值符号左边。我们再解引指针,这样会把malloc函数返回的地址赋值给pi中存放的地址所在的内存单元。通俗理解就是修改了pi指向的值,但是未修改pi执行的地址的值。有点难理解。可以产考下个人对指针书写的理解。
认识指针

正确的方法如下

1
2
pi = (int*) malloc(sizeof(int));

解释下代码意识,方便记忆

错误的方法

*pi = (int*) malloc(sizeof(int));
pi指向的内存地址的值 强转为int 声明一个int单位堆内存

正确的方法

pi = (int*) malloc(sizeof(int));
pi指向的内存地址的值 强转为int 声明一个int单位堆内存

ps:但是我们可以这样赋值

1
int *pi = (int*) malloc(sizeof(int));

在编译器看来,作为初始化操作符的=和作为赋值操作符的=不一样!!


内存泄露

如果不再使用已分配的内存却没有将其释放就会发生内存泄露,导致内存泄露的主要情况可能如下

  • 丢失内存地址
  • 应该调用free函数却没有调用(隐式泄露)

丢失内存地址举例

1
2
3
4
5
int *pi = (int*) malloc(sizeof(int));
*pi = 5;
...
pi = (int*) malloc(sizeof(int));

在第一次malloc申请一个堆的时候,操作系统分配了一个内存地址给我们,并且赋值给了指针pi,我们往这个地址中写入了数据(int 5)。后续申请一个堆,覆盖了之前申请的地址,指向5的内存地址的值就丢失了,但是int 5 占用的内存空间并没有释放掉。

动态内存分配函数

这里要介绍几个可以用来管理动态内存的函数

名称 作用
malloc 从堆上分配内存
realloc 在之前分配的内存块基础上,将内存重新分配更大或者更小
calloc 从堆上分配内存并清零
free 将内存块返回堆

malloc

从堆上分配一块内存,所分配的字节数由该函数唯一的参数指定,返回值是void指针,如果内存不足返回NULL,此函数不会清空或者修改内存,所以我们认为新分配的内存包含垃圾数据函数原型如下:

1
void* malloc(size_t);

执行malloc函数会进行以下操作:

  1. 从堆上分配内存
  2. 内存不会被修改或者清空
  3. 返回首字节的地址

注:当malloc无法分配内存时会返回NULL,在使用它返回指针之前先检查NULL是个好的习惯

1
2
3
4
5
6
int *pi = (int*) malloc(sizeof(int));
if(pi != NULL){
//指针没问题
}else{
//无效的指针
}

场景一: 要不要强制类型转换?

在两种不相互不兼容的指针类型之间赋值需要对malloc使用显式转换类型,避免警告。如果是赋值给void指针就不需要显式转换了,但是个人觉得malloc后强制类型转换是一个比较好的编码习惯,因为

  • 这样可以说明malloc函数的用意;
  • 可以兼容C++和早期的C编译器

场景二:分配内存失败

如果声明一个指针,但是没有在使用它之前为它指向的地址分配内存,那么内存通常会包含垃圾,这样会导致一个无效内存引用的错误

1
2
3
int *pi;
...
printf("%d\n",*pi);

字符串操作这类问题比较常见

1
2
3
char *name;
printf("enter a name:");
scanf("%s",name);

这看起来貌似没什么问题,天衣无缝。但是实际上name的这块内存还没有分配,也就是指针没有指向任何的内存地址。这容易和 char name;声明一个字符变量搞混。


场景三:没有给malloc传递正确的参数

  • 确认可分配的最大内存数
  • malloc传递不能为负数

场景四:静态,全局指针和malloc

初始化静态或全局变量时不能调用函数,这样会编译时会产生一个错误信息。列如

1
static int *pi = malloc(sizeof(int));

对于静态变量,可以通过在后面用一个单独的语句给变量分配内存来避免这个问题。

1
2
static int *pi;
pi = malloc(sizeof(int));

calloc

calloc 会在分配的同时清空内存,函数原型如下:

1
void *calloc(size_t numElements, size_t elementSize);

清空内存是指将内容设置为二进制0
下列为pi分配20字节,全包含0:

1
int *pi = calloc(5,sizeof(int));

使用malloc这样实现

1
2
int *pi = malloc(5 * sizeof(int));
memset(pi,0,5*sizeof(int));

memset函数会用某个值填充内存块,第一个参数是指向要填充缓冲区的指针,第二个参数是填充缓冲区的值,最后一个是要填充的字节数

realloc

我们会时不时的增加或者减少指针分配给我们的内存,毕竟程序变换和产品爸爸的心情一样的嘛~如果是动态的变长数组这个方法尤为有用。

1
void *realloc(void *ptr, size_t size);
名称 介绍
*ptr 指向原内存块的指针
size 请求大小
void * 返回指向重新分配的内存指针

如果请求的比当前的分配小,那么多余的内存会返还给堆,不能保证多余的内存会清空。如果请求的比当前的大,那么就尽量分配紧挨着当前分配内存,如果没有就在堆上其他区域分配把当前的内存复制到新的区域去

realloc函数行为

第一个参数 第二个参数 行为
同malloc
非空 0 原内存块被释放
非空 比原内存块小 利用当前块分配更小的块
非空 比原内存块大 要么当前位置要么其他位置分配跟大的快

realloc 在变小了指针内存过后溢出的内存不会马上释放,但是我们也修改不了了

free

函数原型

1
void free(void *ptr);

特别需要明白的概念是,free释放掉内存后,我们的指针还是指向的这块malloc函数分配的内存地址,这块内存被返回给了堆,尽管指针依然指向这块区域,但是我们应该将它看成指向了垃圾数据。这块区域稍后可能会被重新的分配写入不可预期的内容。

需要注意的几点:

  • 将已释放的指针赋值为NULL是一个好习惯
  • 重复释放运行会导致堆损坏和终止程序,同一块内存我们没有理由释放两次
  • 释放的内存会返回给堆供应用程序的后续使用,free不会体现在程序的内存使用上

迷途指针

迷途指针上面在注意点一有说到,将已释放的指针赋值为NULL可以避免掉。迷途指针是指:内存已经释放,而指针还在引用原始内存。

使用迷途指针会造成一系列的麻烦:

  • 如果访问内存,则行为不可预期;
  • 如果内存不可访问,则是段错误;
  • 潜在的安全隐患
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~