C语言的灵魂——指针(2)

前言:上期我们介绍了如何理解地址,内存,以及指针的一些基础知识和运算;这期我们来介绍一下const修饰指针,野指针,assert断言,指针的传址调用。
上一篇指针(1)

文章目录

  • 一,const修饰指针
    • 1,const修饰变量
    • 2,const修饰指针变量
      • 第一种const在*左边
      • 第二种const在*右边
      • 第三种const在*左和右边
  • 二,野指针
    • 1,野指针的成因
    • 2,如何规避野指针
  • 三,assert断言
  • 四,指针的使用和传址调用
    • 1,传值调用
    • 2,传址调用
  • 五,strlen的模拟实现

一,const修饰指针

1,const修饰变量

我们先从const修饰变量说起,在学函数的时候我们知道被const修饰后的变量在C语言中变成了常变量,它是一个变量但具有常量属性(不变的属性)是不能被修改的。

#include<stdio.h>
int main()
{int n=0;n=100;//没有加上const之前n可以修改const int m=0;m=100;//加上了const之后m就不能被修改了return 0;
}

在这里插入图片描述
我们可以看到被const修饰后的m修改时会报错,那 我们怎么验证它是一个变量呢?还记得在数组篇说过:数组定义变量的大小时必须是一个常量而不能是一个变量吗?这样我们就可以用数组来检验,如果数组报错说明是变量,不报错说明是常量。

#include<stdio.h>
int main()
{const int n=10;int arr[n]={0};return 0;
}

在这里插入图片描述
我们们看到编译器报错就说明被const修饰的n就是一个变量。那么既然const修饰变量会导致变量不能被修改,那么const修饰指针会有什么样的结果呢?接下来就来看看const修饰指针会有什么样的效果。

延用上面的例子分析一下为什么m不能被修改?假如想修改该怎么做呢?

#include<stdio.h>
int main()
{int n=0;n=100;//没有加上const之前n可以修改const int m=0;m=100;//加上了const之后m就不能被修改了return 0;
}

上述代码中m是不能被修改的,其实m本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对m就⾏修改,就不符合语法规则,就报错致使没法直接修改n。

我们说指针的好处就是可以间接访问内存,但如果我们绕过m取得m的地址然后再去修改m就可以做到了,虽然这是在打破语法规则但确实能够达到我们的目的。

#include<stdio.h>
int main()
{const int m=0;m=100;//加上了const之后m就不能被修改了int *p=&m;*p=100;printf("%d\n",*p);return 0;
}

在这里插入图片描述
显然m被修改了,但是思考一下我们加const的用意是什么?我们其实是要固定m的值使他不能被修改,但是我们却有方法让他修改,这就打破了const的限制所以这与我们的初衷是违背的。这是一个漏洞要避免这个漏洞要怎么做呢?
答案是用const来修饰指针,使指针变量p拿不到m的地址从而无法间接修改m的值。

2,const修饰指针变量

首先const修饰指针变量有三种情况:

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

第一种const在*左边

我们先来看一段代码:

#include<stdio.h>
int main()
{int n=0;int *p1=&n;*p1=100;printf("%d\n",*p1);//没加const 打印的结果是100int m=0;const int *p2=&m;*p2=100printf("%d\n",*p2);//加了const *p2这行代码报错return 0;
}

在这里插入图片描述

由此我们可以知道在 *号 左边加上const是限制解引用这个操作,即限制修改指针变量p所指向的变量的内容(即 *p ),但能否修改指针变量本身( p )来改变p所储存的地址从而再解引用改变所想要改变的值呢?我们不妨写个代码来验证一下:

#include<stdio.h>
int main()
{int n = 0;const int* p = &n;int m = 100;//创建第三变量int* x = &n;//要想改变n的值只能再重新创建一个指针变量p = &m;//*p = 100;//指针变量p在*左边加了const 所以解引用已经被禁用*x = 10;printf("*p=m=%d\n", *p);printf("*x=n=%d\n", *x);printf("n=%d\n", n);return 0;
}

