一、牛刀小试

在讲解本次内容前,先来看个小栗子:

#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,目的是为了防止野指针的出现。

下面让我们来运行一下:

对指针的深入理解-LMLPHP

那么疑问来了:在调用 safe_free(p) 时,我明明在函数中将指针 ptr 置为了 NULL,为什么第 20 行对 p 进行输出时,还是输出了 0x841010?

下面让我们带着疑问来学习接下来的知识,发车了~

二、变量、地址和值的关系

首先,我们来对变量、地址和值他们之间的关系进行一个概述。

2.1 变量、地址和值

我们在代码中声明的每一个变量(包括指针变量):

  1. 首先该变量要有一个地址
  2. 其次该变量要有值

int a = 10

  • 该变量的地址为 &a(假设为 0x7fffffffe214)

  • 该变量的值为 10

    对指针的深入理解-LMLPHP

又如 int *p = NULL

  • 该变量的地址为 &p(假设为 0x7fffffffe208)
  • 该变量的值为 NULL

对指针的深入理解-LMLPHP

如果让 p 指向 a 呢?即调用p = &a,那么就会变成这样:

对指针的深入理解-LMLPHP

  • 变量 p 的地址保持不变,依旧是 &p(0x7fffffffe208)
  • 变量 p 的值变为了 a 的地址(0x7fffffffe214)

那如果声明个二级指针并指向 p 呢?即 int **pp = &p,就变成了这样:

对指针的深入理解-LMLPHP

  • 二级指针 pp 的地址为 &pp(0x7fffffffe218)
  • 二级指针 pp 的值为 p 的地址 0x7fffffffe208

到这儿是不是对变量、地址和值之间的关系恍然大明白了~

如果我们想取出地址中的值,就需要使用星号运算符(*),下面我们来对 * 这个运算符做个简单介绍。

2.2 *运算符

星号运算符(*)在不同的表达式中具有不同的含义:

  1. 表示乘法运算符,如 int a = 1 * 10
  2. 表示指针变量,如 int *p = &a,表明声明了一个指针类型的变量 p,并将其指向变量 a 的地址
  3. 表示解引用,如 int b = *p,表明取出指针 p 所指向地址的值,也就是 10

2.3 解惑

当我们对变量、地址和值的关系有了一个概念后,我们回过头来看一下「一、小试牛刀」中的程序:

对指针的深入理解-LMLPHP

  1. 第 14 行声明了一个指针变量 p,并为其开辟了一块内存空间(p 的地址为 0x7ffda476f028,值为 0x841010);

  2. 第 18 行调用 safe_free 函数并传入变量 p 的值 0x841010;

  3. 在函数内,对 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;
}

运行结果:

对指针的深入理解-LMLPHP

当指针和整数存在关联后,那么我们对地址的操作就更多了,如当我们在中间过程中频繁拷贝一个超大字符串时,可以考虑只拷贝这个超大字符串的 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;
}

运行结果:

对指针的深入理解-LMLPHP

四、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 指针,而是指向「垃圾」的指针。

造成野指针的原因主要有:

  1. 指针变量没有初始化,任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的,它会乱指一气。在初始化的时候要么指向非法的地址,要么指向 NULL。

  2. 指针变量被 free 之后,没有被及时置为 NULL。free 函数只是把指针所指的内存给释放掉了,但并没有把指针本身干掉。

  3. 指针操作超越了变量的作用范围, 注意其生命周期。

4.4 关于 free 与 malloc 函数使用需要注意的一些地方

  1. 当不需要再使用申请的内存时,记得释放,释放要及时置空,防止程序后面不小心使用了它。
  2. 这两个函数应该配对使用,如果 malloc 后不 free,就会造成内存泄露。什么叫内存泄漏, 简单的说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存就会越多,最终用尽全部内存,整个系统崩溃。但释放只能一次,如果释放两次及以上就会出现错误(释放空指针例外)。
  3. 虽然 malloc 函数的类型是 void *,任何类型的指针都可以转换成 void *,但是最好还是在前面进行强制类型转换,因为这样可以躲过一些编译器的检查。

4.5 形象的比喻

CRT的内存管理模块是一个管家。
你的程序(以下简称「你」)是一个客人。
管家有很多水桶,可以用来装水的。

malloc 的意思就是「管家,我要 XX 个水桶」。
管家首先看一下有没有足够的水桶给你,如果没有,那么告诉你不行。
如果有,那么登记这些水桶已经被使用了,然后告诉你「拿去用吧」。

free 的意思就是说「管家,这些水桶我用完了,还你」。
至于你是不是先把水倒干净了(是不是清零)再给管家,那么是自己的事情了。
管家也不会将你归还的水桶倒倒干清(他有那么多水桶,每个归还都倒干净岂不累死了),反正其他用的时候自己会处理的啦。

free 之后将指针清零只是提醒自己,这些水桶已经不是我的了,不要再往里面放水了。

如果 free 了之后还用那个指针的话,就有可能管家已经将这些水桶给了其他人装饮料用了,而你却往里面装污水。
好的管家可能会对你的行为表示强烈的不满, kill 你(非法操作)--这是最好的结果,你知道自己错了。
一些不好的管家可能忙不过来,有时候抓到你作坏事就惩罚你,有时候却不知道去哪里了--这是你的恶梦,不知道什么时候、怎么回事,自己就被 kill 了。

不管怎么样,这种情况下很有可能有人要喝污水。
所以啊,好市民当然是归还水桶给管家后就不要再占着啦~

参考资料

05-21 10:20