系统性学习C语言-第十二讲-深入理解指针(2)

article/2025/6/18 23:23:57

系统性学习C语言-第十二讲-深入理解指针(2)

  • 1. ` const ` 修饰指针
    • 1.1 ` const ` 修饰变量
    • 1.2 ` const ` 修饰指针变量
  • 2. 野指针
    • 2.1 野指针成因
    • 2.2 如何规避野指针
      • 2.2.1 指针初始化
      • 2.2.2 小心指针越界
      • 2.2.3 指针变量不再使用时,及时置 ` NULL ` ,指针使用之前检查有效性
      • 2.2.4 避免返回局部变量的地址
  • 3. assert 断言
  • 4. 指针的使用和传址调用
    • 4.2 传值调用和传址调用

1. const 修饰指针

1.1 const 修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。

但是如果我们希望⼀个变量加上⼀些限制,不能被修改,怎么做呢?这就是 const 的作用。

#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的const int n = 0;n = 20;//n是不能被修改的return 0;
}

上述代码中 n 是不能被修改的,其实 n 本质是变量,只不过被 const 修饰后,在语法上加了限制,

只要我们在代码中对 n 进行修改,就不符合语法规则,就报错,致使没法直接修改 n

在这里插入图片描述

#include <stdio.h>
int main()
{const int n = 0;printf("n = %d\n", n);int* p = &n;*p = 20;printf("n = %d\n", n);return 0;
}

输出结果:
在这里插入图片描述
我们可以看到变量确实被修改了,但是我们还是要思考⼀下,为什么 n 要被 const 修饰呢?

就是为了不能被修改,如果 p 拿到 n 的地址就能修改 n ,这样就打破了 const 的限制,

这是不合理的,所以应该让 p 拿到 n 的地址也不能修改 n ,那接下来怎么做呢?

1.2 const 修饰指针变量

⼀般来讲 const 修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不⼀样的。

int * p;//没有const修饰?
int const * p;//const 放在*的左边做修饰
int * const p;//const 放在*的右边做修饰

我们看下面代码,来分析具体分析⼀下:

代码1 - 测试无 const 修饰的情况

#include <stdio.h>
//代码1 - 测试⽆const修饰的情况
void test1()
{int n = 10;int m = 20;int* p = &n;	*p = 20;//ok?p = &m; //ok?
}

在这里插入图片描述
通过观察可以看到编译是可以通过的,说明代码的操作是没有问题的。

代码2 - 测试 const 放在 * 的左边情况

//代码2 - 测试const放在*的左边情况
void test2()
{int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}

在这里插入图片描述
通过编译结果我们可以得出,当 const 被放在 * 左边时,我们无法对地址解引用进行更改,

编译器会产生报错。

代码3 - 测试 const 放在 * 的右边情况

//代码3 - 测试const放在*的右边情况
void test3()
{int n = 10;int m = 20;int * const p = &n;*p = 20; //ok?p = &m; //ok?
}

在这里插入图片描述
通过编译结果我们可以分析出,在 const 放在 * 的右边,我们可以通过地址的解引用来改变变量,

但是我们不能对地址进行更改。

代码4 - 测试 * 的左右两边都有 const 的情况

//代码4 - 测试*的左右两边都有const
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20; //ok?p = &m; //ok?
}

在这里插入图片描述
通过编译的结果我们可以分析出,在 * 的左右两边都有 const 的情况下,

我们即无法对地址解引用来更改变量,也无法对地址的值进行改变。

结论:const修饰指针变量的时候

  • const 如果放在 * 的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
    但是指针变量本身的内容可变。

  • const 如果放在 * 的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指
    向的内容,可以通过指针改变。

2. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

2.1 野指针成因

1. 指针未初始化

#include <stdio.h>
int main()
{int *p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;
}

在如图所示的代码中,指针 p 并未进行初始化,它的地址所指向的空间是未知的,为野指针。

2. 指针越界访问

#include <stdio.h>
int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i = 0; i <= 11; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}return 0;
}

在如图所示的代码中,指针 p 指向的范围超出了数组 arr 的范围,它的地址所指向的空间是未知的,为野指针。

3. 指针指向的空间释放

#include <stdio.h>
int* test()
{int n = 100;return &n;
}int main()
{int*p = test();printf("%d\n", *p);return 0;
}

在如图的代码中,函数 test 中定义的变量 n 为局部变量,在函数 test 结束后就会销毁,但函数 test 返回的是变量 n 的地址,

在变量被销毁后,这片地址的区域就是未知的,不再有意义,函数 test 返回的无意义的地址存储在了变量 p 中,

此时变量 p 变为了野指针。

2.2 如何规避野指针

2.2.1 指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值 NULL

