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

系统性学习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.pswp.cn/web/82009.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《高等数学》(同济大学·第7版) 第一节《映射与函数》超详细解析

集合&#xff08;Set&#xff09;—— 最基础的数学容器 定义&#xff1a; 集合是由确定的、互不相同的对象&#xff08;称为元素&#xff09;组成的整体。 表示方法&#xff1a; 列举法&#xff1a;A {1, 2, 3} 描述法&#xff1a;B {x | x > 0}&#xff08;表示所有大于…

Spring Boot整活指南:从Helo World到“真香”定律

&#x1f4cc; 一、Spring Boot的"真香"本质&#xff08;不是996的福报&#xff09; 你以为Spring Boot只是个简化配置的工具&#xff1f;Too young&#xff01;它其实是程序员的​​摸鱼加速器​​。 ​​经典场景还原​​&#xff1a; 产品经理&#xff1a;“这个…

打字练习:平台推荐

1.打字练习 . 1&#xff09;平台推荐 下面推荐两个打字练习平台 Keybr&#xff1a;https://www.keybr.com/ TypingClub&#xff1a;https://www.edclub.com/sportal/ . 2&#xff09;平台对比 特性KeybrTypingClub核心优势AI智能弱项训练结构化课程体系适合人群开发者/…

ASP.NET Core 中JWT的基本使用

文章目录 前言一、JWT与RBAC二、JWT 的作用三、RBAC 的核心思想四、使用1、配置文件 (appsettings.json)2、JWT配置模型 (Entity/JwtSettings.cs)3、服务扩展类&#xff0c;JWT配置 (Extensions/ServiceExtensions.cs)4、用户仓库接口服务5、认证服务 (Interface/IAuthService.…

(19)java在区块链中的应用

&#x1f517; Java在区块链中的应用&#xff1a;智能合约开发全攻略 TL;DR: Java在区块链领域主要通过Hyperledger Fabric、Web3j和专用JVM实现智能合约开发&#xff0c;相比Solidity具有更强的企业级支持和开发效率&#xff0c;但在执行效率和Gas消耗方面存在差异&#xff0c…

深入理解设计模式之访问者模式

深入理解设计模式之访问者模式&#xff08;Visitor Pattern&#xff09; 一、什么是访问者模式&#xff1f; 访问者模式&#xff08;Visitor Pattern&#xff09;是一种行为型设计模式。它的主要作用是将数据结构与数据操作分离&#xff0c;使得在不改变数据结构的前提下&…

div或button一些好看实用的 CSS 样式示例

1&#xff1a;现代渐变按钮 .count {width: 800px;background: linear-gradient(135deg, #72EDF2 0%, #5151E5 100%);padding: 12px 24px;border-radius: 10px;box-shadow: 0 4px 15px rgba(81, 81, 229, 0.3);color: white;font-weight: bold;border: none;cursor: pointer;t…

【基于STM32的新能源汽车智能循迹系统开发全解析】

基于STM32的新能源汽车智能循迹系统开发全解析&#xff08;附完整工程代码&#xff09; 作者声明 作者&#xff1a; 某新能源车企资深嵌入式工程师&#xff08;专家认证&#xff09; 技术方向&#xff1a; 智能驾驶底层控制 | 车规级嵌入式开发 原创声明&#xff1a; 本文已申…

HTML Day02

Day02 0. 引言1. 文本格式化1.1 HTML文本格式化标签1.2 HTML"计算机输出"标签1.3 HTML 引文&#xff0c;引用及标签定义 2. HTML链接2.1链接跳转原理&#xff08;有点乱可跳过&#xff09;2.2 HTML超链接2.3 target属性2.4 id属性2.4.1 id属性在页面内和不同页面的定…

MIT 6.S081 2020 Lab6 Copy-on-Write Fork for xv6 个人全流程

文章目录 零、写在前面一、Implement copy-on write1.1 说明1.2 实现1.2.1 延迟复制与释放1.2.2 写时复制 零、写在前面 可以阅读下 《xv6 book》 的第五章中断和设备驱动。 问题 在 xv6 中&#xff0c;fork() 系统调用会将父进程的整个用户空间内存复制到子进程中。**如果父…

