【C语言】深入理解指针(1)

发布于:2025-08-30 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

一、内存和地址

1.内存和地址的引入

2.计算机中的内存划分

3.内存的编址问题

4.小结

二、地址和指针变量(重要)

1.使用取地址操作符&得到变量的地址

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

1)指针变量(重要)

2)解引用操作符*(重要)

3)自问自答

3.指针变量的大小

1)结论

2)自问自答

4.指针变量的用法

1)void*类型的指针

①自问自答

2)const修饰指针变量

3)指针变量加减整数

4)指针变量减指针变量

5)指针变量大小比较

5.野指针

1)野指针成因

①指针变量未初始化

②指针变量越界访问

③指针变量保存的地址空间被释放

2)如何避免野指针

①使用指针变量之前初始化

②不要越界访问

③指针变量不用后及时置为NULL,使用前检查

④使用assert断言

6.使用指针变量实现传址调用


一、内存和地址

1.内存和地址的引入

你要去你朋友住的楼栋里寻找TA,如果不知道TA住的具体位置只能挨个敲门去找,而如果你知道了TA住的房间号,那么就能简单直接地找到TA了

数据在内存中的存储也是同理

内存可以类比成楼栋,

数据存放的地址(也称作指针)可以类比成房间号

数据可以类比成朋友,

通过房间号可以直接找到房间里的朋友

通过指针可以直接访问到该地址下的数据

2.计算机中的内存划分

CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中

计算机把内存划分为一个个的内存单元(房间),每个内存单元可以存放1字节的数据

1byte = 8bit

1KB = 1024byte

1MB = 1024KB

(一个十六进制数占4bit,即半个byte)

......

每个内存单元也都有一个编号(这个编号就相当于房间号),有了这个内存单元的编号(如0XFFFFFFFF,同时可以看出示例的内存系统是32位的),CPU就可以快速找到任意一个内存空间

我们可以理解为:内存单元的编号==地址==指针

3.内存的编址问题

CPU与内存进行交互是通过总线进行的

  • CPU先通过控制总线告诉内存它要读数据还是写数据
  • 然后通过地址总线选择要读/写的地址
  • 最后再由数据总线进行数据的读/写

今天重点说明地址总线

CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,因为内存中字节很多,所以需要给内存进行编址

我们可以简单理解,32位机器有32根地址总线,每根线只有高低电平两态用于表示1和0

32根地址线,就能表示2^32种含义,每一种含义都能代表一个地址,从而实现了对内存单元的编址

地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器

需要注意的是:

程序里面涉及的地址是虚拟地址,要想和内存中的物理地址联系起来,需要通过操作系统和硬件的复杂协同来真正对应起来

4.小结

  • 内存会被划分为一个个的内存单元,每个内存单元大小都是1字节
  • 每个内存单元都有自己的编号,也就是地址,也就是指针,编号==地址==指针

二、地址和指针变量(重要)

1.使用取地址操作符&得到变量的地址

再次说明:

  • 地址==编号==指针

变量创建的本质是在内存中开辟一块空间

例如

地址==编号==指针

用取地址操作符&就可以得到所创建变量在虚拟内存中的最低位地址(地址==编号==指针),

如果想要将结果显示出来,需要借助地址类型的变量(如本例当中的int*)将得到的地址存储起来,

然后使用%p进行地址类型的格式输出

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

1)指针变量(重要)

专门用于存放指针(地址)的变量叫做指针变量(地址)

我们通过取地址操作符&拿到变量的地址后有时候需要存储起来,这时候需要使用指针变量(也可以叫地址变量,或者编号变量)这一个容器将地址存放起来

指针变量也是一种变量,这种变量是专门用来存放地址的,锤子的眼中都是钉子,存放在指针变量中的值都会被当作地址

需要存放什么类型变量的地址就创建该类型的指针变量

  • 存放int类型变量的地址使用int*创建指针变量
  • 存放char类型变量的地址使用char*创建指针变量
  • 指针变量既然作为一个变量,那么在虚拟内存中当然也有自己的地址,如果需要存放int*类型变量的地址,那么就用int**创建存放指针变量地址的指针变量
  • 以此类推

指针变量的创建格式为 数据类型*  变量名,int*就是变量类型

例如

2)解引用操作符*(重要)

我们将地址保存起来,未来是要使用的,通过解引用操作符(也称为间接引用操作符)*可以得到指针变量存储的地址下的具体数据

例如

  • p是int*类型的指针变量,存放的是a的地址
  • 将p解引用,即*p,相当于抽丝剥茧,利用p中存放的地址找到原变量a
  • 所以 *p==a,对*p1的操作就是对a的操作

  • p存放的是a的地址,
  • *p表示通过p存放的地址找到地址具体对应的变量