NULL 是 C语言 中定义的⼀个标识符常量,值是 0 ,0 也是地址,这个地址是无法使用的,读写该地址会报错。

定义 NULL 中的文件源码

#ifdef __cplusplus#define NULL 0
#else#define NULL ((void *)0)
#endif

初始化如下:

#include <stdio.h>int main()
{int num = 10;int*p1 = &num;int*p2 = NULL;return 0;
}

2.2.2 小心指针越界

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

例如:

#include<stdio.h>
int main()
{int arr[10] = { 0 };int* p = &arr[12];
}

这里我们只给数组 arr 申请了十个空间用于存储变量,即数组 arr 的最大下标为 9 ,但时我们的 p 指针却超出范围,

存储着下标 12 处的地址,这是代码发生了指针越界,产生了野指针, p 指针现在所指向的空间时未知的。

2.2.3 指针变量不再使用时,及时置 NULL ,指针使用之前检查有效性

当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,

我们可以把该指针置为 NULL 。因为约定俗成的⼀个规则就是:只要是 NULL 指针就不去访问,

同时使用指针之前可以判断指针是否为 NULL

我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一颗树把野狗拴起来,就相对安全了,

给指针变量及时赋值为 NULL ,其实就类似把野狗栓起来,就是把野指针暂时管理起来。

不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为 NULL

看看是不是被拴起来起来的野狗,如果不是我们再去使用。

int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int i = 0;for(i=0; i<10; i++){*(p++) = i;}//此时p已经越界了,可以把p置为NULLp = NULL;//下次使⽤的时候,判断p不为NULL的时候再使⽤//...p = &arr[0];//重新让p获得地址if(p != NULL) //判断{//...}return 0;}

2.2.4 避免返回局部变量的地址

如造成野指针的第 3 个例子,不要返回局部变量的地址。

3. assert 断言

assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断⾔”。

assert(p != NULL);

上面代码在程序运行到这一行语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运行,否则就会终止运⾏,

并且给出报错信息提示。

assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值非零),assert() 不会产⽣任何作用,程序继续运行。

如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写入⼀条错误信息,显示没有通过的表达式,

以及包含这个表达式的文件名和行号。

assert() 的使用程序员是非常友好的,使用 assert() 有几个好处:它不仅能自动标识文件和出问题的行号,

还有⼀种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断言,

就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG

#define NDEBUG
#include <assert.h>

然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。

如果程序又出现问题,可以移除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启用了 assert() 语句。

assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。

⼀般我们可以在 Debug 中使用,在 Release 版本中选择禁用 assert 就⾏,在 VS 这样的集成开发环境中,在 Release 版本中,

直接就是优化掉了。这样在 debug 版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。

4. 指针的使用和传址调用

库函数 strlen 的功能是求字符串长度,统计的是字符串中 \0 之前的字符的个数。

函数原型如下:

size_t strlen ( const char * str );

参数 str 接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回长度。

如果要模拟实现只要从起始地址开始向后逐个字符的遍历,只要不是 \0 字符,计数器就 + 1 ,这样直到 \0 就停止。

参考代码如下:

int my_strlen(const char * str)
{int count = 0;assert(str);  //防止传入的指针为空while(*str)   //当 *str 不为 /0 进入循环{count++;  //计数器 + 1str++;    //将字符变更为下一个字符}return count; //返回计数器的数值
}int main()
{int len = my_strlen("abcdef");printf("%d\n", len);return 0;
}

4.2 传值调用和传址调用

学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?

例如:写⼀个函数,交换两个整型变量的值

⼀番思考后,我们可能写出这样的代码:

#include <stdio.h>void Swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}

当我们运行代码,结果如下:

在这里插入图片描述
我们发现其实没产生交换的效果,这是为什么呢?

尝试调试,解决问题。

在这里插入图片描述
我们发现在 main 函数内部,创建了 aba 的地址是 0x00cffdd0b 的地址是 0x00cffdc4

在调⽤ Swap1 函数时,将 ab 传递给了Swap1函数,在 Swap1 函数内部创建了形参 xy 接收 ab 的值,

但是 x 的地址是 0x00cffcecy 的地址是 0x00cffcf0xy 确实接收到了 ab 的值,

不过 x 的地址和 a 的地址不⼀样,y 的地址和 b 的地址不⼀样,相当于 xy 是独⽴的空间,

那在 Swap1 函数内部交换 xy 的值,自然不会影响 ab ,当 Swap1 函数调⽤结束后回到 main 函数,

ab 的没法交换。

Swap1 函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

所以 Swap1 是失败的了。

那怎么办呢?

现在要解决的就是当调用 Swap 函数的时候,Swap 函数内部操作的就是 main 函数中的 ab ,直接将 ab的值交换了。

