6、指针
指针
指针简介
从根本上看,指针(porinter)是一个值为内存地址的变量(或数据对象)。正如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。
查找地址:&运算符
一元运算符&给出变量的存储地址。如果pooh是变量名,那么&pooh就是pooh的地址。可以把地址看作变量在内存中的位置
int pooh = 3;
// 打印pooh的地址
printf("%p\n", &pooh);
间接运算符:*
间接运算符(indirection operator)也可以加解引用运算符(dereferencing operator)。注意不要把间接运算符和二元乘法运算符混淆,虽然使用的符号相同,但是语法功能不同
可以获取地址指向的值
int pooh = 3;
// &pooh是指向pooh的地址,使用*运算符获取&pooh地址指向的值
printf("%d\n", *&pooh);
声明指针
声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。
另外,程序必须知道储存在指定地址上的数据类型。例如:long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭
int *pi; // 指向int类型变量的指针
char *pc; // 指向char类型变量的指针
float *pf; // 指向float类型变量的指针
类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。int *pi;
声明的意思是pi是一个指针,*pi是int类型
指针可进行的操作
- 赋值:可以把地址赋值给指针
- 解引用:*运算符给出指针指向地址上存储的值
- 取址:和所有变量一样,指针变量也有自己的地址和值
- 指针与整数相加:整数和指针所指向类型的大小(以字节为单位)相乘,再与初始地址相加
- 递增指针:指向数组元素的指针可以让指针移动到数组的下一个元素
- 指针减去一个整数:指针必须是第1个运算对象,整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。
- 递减指针:除了递增指针还可以递减指针。
- 指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。
- 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int urn[5] = { 100, 200, 300, 400, 500 };
int* ptr1, * ptr2, * ptr3;
ptr1 = urn; // 把一个地址赋值给指针
ptr2 = &urn[2]; // 把一个地址赋值给指针
// 解引用指针,以及获得指针的地址
printf("指针值, 解引用指针, 指针地址:\n");
printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
// 指针加法
ptr3 = ptr1 + 4;
printf("\n指针加上整数:\n");
printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
ptr1++; // 递增指针
printf("\nptr1自增后的值:\n");
printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
ptr2--; // 递减指针
printf("\nptr2自减后的值:\n");
printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
--ptr1; // 恢复为初始值
++ptr2; // 恢复为初始值
printf("\n指针被重置为初始值:\n");
printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
// 一个指针减去另一个指针
printf("\n一个指针减去另一个指针:\n");
printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
// 一个指针减去一个整数
printf("\n一个指针减去一个整数:\n");
printf("ptr3 = %p, ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
return 0;
}
// 运行结果
/*
指针值, 解引用指针, 指针地址:
ptr1 = 00AFFBCC, *ptr1 = 100, &ptr1 = 00AFFBC0
指针加上整数:
ptr1 + 4 = 00AFFBDC, *(ptr1 + 4) = 500
ptr1自增后的值:
ptr1 = 00AFFBD0, *ptr1 = 200, &ptr1 = 00AFFBC0
ptr2自减后的值:
ptr2 = 00AFFBD0, *ptr2 = 200, &ptr2 = 00AFFBB4
指针被重置为初始值:
ptr1 = 00AFFBCC, ptr2 = 00AFFBD4
一个指针减去另一个指针:
ptr2 = 00AFFBD4, ptr1 = 00AFFBCC, ptr2 - ptr1 = 2
一个指针减去一个整数:
ptr3 = 00AFFBDC, ptr3 - 2 = 00AFFBD4
*/
指针和一维数组
数组名就是数组元素的首地址
int arr[3] = {0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
// arr与&arr[0]都表示数组首元素的地址
指针的自增与自减
地址按字节编址,short类型占用2字节,double类型占用8字节。
在C中,指针加1指的是增加一个存储单元。
对数组而言,这意味着加1后的地址是下一个元素的地址,而不是下一个字节的地址。
这是为什么必须声明指针所指向对象类型的原因之一。
只知道地址不够,因为计算机要知道储存对象需要多少字节
指针加1,指针的值递增他所指向类型的大小(以字节为单位)
int arr[4] = {0};
// arr与&arr[0]都表示同一个地址
// (arr + 1)与arr[1]也表示同一个地址
printf("arr=%p, &arr[0]=%p\n", arr, &arr[0]);
printf("arr+1=%p, &arr[1]=%p\n", arr + 1, &arr[1]);
// 运行结果
/*
arr=000000000062FE10, &arr[0]=000000000062FE10
arr+1=000000000062FE14, &arr[1]=000000000062FE14
*/
也就是说,定义 arr[n]
的意思是 *(arr + n)
。可以解释为:到内存arr的位置,然后指针移动 n 个单位,检索在那里的值
注意:不要混淆 *(arr+2)
和 *arr + 2
,间接运算符的优先级高于+,
*(arr + 2) == arr[2];
*arr + 2 == arr[0] + 2;
数组作为函数参数
函数需要处理数组,就需要获取数组的首地址和数组范围
-
函数的有两种方式接收数组的首地址(以接收int类型的数组为例)
-
int sum(int *arr, int len);// 指针表示法
-
int sum(int arr[], int len);// 数组表示法,让函数处理数组的意图更加明显
int *arr 和 int arr[] 都表示arr是指向int类型的指针。但是int arr[] 可以明确表明要接收int类型的数组
-
-
函数也有两种方式接收数组的范围(函数实现的功能是将数组中的所有数相加)
-
// 声明 int sum(int arr[], int len);// 将数组的长度len传进来 // 定义 int sum(int arr[], int len) { int count = 0; for(int i = 0;i < len;i++) { count += arr[i]; } return count; }
-
// 声明 int sum(int arr[], int *end);// 将指向数组最后一个元素的指针传入进来 // 定义 int sum(int arr[], int *end) { int count = 0; while(arr < end) { count += *arr; arr++; } return count; }
-
保护数组中的数据
函数在处理数组时,必须传递指针,因为效率高。
但是传递地址会导致一些问题,如果在函数中修改了数组的值,那么原数组也会受到影响。假设不想再函数中意外的修改了数组的值,就对形参使用 const
const修饰过的变量都会被编译器视为常量,而常量是不可修改的
// 声明
int sum(const int arr[], int len);
// 定义
int sum(const int arr[], int len)
{
// arr[0] = 1; // 禁止修改数组中的值
int count;
for(int i = 0;i < len;i++)
{
count += arr[i];
}
return count;
}
指针和多维数组
指向多维数组的指针
以二维数组为例
int arr[3][4];
/*
arr:二维数组首元素的地址(每个元素都是内含4个int类型元素的一维数组),等价于:arr[0]
arr+2:二维数组的第3个元素(即一维数组)的地址,等价于:arr[2]
*(arr+2):二维数组的第3个元素(即一维数组)的首元素(一个int类型的值)的地址,等价于:&arr[2][0]
*(arr+2)+1:二维数组的第3个元素(即一维数组)的第2个元素(也是一个int类型的值)的地址,等价于:&arr[2][1]
*(*arr+2)+1):二维数组的第3个元素(即一维数组)的第2个int类型元素的值,即数组的第3行第2列的值,等价于:arr[2][1]
*/
上述的验证代码如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
printf("arr = %p\narr[0] = %p\n", arr, arr[0]);
printf("\narr + 2 = %p\narr[2] = %p\n", arr + 2, arr[2]);
printf("\n*(arr + 2) = %p\n&arr[2][0] = %p\n", *(arr + 2), &arr[2][0]);
printf("\n*(arr + 2) + 1 = %p\n&arr[2][1] = %p\n", *(arr + 2) + 1, &arr[2][1]);
printf("\n*(*(arr + 2) + 1) = %d\narr[2][1] = %d\n", *(*(arr + 2) + 1), arr[2][1]);
return 0;
}
// 运行结果
/*
arr = 006FF770
arr[0] = 006FF770
arr + 2 = 006FF790
arr[2] = 006FF790
*(arr + 2) = 006FF790
&arr[2][0] = 006FF790
*(arr + 2) + 1 = 006FF794
&arr[2][1] = 006FF794
*(*(arr + 2) + 1) = 10
arr[2][1] = 10
*/
声明指向二维数组的指针
声明一个指针变量pz指向一个二维数组(如,zippo[3][2]
)
在编写处理类似 zippo 这样的二维数组时会用到这样的指针。把指针声明为指向int的类型还不够。因为指向int只能与 zippo[0]的类型匹配,说明该指针指向一个 int 类型的值。但是 zippo 是它首元素的地址,该元素是一个内含两个 int 类型值的一维数组。因此,pz必须指向一个内含两个 int 类型值的数组,而不是指向一个 int 类型值,其声明如下:
int(* pz)[2]; //pz 指向一个内含两个int 类型值的数组
以上代码把pz声明为指向一个数组的指针,该数组内含两个int 类型值。为什么要在声明中使用圆括号?因为[]的优先级高于*。考虑下面的声明:
int * pax[2];// pax是一个内含两个指针元素的数组,每个元素都指向 int 的指针
由于[]优先级高,先与pax结合,所以pax成为一个内含两个元素的数组。然后*表示 pax数组内含两个指针。最后,int 表示pax 数组中的指针都指向 int 类型的值。
因此,这行代码声明了两个指向 int的指针。而前面有圆括号的版本,*先与pz结合,因此声明的是一个指向数组(内含两个int类型的值)的指针。
示例代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int arr[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12} };
int(*parr)[4] = arr;
printf("parr = %p, arr = %p\n", parr, arr);
printf("parr[0][0] = %d, arr[0][0] = %d\n", parr[0][0], arr[0][0]);
return 0;
}
// 运行结果
/*
parr = 012FF928, arr = 012FF928
parr[0][0] = 1, arr[0][0] = 1
*/
虽然 parr 是一个指针,不是数组名,但是也可以使用parr[2][1]
这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:
arr[m][n] == *(*(arr+m)+ n)
parr[m][n] == *(*(parr + m)+ n)
多维数组作为函数参数
定义如下二维数组:int arr[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
,将他传入函数中,返回数组中所有元素相加的值
-
使用指针表示法
int sum(int (*parr)[4] ); // 该指针指向包含4个int类型元素的一维数组
-
使用数组表示法
int sum(int parr[][4] ); // parr[] 等价于 *parr
注意:
-
int sum(int parr[][]); // 错误的声明 /* 编译器会把数组表示法转换成指针表示法,例如:把parr[1]转换为parr+1,所以编译器要知道parr所指向的对象大小 */
-
int sum(int parr[3][4]); // 有效的声明,但是编译器会忽略掉第一个中括号的值
-
一般而言,声明一个指向N维数组的指针时,只能省略最左边中括号中的值
int sum(int parr[][3][4][5]); // 第一个中括号表示这是一个指针,而其他中括号用于描述指针所指向数据对象的类型 int sum(int (*parr)[3][4][5]); // 与上面的函数声明是等价的
变长数组
c规定,数组的维数必须是常量,也就是中括号内的数,只能是常量,不能用变量
如果想要创建一个能处理任意大小二维数组的函数,比较繁琐(必须把数组作为一维数组传递,然后让函数计算每行的开始处)。
鉴于此,C99新增了变长数组(variable-length array,VLA),允许使用变量表示数组的维度。如下
int row = 4;
int col = 3;
double VLA_arr[row][col];
变长数组有一些限制。变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符。而且,不能在声明中初始化它们。最终,C11把变长数组作为一个可选特性,而不是必须强制实现的特性。(vs也不支持变长数组)
注意:变长数组不能改变大小
变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度。
函数中使用变长数组
声明一个带二维变长数组参数的函数
int sum(int row, int col, int VLA_arr[row][col]); // VLA_arr是一个变长数组
注意:前两个形参(row和col)用作第3个形参二维数组 VLA_arr 的两个维度。因为 VLA_arr 的声明要使用row和col,所以在形参列表中必须在声明 VLA_arr 之前先声明这两个形参。
因此,下面的原型是错误的
int sum(int VLA_arr[row][col],int row,int col); // 无效的顺序
C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
int sum(int,int,int ar[*][*]); // ar是一个变长数组(VLA),省略了维度形参名
变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小。普通数组都是静态内存分配,即在编译时确定数组的大小。由于数组大小是常量,所以编译器在编译时就知道了
复合字面量
可以理解为匿名对象
字面量是除了符号常量外的常量。例如:5是int类型字面量;3.14是double类型的字面量;‘Y' 是char类型的字面量;“hello” 是字符串字面量
发布C99标准的委员会认为,如果有代表数组和结构内容的复合字面量,在编程时会更方便
数组复合字面量初始化
// 普通的数组声明
int diva[2] = {10, 20};
// 复合字面量创建一个和diva数组相同的匿名数组
(int [2]){10, 20}; // 去掉声明中的数组名,留下的int[2]即是复合字面量的类型名
// 初始化有名数组时可以省略数组大小,复合字面量也可以省略大小
(int []){3,4,5}; // 编译器会自动计算数组当前的元素个数
因为复合字面量是匿名的,所以不能先创建然后再使用他,必须在创建的同时使用他
-
使用方法一:使用指针记录地址
// int类型指针 int *pt; // 复合字面量的类型名也代表数组首元素的地址,所以可以把他赋给int指针 pt = (int [2]){1,2};
这种用法也适用于多维数组,以二维数组为例
// 声明一个指向二维数组的指针 int (*pt)[3]; pt = (int [2][3]{ {1,2,3}, {4,5,6} });
-
使用方法二:将复合字面量作为实参传递给函数。把数组传入函数时可以不用先创建数组
// 函数原型 void sum(const int arr[], int len); // (int []){1,2,3,4} 是一个含有4个int类型元素的复合字面量数组 sum((int []){1,2,3,4}, 4);
这种用法也适用于多维数组,以二维数组为例
// 函数原型 void sum(const int arr[][3], int row); // (int [2][3]) 就是该复合字面量的类型,即2*3的int数组 sum((int [2][3]){ {1,2,3}, {4,5,6} }, 2);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY