• 对指针的深入理解


    一、牛刀小试

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

    #include 
    #include 
    
    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 变量、地址和值

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

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

    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:

    1. 每个变量都有一个地址
    2. 地址唯一标识一块内存空间
    3. 指针也是变量,也有一个地址
    4. 指针的值用来存放变量的地址

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

    2.2 *运算符

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

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

    2.3 解惑

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

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

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

    3. 在函数内,对 ptr 所指内存进行 free 并将 ptr 置为 NULL。

    所以我们想要通过函数实现「free 内存并将原指针置空」的效果,一级指针是无法完成的,得使用二级指针:

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

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

    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 了。

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

    参考资料


    __EOF__

  • 本文作者: MElephant
  • 本文链接: https://www.cnblogs.com/hyacinthLJP/p/17417959.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    CF1004F Sonya and Bitwise OR(线段树平衡复杂度+or 前缀性质)
    (大数据应用考察)全国水资源分析可视化
    采用医疗AI、自然语言处理技术的java智能导诊导医系统源码
    java毕业生设计养老院信息管理计算机源码+系统+mysql+调试部署+lw
    Tempo数据分析平台,助力企业高效完成数据预处理工作
    Tomcat 集群介绍
    C++菱形继承问题
    神经网络中的线性和非线性---学习笔记
    Vue与TypeScript的配合:如何在Vue项目中使用TypeScript,利用静态类型提高代码的可维护性
    【腾讯云原生降本增效大讲堂】Kubernetes资源拓扑感知调度
  • 原文地址:https://www.cnblogs.com/hyacinthLJP/p/17417959.html