目录
2.2.3 解引用操作符
一.内存和地址
在讲内存和地址之前,我们先讲一个生活中的案例: 假设有⼀栋宿舍楼,把你放在楼⾥,楼上有100个房间,但是房间没有编号,你的⼀个朋友来找你玩, 如果想找到你,就得挨个房子去找,这样效率很低,但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号,有了房间号,如果你的朋友得到房间号,就可以快速的找房间,找到你。
如果把上⾯的例⼦对照到计算机中,又是怎么样呢? 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的 数据也会放回内存中,那我们买电脑的时候,电脑上内存是 8GB/16GB/32GB 等,那这些内存空间如 何高效的管理呢? 其实也是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节。 计算机中常见的单位(补充): ⼀个比特位可以存储⼀个2进制的位1或者0
其中,每个内存单元,相当于⼀个学生宿舍,一个字节空间里面能放8个比特位,就好比同学们住 的八人间,每个人是⼀个比特位。 每个内存单元也都有⼀个编号(这个编号就相当 于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。 生活中我们把门牌号也叫地址,在计算机中我们 把内存单元的编号也称为地址。C语言中给地址起了新的名字:指针
所以我们可以理解为: 内存单元的编号 == 地址 == 指针
二.指针变量和地址
2.1取地址操作符&
理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:
比如,上述的代码就是创建了整型变量a,内存中 申请4个字节,用于存放整数10,其中每个字节都有地址,上图中4个字节的地址分别是:
那我们如何能得到a的地址呢? 这里就得学习⼀个操作符(&)-取地址操作符
变量在内存中的存储
按照我画图的例子,会打印处理:006FFD70 &a取出的是a所占4个字节中地址较小的字节的地 址。 虽然整型变量占用4个字节,我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。
2.2指针变量和解引用操作符(*)
2.2.1指针变量
那我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x006FFD70,这个数值有时候也是需要 存储起来,方便后期再使用的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中
比如:
指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
2.2.2指针变量的类型
整型指针(Integer Pointer):
指向整数的指针。在C语言中,int
类型的指针使用 int*
声明。
浮点型指针(Floating-Point Pointer):
指向浮点数的指针,包括 float
和 double
类型的指针。float
类型的指针使用 float*
声明,double
类型的指针使用 double*
声明。
字符指针(Character Pointer):
通常用于指向字符串(字符数组的首地址)。在C语言中,字符串是以字符数组的形式存储的,并以空字符 \0
结尾。char
类型的指针使用 char*
声明。
结构体指针(Structure Pointer):
指向结构体变量的指针。结构体是C语言中用户自定义的数据类型,可以包含多个不同类型的成员。结构体指针的声明方式是在结构体类型名后加上星号(*)。
指针的指针(Pointer to Pointer):
也被称为二级指针,它是指向指针的指针。这种指针用于处理指针数组或者动态分配指针等场景。
数组指针(Pointer to Array):
这种指针实际上是指向数组首元素的指针,但是在声明时指定了数组的大小。不过,在大多数情况下,我们讨论的是指向数组首元素的指针,即普通指针。
函数指针(Function Pointer):
指向函数的指针。函数指针在C语言中非常有用,尤其是在回调函数、中断处理函数等场景中。
2.2.3 解引用操作符
我们将地址保存起来,未来是要使用的,那怎么使用呢? 在现实生活中,我们使⽤地址要找到⼀个房间,在房间里可以拿去或者存放物品。 C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这里必须学习⼀个操作符叫解引⽤操作符(*)。
上⾯代码中第7行就使用了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0
2.3 指针变量的大小
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
三. 指针变量类型的意义
3.1 指针的解引⽤
对比,下⾯2段代码,主要在调试时观察内存的变化。
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。 结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。 比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。
3.2 指针+-整数
先看⼀段代码,调试观察地址的变化
我们可以看出,char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可以-1。
结论:指针的类型决定了指针向前或者向后走⼀步有多大(距离)。
3.3 void* 指针
在指针类型中有⼀种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指 针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针的+-整数和解引用的运算。
举例:
在上⾯的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警 告(如下图),是因为类型不兼容。而使用void*类型就不会有这样的问题。
那么void*类型的指针有哪些用处呢?
⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果,使得⼀个函数来处理多种类型的数据。
在C语言中,void*
指针因其能够指向任意类型的数据的特性,常被用于实现泛型编程(或称为泛型编程的原始形式,因为C语言本身并不直接支持现代意义上的泛型编程)。泛型编程允许编写与数据类型无关的代码,增加了代码的复用性和灵活性。下面通过一个简单的例子来说明 void*
指针在泛型编程中的应用。
在这个例子中,bubble_sort
函数是一个通用的排序函数,它接受一个 void*
类型的数组、数组中的元素数量、每个元素的大小以及一个比较函数作为参数。这个函数通过 char*
指针和偏移量来访问和交换数组中的元素,这是因为它不知道元素的确切类型。compare_ints
函数是一个具体的比较函数,用于比较两个整数。
四.const修饰指针
4.1 const修饰变量
在C语言中,const
关键字用来修饰变量,表示这个变量是一个常量,即它的值在初始化之后就不能被修改了。你可以把 const
看作是一个“保证书”,它告诉编译器和阅读代码的人:“这个变量一旦被赋予了一个值,就不要再改变了。”
这里,MAX_SIZE
是一个常量,它被初始化为100。在程序的后续部分,你不能修改 MAX_SIZE
的值,比如你不能写 MAX_SIZE = 200;
,因为编译器会报错,说你试图修改一个常量。
使用 const
有几个好处:
- 提高代码的可读性:读者可以一眼看出哪些变量是不应该被修改的。
- 提高代码的安全性:防止意外修改重要数据,减少错误。
- 有助于编译器优化:因为编译器知道
const
变量的值不会变,所以它可以更高效地处理这些变量。
需要注意的是,虽然 const
变量不能被修改,但指向 const
变量的指针是可以被修改的(即指针本身可以指向不同的地址),只是不能通过这个指针去修改它所指向的 const
变量的值。另外,还有一种指针类型,即指向 const
的指针(const *
),这种指针指向的值不能通过它修改,但指针本身可以指向其他地址(如果它不是指向 const
的指针的指针)。
4.2 const修饰指针变量
⼀般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不⼀样的。
1. const
在 *
左侧(const *
)
当 const
放在 *
左侧时,它表示指针所指向的内容是常量,即不能通过这个指针来修改它所指向的数据。但是,指针本身是可以修改的,即它可以指向另一个地址。
在这个例子中,ptr
是一个指向 const int
的指针,意味着你不能通过 ptr
来修改 a
的值,但是 ptr
可以重新指向 b
。
2. const
在 *
右侧(* const
)
当 const
放在 *
右侧时(注意这里通常需要先有一个指针类型声明,比如 int *
,然后在整个声明后加 const
,或者更常见的是通过类型别名来声明),它表示指针本身是常量,即指针的指向不能改变,但是它所指向的内容是可以修改的(当然,如果所指向的内容本身是常量,则不能修改)。
然而,直接写成 int * const ptr;
是最常见的形式,表示 ptr
是一个指向 int
的常量指针。
在这个例子中,ptr
是一个指向 int
的常量指针,意味着你不能改变 ptr
指向的地址(即它必须一直指向 a
),但是你可以通过 ptr
来修改 a
的值。
结论:const修饰指针变量的时候
• const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变。
• const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变。
五. 指针运算
5.1 指针与整数的加减
当指针与整数进行加减运算时,实际上是在对指针所指向的内存地址进行偏移。偏移的字节数等于整数乘以指针所指向类型的大小。
在这个例子中,ptr
是一个指向 int
的指针。由于 int
类型的大小通常是 4 个字节(这取决于编译器和平台),所以 ptr++
会使 ptr
指向下一个 int
类型的内存地址,即地址增加了 4 个字节。类似地,ptr += 2;
会使 ptr
指向 arr
数组中索引为 2 的元素之后的元素,即索引为 3 的元素。
5.2 指针之间的减法
两个指向同一数组(或同一块连续内存区域)元素的指针可以进行减法运算,结果是一个整数,表示两个指针之间相隔的元素数量(而不是字节数)。
在这个例子中,ptr2 - ptr1
的结果是 3,表示 ptr2
和 ptr1
之间相隔了 3 个 int
类型的元素。
注意
- 指针的加减运算只适用于指向同一数组(或同一块连续内存区域)的指针。
- 指针的加减运算结果不是简单的地址算术运算,而是基于指针所指向类型的大小进行的偏移。
- 指针之间通常不进行乘法和除法运算,这些运算没有定义的意义。
- 指针运算时需要注意不要越界访问内存,否则可能导致未定义行为,如程序崩溃。
六.野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 指针未初始化
当指针变量被创建后,如果没有被初始化,它将包含一个随机的内存地址。这个地址很可能是无效的,指向了未知的内存区域。尝试通过这个指针进行读写操作将是不安全的。
在这个例子中,p
是一个未初始化的指针,它可能指向任何随机的内存地址。尝试通过 *p = 10;
写入数据是非常危险的。
6.2 指针越界访问
当指针指向数组时,如果指针的偏移超出了数组的实际范围,那么指针就变成了野指针。
在这个例子中,虽然 p
最初指向 arr
的有效内存,但在循环中,当 i
等于 4 时,p
已经超出了 arr
的范围,成为了野指针。
6.3 释放后的指针未置为NULL
当使用 free
或 delete
(在C++中)释放指针所指向的内存后,指针本身的值并不会自动变为 NULL
。如果之后不将指针置为 NULL
,而继续使用该指针,它将成为野指针。
在这个例子中,虽然 p
所指向的内存已经被释放,但 p
的值仍然是之前分配的内存地址。如果后续代码不小心再次使用 p
,就会尝试访问已经释放的内存,这是不安全的。
如何避免野指针
- 初始化指针:在定义指针的同时初始化它,要么指向有效的内存地址,要么设置为
NULL
。 - 检查指针的有效性:在使用指针之前,检查它是否为
NULL
。 - 避免越界访问:确保指针的偏移不会超出它所指向的内存区域的范围。
- 释放内存后置为NULL:释放指针所指向的内存后,立即将指针置为
NULL
。
七.传值调用和传址调用
7.1 传值调用(Call by Value)
在传值调用中,函数的参数是调用者提供的值的副本。函数内部对参数所做的任何修改都不会影响到函数外部的原始数据。
在这个例子中,modifyValue
函数试图修改其参数 x
的值,但这个修改只影响到了 x
这个局部变量的副本,对 main
函数中的 a
没有任何影响。
7.2 传址调用(通过指针实现)
在C语言中,没有直接的传址调用机制,但我们可以通过传递指针来实现类似的效果。当传递一个指针给函数时,函数接收的是指向原始数据的内存地址的副本。但通过这个地址,函数可以访问并修改原始数据。
在这个例子中,modifyAddress
函数接收一个指向整数的指针 p
。通过解引用这个指针(*p
),函数能够访问并修改指针所指向的原始数据(即 main
函数中的 a
)。因此,当 modifyAddress
函数执行完毕后,main
函数中的 a
的值已经被修改为 10。
总结:
传值调用中,函数接收的是参数的副本
传址调用(在C语言中通过传递指针实现)中,函数接收的是指向原始数据的指针,可以通过这个指针访问并修改原始数据。
后言
觉得文章有帮助的小伙伴们点点赞,点点关注哦
共勉!