深入理解指针(1)

在之前我们学习了许多c语言的基础知识,让我们初步了解了c语言,接下来将来到c语言中一个重点的知识章节--指针,学习完指针后将会让我们对c语言有更深入的理解,接下来就开始指针的讲解

0c4cd194906642719252a97dc4cf10c9.png

1.内存与地址

1.指针

在了解内存与地址前,先来看一个生活中的案例
我们都知道当到一个陌生的楼房里时,,如果这个楼房很大通过门牌号去查找将会是一个快速找到的方法,如果得到房间号,就可以快速的找房间

1ddf563053c846b9978e70b9d8ade869.png

在生活中有了门牌号就可以快速找到房间,大大提升了效率

如果把上面的例子对照到计算机中,又是怎么样呢?
我们知道在计算机中都有内存,可能是4G/8G/16G等等,但无论是多大的内存都是一份相当大的空间,而数据又是放在内存当中的,我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中

5bea9bff736e4a40a9e0ee7896b7e9fa.png

那在内存这么大的空间里,数据是依据什么来找到的呢?那那些内存空间如何高效的管理呢?
在生活当中把楼房划分成一个个房间,其实在内存当中也是相同的道理,为了更高效的对内存进行使用和管理,其实也是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节我们可以理解为内存像楼房一样被划分成了许多房间,每个房间就是一个内存单元

有的读者会提出1个字节是什么?
这时我们就要知道计算机中的单位

bit - ⽐特位
Byte - 字节         1Byte=8Bit
KB                       1kB=1024Byte
MB                      1MB=1024KB
GB                       1GB=1024MB        
TB                        1TB=1024GB
PB                        1PB=1024TB
 

其中1比特在内存中可以存储1个二进制位中的1或者0

在计算机中的每个内存单元,相当于⼀个学生宿舍,⼀个字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是⼀个比特位。

为了CPU能更高效调用内存,就如给楼房的房间编号一样,内存中每个内存单元也有不同的编号,这也就是内存单元的地址,在c语言中给这些地址起了新的名字:指针

在此可以理解为:内存单元编号=地址=指针

2.如何理解地址的产生

通过以上的讲解我们知道CPU是通过地址来获取内存中的数据,再将处理后的数据存放在内存当中,那么该过程中内存单元的地址是怎么传送的呢?
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。但是硬件与硬件之间是互相独立的,要使它们之间建立通信这就需要用"线"连起来

CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。在这两者之间有地址总线,控制总线,数据总线。在此我们需要了解的是地址总线

 CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节
很多,所以需要给内存进行编址。且计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么⼀根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。

2.指针变量与地址

1.取地址操作符(&)

int main()
{int a = 20;return 0;
}

变量创建的本质其实其实是在内存当中申请内存空间 例如以上代码就申请了4个字节的内存空间来存放20

以上就是20所存放的内存空间地址,但为什么显示的是14的地址呢?
其实这的14是用16进制表示,转换成10进制就是20

在以上还可以发现20所存放每个字节都有地址,再一次验证了在上文提到的每个内存单元都有地址

通过调试中发现内存中都没有关于a变量的信息,那么a是否对于编译器来说是没有作用的呢
其实确实是这样的,对于编译器来说通过地址就可以找到内存空间,其实这个a是给我们程序员来看的,让我们知道使用a就是使用对应内存的4个字节

那我们如何才能拿到a的地址呢?
这时就要用到&--(取地址操作符),若在以上代码中加入&a,它的作用就是拿到变量的地址

注:这里的&是一个单目操作符,不要与按位与&混淆,按位与是一个双目操作符

我们知道变量是有4个地址,那么&之后取出的是哪个呢? 

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

这时就要用到%p这个占位符,%p的作用在printf函数中是打印出地址

运行程序就可以看到打印出的是a首个字节的地址,所以就可以知道&取出的是第一个字节的地址

2.指针变量和解引用操作符(*) 

1.指针变量 

 那如果想把变量的地址再存储到另一个变量当中应该如何操作呢?
如果这时将&a存放到pa变量中,那么这个变量的类型就是int*
pa是用来存放指针(地址)的,所以pa是指针变量
int*pa=&a 在这里pa左边写的是 int* , * 是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int)类型的对象

2.解引用操作符

#include<stdio.h>
int main()
{int a = 20;int* pa=&a;return 0;
}

在以上代码中pa里存放着a的地址,那如果要通过pa来找到a应该怎么表示呢?
这时就要用到*(解引用操作符),*pa就是通过pa这个指针变量内存放的值找到a

例如要把a的值改成200,就可以通过*pa来实现

#include<stdio.h>
int main()
{int a = 20;int* pa=&a;*pa=200;printf("%d",a);return 0;
}

 这时有的读者会发问为什么要通过pa来将a的值修改,不是可以直接让a=200来实现吗?