xhr、fetch和axios

XMLHttpRequest (XHR) XMLHttpRequest 是最早用于在浏览器中进行异步网络请求的 API。它允许网页在不刷新整个页面的情况下与服务器交换数据。 // 创建 XHR 对象 const xhr new XMLHttpRequest();// 初始化请求 xhr.open(GET, https://api.example.com/data, true);// 设置请…

电脑驱动程序更新工具, 3DP Chip 中文绿色版,一键更新驱动!

介绍 3DP Chip 是一款免费的驱动程序更新工具&#xff0c;可以帮助用户快速、方便地识别和更新计算机硬件驱动程序。 驱动程序更新工具下载 https://pan.quark.cn/s/98895d47f57c 软件截图 软件特点 简单易用&#xff1a;用户界面简洁明了&#xff0c;操作方便&#xff0c;…

机器学习与深度学习06-决策树02

目录 前文回顾5.决策树中的熵和信息增益6.什么是基尼不纯度7.决策树与回归问题8.随机森林是什么 前文回顾 上一篇文章地址&#xff1a;链接 5.决策树中的熵和信息增益 熵和信息增益是在决策树中用于特征选择的重要概念&#xff0c;它们帮助选择最佳特征进行划分。 熵&#…

【Kotlin】数字字符串数组集合

【Kotlin】简介&变量&类&接口 【Kotlin】数字&字符串&数组&集合 文章目录 Kotlin_数字&字符串&数组&集合数字字面常量显式转换数值类型转换背后发生了什么 运算字符串字符串模板字符串判等修饰符数组集合通过序列提高效率惰性求值序列的操…

oscp练习PG Monster靶机复现

端口扫描 nmap -A -p- -T4 -Pn 192.168.134.180 PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.41 ((Win64) OpenSSL/1.1.1c PHP/7.3.10) |_http-server-header: Apache/2.4.41 (Win64) OpenSSL/1.1.1c PHP/7.3.10 | http-methods:…

近期知识库开发过程中遇到的一些问题

我们正在使用Rust开发一个知识库系统&#xff0c;遇到了一些问题&#xff0c;在此记录备忘。 错误&#xff1a;Unable to make method calls because underlying connection is closed 场景&#xff1a;在docker中调用headless_chrome时出错 原因&#xff1a;为减小镜像大小&am…

Ubuntu 22.04 系统下 Docker 安装与配置全指南

Ubuntu 22.04 系统下 Docker 安装与配置全指南 一、前言 Docker 作为现代开发中不可或缺的容器化工具&#xff0c;能极大提升应用部署和环境管理的效率。本文将详细介绍在 Ubuntu 22.04 系统上安装与配置 Docker 的完整流程&#xff0c;包括环境准备、安装步骤、权限配置及镜…

C#获取磁盘容量:代码实现与应用场景解析

C#获取磁盘容量&#xff1a;代码实现与应用场景解析 在软件开发过程中&#xff0c;尤其是涉及文件存储、数据备份等功能时&#xff0c;获取磁盘容量信息是常见的需求。通过获取磁盘的可用空间和总大小&#xff0c;程序可以更好地进行资源管理、预警提示等操作。在 C# 语言中&a…

2025年- H56-Lc164--200.岛屿数量(图论,深搜)--Java版

1.题目描述 2.思路 &#xff08;1&#xff09;主函数&#xff0c;存储图结构 &#xff08;2&#xff09;主函数&#xff0c;visit数组表示已访问过的元素 &#xff08;3&#xff09;辅助函数&#xff0c;用递归&#xff08;深搜&#xff09;&#xff0c;遍历以已访问过的元素&…

详细到用手撕transformer下半部分

之前我们讨论了如何实现 Transformer 的核心多头注意力机制&#xff0c;那么这期我们来完整地实现整个 Transformer 的编码器和解码器。 Transformer 架构最初由 Vaswani 等人在 2017 年的论文《Attention Is All You Need》中提出&#xff0c;专为序列到序列&#xff08;seq2s…