那么就可以使用指针了,在 main 函数中将 ab 的地址传递给 Swap 函数,

Swap 函数里边通过地址间接的操作 main 函数中的a和b,并达到交换的效果就好了。

#include <stdio.h>void Swap2(int*px, int*py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap2(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}

将程序改进后,我们直接将变量 ab 的地址传入进函数中,这次通过地址对变量进行更改,就不会再出现错误,

通过地址操作的空间与原变量是绑定的,不再是原变量的拷贝,在改变地址所指向的变量时,我们成功对原变量进行了更改。

看输出结果:

在这里插入图片描述
我们可以看到实现成 Swap2 的方式,顺利完成了任务,这里调用 Swap2 函数的时候是将变量的地址传递给了函数,

这种函数调用方式叫:传址调用。

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;

所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。

如果函数内部要修改主调函数中的变量的值,就需要传址调用。

到此,第十二讲 - 深入理解指针(2)部分的内容到此结束
如对文章有更好的意见与建议,一定要告知作者,读者的反馈对于我十分重要,希望读者们勤勉励学,精益求精,
我们下篇文章再见👋。


http://www.hkcw.cn/article/MwwwNgjgkv.shtml

相关文章

Linux安装redis

Linux redis路径 https://download.redis.io/releases/解压安装Redis 解压 tar -zvxf redis-6.0.5.tar.gz 由于redis是c语言编写的&#xff0c;所以我们需要先安装gcc&#xff0c;安装的命令如下&#xff1a; yum install gcc-c安装 输入命令&#xff1a; make PREFIX/usr/…

NumPy 数组计算:广播机制

文章目录 NumPy 数组计算&#xff1a;广播机制一、广播机制简介二、广播机制的规则1. 广播机制示例 12. 广播机制示例 23. 广播机制示例 3 三、广播机制实战1. 数组的中心化2. 绘制二维函数 NumPy 数组计算&#xff1a;广播机制 我们在NumPy数组的计算&#xff1a;通用函数中看…

Codesys FOR 循环之轴控

关于多伺服的轴控,不管怎么写都会很复杂,要么编程的时候代码行数多,要么是后期检查时非常麻烦,目前还未找到一个两全其美的方法,今天介绍的是通过FOR循环的轴控,就属于后者,代码行数较少,控制的轴数也没有限制,不需要一个轴一个的复制FB块,但是想在调试的时候实时查看…

欧冠决赛杜埃梅开二度 新星闪耀赛场

北京时间6月1日,本赛季的欧冠决赛中,19岁的杜埃表现出色,梅开二度并送出一次助攻,帮助巴黎圣日耳曼在比赛进行到73分钟时以4-0领先国际米兰。据统计,杜埃成为自1964年国际米兰名宿桑德罗-马佐拉以来,首位在欧冠决赛中完成梅开二度并且送出助攻的球员。本赛季,杜埃代表巴…

使用VSCode在WSL和Docker中开发

通过WSL&#xff0c;开发人员可以安装 Linux 发行版&#xff08;例如 Ubuntu、OpenSUSE、Kali、Debian、Arch Linux 等&#xff09;&#xff0c;并直接在 Windows 上使用 Linux 应用程序、实用程序和 Bash 命令行工具&#xff0c;不用进行任何修改&#xff0c;也无需使用传统虚…

《汇编语言》第12章 内中断——实验12 编写0号中断的处理程序

编写0号中断的处理程序&#xff0c;使得在除法溢出发生时&#xff0c;在屏幕中间显示字符串"divide error&#xff01;"&#xff0c;然后返回到DOS。 要求&#xff1a;仔细跟踪调试&#xff0c;在理解整个过程之前&#xff0c;不要进行后面课程的学习。 ;sy12.asm …

黑马k8s(十八)

一&#xff1a;安全认证 1.安全认证-概述 2.安全认证-认证方式 认证管理 3.安全认证-授权管理 因为没有授予角色deployment的权限&#xff0c;所以不能查看 4.安全认证-准入控制 二&#xff1a;DashBoard 之前在kubernetes中完成的所有操作都是通过命令行工具kubectl完成的…

python:PyMOL 使用教程 及实用示例

安装参阅&#xff1a;开源版PyMol安装保姆级教程 百度网盘下载 提取码&#xff1a;csub 简介: PyMOL是一个Python增强的分子图形工具。它擅长蛋白质、小分子、密度、表面和轨迹的3D可视化。它还包括分子编辑、射线追踪和动画。 PyMol的名字来源于“Py”表示该软件基于Python这…

第十二节:第三部分:集合框架:List系列集合:特点、方法、遍历方式、ArrayList集合的底层原理