在电视剧狂飙中当高齐强身份很高时有一些事就不再适合亲自出手,就比如说他对老墨说我想吃鱼,老墨就会帮他处理棘手的事    有时我们只是拿到了地址信息,这时就只能通过解引用指针变量的方式来改变变量的值

3.指针变量的大小

首先我们要知道指针变量是用来存放地址的,一个地址需要多大空间,那么指针变量就有多大

前面的内容我们了解到,32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个二进制位组成的⼆进制序列,存储起来需要8个字节的空间,指针变量的大小就是8个字节

结论:
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节 

• 指针变量的大小与指针变量的类型无关,只要指针类型的变量,在相同的平台下,大小都是相同的。


3. 指针变量类型的意义 

在以上讲解中我们知道了指针变量的大小与指针的类型无关,那么指针变量的类型有什么意义呢?
其实指针变量大小还是有意义的,接下来我们将学习其意义

1. 指针的解引用


在以上当中将a的值初始化为0x11223344,因为0x之后表示16进制的数,而一个16进制数又可以转化为4个二进制数,两个16进制数就可以转化为8个二进制数,所占空间就为1字节
这样就可以让存储这个数内存当中的每个内存单元都存放数

当讲&a放入int*类型的指针变量时,*pa会发现a所指向都每个内存单元都被被改成了0,*pa=0一次访问了4个字节

 在以上可以发现当讲指针变量pa的类型改为char*时,就*pa=0就只能将a当中的一个字节改为0,而且他的未改变,*pa=0一次访问了1个字节

 结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)

2. 指针+-整数

由以上代码可见&n+与pa+1都使得地址增加了4,而pc+1只让地址增加了1 

 我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。

正如以上代码可以发现指针可以+1,那也可以-1。

 

结论:指针的类型决定了指针向前或者向后走一步有多大(距离) 

3 void* 指针

在以上代码中可以看到当用char*类型的指针变量pc存储整形变量n的地址时,编译器会爆出以上警告

而当用void*来接收&n时候就不会出现警告

在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指
),这种类型的指针可以用来接受任意类型地址。 

注:void* 类型的指针也是有局限性的,不能直接进行指针的+-整数和解引用的运算。

那么 void* 类型的指针到底有什么用呢?
⼀般 void* 类型的指针是使用在函数参数的部分,
用来接收不同类型数据的地址这样的设计可以
实现
泛型编程的效果。使得⼀个函数来处理多种类型的数据,在之后深入会讲解。
 

4. 关键字const 

1.const修饰变量

当对被const修饰后的变量重新赋值时,程序如以上一样就会报错

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

2.const修饰指针变量

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

int main()
{int a = 20;const int*pa=&a;*pa = 0;return 0;
}

 

int main()
{int a = 20;int const*pa=&a;*pa = 0;return 0;
}

 通过以上3个不同的代码可以发现
• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。
但是指针变量本身的内容可变。
• const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指
向的内容,可以通过指针改变。

5. 指针运算

1.指针+- 整数

我们知道数组在内存当中是练习存放的,之前找数组元素都是通过数组下标来查找的,学习了指针后,这时就可以通过地址都方法找到数组元素 

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int main()
{int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]);for(int i=0;i<sz,i++){printf("%d ", *(p+i));}return 0;
}

 

2.指针-指针

我们首先要知道指针-指针的绝对值计算的是指针和指针之间的元素个数
注:前提条件是两个指针指向的是同一个内存空间

我们知道利用库函数strlen可以计算字符串的长度,那么如果不使用这个库函数,应该如何去计算呢?

#include<stdio.h>
size_t my_strlen(char*s)
{char* p = s;while (*p != '\0'){p++;}return p-s;
}int main()
{char arr[] = "abcdef";//这里数组名表示首元素的地址printf("%zd", my_strlen(arr));return 0;
}

因为字符串结束的标志是\0,所以通过\0之前的指针-第一个字符的指针就得出这个字符串的长度

这时有的读者会想那是否有指针+指针呢?
其实这就和日期一样,日期-日期可计算出之间差的天数,日期+日期是没有意义的,指针也同理

3.指针的关系运算

指针之间的关系运算其实就是对指针之间的大小进行比较

例如以上打印数组也可以用关系运算来实现代码

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

6.野指针

1.野指针形成原因

1. 指针未初始化

局部变量指针未初始化,默认为随机值

 

 2. 指针越界访问

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

以上代码中数组大小初始化为10,而arr[10]已经不在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

2.如何规避野指针

1.指针初始化

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

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

例如以上代码我们知道p2为野指针就给它初始化赋值NULL,这样再之后就知道p2是野指针就不要去访问 如果解引用p2程序就会出现错误