3)自问自答

为什么不直接操作a而要通过p1找到a的地址后再*p解引用简介操作a?

类比于高启强和老莫,有些时候高启强不好直接出面做一些事情,需要借老莫之手完成

不直接操作a,而是借用*p间接操作a

3.指针变量的大小

1)结论

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

同理64位机器,假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要 8个字节的空间,指针变的大小就是8个字节。

  • 指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的
  • 32位平台下地址是32个bit位,指针变量大小是4个字节
  • 64位平台下地址是64个bit位,指针变量大小是8个字节

2)自问自答

既然在相同的平台下指针变量的大小都是一样的为什么不同类型的变量地址需要使用不同类型的指针变量存储?

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

比如 char* 的指针变量解引用就只能访问一个字节(正好是一个字符的大小),而 int* 的指针变量解引用就能访问四个字节(正好是一个整型的大小)。

4.指针变量的用法

1)void*类型的指针

当我们用char*类型的指针变量保存int类型的地址编译器会提示警告

而使用void*类型的指针变量保存int类型的地址则不会有兼容性的警告

但如果使用解引用void*类型的指针变量会发生报错

我们可以看到,void* 类型的指针可以接收不同类型的地址,但是无法直接进行指针运算

①自问自答

那么 void* 类型的指针到底有什么用呢?

⼀般 void* 类型的指针使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据,具体实现后续再讲

2)const修饰指针变量

前面提到过,一个变量使用const修饰后将会被保存在静态区,变成只读变量,无法被修改

如果我们绕过n,使用n的地址去修改n就能实现对n的修改

但是我们思考⼀下,n被const修饰就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,应该让p拿到n的地址也不能修改n,所以我们使用const将指针变量进行约束

  • const在*的左边,修饰内容包括*和变量p,此时p保存的地址可以被修改,但保存的地址中的数据不能被修改
  • const在*的右边,修饰内容只有变量p,此时p保存的地址不能被修改,但保存的地址中的数据可以被修改

小结

  • 所以如果是不想修改p保存的地址就把const放在*右边
  • 如果不想修改p保存的地址中的值就把const放在*左边

3)指针变量加减整数

我们可以看出:

  • char*类型的指针变量解引用只能访问1个字节(正好是一个字符的大小),而char* 类型的指针变量+1跳过的也是1个字节
  • int*类型的指针变量解引用能访问4个字节(正好是一个整型的大小),而int* 类型的指针变量+1跳过的也是4个字节

指针的类型决定了指针向前或者向后走一步有多大,这就是指针变量的类型差异带来的变化

因为数组在内存中的地址是连续的,所以我们可以利用这一特性遍历数组

4)指针变量减指针变量

日期 + 天数 == 另一个日期

日期 - 天数 == 另一个日期

日期 - 日期 == 天数

没有日期 + 日期、日期*日期、日期/日期的运算

可以将指针类比为日期,整数类比为天数

指针 + 整数 == 另一个指针

指针 - 整数 == 另一个指针

指针 - 指针 == 两个指针之间的元素个数

没有指针 + 指针、指针*指针、指针/指针的运算

5)指针变量大小比较

指针变量可以比较大小,且存放的地址高的指针变量比存放的地址低的指针变量大

5.野指针

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

1)野指针成因

①指针变量未初始化

②指针变量越界访问

③指针变量保存的地址空间被释放

2)如何避免野指针

①使用指针变量之前初始化

如果明确知道指针指向哪里就直接赋值地址

如果不知道指针应该指向哪里,可以给指针赋值NULL

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

②不要越界访问
③指针变量不用后及时置为NULL,使用前检查
④使用assert断言

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

例如

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

assert() 因为引入了额外的检查,所以增加了程序的运行时间

一般我们可以在 Debug 中使用,在 Release 版本中选择禁用 assert 就行,在 VS 这样的集成开发环境中,Release 版本直接就是优化掉了assert

这样在debug版本写有利于程序员排查问题,在Release 版本中不影响用户使用时程序的效率

6.使用指针变量实现传址调用

我们知道函数在调用时一般变量的传递方法是传值调用,即主函数内的变量是实参,被调用函数内部使用的变量是形参,形参只是将实参的值进行了拷贝,两处的变量有各自的空间,形参的变化不会影响到实参

以两数的交换为例:

如果想要函数操作的就是主函数内的变量,那么就需要使用传址调用,即将a和b的地址传给函数作为参数,这时两处的变量就是同一处的变量,在函数内部对变量的操作就能影响到主函数中的变量


网站公告

今日签到

点亮在社区的每一天
去签到