对指针的深入理解
一、牛刀小试
在讲解本次内容前,先来看个小栗子:
#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;
}
- 我们在代码中定义了一个更安全的 free 函数
safe_free
,在该函数中我们事先对指针 ptr 进行了参数校验,并在 free 后及时将其置 NULL,目的是为了防止野指针的出现。
下面让我们来运行一下:
那么疑问来了:在调用 safe_free(p)
时,我明明在函数中将指针 ptr 置为了 NULL,为什么第 20 行对 p 进行输出时,还是输出了 0x841010?
下面让我们带着疑问来学习接下来的知识,发车了~
二、变量、地址和值的关系
首先,我们来对变量、地址和值他们之间的关系进行一个概述。
2.1 变量、地址和值
我们在代码中声明的每一个变量(包括指针变量):
- 首先该变量要有一个地址
- 其次该变量要有值
如 int a = 10
:
-
该变量的地址为 &a(假设为 0x7fffffffe214)
-
该变量的值为 10
又如 int *p = NULL
:
- 该变量的地址为 &p(假设为 0x7fffffffe208)
- 该变量的值为 NULL
如果让 p 指向 a 呢?即调用p = &a
,那么就会变成这样:
- 变量 p 的地址保持不变,依旧是 &p(0x7fffffffe208)
- 变量 p 的值变为了 a 的地址(0x7fffffffe214)
那如果声明个二级指针并指向 p 呢?即 int **pp = &p
,就变成了这样:
- 二级指针 pp 的地址为 &pp(0x7fffffffe218)
- 二级指针 pp 的值为 p 的地址 0x7fffffffe208
到这儿是不是对变量、地址和值之间的关系恍然大明白了~
Notes:
- 每个变量都有一个地址
- 地址唯一标识一块内存空间
- 指针也是变量,也有一个地址
- 指针的值用来存放变量的地址
如果我们想取出地址中的值,就需要使用星号运算符(*
),下面我们来对 *
这个运算符做个简单介绍。
2.2 *运算符
星号运算符(*
)在不同的表达式中具有不同的含义:
- 表示乘法运算符,如
int a = 1 * 10
; - 表示指针变量,如
int *p = &a
,表明声明了一个指针类型的变量 p,并将其指向变量 a 的地址 - 表示解引用,如
int b = *p
,表明取出指针 p 所指向地址的值,也就是 10
2.3 解惑
当我们对变量、地址和值的关系有了一个概念后,我们回过头来看一下「一、小试牛刀」中的程序:
-
第 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;
}
运行结果:
四、free 函数浅谈
注:以下内容摘自参考资料 2 和 3。
4.1 free 函数介绍
free
函数用来释放 malloc/calloc/realloc
出来的内存空间。
操作系统在调用 malloc
函数时,会默认在 malloc
分配的物理内存前面分配一个数据结构,这个数据结构记录了这次分配内存的大小,在用户眼中这个操作是透明的。当用户需要 free 时,free
函数会把指针退回到这个结构体中,找到该内存的大小,这样就可以正确的释放内存了。
4.2 free 到底释放了什么
free
函数只是将指针指向的内存归还给了操作系统,并不会把指针置为 NULL,为了放置访问到被操作系统重新分配后的错误数据,所以在调用 free()
之后,通常需要手动将指针置为 NULL。
从另一个角度来看,内存这种底层资源都是由操作系统来管理的,而不是编译器,编译器只是向操作系统提出申请。所以 free
函数是没有能力去真正的 free 内存的,它只是告诉操作系统它归还了内存,然后操作系统就可以修改内存分配表,以供下次分配。
free 后的指针仍然指向原来的内存地址,即你仍然可以继续使用,但很危险,因为操作系统已经认为这块内存可以使用了,它会毫不考虑的将这块内存分配给其他程序,于是你下次使用的时候可能就已经被别的程序改掉了,这种情况就叫「野指针」,所以最好 free 后及时将指针置空。
4.3 野指针
何谓「野指针」,在这里补充一下:野指针是指程序员不能控制的指针,野指针不是 NULL 指针,而是指向「垃圾」的指针。
造成野指针的原因主要有:
-
指针变量没有初始化,任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的,它会乱指一气。在初始化的时候要么指向非法的地址,要么指向 NULL。
-
指针变量被 free 之后,没有被及时置为 NULL。
free
函数只是把指针所指的内存给释放掉了,但并没有把指针本身干掉。 -
指针操作超越了变量的作用范围, 注意其生命周期。
4.4 关于 free 与 malloc 函数使用需要注意的一些地方
- 当不需要再使用申请的内存时,记得释放,释放要及时置空,防止程序后面不小心使用了它。
- 这两个函数应该配对使用,如果 malloc 后不 free,就会造成内存泄露。什么叫内存泄漏, 简单的说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存就会越多,最终用尽全部内存,整个系统崩溃。但释放只能一次,如果释放两次及以上就会出现错误(释放空指针例外)。
- 虽然
malloc
函数的类型是void *
,任何类型的指针都可以转换成void *
,但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。
4.5 形象的比喻
CRT的内存管理模块是一个管家。
你的程序(以下简称「你」)是一个客人。
管家有很多水桶,可以用来装水的。
malloc 的意思就是「管家,我要 XX 个水桶」。
管家首先看一下有没有足够的水桶给你,如果没有,那么告诉你不行。
如果有,那么登记这些水桶已经被使用了,然后告诉你「拿去用吧」。
free 的意思就是说「管家,这些水桶我用完了,还你」。
至于你是不是先把水倒干净了(是不是清零)再给管家,那么是自己的事情了。
管家也不会将你归还的水桶倒倒干清(他有那么多水桶,每个归还都倒干净岂不累死了),反正其他用的时候自己会处理的啦。
free 之后将指针清零只是提醒自己,这些水桶已经不是我的了,不要再往里面放水了。
如果 free 了之后还用那个指针的话,就有可能管家已经将这些水桶给了其他人装饮料用了,而你却往里面装污水。
好的管家可能会对你的行为表示强烈的不满, kill 你(非法操作)--这是最好的结果,你知道自己错了。
一些不好的管家可能忙不过来,有时候抓到你作坏事就惩罚你,有时候却不知道去哪里了--这是你的恶梦,不知道什么时候、怎么回事,自己就被 kill 了。
不管怎么样,这种情况下很有可能有人要喝污水。
所以啊,好市民当然是归还水桶给管家后就不要再占着啦~