文章目录
1.内存和地址2.指针变量3.指针操作符3.1 取地址操作符 &3.2 解引用操作符 * 4.指针变量类型的意义4.1指针的解引用4.2指针 + - 整数4.3 void*指针 5.const 的修饰6.指针运算6.1指针 +- 整数6.2指针 - 指针 7.野指针7.1成因7.1.1未初始化7.1.2越界访问7.1.3指向的空间释放 7.2如何规避 8.assert 的断言9.指针的应用希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力!
这一篇 vlog 开始我们将对C语言中极其重要的章节——指针,进行全方位的深入理解,指针几乎占据了C语言大部分重要代码,学会了指针可以实现更多复杂有效的功能,博主将用5篇vlog带领读者们进行你从未感受过的全面细致指针理解之旅!
1.内存和地址
内存是用于暂时存储 CPU(中央处理器)正在处理的数据以及与硬盘等外部存储设备交换的数据的硬件设备。它充当了 CPU 和其他设备之间数据传输的中转站,使得计算机各个部件能够高效协同工作
我们在买电脑时,有 8GB/16GB/32GB等内存选择,读取数据和处理后的放回数据都是经过内存处理的,读取数据和放回数据也要找到相应的地址放回,电脑也像人一样,要进行高效的内存空间管理,所以电脑把每一个字节的空间作为一个内存单元
每个内存单元,就好比一间间酒店房间,每个房间能住 8 个比特位,房间(内存单元)都有一个门牌编号(地址),有了门牌号,就能快速找到相应的房间,即CPU能通过地址快速找到内存空间,在C语言中,给地址起了个名字叫指针
内存单元编号=地址=指针
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址,计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的,在硬件层面上就设计好了
2.指针变量
int a = 5
int * p = &p
这里的 int* 是变量 p 的类型,也就是一个整型指针,*就是说明 p 是一个指针变量,前面的 int 表示 p 指向的是整型类型的对象,p 中存放的是 a 的地址
注意指针变量是有大小的
• 32位平台下地址是32个bit位,指针变量大小是4个字节
• 64位平台下地址是64个bit位,指针变量大小是8个字节
• 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的
3.指针操作符
3.1 取地址操作符 &
通过取地址操作符取出的地址是一个数值,比如:0x004AAC78,取地址这一操作就是为了把地址取出放在指针变量中,方便后期使用
#include <stdio.h>int main() { int a = 10; int * pa = &a;//取出a的地址并存储到指针变量pa中 return 0; }
3.2 解引用操作符 *
那么我们把地址存储在指针变量后要如何将存放在里面的东西取出使用呢?
在知道地址的前提下,可以通过解引用操作符找到指针指向的对象
#include <stdio.h>int main(){ int a = 100; int* pa = &a; *pa = 0; return 0;}
pa 通过解引用找到 a 并将他的值改成 0 ,就好像通过门牌号找到特定酒店房间里的特定物品,并将其替换了,但这是就会产生一个疑惑,为什么不直接通过对 a 进行赋值改变它的值,不一定非要用指针吧,确实在当前情况下使用指针略显麻烦,但是在程序代码更加复杂,或者是不同的操作情景下,指针是一种妙用,后续的实例将会逐步深入理解指针的必要性和实用性
4.指针变量类型的意义
指针变量的大小与类型无关,只与操作平台有关,在同一平台下,大小都是一样的,那么指针类型的意义在哪儿呢?
4.1指针的解引用
#include <stdio.h>int main(){ int n = 0x11223344; int *pi = &n; *pi = 0; return 0;}
调试我们可以看到,代码会将n的4个字节全部改为0,如果把指针类型改成 char 呢?答案是代码只是将n的第⼀个字节改为0,这说明了指针类型决定了解引用的权限,也就是指针解引用的访问权限
4.2指针 + - 整数
#include <stdio.h> int main() { int n = 10; char *pc = (char*)&n; int *pi = &n; printf("%p\n", &n); printf("%p\n", pc); printf("%p\n", pc+1); printf("%p\n", pi); printf("%p\n", pi+1); return 0; }
%p用来打印地址,我们可以通过调试查看地址变化
由图显而易见,char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节
所以指针类型决定了指针加减整数时一步走多大距离
4.3 void*指针
void* 指针是一种特殊的指针,他没有特定的类型,不像int*、char那样指向特定的数据所在的内存地址,但也是有指向地址的,不过是对其指向的内容类型,或进行操作时的方式不明确,即void指针不能进行解引用操作和加减整数操作
报错:
#include <stdio.h>int main(){ int a = 10; int* pa = &a; char* pc = &a; *pa = 10; *pc = 0; return 0;}
改为:void*指针接收
#include <stdio.h>int main(){ int a = 10; void* pa = &a; void* pc = &a; *pa = 10; *pc = 0; return 0;}
通过调试可以发现 void指针可以接收不同类型的地址,但无法运算,所以void指针一般用于函数参数部分,用于接受不同类型数据的地址
5.const 的修饰
const是一个关键字,用于声明常量一旦一个变量被声明为const,它的值就不能被修改,const放在 * 的左边或右边有不同的意义
int * p 没有const修饰
int a = 10
int constp = &a const 放在 * 的左边做修饰
int const p = &a const 放在 * 的右边做修饰
const如果放在*的左边,修饰的是指针指向的内容
保证指针指向的内容不能通过指针来改变,但是指针变量本身的内容可变
即 a=10 这个值不能改变,变量 p 指向 a 可以改成指向别的变量
const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改
但是指针指向的内容,可以通过指针改变
即 a=10 这个值能改变,变量 p 指向 a 不可以改成指向别的变量
6.指针运算
6.1指针 ± 整数
比如打印数组可以用指针打印,更加清晰有效
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i<sz; i++) { printf("%d ", *(p+i));//p+i 这⾥就是指针+整数 } return 0; }
具体的分析在我“打印数组的多种方式”的一篇博客有提到
传送门:打印数组的多种方式
6.2指针 - 指针
代替 strlen 函数(计算字符或字符串长度),实现一个自定义的函数 my_strlen 来计算输入字符串的长度
#include <stdio.h>int my_strlen(char *s){ char *p = s; while(*p != '\0' ) p++; return p-s;}int main(){ printf("%d\n", my_strlen("abc")); return 0;}
当在 main 函数中调用 my_strlen(“abc”) 时,传给 my_strlen 函数的是字符串 “abc” 的首字符的地址
在 C 语言中,字符串常量(如这里的 “abc”)在内存中是以字符数组的形式存储的,并且会在末尾自动添加一个字符串结束标志 ‘\0’ ,当把字符串常量作为参数传递给函数时,实际上传递的就是这个字符数组的首元素(也就是首字符)的地址
在 my_strlen 函数内部,通过这个接收到的地址(形参 s),就可以从字符串的开头开始逐个访问字符,直到遇到字符串结束标志 ‘\0’,从而实现对字符串长度的计算
7.野指针
野指针是指程序中指向一块已释放内存或未初始化的内存区域的指针,指向的位置是随机的,不可知的
它可能指向任意的内存地址,这个随机地址就可能是系统正在使用的或者不允许访问的内存区域
7.1成因
7.1.1未初始化
#include <stdio.h>int main(){ int *p;//局部变量指针未初始化,默认为随机值 *p = 20; return 0;}
int * p 没有对应的地址存放,那他可能就会存放一个随机的地址
7.1.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;}
当循环执行到 i = 10 及之后时,指针 p 已经超出了数组 arr 的范围。数组 arr 在内存中是连续分配的一段空间,大小刚好能容纳 10 个整数。当 p 不断递增并超出这个范围后,它就指向了数组 arr 所占用内存空间之外的未知区域,此时 p 就变成了野指针
7.1.3指向的空间释放
这部分我们放在后续的动态内存部分讲解,目前知道这种情况会造成野指针形成即可
7.2如何规避
1.对指针变量都进行初始化操作
2.注意数组等变量的范围,小心指针越界
3.指针不使用时,及时置之为NULL空指针(定义用来表示指针不指向任何有效的内存地址,也就是指针为 空的情况,当一个指针被赋值为 NULL 时,意味着该指针当前没有指向任何有意义的对象或者内存区域)
4.不要返回局部变量的地址
8.assert 的断言
assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报
错终止运行,这个宏常常被称为“断言”
assert(p != NULL)
p 等于 NULL 程序终止运行,p 不等于 NULL 程序继续执行
使用assert有好有坏
好处是它不仅能自动标识文件和出问题的行号,还有⼀种无需更改代码就能开启或关闭 assert() 的机制,如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG
坏处是因为引入了额外的检查,增加了程序的运行时间
这里拓展一下不同版本的发布环境
Debug 称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序,程序员在写代码的时候,需要经常性的调试代码,就将这里设置为 Debug ,这样编译产生的是 Debug 版本的可执行序,其中包含调试信息,是可以直接调试的
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用,当程序员写完代码,测试再对程序进行测试,直到程序的质量符合交付给用户使用标准,这个时候就会设置为 Release ,编译产生的就是 Release 版本的可执行程序,这个版本是用户使的,无需包含调试信息等为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序
一般宏是在 Debug 中使用的,在VS环境下的 Release 版本不影响用户使用
9.指针的应用
这里介绍两个概念:传值调用和传址调用
传值调用:当调用一个函数并传递参数时,实际传递给函数的是参数值的副本,也就是说,函数内部对参数进行操作,不会影响到函数外部原来的变量值
传址调用:当调用一个函数并传递参数时,传递的是变量的地址(在一些语言中也可能表述为传递指向变量的指针等类似含义),这意味着函数内部通过该地址可以直接访问和操作函数外部的原始变量,对参数的任何修改都会反映到原始变量上
具体实例理解可以参考“交换变量的多种方法(面试题)”这篇博客
传送门:交换变量的多种方法(面试题)