C语言-指针讲解(2)


文章目录

  • 1.野指针
    • 1.1 什么是野指针
    • 1.2 造成野指针的原因有哪些呢
      • 1.2.1造成野指针具体代码实例:
    • 1.3 如何避免野指针呢?
      • 1.3.1如何对指针进行初始化?
      • 1.3.2如何才能小心指针越界?
      • 1.3.3 指针变量不再使用时,如何及时置NULL,在指针使用之前检查有效性?
  • 2.assert断言
    • 2.1 什么是assert断言
    • 2.2 如何使用assert断言呢?
    • 2.3 使用assert有什么好处呢?
  • 3.指针的使用和传址调用
    • 3.1 学习指针的目的是什么?
    • 3.2 什么是传址调用?
    • 3.3 怎么进行传址调用?
  • 4.数组名的理解
    • 4.1 arr和&arr的区别
  • 5.二级指针
    • 5.1 什么是二级指针
  • 5.2 指针变量的地址存放在哪里呢?
    • 5.3 对于二级指针的运算是怎么样的呢?
  • 6.指针数组
    • 6.1 什么是指针数组呢?
  • 7.指针数组模拟二维数组

通过前面的介绍
C语言指针详解(一)超详细~
相信大家对指针的基本概念及用法有了初步的了解。

我们来回顾一下上次那个博客讲了什么吧~
1.指针就是变量,用于存放地址的,地址唯一标识的一块内存空间。
2.指针的大小分别是4/8个字节(32位平台/64位平台)
3.指针是有类型的,指针的类型决定了指针±整数的步长,以及指针解引用的权限有多大。
4.指针的运算。
那么这次博主给大家继续深入理解指针的其他高级用法吧
这是本次我们要讲解的知识点:


1.野指针

1.1 什么是野指针

野指针,顾名思义,就是指针指向的位置是不可知的。就好比如没有主人的流浪狗一样。

1.2 造成野指针的原因有哪些呢

1.指针未被初始化
2.指针越界访问
3.指针指向的空间释放
前面两个造成野指针原因都比较容易理解,所以我们一会重点讲一下第三个

1.2.1造成野指针具体代码实例:

1.指针未被初始化

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

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;
}

3.指针指向的空间释放

int* test()//由于返回的是n的地址,因此函数返回的是int*类型
{int n = 100;//在test函数中创建了局部变量n,return &n;//当我们在中间的函数做了一些事情后,我们就返回n,把n的地址返回到指针变量p来接收}
int main()
{int* p = test();//由于返回的是地址,所以我们拿指针变量p来接收printf("%d\n", *p);return 0;
}

从上面这个代码中,当test()函数中变量n申请了一块空间,而出这个test()函数的时候,这个n的地址就会被销毁,并还给操作系统。然后回到main函数,但是指针变量p仍然记住n的地址,如果到时通过对p进行解引用操作,来改变它所指向对象的值。这就属于是非法访问了,足矣说明p是个野指针。

1.3 如何避免野指针呢?

1.指针初始化
2.小心指针越界
3.指针变量不再使用时,及时置NULL,指针使用之前检查有效性。

1.3.1如何对指针进行初始化?

如果不知道指针指向哪里,可以先给指针复制NULL,NULL是C语言中定义的一个标识符常量,值是0,但是这个地址是无法直接使用的。

初始化如下:

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

1.3.2如何才能小心指针越界?

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

1.3.3 指针变量不再使用时,如何及时置NULL,在指针使用之前检查有效性?

  • 当指针变量指向一块区域的时候,我们可以通过指针访问该区域,如果我们后期不再使用这个指针访问空间的时候,我们可以先把该指针置为NULL。
  • 然后到下次使用该指针变量之前,我们要先判断它是否为NULL,如果是就不能指直接使用,不是的话我们才能使用。

2.assert断言

2.1 什么是assert断言

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

2.2 如何使用assert断言呢?

在这里插入图片描述

assert(p != NULL);

当上面代码在程序运行到这一行程序时,验证变量p是否等于NULL。如果确实不等于NULL,程序会继续运行,否则就会终止运行,并且给出报错信息提示。

2.3 使用assert有什么好处呢?

  • 它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。
  • 如果已经确认程序没有问题,不需要再做断言,就在#include<assert.h>语句的前面,定义一个NDEBUG。
    具体代码如下:
#define NDEBUG
#include <assert.h>

需要注意的是,assert()也是有缺点的。由于引入了额外的检查,会增加程序的运行时间。


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

3.1 学习指针的目的是什么?

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

比方说,我们要写一个函数,来交换两个整数变量的值。

经过一番思考后,我们可能会写出这个代码出来~

#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函数中,我们调用了swap函数。并把变量a和b作为传过去,形参用x和y来接收。但我们发现,这里的变量a和变量x的地址不相同,变量b和变量y的地址也不相同。这也说明形参x和y是一个独立的空间。当swap函数调用结束后返回main函数,a和b的变量依然无法交换,swap在使用的时候,本质上就是把变量本身传递给函数,这也就是我们常说的传值调用

因此我们得出以下结论:

实参传递给行参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参,所以swap是失败的。

那我们怎么解决呢?
在这里插入图片描述
我们得借助函数间传址调用来解决。

3.2 什么是传址调用?

传址调用,顾名思义就是将main函数中的变量地址传到所调用的函数中,然后在被调函数中,通过地址间的操作即可实现两个数的交换。

3.3 怎么进行传址调用?

那回到刚刚那种情景,我们只需把变量a和b的地址分别传给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);Swap1(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}