2. 不要让指针越界访问

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

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

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,当不再使用这个指针访问空间的
时候,我们可以把该指针置为NULL。  

我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓起来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗。对于指针也是,在使用指针之前要判断是不是野指针,不是才能使用

 

int main()
{int*pa=NULL;if(pa!=NULL){*pa=100;}return 0;
}

正如以上代码一开始将pa置为空指针,用一个if语句使得pa不为空指针的时候再对pa解引用并赋值 

4. 避免返回局部变量的地址

就如以上提到的不要返回局部变量的地址,因为出作用域后局部变量会被销毁

7.传址调用与传值调用

在编写一些程序时,穿给函数的实参是数值时是无法解决问题的,例如要实现a,b两个变量的交换以下代码能实现吗?

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Exchange(int x, int y)
{int z = x;x = y;y = z;
}int main()
{int a = 10;int b = 20;Exchange(a, b);printf("a=%d b=%d", a, b);return 0;
}

 

这时就会发现a和b没有互换,这是为什么呢?我们调试程序看看 

 这时就会发现存放a的地址与存放x的地址不同,存放b的地址与存放y的地址不同,以上代码只是交换了x和y,但因为x和y是形参只是实参的一份临时拷贝,所以a和b没能实现交换
像以上Exchnage函数在使用的时候,是把变量本身直接传递给了函数,这种叫
传值调用

那要使用调用函数的方法实现两个数交换,正确方法是什么样的呢?

这时就要用到传址调用,将a和b的地址传给形参

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Exchange(int*x, int*y)
{int z = *x;*x = *y;*y = z;
}int main()
{int a = 10;int b = 20;Exchange(&a, &b);printf("a=%d b=%d", a, b);return 0;
}

通过以上调试发现存放a的地址与存放x的地址相同,存放b的地址与存放y的地址相同
以上就是利用
传址调用

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

以上就深入理解指针(1)的全部内容,希望看完以上内容你能有所收获,接下来还会继续更新指针的其他内容,未完待续....

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

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

相关文章

2024.5.8 关于 SpringCloud —— Ribbon 的基本认知

目录 Ribbon 负载均衡原理 工作流程 Ribbon 负载均衡规则 Ribbon 负载均衡自定义化 代码方式修改规则 配置文件方式修改规则 小总结 Ribbon 设定饥饿加载 Ribbon 负载均衡原理 工作流程 order-service 使用 RestTemplate 发送请求&#xff0c;随后该请求将会被 Ribbon 所…

修改el-checkbox样式

一定要在最外层&#xff1b; //未选中框/deep/ .el-checkbox__inner{border-color: #0862a3;}//选中框/deep/ .el-checkbox__input.is-checked .el-checkbox__inner{background-color: #0862a3;border-color: #0862a3;}//未选中框时右侧文字/deep/ .el-checkbox__label{}//选中…

认养小游戏功能介绍

认养小游戏通常模拟了真实的农业生产过程&#xff0c;让玩家能够在线上体验种植、养殖的乐趣。以下是一些常见的认养小游戏功能介绍&#xff1a; 选择认养的农产品&#xff1a;首先&#xff0c;玩家可以从游戏中提供的多种农产品中选择自己想要认养的种类&#xff0c;如蔬菜、…

JavaEE技术之MySql高级-搭建主从复制(主从同步原理、一主多从配置)

文章目录 MySQL主从同步1、MySQL主从同步原理2、一主多从配置2.1、准备主服务器2.2、准备从服务器2.3、启动主从同步2.4、实现主从同步2.5、停止和重置2.6、常见问题问题1问题2 MySQL主从同步 1、MySQL主从同步原理 基本原理&#xff1a; slave会从master读取binlog来进行数据…

easy_signin_ctfshow_2023愚人杯

https://ctf.show/challenges#easy_signin-3967 2023愚人杯信息检索&#xff0c;在请求荷载中发现一个base64 face.pngencode ZmFjZS5wbmc解密结果 flag.pngencode ZmxhZy5wbmc尝试一下 返回内容 Warning: file_get_contents(flag.png): failed to open stream: No such file…

专项技能训练五《云计算网络技术与应用》实训8-1:建立基于OpenvSwitch的GRE隧道

文章目录 建立基于OpenvSwitch的GRE隧道1. 使用VMware安装2个CentOS 7虚拟机&#xff0c;安装时记得都开启CPU虚拟化&#xff0c;第一台命名为“Docker”&#xff0c;第二台命名为“KVM”。2. 安装完虚拟机后&#xff0c;进入虚拟机&#xff0c;修改网络配置&#xff08;onboot…

导电材料——分类、性能与性质