List系列集合特点 List集合的特有方法 List集合支持的遍历方式 ArrayList集合的底层原理 ArrayList集合适合的应用场景 代码&#xff1a;List系列集合遍历方式 package com.itheima.day19_Collection_List;import java.util.ArrayList; import java.util.Iterator; import jav…

ZC-OFDM雷达通信一体化减小PAPR——SC-FDMA技术

文章目录 前言一、SC-FDMA 技术1、简介2、原理 二、MATLAB 仿真1、核心代码2、仿真结果 三、资源自取 前言 在 OFDM 雷达通信一体化系统中&#xff0c;信号的传输由多个子载波协同完成&#xff0c;多个载波信号相互叠加形成最终的发射信号。此叠加过程可能导致信号峰值显著高于…

【算法】贪心算法

一、贪心算法基本思想 贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从 整体最优考虑&#xff0c;它所作出的选择只是在某种意义上的局部最优选择。 我们希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不 能对所有问题都得到整体最优解&#xff08;O…

通义灵码深度实战测评:从零构建智能家居控制中枢,体验AI编程新范式

一、项目背景&#xff1a;零基础挑战全栈智能家居系统 目标&#xff1a;开发具备设备控制、环境感知、用户习惯学习的智能家居控制中枢&#xff08;PythonFlaskMQTTReact&#xff09; 挑战点&#xff1a; 需集成硬件通信(MQTT)、Web服务(Flask)、前端交互(React) 调用天气AP…

C 语言开发中常见的开发环境

目录 1.Dev-C 2.Visual Studio Code 3.虚拟机 Linux 环境 4.嵌入式 MCU 专用开发环境 1.Dev-C 使用集成的 C/C 开发环境&#xff08;注&#xff1a;较老旧方案&#xff0c;适合基础学习&#xff09; 2.Visual Studio Code 结合 C/C 扩展 GCC/MinGW 编译器&#xff0c;配置…

关于用Cloudflare的Zero Trust实现绕过备案访问国内站点说明

cloudflare 是一个可免费的CDN&#xff0c;CDN&#xff08;Content Delivery Network&#xff0c;内容分发网络&#xff09;加速国内网站&#xff0c;通常是已备案的。Zero Trust类似FRP&#xff0c;可以将请求转发到目标服务器。在使用Zero Trust绕过备案访问国内网站需要&…

火语言UI组件--播放器

【组件功能】&#xff1a;引用网络播放地址的视频播放器。 样式预览 设置 基础设置 属性名称属性释义输入值类型网络资源地址(url)播放视频的网络地址字符串类型音量(volume)播放视频的音量&#xff08;参考值&#xff1a;0 ~ 1)浮点型(Float)自动播放(autoplay)视频是否自动…

Linux基本指令

文章目录 1.ls指令1.1 ls -l指令1.2 ls-a指令1.2.1文件的类型1.2.2隐藏文件1.2.3[.]\[..]的含义 1.3 ls -d指令1.4 ls-F指令1.5ls指令子功能大全 2.pwd指令2.1路径分割符2.2/根目录 3.mkdir指令3.1 mkdir-p3.2mkdir常用功能 4.cd指令4.1多叉树概念4.2绝对/相对路径4.2.1绝对路径…

桥 接 模 式

在玩游戏的时候我们常常会遇到这样的机制&#xff1a;我们可以随意选择不同的角色&#xff0c;搭配不同的武器。这时只有一个抽象上下文的策略模式就不那么适用了&#xff0c;因为一旦我们使用继承的方式&#xff0c;武器和角色总有一方会变得难以扩展。这时&#xff0c;我们就…

leetcode3128. 直角三角形-medium

1 题目&#xff1a;直角三角形 官方标定难度&#xff1a;中 给你一个二维 boolean 矩阵 grid 。 如果 grid 的 3 个元素的集合中&#xff0c;一个元素与另一个元素在 同一行&#xff0c;并且与第三个元素在 同一列&#xff0c;则该集合是一个 直角三角形。3 个元素 不必 彼此…

数据资产入表的数据质量评估

在数据资产入表过程中&#xff0c;对数据质量进行全面、系统的评估至关重要。下面将从数据完整性评估、数据准确性校验、数据一致性检查、数据时效性分析、数据可信度评价、数据规范性审核、数据安全性检测和数据可用性考察等方面&#xff0c;对数据资产入表的数据质量进行详细…

精简多功能办公软件

今天向大家推荐一款功能强大的实用软件。 软件介绍 这款名为"一个MH"的软件界面简洁明了&#xff0c;虽然体积小巧&#xff0c;却集成了多种实用功能&#xff0c;相当于整合了多个软件的功能于一身。软件将各类工具进行了系统分类&#xff0c;并配备了便捷的搜索功…