运行结果如下:
在这里插入图片描述

4.数组名的理解

在上一次博客C语言指针详解(一)超详细~
我们曾写过两行代码:

int arr[10]={1,2,3,4,5,6,7,8,9,10};
int *p =&arr[0];

这里我们是使用&arr[0]的方式拿到了数组第一个元素的地址。但是数组名本来就是地址,不信我们可以拿VS编译器来测试一下。
在这里插入图片描述

从上图,我们发现数组名和数组首元素的地址打印出的结果是一模一样的。

因此我们可以得出这个结论:数组名是数组首元素(第一个元素)的地址
但是呢,有同学会有疑问,如果数组名是数组首元素的地址,那这个代码该怎么理解?

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("%d\n", sizeof(arr));return 0;
}

从下图,我们发现输出结果是40。
在这里插入图片描述
为什么不是4/8呢?如果数组是首元素的地址,按理说输出的应该是4/8才对。

其实数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小。
  • &数组名表示整个数组,取出的是整个数组的地址,(整个数组的地址和数组的首元素的地址是有区别的)。

除此之外,其他地方使用数组名,数组名都表示首元素的地址。


这时,或许还会同学不理解,他们也许会再测试一下这个代码:

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("arr = %p\n", arr);printf("&arr = %p\n", &arr);return 0;
}

发现这三个打印的结果都一样,会再次出现疑惑?
在这里插入图片描述
那接下来我来介绍他们之间的区别。

4.1 arr和&arr的区别

我们直接上代码分析~

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0] = %p\n", &arr[0]);printf("&arr[0]+1 = %p\n", &arr[0]+1);printf("arr = %p\n", arr);printf("arr+1 = %p\n", arr+1);printf("&arr = %p\n", &arr);printf("&arr+1 = %p\n", &arr+1);return 0;
}

运行结果:
在这里插入图片描述

  • 从上图,我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,是因为&arr[0]和arr都是首字符地址,+1就是跳过一个元素。
  • 但是&arr和&arr+1是相差40个字节,这就是因为&arr是数组的地址,因此这里+1就是跳过整个数组。

相信到这里大家应该搞清楚数组名的意义了吧。
除了有两个例外,其他的数组名都是数组首元素的地址。


5.二级指针

5.1 什么是二级指针

二级指针指向的是一级指针的指针,也就是说一个指针指向的是另外的指针,同时二级指针也是存放一级指针的地址,则称之为二级指针。

5.2 指针变量的地址存放在哪里呢?

这个我们可以先画个图来分析一下~

在这里插入图片描述

比方说,我们从上图可以得知,我们可以得知指针变量pa存放的是a的地址,而指针变量ppa存放的是指针变量pa的地址,你们由这个规律,我们就能推导出指针变量pppa存放的是指针变量ppa的地址。

另外,这里有个小细节需要大家注意的是,由于pa中的p左边的*代表pa是个指针变量,而前面的int代表pa是个int类型的指针变量。那同理,ppa中的p左边的 *代表ppa是个指针变量,而旁边还有一个 *。代表的是ppa是一个int *类型的指针变量。

5.3 对于二级指针的运算是怎么样的呢?

我们先来看下面代码,然后再逐一进行分析。

#include <stdio.h>
int main() {int a = 10;int* p = &a;//p是一级指针int** pp = &p;//pp是二级指针printf("%d\n", **pp);return 0;
}

从上图可以得知,首先,** pp先通过*pp找到p,然后我们再对p进行解引用操作: *p,那找到的就是a,那么最终输出的结果就是10。