本篇为西安交通大学本科课程《电气材料基础》的笔记。 导电材料指的是能在电场下传导电流的材料。导体价电子所在能带为半满带&#xff0c;且相邻能级间隔小&#xff0c;外电场下电子很容易从低能级跃迁到高能级上&#xff0c;大量的电子很容易获得能量进行共有化运动&#xf…

C++入门系列-析构函数

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 析构函数 概念 析构函数&#xff0c;与构造函数功能相反&#xff0c;析构函数不是完成对对象本身的销毁&#xff0c;局部对象销毁工作是由编译器完成的&#xff0c;而对象在销…

[蓝桥杯]真题讲解:班级活动(贪心)

[蓝桥杯]真题讲解&#xff1a;班级活动&#xff08;贪心&#xff09; 一、视频讲解二、正解代码1、C2、python33、Java 一、视频讲解 [蓝桥杯]真题讲解&#xff1a;班级活动&#xff08;贪心&#xff09; 二、正解代码 1、C #include<bits/stdc.h> using namespace st…

【计算机网络】循环冗余校验:Cyclic Redundancy Check

1. 任务目标 利用循环冗余校验&#xff08;CRC&#xff09;检测错误。 循环冗余校验&#xff08;英语&#xff1a;Cyclic redundancy check&#xff0c;通称 CRC&#xff09;是一种根据网上数据包或计算机文件等数据产生简短固定位数校验码的一种散列函数&#xff0c;主要用来…

2024年5月6日优雅草蜻蜓API大数据服务中心v2.0.3更新

v2.0.3更新 2024年5月6日优雅草蜻蜓API大数据服务中心v2.0.3更新-修复改版后搜索框漏掉的bug-增加搜索框 提示&#xff1a;优雅草大数据中心已经 上线137天 稳定运行 1181555 次 累积调用 目前大数据中心用户呈现增长趋势&#xff0c;目标2024年11月底突破1亿次调用&#xf…

社工库信息查询

此网站需要注册账号&#xff0c;新用户注册送3点券&#xff0c;每日签到可获得1.5点券。也可通过充值来查 我这里有方法可以利用缺陷来无限获取点券查人

交通数据三维可视化呈现与可视化分析系统开发(附程序源码)

目录 01 系统介绍 02 功能介绍 文件管理功能 模型研究 可视化分析功能 今天分享一套“交通数据三维可视化呈现与可视化分析系统”&#xff0c;并开放程序源代码下载&#xff0c;内容涉及开源空间数据库的使用、三维引擎的二次开发、矢量和栅格数据管理、交通流量分析模型框…

AI图书推荐:使用FastAPI框架构建AI服务

《使用FastAPI构建生成式AI服务》&#xff08;Building Generative AI Services with FastAPI (Early Release) &#xff09;是一本由Ali Parandeh编写的书籍&#xff0c;计划于2025年3月首次出版&#xff0c;该书以实践为导向&#xff0c;指导读者如何开发具备丰富上下文信息的…

【nodejs 命令行交互神器 - inquirer.js】

需求 大家在开发时&#xff0c;有时需要从命令行读取用户的输入&#xff0c;或者让用户选择。在nodejs中&#xff0c;这个怎么实现? 原生实现 ❌ process.stdin.setEncoding(utf8);process.stdin.on(readable, () > {let chunk;// 使用循环确保我们读取所有的可用输入wh…

C语言(指针)1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

前端笔记-day03

文章目录 01-初始CSS02-CSS引入方式03-标签选择器04-类选择器05-id选择器06-通配符选择器07-画盒子08-字体大小09-文字粗细10-字体倾斜11-行高12-行高垂直居中13-字体族14-font复合属性15-文本缩进16-文本对齐方式17-图片对齐方式18-文本修饰线19-文字颜色20-调试工具21-综合案…

容器监控与日志管理

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、Docker监控工具 二、容器日志工具docker logs 三、第三方日志工具 四、容器日志驱动 五、示例 5.1、查看容器中运行的进程的信息 5.2、查看…

SOCKET编程(4):SOCKET实战

SOCKET实战 Writen()、Readn()函数 send()函数存在需要发送的字符数len小于函数返回的已发送的字符数(ssize_t)的问题 recv()函数存在需要接收的字符数len小于函数返回的已接收的字符数(ssize_t)的问题 解决上述问题通过Writen()、Readn()函数实现 //buff是数据存储地址&a…

在Leaflet中点对象使用SVG和Canvas两种模式的对比

目录 前言 一、关于SVG和Canvas 1、SVG知识 2、Canvas知识 3、优缺点 二、SVG和Canvas在Leaflet的使用 1、相关类图 2、Leaflet的默认展示方式 三、SVG和Canvas实例及性能对比 1、SVG模式及性能对比 2、Canvas优化 总结 前言 众所周知&#xff0c;在Leaflet当中&#…