【C语言】深度剖析指针(二):指针与数组,字符,函数的深度关联
文章目录
- 一、数组与指针的关联
- 1. 数组名的理解
- 1.1 数组名即首元素地址
- 1.2 数组名的特殊情况
- 2.使用指针访问数组
- 2.1 指针访问数组的方式
- 2.2 指针与数组下标的等价性
- 3. 一维数组传参的本质
- 3.1 数组传参的误区
- 3.2 形参的写法
- 4. 冒泡排序
- 4.1 基本实现
- 4.2 优化版本
- 5. 二级指针
- 5.1 二级指针的定义与使用
- 5.2 二级指针的运算
- 6. 指针数组
- 二、字符,函数与指针的关联
- 1. 字符指针变量
- 经典笔试题分析
- 2.数组指针变量
- 2.1 数组指针的定义
- 2.2 数组指针的初始化
- 3. 二维数组传参的本质
- 4. 函数指针变量
- 4.1 函数指针的创建
- 4.2 函数指针的使用
- 4.3 复杂代码解析
- 5. 函数指针数组
- 6. 转移表:函数指针数组的应用
- 传统实现(switch版)
- 转移表实现(函数指针数组版)
一、数组与指针的关联
1. 数组名的理解
在大多数情况下,数组名代表的是数组首元素的地址,但存在两个特殊情况。
1.1 数组名即首元素地址
通过代码测试可以发现,数组名和数组首元素的地址打印结果完全一致。例如:
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};printf("&arr[0] = %p\n", &arr[0]);printf("arr = %p\n", arr);return 0;
}
输出结果中,&arr[0]
和arr
的地址相同,这表明数组名就是数组首元素的地址。
1.2 数组名的特殊情况
sizeof(数组名)
:此时数组名表示整个数组,计算的是整个数组的大小,单位为字节。
例如int arr[10]
,sizeof(arr)
的结果为40(因为每个int型元素占4字节,10个元素共40字节)。&数组名
:这里的数组名也表示整个数组,取出的是整个数组的地址。虽然&arr
与arr
、&arr[0]
打印出的地址值相同,但含义不同。&arr + 1
会跳过整个数组arr + 1
和&arr[0] + 1
只会跳过一个元素。
对于前两组,每次跳过一个元素,即4字节。第三组跳过十六进制的28,即十进制下的40字节,也就是一个数组的长度。
2.使用指针访问数组
数组中的元素在内存中存放是连续的。借助指针可以方便地访问数组元素,其核心原理是利用指针的偏移来获取数组元素的地址,再通过解引用操作访问元素。
2.1 指针访问数组的方式
arr[i] == *(p+i)
#include <stdio.h>
int main()
{int arr[10] = {0};int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);int* p = arr;// 输入for(i = 0; i < sz; i++){scanf("%d", p + i);}// 输出for(i = 0; i < sz; i++){printf("%d ", *(p + i));//可以写成arr[i],p[i],或*(arr+i)}return 0;
}
在上述代码中,p + i
表示数组第i个元素的地址,*(p + i)
则是该元素的值。
2.2 指针与数组下标的等价性
p[i]
本质上等价于*(p + i)
,数组元素的访问在编译器处理时,会被转换为首元素地址加偏移量的形式。同样,arr[i]
等价于*(arr + i)
。
3. 一维数组传参的本质
数组传参时,本质上传递的是数组首元素的地址,而非整个数组。
3.1 数组传参的误区
如果想要打印数组所有元素:
输出结果只有1。打开调试我们发现sz的值为1.
事实上,在函数内部无法通过sizeof(arr) / sizeof(arr[0])
来计算数组的元素个数。数组名是首元素的地址,因为此时arr
作为函数参数,实际是一个指针变量,sizeof(arr)
计算的是指针的大小。
int sz = sizeof(arr) / sizeof(arr[0]);
// 4 4
3.2 形参的写法
一维数组传参时,形参可以写成数组形式(如int arr[]
),也可以写成指针形式(如int* arr
),二者本质上是一致的。
注意:
- 数组传参是、本质上是传递了数组首元素的地址,因此形参访问数组的和实参是同一个数组。
- 正因为形参与实参是同一数组,所以形参的数组不会再单独创建数组空间。形参的大小可以省略。
void my_printf(int arr[])//这样的写法也是正确的
正确写法:
4. 冒泡排序
常见的排序算法有:冒泡排序,插入排序,选择排序,选择排序,快速排序,希尔排序,堆排序等
冒泡排序的核心思想是两两相邻的元素进行比较,不符合排序要求则交换位置,重复此过程直到整个数组有序。
4.1 基本实现
void bubble_sort(int arr[], int sz)
{int i = 0;for(i = 0; i < sz - 1; i++){int j = 0;for(j = 0; j < sz - i - 1; j++){if(arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}
4.2 优化版本
为了提高效率,可添加一个标志位,当某一趟排序中没有元素交换时,说明数组已经有序,可提前结束排序。
void bubble_sort(int arr[], int sz)
{int i = 0;for(i = 0; i < sz - 1; i++){int flag = 1; // 假设本趟有序int j = 0;for(j = 0; j < sz - i - 1; j++){if(arr[j] > arr[j + 1]){flag = 0; // 发生交换,说明无序int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}if(flag == 1) // 本趟无交换,数组已有序break;}
}
5. 二级指针
指针变量也是变量,它也有地址,存放指针变量地址的变量就是二级指针。
5.1 二级指针的定义与使用
#include <stdio.h>
int main()
{int a = 10;int* p = &a; // p是一级指针,指向aint** pp = &p; // pp是二级指针,指向p// 通过二级指针修改a的值**pp = 30; // 等价于*p = 30,也等价于a = 30return 0;
}
5.2 二级指针的运算
*pp
:对二级指针解引用,得到的是一级指针p
。**pp
:先通过*pp
得到p
,再对p
解引用,得到的是变量a
。- 32位环境下,
p+1
跳过四个字节,pp+1
跳过四个字节
6. 指针数组
指针数组是一种数组,其每个元素都是指针(地址)。
-
概念:例如
int* arr[5]
,定义了一个包含5个元素的数组,每个元素都是int*
类型的指针,可用来存放int型变量的地址。 -
指针数组模拟二维数组 :利用指针数组可以模拟二维数组的效果,虽然其物理存储并非真正的二维连续空间,但逻辑上可以当作二维数组来访问。
arr1
,arr2
,arr3
都是数组名,是数组首元素的地址,相当于三个指针。把这三个指针存放在arr
数组中,arr
就是一个指针数组。这样可以模拟出一个二维数组。
#include <stdio.h>
int main()
{int arr1[] = {1,2,3,4,5};int arr2[] = {2,3,4,5,6};int arr3[] = {3,4,5,6,7};int* arr[3] = {arr1, arr2, arr3}; // 指针数组,每个元素指向一个一维数组for(int i = 0; i < 3; i++){for(int j = 0; j < 5; j++){printf("%d ", arr[i][j]); // 类似二维数组的访问方式,本质上是指针的运算}printf("\n");}return 0;
}
上述代码中,arr[i][j]
等价于*(parr[i] + j)
,通过指针数组的元素(指向一维数组的指针)加上偏移量来访问相应元素。
二、字符,函数与指针的关联
1. 字符指针变量
字符指针(char*
)是指向字符或字符串的指针,其使用方式主要有两种:
- 指向单个字符
char ch = 'w';
char *pc = &ch; // 存储单个字符的地址
*pc = 'w'; // 通过指针修改字符
- 指向字符串
const char* pstr = "abcdef"; // 存储字符串首字符地址
- 观察以下两种字符串写法:
char arr[10] = "abcdef";//字符数组
char* p = arr;
char* p = "abcdef";//常量字符串
注意:
- 第一种写法是字符数组,数组内容可以改变。第二种是字符串常量,不可以变动。
- 第二种写法并非将整个字符串存入指针,而是将常量字符串
"abcdef"
的首字符'a'
的地址存入p
。 - C/C++会将常量字符串存储在单独的内存区域,多个指向同一常量字符串的指针会指向同一块内存。
- 整型数组打印需要使用
for
循环打印,而字符数组或者字符串常量只需要提供首字符的地址,也不需要解引用。占位符应使用%s
经典笔试题分析
题目:指明输出结果
#include <stdio.h> int main() {char str1[] = "hello world.";char str2[] = "hello world.";const char *str3 = "hello world.";const char *str4 = "hello world.";if(str1 ==str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n");if(str3 ==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0; }
str1[]
和str2[]
是字符数组,都会在内存中申请空间。str3
和str4
是字符串常量。相同的字符串常量没必要保存两份,因为字符串常量本身就是不可改变的。所以str3
和str4
共同指向同一个字符串
运行结果:
结论:用相同常量字符串初始化数组时,会开辟不同内存;而指针指向常量字符串时,会共享同一块内存。
2.数组指针变量
2.1 数组指针的定义
数组指针是指向数组的指针,而非指针数组(数组中存放指针)。其定义形式为:
int (*p)[10]; // p是数组指针,指向包含10个int的数组
- 括号确保
p
先与*
结合,表明是指针;[10]
表示指向的数组有10个元素;int
表示数组元素类型。 - 类型:
int(*) [10]
数组指针。 - 指针类型决定了+整数跳过几个字节。
p+1
跳过十六进制下的28,也是就十进制下的40字节。
2.2 数组指针的初始化
通过&数组名
获取数组的地址(区别于数组名本身表示的首元素地址):
int arr[10] = {0};
int (*p)[10] = &arr; // 存储数组的地址
调试可见,&arr
与p
的类型完全一致(均为int(*)[10]
)。
3. 二维数组传参的本质
过去我们习惯写的二维数组传参:
void print(int arr[3][5],int r,int c)
{for (int i = 0; i < r; i++){for (int j = 0;j < c;j++){printf("%d ", arr[i][j]);}printf("\n");}
}int main()
{int arr[3][5] = { { 1,2,3,4,5 }, { 2,3,4,5,6 },{ 3,4,5,6,7 } };print(arr, 3, 5);return 0;
}
二维数组可看作“数组的数组”,其首元素是第一行的一维数组。因此,二维数组传参本质是传递第一行一维数组的地址形参可写成两种形式:
- 二维数组形式
void test(int a[3][5], int r, int c) { ... }
- 数组指针形式
void test(int (*p)[5], int r, int c) {//(*p)表明是 p指针,int[5]是指向的类型// 通过指针访问元素:*(*(p+i)+j) 等价于 a[i][j]for(i=0; i<r; i++)for(j=0; j<c; j++)printf("%d ", *(*(p+i)+j));
}
4. 函数指针变量
4.1 函数指针的创建
函数也有自己的地址,可以通过函数名或者&函数名
打印地址。在这里二者是完全相同的。
函数指针用于存储函数的地址,函数名或&函数名
均可表示函数地址:
void test() { printf("hehe\n"); }// 函数指针定义
void (*pf1)() = &test; // 存储test函数的地址
void (*pf2)() = test; // 等价于上面的写法
char* test(char c,int n)
{//...
}
int main()
{char* (*pf) (char,int) = test;
// | |
// 函数的返回类型 参数类型和个数
}
指针类型: char* (*) (char,int)
4.2 函数指针的使用
通过函数指针调用函数:
int Add(int x, int y) { return x + y; }int main() {int(*pf)(int, int) = Add;printf("%d\n", (*pf)(2, 3)); // 输出5printf("%d\n", pf(3, 5)); // 输出8(*可省略)return 0;
}
函数名与&函数名都可以表示函数地址,所以*
写与不写都可以调用函数指针
4.3 复杂代码解析
代码1:
( *( void (*) () ) 0 )();
- 先从
void (*p)()
入手,这是一个函数指针,去掉变量名void(*)()
就是它的类型。 (类型)
是强制类型转换,( void (*) () ) 0
这里也就是把0转为函数指针类型,这就意味着假设在0地址处,放置一个无参,返回类型为void
的函数。- 最终解引用调用这个函数
代码2:
void ( *signal( int , void (*) (int) ) ) (int);
void (*) (int)
是函数指针类型。signal
是一个函数,参数为int
和函数指针void(*)(int)
- 把
signal
函数部分省略掉,得到void(*)(int)
。 ,就是signal
函数的返回类型。这句代码是函数声明。 - 可以理解为:
void(*)(int) signal(int,void(*)(int));//但是这样的写法是错误的,必须把函数名与*方在一起
typedef
类型重定义:
将复杂的类型重新定义一种简化形式,例如:
typedef unsigned int u_int;//用 u_int 表示 unsigned int
对于数组指针:int (*p)[5]
p 的类型是int (*)[5]
,但类型名应与*
放在一起,合起来写:
typedef int (*parr)[5] //用 parr表示 int (*)[5]
代码2 可通过typedef
类型来简化:
typedef void(*pfun_t)(int);pfun_t signal(int, pfun_t); // 简化后
5. 函数指针数组
函数指针数组是存放函数指针的数组,定义形式为:
int (*parr[10])(int, int); // 存放 10个int(*)(int,int)类型的函数指针
举个例子:
int add(int x, int y){return x + y;}
int sub(int x, int y){return x - y;}
int mul(int x, int y){return x * y;}
int div(int x, int y){return x / y;}int main()
{int (*pf1)(int, int) = add;//pf1是函数指针变量int (*pfarr[])(int, int) = { add,sub,mul,div };//pfarr是函数指针数组//用法:for (int i = 0; i < 4; i++){pfarr[i](8,4);//依次调用四个函数}return 0;
}
6. 转移表:函数指针数组的应用
转移表通过函数指针数组简化多分支逻辑(如switch
语句)。以计算器为例:
传统实现(switch版)
switch(input) {case 1: ret = add(x, y); break;case 2: ret = sub(x, y); break;// ...
}
转移表实现(函数指针数组版)
int(*p[5])(int x, int y) = {0, add, sub, mul, div}; // 转移表// 调用时直接通过下标访问
ret = (*p[input])(x, y); // input为1~4时对应加减乘除
转移表减少了代码冗余,使逻辑更清晰,扩展性更强。