VS运行结果如下所示:
在这里插入图片描述



6.指针数组

6.1 什么是指针数组呢?

俗话说,存放整型的数组是整形数组。
存放字符的数组是字符数组。
在这里插入图片描述
那么同理,存放指针的数组则是指针数组。

并且指针数组的每个元素都是用来存放地址(指针)的。
如下图所示:
在这里插入图片描述

我们会发现指针数组每个元素都是存放地址的,又可以指向一块区域。



7.指针数组模拟二维数组

#include <stdio.h>
int main()
{int arr1[] = {1,2,3,4,5};int arr2[] = {2,3,4,5,6};int arr3[] = {3,4,5,6,7};//数组名是数组首元素的地址,类型是int*的,就可以存放在parr数组中int* parr[3] = {arr1, arr2, arr3};int i = 0;int j = 0;for(i=0; i<3; i++){for(j=0; j<5; j++){printf("%d ", parr[i][j]);//parr[i][j]==*(*(parr+i)+j))}printf("\n");}return 0;
}

在这里插入图片描述

从图中,我们可以看出parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。
需要注意的是,上面的代码模拟出的二维数组的效果,实际上并未完全是二维数组,以为每一行并非是连续的。


** 好啦!今天博主就分享到这里**
在这里插入图片描述
** 如果觉得博主讲得不错的话。欢迎大家一键三连支持一下**

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

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

相关文章

单链表的实现

