对指针的深入理解

这篇具有很好参考价值的文章主要介绍了对指针的深入理解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、牛刀小试

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

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

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

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

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

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

不管怎么样,这种情况下很有可能有人要喝污水。
所以啊,好市民当然是归还水桶给管家后就不要再占着啦~文章来源地址https://www.toymoban.com/news/detail-451969.html

参考资料

  • C语言指针进阶(上)_长月.的博客-CSDN博客
  • free函数_纵使风吹的博客-CSDN博客
  • free()函数详解_free函数_白河、愁的博客-CSDN博客
  • 为何Linux内核常常用unsignedlong来代替指针 - 知乎 (zhihu.com)

到了这里,关于对指针的深入理解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 工欲善其事必先利其器--CMake牛刀小试

    这里假设用户已经安装好MinGW编译套件!并配置好环境变量!具体怎么下载和配置网上教程非常多,这里贴上一个链接:不仅教你安装MinGW还教你安装VScode配置 1、学习c plus plus编码为什么要学习CMake? 在Windows下使用集成开发环境(IDE)开发项目时,一般会使用IDE自带的构建工

    2024年02月06日
    浏览(42)
  • 爬虫之牛刀小试(十):爬取某宝手机商品的销量,价格和店铺

    首先淘宝需要登录,这一点如果用selenium如何解决,只能手动登录?如果不用selenium,用cookies登录也可。但是验证码又是一个问题,现在的验证码五花八门,难以处理。 我们回到正题,假设你已经登录上淘宝了,接着我们需要找到输入框和搜索按钮,输入“手机”,点击搜索

    2024年04月10日
    浏览(62)
  • C语言之指针篇【超详细讲解,带你层层深入理解指针】

    目录 一、关于指针 二、指针类型 1、整型指针的访问权限说明: 2、字符指针的访问权限说明: 3、指针的类型决定向前或向后一步走了多大距离 三、野指针相关知识 1、野指针的成因 ①指针未初始化 ②指针的越界访问 ③指针所指向的空间释放了 2、如何规避野指针 ①指针

    2024年02月02日
    浏览(39)
  • Mapreduce小试牛刀(1)

    1.与hdfs一样,mapreduce基于hadoop框架,所以我们首先要启动hadoop服务器 --------------------------------------------------------------------------------------------------------------------------------- 2.修改hadoop-env.sh位置JAVA_HOME配置,在JAVA_HOME前面加上export,重启主虚拟机,最好也把另外两个节点同位置的

    2024年02月04日
    浏览(41)
  • 网页学习-小试牛刀

    分为三大部分: HTML 、 CSS 和 JavaScript 。 HTML(Hyper Text Markup Language,即超文本标记语言),网页骨架。 CSS(Cascading Style Sheets,层叠样式表),使页面变得美观、优雅,网页皮肤。 JavaScript(简称JS,是一种脚本语言),实现实时、动态、交互的页面功能,网页肌肉。 学习目的

    2023年04月22日
    浏览(64)
  • 运维Shell脚本小试牛刀(二)

    运维Shell脚本小试牛刀(一) 运维Shell脚本小试牛刀(二) 运维Shell脚本小试牛刀(三)::$(cd $(dirname $0); pwd)命令详解 [root@www shelldic]# cat checkpass.sh  #!/bin/bash - #================================================================================================================== # # #                          

    2024年02月10日
    浏览(40)
  • 快速上手kettle(二)小试牛刀

    目录 一 、前言 二 、两个小目标 三、 kettle核心概念介绍 3.1 转换 3.1.1 步骤(Step) 3.1.2 跳(Hop) 3.1.3 元素据 3.1.4 数据类型 3.1.5 并发执行 3.2 作业 四、实践操作 4.1 案例1 将csv文件转换成excel文件 4.1.1 在kettle中新建一个转换 4.1.2选择输入控件并设置 4.1.3 选择输出控件并设置 4.

    2024年02月06日
    浏览(46)
  • 运维Shell脚本小试牛刀(一)

    运维Shell脚本小试牛刀(一) 运维Shell脚本小试牛刀(二) 运维Shell脚本小试牛刀(三)::$(cd $(dirname $0); pwd)命令详解 运维Shell脚本小试牛刀(四): 多层嵌套if...elif...elif....else fi_蜗牛杨哥的博客-CSDN博客 Cenos7安装小火车程序动画 运维Shell脚本小试牛刀(五):until循环 运维Shell脚本小试牛刀

    2024年02月11日
    浏览(43)
  • 【LED子系统】八、小试牛刀

    个人主页:董哥聊技术 我是董哥,高级嵌入式软件开发工程师,从事嵌入式Linux驱动开发和系统开发,曾就职于世界500强公司! 创作理念:专注分享高质量嵌入式文章,让大家读有所得!

    2024年02月06日
    浏览(42)
  • 手动开发-实现SpringMVC底层机制--小试牛刀

    在这里说的底层机制的实现主要是指:前端控制器、Controller、Service注入容器、对象自动装配、控制器方法获取参数、视图解析、返回json数据。 前端控制器就是核心控制器。在这里我们可以设计一个Servlet来充当核心控制器: LingDispatcherServlet.java .这个控制器的作用主要是接收

    2024年02月08日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包