在讲解本次内容前,先来看个小栗子:
#include <stdio.h> #include <stdlib.h> void safe_free(void *ptr) { if (ptr) { free(ptr); ptr = NULL; } } int main() { int *p = (int *)malloc(sizeof(int)); printf("[before:addr] %p\n", &p); printf("[before:value] %p\n", p); safe_free(p); printf("\n[after:addr] %p\n", &p); printf("[after:value] %p\n", p); return 0; }
safe_free
,在该函数中我们事先对指针 ptr 进行了参数校验,并在 free 后及时将其置 NULL,目的是为了防止野指针的出现。下面让我们来运行一下:
那么疑问来了:在调用 safe_free(p)
时,我明明在函数中将指针 ptr 置为了 NULL,为什么第 20 行对 p 进行输出时,还是输出了 0x841010?
下面让我们带着疑问来学习接下来的知识,发车了~
首先,我们来对变量、地址和值他们之间的关系进行一个概述。
我们在代码中声明的每一个变量(包括指针变量):
如 int a = 10
:
该变量的地址为 &a(假设为 0x7fffffffe214)
该变量的值为 10
又如 int *p = NULL
:
如果让 p 指向 a 呢?即调用p = &a
,那么就会变成这样:
那如果声明个二级指针并指向 p 呢?即 int **pp = &p
,就变成了这样:
到这儿是不是对变量、地址和值之间的关系恍然大明白了~
Notes:
- 每个变量都有一个地址
- 地址唯一标识一块内存空间
- 指针也是变量,也有一个地址
- 指针的值用来存放变量的地址
如果我们想取出地址中的值,就需要使用星号运算符(*
),下面我们来对 *
这个运算符做个简单介绍。
星号运算符(*
)在不同的表达式中具有不同的含义:
int a = 1 * 10
;int *p = &a
,表明声明了一个指针类型的变量 p,并将其指向变量 a 的地址int b = *p
,表明取出指针 p 所指向地址的值,也就是 10当我们对变量、地址和值的关系有了一个概念后,我们回过头来看一下「一、小试牛刀」中的程序:
第 14 行声明了一个指针变量 p,并为其开辟了一块内存空间(p 的地址为 0x7ffda476f028,值为 0x841010);
第 18 行调用 safe_free
函数并传入变量 p 的值 0x841010;
在函数内,对 ptr 所指内存进行 free 并将 ptr 置为 NULL。
所以我们想要通过函数实现「free 内存并将原指针置空」的效果,一级指针是无法完成的,得使用二级指针:
#include <stdio.h> #include <stdlib.h> void safe_free(void **ptr) // 使用二级指针 { if (*ptr) { free(*ptr); *ptr = NULL; } } int main() { ... safe_free((void **)&p); // 传入指针 p 的地址 ... }
一般而言,最好用的方式还是宏定义,通过宏定义的方式将 free 操作进行封装,既可以避免对空指针的操作,也可以在 free 后计时将其置为 NULL,防止野指针的出现:
#define safe_free(ptr) \ { \ if (ptr) \ { \ free(ptr); \ ptr = NULL; \ } \ }
不同含义的*的优先级,待补充
指针和整数在 C 语言里面是两种不同含义的:
指针和整数(这里主要指 unsigned long,因为 unsigned long 的位数一般等于 CPU 可寻址的内存地址位数)本身是八竿子打不着的,但是它们之间的一个有趣联系是:如果我们只是关心这个地址的值,而不是关心通过这个地址去访问内存,这个时候,内核经常喜欢用 unsigned long 代替指针。
我们可以通过一个简单的例子来感受一下:
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef unsigned long ULONG; unsigned long func() { char *ptr = (char *)malloc(24); // 声明一个字符指针,并开辟空间 strcpy(ptr, "hello world!"); // 向新开辟的空间中写入数据 return (ULONG)ptr; // 以无符号长整型的形式返回 } int main() { char *p = (char *)func(); // 将 func 的地址强制转换为 char * puts(p); return 0; }
运行结果:
当指针和整数存在关联后,那么我们对地址的操作就更多了,如当我们在中间过程中频繁拷贝一个超大字符串时,可以考虑只拷贝这个超大字符串的 ULONG 地址,等最终需要使用这个字符串时,再将其转换为 char *
。
#include <stdio.h> #include <stdlib.h> #include <string.h> #define STRLEN_24 24 #define STRLEN_1024 1024 #define safe_free(ptr) \ { \ if (ptr) \ { \ free(ptr); \ ptr = NULL; \ } \ } typedef unsigned long ULONG; typedef struct tagStr { char *str; char addr[STRLEN_24]; // ULONG 最大值不超过 20 位 } STR_S; // 提取字符串中的 ULONG ULONG Str2ULong(char *str, int len) { ULONG ans = 0; int i; for (i = 0; i < len; i++) { ans = ans * 10 + (str[i] - '0'); } return ans; } char *func() { STR_S *pstTmp = (STR_S *)malloc(sizeof(STR_S)); memset(pstTmp, 0, sizeof(STR_S)); pstTmp->str = (char *)malloc(STRLEN_1024); // 我们暂且假设 str 中存了 1024 个数据 strcpy(pstTmp->str, "我存了 1024 个数据..."); snprintf(pstTmp->addr, STRLEN_24, "%lu", pstTmp->str); // 将 str 所指内存的地址以 ULONG 的形式保存在字符数组中 char *addr = (char *)malloc(STRLEN_24); strcpy(addr, pstTmp->addr); safe_free(pstTmp); // 释放掉 pstTmp,防止内存泄漏 return addr; // 返回保存有 pstTmp->str 内存地址的字符串 } int main() { char *addr = func(); // 接收保存有内存地址的字符串 char *str = (char *)Str2ULong(addr, strlen(addr)); // 将字符串中的内存地址解析出来 puts(str); // 输出看是否符合预期 safe_free(addr); safe_free(str); return 0; }
运行结果:
注:以下内容摘自参考资料 2 和 3。
free
函数用来释放 malloc/calloc/realloc
出来的内存空间。
操作系统在调用 malloc
函数时,会默认在 malloc
分配的物理内存前面分配一个数据结构,这个数据结构记录了这次分配内存的大小,在用户眼中这个操作是透明的。当用户需要 free 时,free
函数会把指针退回到这个结构体中,找到该内存的大小,这样就可以正确的释放内存了。
free
函数只是将指针指向的内存归还给了操作系统,并不会把指针置为 NULL,为了放置访问到被操作系统重新分配后的错误数据,所以在调用 free()
之后,通常需要手动将指针置为 NULL。
从另一个角度来看,内存这种底层资源都是由操作系统来管理的,而不是编译器,编译器只是向操作系统提出申请。所以 free
函数是没有能力去真正的 free 内存的,它只是告诉操作系统它归还了内存,然后操作系统就可以修改内存分配表,以供下次分配。
free 后的指针仍然指向原来的内存地址,即你仍然可以继续使用,但很危险,因为操作系统已经认为这块内存可以使用了,它会毫不考虑的将这块内存分配给其他程序,于是你下次使用的时候可能就已经被别的程序改掉了,这种情况就叫「野指针」,所以最好 free 后及时将指针置空。
何谓「野指针」,在这里补充一下:野指针是指程序员不能控制的指针,野指针不是 NULL 指针,而是指向「垃圾」的指针。
造成野指针的原因主要有:
指针变量没有初始化,任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的,它会乱指一气。在初始化的时候要么指向非法的地址,要么指向 NULL。
指针变量被 free 之后,没有被及时置为 NULL。free
函数只是把指针所指的内存给释放掉了,但并没有把指针本身干掉。
指针操作超越了变量的作用范围, 注意其生命周期。
malloc
函数的类型是 void *
,任何类型的指针都可以转换成 void *
,但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。CRT的内存管理模块是一个管家。
你的程序(简称“你”)是一个客人。
管家有很对水桶,可以用来装水的。
malloc 的意思就是「管家,我要 XX 个水桶」。
管家首先看一下有没有足够的水桶给你,如果没有,那么告诉你不行。
如果够,那么登记这些水桶已经被使用了,然后告诉你「拿去用吧」。
free 的意思就是说「管家,这些水桶我用完了,还你」。
至于你是不是先把水倒干净了(是不是清零)再给管家,那么是自己的事情了。
管家也不会将你归还的水桶倒倒干清(他有那么多水桶,每个归还都倒干净岂不累死了),反正其他用的时候自己会处理的啦。
free 之后将指针清零只是提醒自己,这些水桶已经不是我的了,不要再往里面放水了。
如果 free 了之后还用那个指针的话,就有可能管家已经将这些水桶给了其他人装饮料用了,而你却往里面装污水。
好的管家可能会对你的行为表示强烈的不满, kill 你(非法操作)--这是最好的结果,你知道自己错了。
一些不好的管家可能忙不过来,有时候抓到你作坏事就惩罚你,有时候却不知道去哪里了--这是你的恶梦,不知道什么时候、怎么回事,自己就被 kill 了。
不管怎么样,这种情况下很有可能有人要喝污水。
所以啊,好市民当然是归还水桶给管家后就不要再占着啦~