单链表的实现 单链表的链表的概念及结构概念结构链表结构的分类链表常用的结构 无头单向不循环链表头文件 SList.h结构体 struct SListNode 源文件 SList.c创建结点 SLNode* SLBuyNode(SLDataType x)初始化链表 void SLInit(SLNode** pphead)链表尾部插入 void SLPushBack(SLNo…

C语言:计算 1! + 2! + 3! + ... + n!

题目&#xff1a; 从键盘输入一个值n&#xff0c;计算 1的阶乘 至 n的阶乘 的和&#xff0c; 如&#xff1a;输入10&#xff0c;计算 1的阶乘 至 n的阶乘 的和 --> 计算&#xff1a;1! 2! 3! ... 10! 思路一&#xff1a; 效率比较低&#xff0c;会重复计算之前计算过的…

Leetcode-234 回文链表

我的解法&#xff1a;使用栈&#xff0c;定义了len略微复杂&#xff0c;拿链表的后半部分和前半部分比较即可&#xff0c;没必要全部比较 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* List…

如何在在线Excel文档中规范单元格输入

在日常的工作中&#xff0c;我们常常需要处理大量的数据。为了确保数据的准确性和可靠性。我们需要对输入的数据进行规范化和验证。其中一个重要的方面是规范单元格输入。而数据验证作为Excel中一种非常实用的功能&#xff0c;它可以帮助用户规范单元格的输入&#xff0c;从而提…

Java用log4j写日志

日志可以方便追踪和调试问题&#xff0c;以前用log4net写日志&#xff0c;换Java了改用log4j写日志&#xff0c;用法和log4net差不多。 到apache包下载下载log4j的包&#xff0c;解压后把下图两个jar包引入工程 先到网站根下加一个log4j2.xml的配置文件来配置日志的格式和参…

matlab中的iddata函数的初步理解和使用程序举例

matlab中的iddata函数的初步理解和程序举例 一、iddata函数功能 iddata函数常用于系统识别分析领域数据分析方面。该函数在时域或频域中&#xff0c;将用于系统识别的输入输出数据及其特性数据的生成对象数据类型。即&#xff0c;可以使用iddata函数封装要标识的系统的输入和…

Mysql数据库 8.SQL语言 外键约束

一、外键约束 外键约束——将一个列添加外键约束与另一张表的主键&#xff08;唯一列&#xff09;进行关联之后&#xff0c;这个外键约束的列添加的数据必须要在关联的主键字段中存在 案例 创建原则&#xff1a;先创建不含外键的表也就是班级表 添加外键的方式 一般使用第一…

HCIE-CCE

1、创建集群 svc网络&#xff0c;10.247 pod网络&#xff0c;10.244 节点网络&#xff0c;192.168.66&#xff08;master和node一致&#xff09; 2、创建节点 上面集群选择了最新版本1.27&#xff0c;CCE从1.27版本开始不再支持docker容器引擎&#xff0c;仅支持containered&…

git笔记

git常见命令 git init :初始化本地仓库&#xff0c;会生成一个.git文件&#xff0c;该文件用于管理和追踪该本地仓库&#xff0c;只有在git仓库下的文件才能被管理! git config user. name "用户名” git config user. email " 邮箱” git config -1 :列出当前git仓库…

python接口自动化测试 —— unittest框架suite、runner详细使用

test suite 测试套件&#xff0c;理解成测试用例集一系列的测试用例&#xff0c;或测试套件&#xff0c;理解成测试用例的集合和测试套件的集合当运行测试套件时&#xff0c;则运行里面添加的所有测试用例 test runner 测试运行器用于执行和输出结果的组件 test suite、tes…

Flask(Jinja2) 服务端模板注入漏洞(SSTI)

Flask&#xff08;Jinja2&#xff09; 服务端模板注入漏洞(SSTI) 参考 https://www.freebuf.com/articles/web/260504.html 验证漏洞存在 ?name{{7*7}} 回显49说明漏洞存在 vulhub给出的payload: {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__…

【uniapp小程序下载】调用uni.uploadfile方法在调试工具里是没有问题的,但是线上版本和体验版就调用不成功,真机调试也没问题

把你的下载地址前缀添加到合法域名就解决了 在调试工具里成功了是因为勾选了下面这项 下面是我的下载并打开函数 methods: {// 下载downloadFileFn(data) {if (this.detailsObj.currentUserBuy) {uni.downloadFile({// data是路径url: https:// data,success(res) {//保存到本…

未来已来,“码”上见证---通义灵码

为了撰写一份关于通义灵码的产品测评&#xff0c;我将构建一个基于提供的产品介绍和评测内容要求的框架给大家介绍这款产品。 功能使用维度 代码智能生成 使用场景&#xff1a;开发中遇到需要编写新功能、单元测试、或对现有代码进行注释时。 使用效果&#xff1a;预期通义灵…

7.5 SpringBoot 拦截器Interceptor实战 统一角色权限校验

前言 在【7.1】管理员图书录入和修改API,当时预告过:并没有写【校验是否是管理员】的逻辑,因为是通用逻辑,会单写一篇来细讲,那么今天就来安排! 角色权限校验,是保证接口安全必备的能力:有权限才可以操作!所以,一般对于这种通用逻辑,推荐不与主业务逻辑耦合,那么…

二叉树的前序、中序、后序、层序遍历

参考内容&#xff1a; 五分钟让你彻底理解二叉树的非递归遍历 Python实现二叉树的非递归遍历 二叉树遍历——深度优先&#xff08;前中后序&#xff09;广度优先&#xff08;层序遍历&#xff09; 构造二叉树 定义二叉树结构如下 struct node {int data;node *left;node *rig…

C++前缀和算法的应用:统计上升四元组

C前缀和算法的应用&#xff1a;统计上升四元组 本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 题目 给你一个长度为 n 下标从 0 开始的整数数组 nums &#xff0c;它包含 1 到 n 的所有数字&#xff0c;请你返回上…

关于网站安全的一些讨论

互联网的普及和发展为企业和个人提供了巨大的机会&#xff0c;但同时也伴随着网络安全威胁的增加。网站被攻击是一个常见的问题&#xff0c;可能导致数据泄露、服务中断和声誉受损。在本文中&#xff0c;我们将探讨与网络安全紧密相关的因素&#xff0c;分析为什么网站容易受到…

Si4010 一款带有MCU SoC RF发射机芯片 无线遥控器

Si4010是一款完全集成的SoC RF发射机&#xff0c;带有嵌入式CIP-51 8051 MCU&#xff0c;专为1GHz以下ISM频带设计。该芯片针对电池供电的应用进行了优化&#xff0c;工作电压为1.8至3.6 V&#xff0c;待机电流小于10 nA的超低电流消耗。高功率放大器可提供高达10 dBm的输出功率…

Linux Crontab 定时任务

crond 服务 Linux 通过 crond 服务来支持 crontab。 查看 crond 服务是否已经安装 输入下面命令确认 crond 服务是否已安装。 systemctl list-unit-files | grep crond 如果为 enabled&#xff0c;表示服务正运行。 crontab 文件 crontab 要执行的定时任务都被保存在 /etc…

seata1.8安装部署

1.在nacos里面创建命名空间 2.下载seata安装包 3.将下载的seata解压&#xff0c;找到seata/script/server/db目录下对应数据库的sql脚本&#xff0c;创建数据库 undo_log.sql CREATE TABLE undo_log (branch_id bigint(20) NOT NULL COMMENT branch transaction id,xid varcha…