在这里插入图片描述
从运行结果我们可以得出3个结论: 1,要想改变*p 必须借助第三变量m才能改变,但是是不会改变n的值的!2,指针变量p被const修饰后要想改变n的值只能通过再创建一个指针变量来改变!3,指针变量被const修饰后,*p 是无论如何都不能使用了,即使是改变了p也依然不能解引用!

第二种const在*右边

来看一段代码
还是上面的代码我们改一下

#include<stdio.h>
int main()
{int n = 0;int* const p = &n;//在*右边加const//p = &m;//指针变量p在*右边加了const p所存的地址就已经固定不能更改了*p = 100;printf("*p=%d\n", *p);printf("n=%d\n", n);return 0;
}

在这里插入图片描述

从运行结果上来看我们可以知道在*右边加上const 是限制了指针变量p,*p 是可以使用的。上面的代码中由于 *p 可以使用所以改变*p 就相当于改变了n。

第三种const在*左和右边

显然我们知道const放在*的左右两边会导致*p 和p都无法使用,下面来看代码:

#include<stdio.h>
int main() 
{ int n = 10; int m = 20; int const * const p = &n; *p = 20; //ok? nop = &m; //ok? no
}

将代码复制到vs里就会发现报错,结果当然与我们猜想的一样。下面给出一个图来进行总结:
在这里插入图片描述

结论:const修饰指针变量的时候
• const如果放在的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
• const如果放在
的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。

二,野指针

我们先来了解一下它的概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

1,野指针的成因

野指针有几种成因分别是:

  1. 指针未初始化
    int *p; *p=20 *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; 
} 
  1. 指针指向的空间被回收
#include <stdio.h> 
int* test() 
{ int n = 100; return &n; 
}
int main() 
{ int*p = test(); printf("%d\n", *p); return 0; 
}

由于n是一个局部变量,在函数调用完后就已经被回收了 (还给操作系统了),所以返回n的地址是一个随机值。
指针指向的内存空间不属于当前程序,这个时候就是野指针。

2,如何规避野指针

了解了野指针的成因后我们自然有办法去规避它。

1. 及时初始化
如果我们不明确指针指向的对象就及时给指针初始化:int *p=NULL

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

如果我们明确指针所指向的对象就要及时给指针初始化:int n=0; int *p=&n;
2.小心指针越界
以上面的代码来举例:

#include <stdio.h> 
int main() 
{ int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i = 0; i <= 10; i++) //i必须小于等于10防止指针越界{ *(p++) = i; }//程序走到这指针就越界了要即使置为空指针*p=NULL;int m=0;*p=&m;//下次使用该指针的时候再进行判断if(*p!=NULL){}return 0; 
} 

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使⽤这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使⽤指针之前可以判断指针是否为NULL。

为了更加深入的理解我们举一个例子:

我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着⾛,不能去挑逗野狗,有点危险;对于指针也是,在使⽤之前,我 们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使⽤,如果不是我们再去 使⽤。

3. 不要返回局部变量的地址
当局部变量的作用域与该指针的作用域不同时,给指针返回局部变量的地址就相当于没有初始化指针变成了野指针。

三,assert断言

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

举个例子我们运行下面的代码看看会有什么结果:

#include<stdio.h>
#include<assert.h>//assert.h 头文件定义了宏 assert() 所以要包含assert.h
int main()
{int n=0;int *p=NULL;assert(p!=NULL);
}

在这里插入图片描述

上⾯代码在程序运行到这一行语句 assert(p!=NULL) 时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序 继续运行,否则就会终止运行,并且给出报错信息提示。
assert() 宏接受⼀个表达式作为参数。如果该表达式为真(返回值⾮零), assert() 不会产⽣ 任何作⽤,程序继续运⾏。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误 流 stderr 中写⼊⼀条错误信息,显⽰没有通过的表达式,以及包含这个表达式的文件名和行号。

