嵌入式Day7
一、指针基础概念
- 指针与指针变量
- 指针:某1个字节虚拟内存空间的唯一性标识,即地址。
- 指针变量:用于存储指针(其它变量地址)的变量。日常说的指针常指指针变量,但两者含义不同。
- 指针变量特性:本身是变量,有自己的地址;存储的数据是其它变量的地址,通过该地址指向并操作其它变量,实现对内存的操作。32位平台占4个字节,64位平台占8个字节。
- 指针变量声明语法
- 语法格式:数据类型 *指针名。
- 注意事项:声明中的数据类型代表指针指向对象的数据类型,指针不能乱指;指针名是普通变量名,是运算符,非指针名一部分,建议指针名带“p/ptr”等字眼;声明时要紧跟指针名,避免歧义,建议每行只声明一个变量,除非多个变量类型一致且作用相似。
- C语言变量声明建议
- 尽量每行只声明一个变量,除非多个变量类型一致且作用相似。
- 将*紧跟指针名,如“int *ptr;”,明确变量为指针类型。
- 为变量添加有意义的注释,尤其是变量名无法完全描述其用途时。
- 使用描述性和有意义的变量名,注重代码的清晰性和可读性。
二、指针变量的初始化
- 常见初始化方式:用变量名做取地址&运算,然后赋值给指针变量。使用指针访问内存需两次,使用变量名是直接访问内存。
- 空指针初始化:使用空指针NULL给指针变量初始化,NULL代表地址值0,是指针类型变量的默认零值。空指针不能解引用操作,是指针不可用的标志。
- 野指针:指向随机、未定义内存区域的指针,常见于局部变量指针仅声明未初始化的情况。解引用野指针会带来不可预知的后果,程序中要竭力避免,若不确定指针初始化值,可先初始化为NULL。
三、指针类型作为参数传递
- 通过指针传参交换变量值:在C语言函数调用中,传参局部变量的指针可以跨函数修改局部变量取值,符合值传递本质;直接传参基本数据类型不能通过函数调用修改实参数据。
- 指针传参时指针指向的修改问题:传参指针变量在函数内部不能修改原始指针的指向,因为函数内部得到的是实参指针变量的副本。若想修改局部变量指针的指向,需要传参指针的指针,即二级指针。
- 二级指针的操作:二级指针解引用1次可修改原始一级指针的指向,解引用2次可修改原始一级指针指向的数据。若想跨函数修改二级指针的指向,需要传参二级指针的指针,即三级指针。
四、指针和数组名之间的关系
- 数组名的本质与特性:数组名是代表数组在内存中起始地址(基地址、首元素地址)的标识符。多数情况下,数组名可视为指向首元素的指针,但本质并非指针,而是代表整个数组,不能直接用“=”赋值,数组间也不能直接用“=”赋值。
- 数组名不代表首元素指针的场景
&(数组名)
:获取的是指向整个数组的指针(数组指针),虽地址值与首元素地址相同,但指针类型不同,前者为数组指针类型,后者为普通元素类型指针(如int*
)。sizeof(数组名)
:得到的是数组所占内存空间总长度。不过,当数组作为参数传递后,函数内部的数组名在sizeof
操作中会变成指针。
- 数组名可以视为首元素指针的常见场景
- 初始化指针变量:可直接用数组名初始化指针变量,如
int *p = arr;
。 - 数组作为函数参数:数组作为参数传给函数时,函数内部得到的数组名会“退化”为首元素指针。
- 指针算术运算:数组名可直接进行指针算术运算,此时被当作首元素指针使用。
- 初始化指针变量:可直接用数组名初始化指针变量,如
五、数组指针与指针数组
- 数组指针:本质是指针,指向整个数组变量。声明方式如
int (*ptr)[3];
,(*)
使*
运算符先与变量名结合,整体表示一个指向长度为3的int
类型数组的指针变量。 - 指针数组:本质是数组,数组元素类型为指针类型。声明方式如
int *arr[3];
,[]
运算优先级高,先与变量名结合,此数组名为arr
,存储元素类型为int*
,数组长度为3。在C语言中,字符串数组常以指针数组形式实现。
六、取地址数组名和取地址数组首元素/数组名的区别
- 相同点:
&arr
(指向整个数组的数组指针)和&arr[0]
(指向数组第一个元素的指针,数组名在多数情况也视为首元素指针,故&arr[0]
和arr
意思相近 )存储的地址值相同,均为数组第一个元素的地址值。 - 不同点:指针类型不同,
&arr
得到的指针类型是数组指针类型(如int(*)[3]
),&arr[0]
得到的指针类型是单纯的int
类型指针(int*
)。
七、指针类型转换的问题
给指针变量赋值时,使用“=”应尽量保证左右值指针类型一致,避免隐式类型转换。若类型不匹配,可使用强制类型转换,如int *p2 = (int*) &arr;
。
八、把数组作为参数传递
- C语言数组传参特点:C语言数组传参不遵循典型值传递原则,数组名传参给函数后会自动“退化”为首元素指针,函数内部得到的是实参数组名首元素指针的“副本”。
- 优点:传参效率高,无需复制整个原始数组,节省空间;利用首元素指针副本可修改原始数组内容;传参类型更灵活,只要类型相同,不同长度数组可一起处理。
- 缺点与解决方法:数组传参后,函数内部无法用传参的数组名获取数组长度,使用
sizeof(数组名)/sizeof(数组名[0])
是错误做法,因为此时数组名是首元素指针,sizeof(数组名)
结果为指针变量大小(32位平台4字节,64位平台8字节)。函数内部获取数组长度只能通过外部函数调用传参,任何传参数组的C语言函数都必须额外新增数组长度参数。 - 函数内部操作数组元素:数组传参后,在函数内部仍可用“[]”取下标运算符操作数组元素,“
arr[i]
”中的数组名arr
此时已是首元素指针,“[]”本质是指针运算。
九、求数组元素的最值
需求是在main
函数获取数组的最大值和最小值,可通过get_max_min
函数实现。该函数声明为void get_max_min(int arr[], int len, int *pmax, int *pmin);
,其中pmax
和pmin
分别为main
函数中存储最大值和最小值的int
变量的指针。函数内部假设第一个元素为最大最小值,遍历数组进行比较和更新。在main
函数中,需传参数组、数组长度以及存储最值的变量指针。
十、Constant Pointer vs Pointer to Constant
- 概念理解:建议记英文“Constant Pointer”(自身是常量的指针,不可改变指向)和“Pointer to Constant”(不可以通过指针修改指向内容的指针 )。
const
修饰指针的形式与辨析const
修饰指针类型有const int *p;
、int const *p;
、int *const p;
三种形式。当const
直接紧跟指针名时,修饰指针本身,为“Constant Pointer”,指针不可改变指向;当const
不紧跟指针名时,修饰指向的内容,为“Pointer to Constant”,不可通过指针修改指向内容。- 普通指针可修改指向和指向内容;
const
修饰*p
时,指针指向内容不可修改,但指针可修改指向;const
直接修饰指针名时,指针指向不可修改。实际编程中,const
大多修饰“*p
”,常见形式为const int *p = &a;
。
十一、指针的算术运算
指针变量支持的运算包括指针加法、减法、自增自减、两个指针相减以及比较大小,其他运算不合法。
- 指针加上一个整数:指针加上整数
n
,是在原地址基础上增加该指针指向数据类型大小乘以n
个字节。 - 指针减去一个整数:指针减去整数
n
,即在原地址基础上减去该指针指向数据类型大小乘以n
个字节。 - 取下标运算符的原理:取下标
[]
运算符的底层原理是指针算术运算和解引用运算。arr[i]
等价于*(arr + i)
,[]
运算符隐藏了这些底层操作,是一种“语法糖”。由于加法交换律,arr[i]
还等价于i[arr]
,但实际编程中不建议使用i[arr]
这种形式。 - 指针自增和自减:使用
++
或--
对指向数组元素的指针进行操作时,自增使指针指向下一个元素,自减使指针指向前一个元素。自增和自减操作具有副作用,而单纯的加减操作没有副作用。 - 两个指针之间做减法:两个指向同一数组(或连续内存块)元素的指针相减,返回的是它们之间的元素数差,而非实际字节差。
- 指针判等:通过判断两个指针存储的地址值是否相同,来确定两个指针是否指向同一个对象,常用于判断指针是否为空指针
NULL
。 - 指针比较大小:比较的是指针中存储的地址值大小。在数组场景中,如果两个指针都指向同一数组的不同元素,可通过比较大小确定哪个指针指向的元素在数组中的位置更靠前。
十二、指针运算的注意事项
- 避免野指针:运算前确保指针已正确初始化,防止出现野指针。
- 避免对NULL指针运算:使用指针前检查是否为
NULL
,存在为NULL
的可能时先进行空指针检查。 - 确保操作范围合法:在数组内进行指针算术运算时,运算结果的指针必须指向数组内部合法位置,否则可能导致越界访问、产生野指针和未定义行为。
- 限制运算类型:指针只支持加法、减法,不支持乘法、除法、取模运算;两个指针不能相加;不同类型的指针不应混合运算。
- 指针相减条件:两个指针做减法时,必须指向同一个数组或连续内存块,否则结果未定义。
十三、*p++
结构
*p++
结合了解引用运算符和自增运算符。后缀++
优先级高于解引用运算符*
,先返回指针p
原本的地址,再让指针p
指向下一个元素;*p++
整体先返回指针p
原本指向元素的取值,同时使p
指针指向下一个元素。
十四、遍历数组的方式
- 利用指针遍历数组:创建临时指针指向数组首元素,通过指针自增遍历数组。
- 利用索引遍历数组:使用数组下标遍历数组,这种方式更推荐。
十五、二维数组与数组指针分析
- 二维数组与数组指针关系:对于二维数组
int matrix[5][5]
,matrix
可视为首元素指针,其首元素是第一个长度为5的int
类型一维数组,matrix
是指向该数组的指针,即数组指针,从地址值看,它指向元素1;从指针类型看,是int[5] *
类型,声明格式为int (*matrix)[5]
。 - 复杂表达式计算:以
*(*(matrix + 2) +3)
为例,逐步分析指针运算和解引用操作。 - 二维数组作为参数传递时函数声明
void test(int arr[][5], int len);
:len
表示二维数组的长度(行长,即一维数组的个数),5
是二维数组的列长(不能省略)。void test2(int (*arr)[5], int len);
:直接使用数组指针类型声明参数,与上述形式等价。这条消息已经在编辑器中准备就绪。你想如何调整这篇文档?请随时告诉我。