C 语言指针详解(2)
写在前面
这里我默认你看完了 C 语言指针详解(1),
现在继续对指针进行深度剖析
1. 数组名的理解
|
输出结果如下图:
可以看见, 数组名其实是一个地址, 而且和数组首元素的地址相同.
但是在这种情况下例外:
- sizeof( 数组名 ), sizeof 里面单独放一个数组名, 表示的是整个数组, 计算的是整个数组的大小
- &数组名, 得到的是整个数组的地址, 是数组指针, 这个地址和数组首元素的地址相同, 但意义不同.
|
输出结果如下图:
arr
和arr + 1
相差4个字节, &arr[0]
和&arr[0] + 1
相差4个字节, &arr
和&arr + 1
相差20个字节.
小结: 数组名就是数组首元素的地址, 但除了以上两种特殊情况
2. 使用指针访问数组
有了以上知识的支撑, 结合数组的特点, 尝试使用指针访问数组.
|
本质上,
p[i]
等价于*(p + i)
, 同理arr[i]
等价于*(arr + i)
.
编译器在处理数组元素的访问的时候, 也是转换成首元素的地址 + 偏移量得到这个元素的地址, 再解引用访问.
看似[]
是个操作符, 但编译器是按上面的逻辑实现的.
*(arr+i)
==*(i+arr)
, 于是arr[i]
也可以写成i[arr]
,p[i]
也可以写成i[p]
.
3. 一维数组传参的本质
数组也是可以当作参数传递给函数的, 那函数内部能计算这个数组的元素个数吗?
|
程序在 x86 和 x64 环境下的运行结果有所不同, 原因是 x86 的地址是 32 位, 占 4 字节, x64 的地址是 64 位, 占 8 字节.
可以看到, 在函数内部无法获取到这个数组的元素个数!
数组名是数组首元素的地址, 传参时传递的是数组名, 也就是传递的是数组首元素的地址.
函数的形参会使用一个指针变量来接收这个地址, 并不会创建这个数组.
那么在函数内sizeof(arr)
计算的就是一个地址的大小而非数组的大小.
形参写成int arr[]
数组的形式,[]
里的数组元素的个数可写可不写, 编译器不会识别. 数组传参本质上是传指针
4. 二级指针
指针变量也是变量, 是变量就有地址, 如果把指针的地址再保存起来放进另一个指针变量里, 这个指针就是二级指针
|
可以看见指针
p
保存的是变量num
的地址, 即认为p
指向num
, 同理pp
指向p
. 此时pp
就是一个二级指针.
如何理解int** pp = &p;
?
int*
是指针指向元素的类型, 第二个*
表示pp
是个指针变量, 它保存的是指针p
的地址.
*pp
通过对pp
保存的地址解引用, 找到p
, 可以理解为*pp
就是在访问p
.**pp
先通过*pp
找到p
, 再对p
解引用, 可以理解为**pp
就是*p
, 就是访问num
.
5. 指针数组
类比字符数组和整型数组, 指针数组也就是存放指针的数组.
int* arr[10]
就是定义一个指针数组, 数组的元素个数是 10, 数组的元素类型是int*
.
指针数组的每个元素都是用来存放(地址)的, 每个元素又可以指向一块区域. 那我们用数组指针来模拟一下二维数组吧~
|
这段代码只是模拟二维数组的效果, 实际上并非完全是二维数组, 因为它每一行并非连续.
arr[i]
是在访问 arr 数组的第 i 个元素, 也就是第 i 个一维数组,
arr[i][j]
就是在访问第 i 个一维数组中第 j 个元素.
补充:arr[i]
, 相当于*(arr+i)
, 那arr[i][j]
就相当于*(*(arr + i) + j)
.
6. 字符指针变量
字符指针的类型是char*
, 有两种使用方式.
- 第一种:
char ch = 'H'; |
- 第二种:
const char* s = "Hello"; |
第二种并不是把
Hello
字符串放进字符指针s
里,而是把字符串的首地址放入了s
里.
Hello
是字符串常量(含\0
), 常量的本质是不占据任何存储空间, 属于指令的一部分, 编译后不再更改.
字符串中的字符依次存储在内存中一块连续的区域内,并且把空字符\0
自动附加到字符串的尾部作为字符串的结束标志.
故字符个数为n的字符串在内存中应占(n+1)个字节.
字符串常量与指针的关系:
在程序中,字符串常量会生成一个"指向字符的常量指针", 当一个字符串常量出现于一个表达式中时,表达式所使用的值就是这些字符所存储的地址,而不是这些字符本身.
例如:char a[5];a[0] = “a”;就是错误的,报错结果:invalid conversion from ‘const char*’ to ‘char’.
不能把字符串常量赋值给一个字符数组,因为字符串常量的直接值是一个指针,而不是这些字符本身。
例如:char a[10]=“love”,的意思就是用字符串“love”来初始化字符数组a的内存空间,而数组的首地址也就是“love”字符串的地址。
7. 数组指针变量
数组指针, 顾名思义就是指向数组的指针, 它存放的是数组的地址.
|
int (*p)[10]
是一个数组指针,p
先和*
结合, 说明p
是一个指针变量,
[10]
说明p
指向数组的元素个数,int
说明p
指向的数组的元素类型.
这个指针指向一个元素个数为 10 的整型数组. 称p
为数组指针.
[]
的优先级高于*
, 必须加括号来保证p
先和*
结合,
否则就成了int* p[10]
, 这是一个指针数组.
写到这里,小结一下吧~
int arr[10]
, 这是定义了一个整型一维数组, 数组名是arr
, 它的类型是int[10]
.int arr[3][4]
, 这是定义了一个整型二维数组, 数组名是arr
, 它的类型是int[3][4]
.int* arr[10]
, 这是定义了一个整型指针一维数组, 数组名是arr
, 它的类型是int*[10]
.int* arr[3][4]
, 这是定义了一个整型指针二维数组, 数组名是arr
, 它的类型是int*[3][4]
.int (*p)[10]
, 这是定义了一个指向元素个数为 10 的整型数组的数组指针p
, 它的类型是int[10]*
.int (*p)[3][4]
, 这是定义了一个指向元素个数为 3 行 4 列 的整型数组的数组指针p
, 它的类型是int[3][4]*
.- 上强度了, 来看看数组指针数组和指针数组指针
int (*arr[4])[3];
这是定义了一个数组指针数组, 写成int (*)[3] arr[4]
这样是不是好理解?但语法不支持!
首先, 数组指针数组是一个数组, 它的元素个数是 4, 元素类型是一个指向元素个数为 3 的整型数组的数组指针.
int (*arr[4])[3]
这个数组指针数组的类型是int[3]*[4]
.int* (*p)[10];
这是定义了一个指针数组指针,p
是指针变量名, 它指向一个元素个数为 10 的整型指针数组, 它的类型是int*[10]*
.
8. 二维数组传参的本质
二维数组本质上也是一个一维数组, 只不过每个元素是一个一维数组罢了. 可以理解为二维数组的首元素就是第一行, 是一维数组.
int arr[3][4] = { 0 };
这是一个二维数组, arr[0]
就是第一行的数组名, 也就是第一行数组首元素a[0][0]
的地址,
arr[0][0]
就是第一行数组的第一个元素. 可以理解为:*(arr[0]+0)
&arr[0]
是给第一行的数组名取地址, 得到的是第一行数组的地址.
又知道数组名是数组首元素的地址, 那么二维数组的数组名就是第一行的地址, 也就是一维数组的地址(指针).
二维数组传参本质上也是传递地址, 传递的是第一行一维数组的地址(数组指针)
理解了二维数组传参的本质后, 当二维数组传参给函数时, 形参可以写成数组, 也可以写成指针的形式.
9. 函数指针变量
类比数组指针, 不难看出: 函数指针是保存函数的地址的, 可以通过这个地址调用函数.
|
函数名就是函数的地址, & 函数名也可以获得函数的地址
要把函数的地址保存起来, 就得使用函数指针变量, 其实函数指针和数组指针类似
比如这个func
函数, void (*p)() = func;
就是用函数指针p
来保存这个func
函数的地址.
p
的类型是void(*)()
void
是函数的返回值类型, *
表示p
是个指针, ()
是函数的参数, 只需要给出参数的类型即可.
|
可以看到, 不论是否对函数指针解引用, 都可以访问这个函数.
那函数指针数组怎么写呢? int (*parr[3])();
就是这样啦,
parr
先和[3]
结合, 说明parr
是个数组, 数组的元素类型是int (*)()
类型的函数指针.
函数指针数组的用途: 转移表
一、实现菜单功能
在创建菜单系统时,函数指针数组非常有用。例如,一个简单的命令行菜单可能有多个选项,如“新建文件”“打开文件”“保存文件”等操作。每个选项对应一个特定的功能函数。
我们可以定义一个函数指针数组,数组中的每个元素指向一个不同的菜单功能函数。这样,当用户选择一个菜单选项时,我们可以根据选项的索引来调用对应的函数。
假设我们有以下函数声明:
void newFile();
void openFile();
void saveFile();
可以定义函数指针数组如下:
void (*menuFunctions[])(void)={newFile, openFile, saveFile};
当用户选择菜单选项 i (其中 i 是对应功能在数组中的索引)时,就可以通过 menuFunctionsi; 来调用相应的函数。
这种方式使得菜单的扩展非常容易。如果要添加一个新的菜单选项,只需要编写新的功能函数,并将其函数指针添加到函数指针数组中即可,不需要对菜单的调用逻辑进行大规模的修改。
二、事件处理机制
在图形用户界面(GUI)编程或者事件驱动编程中,函数指针数组也有广泛的应用。
当有多个不同类型的事件发生时,例如鼠标点击、键盘按键按下、窗口大小改变等,每个事件都需要有相应的处理函数。
我们可以创建一个函数指针数组,其中每个元素指向一个特定事件的处理函数。例如,在一个简单的GUI库中:
假设我们有事件类型 EVENT_MOUSE_CLICK 、 EVENT_KEY_PRESS 、 EVENT_WINDOW_RESIZE 等,以及对应的处理函数 void handleMouseClick() 、 void handleKeyPress() 、 void handleWindowResize() 。
可以定义函数指针数组 void (*eventHandlers[])(void)={handleMouseClick, handleKeyPress, handleWindowResize};
当一个事件发生时,根据事件的类型确定索引(假设事件类型有对应的整数值),然后调用 eventHandlersindex; 来处理该事件。
这种事件处理机制使得程序结构更加清晰,不同事件的处理逻辑相互独立,易于维护和扩展。如果要添加新的事件类型及其处理函数,只需要将新的函数指针添加到数组中即可。
三、算法选择
在一些需要根据不同情况选择不同算法的场景中,函数指针数组很方便。
比如在一个数值计算程序中,可能有多种排序算法可供选择,如冒泡排序、快速排序、插入排序等。
我们可以定义每个排序算法的函数,如 void bubbleSort(int arr[], int n); 、 void quickSort(int arr[], int n); 、 void insertionSort(int arr[], int n); 。
然后创建一个函数指针数组 void (*sortAlgorithms[])(int arr[], int n)={bubbleSort, quickSort, insertionSort};
根据用户的输入或者程序运行时的某些条件(例如数据规模的大小,小数据规模可能适合插入排序,大数据规模适合快速排序),选择函数指针数组中的某个函数指针来调用相应的排序算法,如 sortAlgorithms[index](array, size); ,其中 index 是根据条件确定的算法在数组中的索引。
这样的设计模式提高了程序的灵活性,可以方便地切换不同的算法而不需要大量修改程序的主要逻辑。
10. 回调函数
回调函数是一个通过函数指针调用的函数, 如果你把函数的指针(地址)作为参数传递给另一个函数, 当这个指针被用来调用其所指向的函数时, 被调用的函数就是回调函数.
回调函数不是由该函数的实现方直接调用, 而是在特定的事件或条件发生时由另外的一方调用, 用于对该事件或田间进行响应.
|
func
就是一个简易的回调函数, 当某个条件发生时, 由函数指针调用这个函数, 被调用的就是回调函数.
写在最后
本章干货较多, 建议小伙伴莫心急, 慢慢啃~