当然assert不仅仅能用来判断指针,还可以判断非指针的问题,上代码:

#include<stdio.h>
#include<assert.h>
int main()
{int a=10;scanf("%d",&a);assert(a==10);
}

我们输入15看看有什么结果:
在这里插入图片描述
我们看到编译器直接报错,其原因是assert括号内表达式的值为假返回0所以编译器直接报错,那如果我们输入10呢?
在这里插入图片描述
我们可以看到输入10编译器就不会报错,因为assert括号内表达式值为真返回非0所以不报错。

assert() 的使⽤对程序员是⾮常友好的,使⽤ assert() 有⼏个好处:

1. 它不仅能⾃动标识文件和 出问题的行号

*2.还有⼀种无需更改代码就能开启或关闭 assert() 的机制

该机制是如果已经确认程序没有问 题,不需要再做断,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。 例如(以上面的代码来举例):

#define NDEBUG
#include<stdio.h>
#include<assert.h>
int main()
{int a=10;scanf("%d",&a);assert(a==10);
}

我们刚刚输入15编译器会报错,现在我们在 #include<assert.h> 语句前面加了 #define NDEBUG 这句话后输入15看看会不会报错:
在这里插入图片描述
发现没有报错,但前提是要在 #include<assert.h> 这句话前 加上 #define NDEBUG 这句话才行!

然后,重新编译程序,编译器就会禁用文件中所有的 assert() 语句。如果程序⼜出现问题,可以移 除这条 #define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

这么好用的assert当然也有缺点:

> assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。 ⼀般我们可以在 Debug 中使⽤,在 Release 版本中选择禁⽤ assert 就⾏,在 VS 这样的集成开 发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题, 在 Release 版本不影响⽤⼾使⽤时程序的效率。

介绍完了assert我们就来看看指针的使用和传址调用

四,指针的使用和传址调用

我们学习指针就是为了使用指针来解决问题,但有什么问题是非指针不可得呢?
举个例子,写一个函数完成两个数得交换(我们先用函数的传值调用)看看能不能实现:

1,传值调用

#include<stdio.h>
void swap1(int a,int b)
{int z=0;z=a;a=b;b=z;
}
int main()
{int a=10;int b=20;printf("交换前a=%d b=%d\n",a,b);swap1(a,b);printf("交换后a=%d b=%d\n",a,b);return 0;
}

在这里插入图片描述

我们看到并没有交换,因为这是传值调用形参只是实参的一份零时拷贝,形参的改变不影响实参。所以Swap1是失败的了。


如果对传值调用还不理解也可以看看我在函数篇讲的形参和实参就知道了
传送门:函数(上)


那怎么办呢?

我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了。

2,传址调用

还是上面的代码我们修改一下:

#include<stdio.h>
void swap2(int *pa,int *pb)
{int z=0;z=*pa;//z=a*pa=*pb;//a=b*pb=z;//b=z
}
int main()
{int a=10;int b=20;int *pa=&a;int *pb=&b;printf("交换前a=%d b=%d\n",a,b);swap2(&a,&b);printf("交换后a=%d b=%d\n",a,b);return 0;
}

在这里插入图片描述

在这里插入图片描述

我们将a和b的地址传给形参pa和pb这样形参和实参就共用一块内存空间,所以形参的改变会影响实参。这就是传址调用!

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改 主调函数中的变量的值,就需要传址调用。

五,strlen的模拟实现

学完了const和assert断言后我们对上次模拟strlen的代码进行修改:

1. 加上const修饰
2. 加上assert断言

#include <assert.h>
size_t my_strlen(const char* p)//在*左边加const防止arr内容被修改
{size_t count = 0;assert(p != NULL);//加上assert断言避免传入的是空指针!while (*p){count++;//计数器p++;}return count;
}int main()
{char arr[] = "abcdef";//a b c d e f \0size_t len = my_strlen(NULL);printf("%zd\n", len);return 0;
}

好了以上就是本章的全部内容啦!
感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/69241.shtml

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

相关文章

Android studio 创建aar包给Unity使用

1、aar 是什么&#xff1f; 和 Jar有什么区别 aar 和 jar包 都是压缩包&#xff0c;可以使用压缩软件打开 jar包 用于封装 Java 类及其相关资源 aar 文件是专门为 Android 平台设计的 &#xff0c;可以包含Android的专有内容&#xff0c;比如AndroidManifest.xml 文件 &#…

ASP.NET Core中Filter与Middleware的区别

中间件是ASP.NET Core这个基础提供的功能&#xff0c;而Filter是ASP.NET Core MVC中提供的功能。ASP.NET Core MVC是由MVC中间件提供的框架&#xff0c;而Filter属于MVC中间件提供的功能。 区别 中间件可以处理所有的请求&#xff0c;而Filter只能处理对控制器的请求&#x…

基础篇05-图像直方图操作

本节将简要介绍Halcon中有关图像直方图操作的算子&#xff0c;重点介绍直方图获取和显示两类算子&#xff0c;以及直方图均衡化处理算子。 目录 1. 引言 2. 获取并显示直方图 2.1 获取&#xff08;灰度&#xff09;直方图 (1) gray_histo算子 (2) gray_histo_abs算子 (3…

MySQL | Navicat安装教程

MySQL | Navicat安装教程 &#x1fa84;个人博客&#xff1a;https://vite.xingji.fun 简介 Navicat 是一款流行的 图形化数据库管理工具&#xff0c;由 PremiumSoft 公司开发&#xff0c;支持多种主流数据库系统&#xff08;如 MySQL、MariaDB、SQL Server、Oracle、Postgre…

硬件实现I2C案例(寄存器实现)

一、需求分析 二、硬件电路设计 本次案例需求与前面软件模拟案例一致&#xff0c;这里不再赘述&#xff0c;不清楚可参见下面文章&#xff1a;软件模拟I2C案例&#xff08;寄存器实现&#xff09;-CSDN博客 值得注意的是&#xff0c;前面是软件模拟I2C&#xff0c;所以并没有…

基于SpringBoot养老院平台系统功能实现六

一、前言介绍&#xff1a; 1.1 项目摘要 随着全球人口老龄化的不断加剧&#xff0c;养老服务需求日益增长。特别是在中国&#xff0c;随着经济的快速发展和人民生活水平的提高&#xff0c;老年人口数量不断增加&#xff0c;对养老服务的质量和效率提出了更高的要求。传统的养…

matlab simulink 汽车四分之一模型轮胎带阻尼

1、内容简介 略 matlab simulink121-汽车四分之一模型轮胎带阻尼 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略

w196Spring Boot高校教师科研管理系统设计与实现

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

数据分析:企业数字化转型的金钥匙

引言&#xff1a;数字化浪潮下的数据金矿 在数字化浪潮席卷全球的背景下&#xff0c;有研究表明&#xff0c;只有不到30%的企业能够充分利用手中掌握的数据&#xff0c;这是否让人深思&#xff1f;数据已然成为企业最为宝贵的资产之一。然而&#xff0c;企业是否真正准备好从数…

Vue 入门到实战 八

第8章 组合API与响应性 目录 8.1 响应性 8.1.1 什么是响应性 8.1.2 响应性原理 8.2 为什么使用组合API 8.3 setup组件选项 8.3.1 setup函数的参数 8.3.2 setup函数的返回值 8.3.3 使用ref创建响应式引用 8.3.4 setup内部调用生命周期钩子函数 8.4 提供/注入 8.4.1 …

Java使用aspose实现pdf转word

Java使用aspose实现pdf转word 一、下载aspose-pdf-21.6.jar包【下载地址】&#xff0c;存放目录结构如图&#xff1b;配置pom.xml。 <!--pdf to word--> <dependency><groupId>com.aspose</groupId><artifactId>aspose-pdf</artifactId>…

使用Node.js搭配express框架快速构建后端业务接口模块Demo

使用Node.js搭配express框架快速构建后端业务接口模块Demo&#xff01;实际开发中&#xff0c;有很多项目&#xff0c;其实都是可以使用node.js来完成对接mysql数据库的&#xff0c;express确实使用起来非常简单&#xff0c;入手快&#xff0c;效率非常高。下面是一个简单的案例…

Python----Python高级(并发编程:协程Coroutines,事件循环,Task对象,协程间通信,协程同步,将协程分布到线程池/进程池中)

一、协程 1.1、协程 协程&#xff0c;Coroutines&#xff0c;也叫作纤程(Fiber) 协程&#xff0c;全称是“协同程序”&#xff0c;用来实现任务协作。是一种在线程中&#xff0c;比线程更加轻量级的存在&#xff0c;由程序员自己写程序来管理。 当出现IO阻塞时&#xff0c;…

Unity 加载OSGB(webgl直接加载,无需转换格式!)

Unity webgl加载倾斜摄影数据 前言效果图后续不足 前言 Unity加载倾斜摄影数据&#xff0c;有很多的插件方便好用&#xff0c;但是发布到网页端均失败&#xff0c;因为webgl 的限制&#xff0c;IO读取失效。 前不久发现一个开源项目: UnityOSGB-main 通过两种方式在 Unity 中…

【Block总结】PSA,金字塔挤压注意力,解决传统注意力机制在捕获多尺度特征时的局限性

论文信息 标题: EPSANet: An Efficient Pyramid Squeeze Attention Block on Convolutional Neural Network论文链接: arXivGitHub链接: https://github.com/murufeng/EPSANet 创新点 EPSANet提出了一种新颖的金字塔挤压注意力&#xff08;PSA&#xff09;模块&#xff0c;旨…

【重新认识C语言----结构体篇】

目录 -----------------------------------------begin------------------------------------- 引言 1. 结构体的基本概念 1.1 为什么需要结构体&#xff1f; 1.2 结构体的定义 2. 结构体变量的声明与初始化 2.1 声明结构体变量 2.2 初始化结构体变量 3. 结构体成员的访…

如何在Vscode中接入Deepseek

一、获取Deepseek APIKEY 首先&#xff0c;登录Deepseek官网的开放平台&#xff1a;DeepSeek 选择API开放平台&#xff0c;然后登录Deepseek后台。 点击左侧菜单栏“API keys”&#xff0c;并创建API key。 需要注意的是&#xff0c;生成API key复制保存到本地&#xff0c;丢失…

电脑开机提示按f1原因分析及终极解决方法来了

经常有网友问到一个问题&#xff0c;我电脑开机后提示按f1怎么解决&#xff1f;不管理是台式电脑&#xff0c;还是笔记本&#xff0c;都有可能会遇到开机需要按F1&#xff0c;才能进入系统的问题&#xff0c;引起这个问题的原因比较多&#xff0c;今天小编在这里给大家列举了比…

【高级篇 / IPv6】(7.2) ❀ 04. 在60E上配置ADSL拨号宽带上网(IPv4) ❀ FortiGate 防火墙

【简介】除了单位用户以外&#xff0c;大部分个人用户目前使用的仍然是30E、50E、60E系列防火墙&#xff0c;固件无法达到目前最高版本7.6&#xff0c;这里以最常用的60E为例&#xff0c;演示固件版本7.2下实现ADSL拨号宽带的IPv6上网。由于内容比较多&#xff0c;文章分上、下…

Qt之设置QToolBar上的按钮样式

通常给QAction设置icon后,菜单栏的菜单项和工具栏(QToolBar)上对应的按钮会同时显示该icon。工具栏还可以使用setToolButtonStyle函数设置按钮样式,其参数为枚举值: enum ToolButtonStyle {ToolButtonIconOnly,ToolButtonTextOnly,ToolButtonTextBesideIcon,ToolButtonTe…