C语言底层学习(3.指针、函数与数组)(超详细)
- 🌈🌈🌈这里是say-fall分享,感兴趣欢迎三连与评论区留言
- 🔥🔥🔥专栏:《C语言入门知识点》、《C语言底层》
- 💪💪💪格言:今天多敲一行代码,明天少吃一份苦头
前言:
在之前发布的指针和数组的关系里,我们已经蛮详细地介绍了他们之间的关系,尤其是一维数组传参的本质,相信大家都已经比较熟悉了,那么对于更深的指针、函数和数组关系,我们本篇就来介绍一下
文章目录
- 前言:
- 正文:
- 1. 字符指针变量
- 2. 数组指针变量
- 3. 二维数组传参的本质
- 4. 函数指针变量
- 4.1 函数指针变量的初始化
- 4.2 函数指针数组
- 4.3 函数指针变量的使用
- 4.4 函数指针的简化
- 4.4.1 两段《C陷阱与缺陷》代码
- 4.4.2 typedef关键字
- 5. 转移表
- 5.1 什么是转移表?
- 5.2 转移表的应用
正文:
1. 字符指针变量
字符指针变量,很显然是字符的地址的变量
#include <stdio.h>int main()
{char ch = 'c';char* pch = &ch;printf("%p\n", pch);printf("%c\n", ch);printf("%c\n", *pch);return 0;
}
运行结果:
0000002D0AB8FC64
c
c
很明显我们就能看出来这种使用字符指针变量的方法,但其实还有一种方法来使用字符指针变量:
int main()
{const char* pch = "abcd";printf("%s\n",pch);printf("%p\n",pch);return 0;
}
abcd
00007FF602D4ACA4
这是一种标准的定义字符串常量的方式,这里的
pch
就是一个字符指针变量,00007FF602D4ACA4
就是他指向的地址
注意:打印字符串的时候使用的是pch
而不是*pch
,这是因为%s
打印的时候本身就要求传字符串首个元素的地址,一直读取到\0。而且在定义字符串的时候,pch
本身就是第一个元素的地址。
- 如图
为了防止大家混淆头晕,这里着重分辨一下这几种字符串相关的类型:
char cch[] = ['a','b','c','d'];//纯字符数组char ch[] = "abcdefg";//字符数组,等价于char ch [] = ["abcdefg"];//也等价于char ch [] = ['a','b','c','d','e','f','g','\0']char* pch = "abcdeff";//字符串的定义方式const char* pchh = "abcdeffg";//标准的字符串常量定义方式
这里来看一段代码,我们来看一下字符数组和字符串常量的区别
#include <stdio.h>
int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char *str3 = "hello bit.";const char *str4 = "hello bit.";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 and str2 are not same
str3 and str4 are same
做一下解释:记得我们之前提到过数组名是地址名,他们的地址不一样,只是里面的内容一致;字符串常量存储在只读数据段,如果是同样的内容,即使变量名不一样,判断出来的也是相同的。
2. 数组指针变量
指向整型的指针变量成为整形指针变量,指向字符的叫字符指针变量,所以指向数组的指针叫数组指针变量
继续类比一下:int*
整形指针变量类型;char*
字符指针变量类型;那么数组指针变量类型长什么样子呢?他们都为本来变量类型加*
所以数组指针变量类型为:int(*) []
,但是注意:
int (*p)[] //这是数组指针变量类型
int* p[] //这是指针数组变量类型
- 注意 !
int (*p)[]
一定要带(),其本质是*先和p结合,本质上是指针
int* p[]
不能带(),本质上是存放指针的数组,变量类型为:int* []
3. 二维数组传参的本质
一维数组传参的本质已经我们已经了解过了,对于数组名其实是首元素地址这件事我们也已经牢记于心了
那么接下来我们先来猜猜看二维数组用指针怎么表示吧:
//对于一维数组
int main()
{int i = 0;int arr[3] = { 0,1,2 };for (i = 0;i < 3;i++){printf("%d ", *(arr + i));}return 0;
}
0 1 2
猜测二维数组指针表示法,假设每次二维数组得到的也是地址:
//对于二维数组
int main()
{int i,j = 0;int arr[3][3] = { {0,1,2},{1,2,3},{2,3,4} };for (i = 0;i < 3;i++){for (j = 0;j < 3;j++){printf("%d ", *(*(arr + i))+j);}printf("\n");}return 0;
}
通过对第一个
(arr+i)
解引用得到第i个数组,再通过对(*(arr + i))+j
解引用得到第i个数组中的第j个数
也就是:
*(*(arr + i) + j) == arr[i][j];
需要注意的是:
*(arr + i) == arr[i];//这是解引用第i个元素地址
*arr + i == arr[0] + i;//这是在解引用第0个元素地址以后加上i
*(*(arr + i) + j) == arr[i][j];//这是解引用第i个数组首元素地址以后解引用其的第j个元素
*(*(arr + i)) + j == arr[i][0];//这是解引用第i个数组首元素地址以后解引用其的首个元素然后加j
所以一定要注意分辨括号的位置
4. 函数指针变量
4.1 函数指针变量的初始化
还还还还是和其他指针变量一致,函数指针变量就是指向函数的地址的指针嘛
经典去掉名称就是原变量类型,再加*就是指针变量类型
void test()
{printf("hello~");
}int main()
{printf("%p\n", test);printf("%p\n", &test);return 0;
}
00007FF6825F1159
00007FF6825F1159
可以看得出来,对于函数来说,他和数组类似,函数名就是他的地址。
那我们把他存进指针咯:
void test()
{printf("hello~");
}int main()
{printf("%p\n", test);printf("%p\n", &test);void (*p1_test)() = &test;void (*p2_test)() = test;printf("%p\n", p1_test);printf("%p\n", p2_test);return 0;
}
00007FF61F841159
00007FF61F841159
00007FF61F841159
00007FF61F841159
完全一致
其中我们注意到对于
test()
这个函数的指针变量类型是void (*)()
其实就是去掉函数名而加*
记得加括号!!!
4.2 函数指针数组
这样我们认识到了函数指针变量,既然是变量,我们一般就能把它储存在数组里,以便使用
还是来研究他的变量类型写法
数组:int* arr = []
指针数组储存的是int*
试试把int*
换成函数指针变量类型int (*) ()
是哪一种?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是
parr1
parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢?
是int (*)()
类型的函数指针。
4.3 函数指针变量的使用
已知函数名是地址,而指针也是地址 ==》用指针代替函数名试试:
int Add(int x, int y)
{return x + y;
}int main()
{int (*p_add)(int, int) = Add;printf("%d\n", Add(2, 3));printf("%d\n", p_add(2, 3));return 0;
}
5
5
好像完全一致,试验成功。
- 函数指针完全可以代替函数
那,有什么用呢?
4.4 函数指针的简化
4.4.1 两段《C陷阱与缺陷》代码
代码一
(*(void (*)())0)();
- 剥洋葱
最右端括号的空括号说明他是一个函数的调用,就像我们刚才看到的
(*p2_test)();
这样的话就说明(void (*)())0
是一个指针,很显然,0是地址。
前面的(void (*)())
是一个变量类型,那这就是一个强制类型转换了
所以这段代码的意思就是:将0这个地址的东西强制转换为(void (*)())
形式的指针然后调用这个指针指向的函数
代码二
void (*signal(int , void(*)(int)))(int);
- 剥洋葱
和上面同理嘛,先是最外侧函数
void (*)(int);
声明
然后呢里面是一个地址signal(int , void(*)(int))
,很显然signal
是一个函数名(也是地址)
那里面就是他的参数了呗,所以它的参数是int
和void(*)(int)
所以这段代码意思就是:声明一个函数signal
,它的返回值是void (*)(int)
类型的函数指针,参数是int
和void(*)(int)
。
4.4.2 typedef关键字
我们看完上面这两个代码时常会想:好麻烦啊,有没有什么方法可以让这些代码看上去简单易懂呢?
还真能有:typedef
关键字
比如说:写无符号整型unsigned int
好长好麻烦
typedef unsigned int uint;
//把unsigned int 重新定义为uint,以后uint就是无符号整型的意思
uint a = 3;
这样的话,所有类型都能重命名了
只不过需要注意的是,函数指针变量的typedef
是这样的:
typedef void(*funct)();//新的类型名必须在*的右边
他的重新定义要在里面,包括数组指针类型也是
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
这两段代码就能简化为:
typedef void(*funct)();
typedef void(*funct_int)(int);int main()
{(*((funct)0))();// 等价于==> (*(void (*)())0)(); 调用funct_int signal(int, funct_int);// 等价于==> void (*signal(int, void(*)(int)))(int); 声明return 0;
}
5. 转移表
5.1 什么是转移表?
转移表(Jump Table) 是一种通过函数指针数组实现的分支控制结构,用于替代复杂的switch-case或if-else语句,提高多分支场景下的执行效率和代码可读性。
5.2 转移表的应用
我们先来看一个模拟计算器的代码
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;
}void mune()
{printf("*************************\n");printf("******** 1.add *******\n");printf("******** 2.sub *******\n");printf("******** 3.mul *******\n");printf("******** 4.div *******\n");printf("******** 0.exit *******\n");printf("*************************\n");
}int main()
{int input = 0;do{int x, y, ret = 0;mune();printf("please enter your choice:\n");scanf("%d", &input);switch (input){case 1:{printf("please enter two oprands,separated by a apace:\n");scanf("%d %d", &x, &y);ret = add(x,y);printf("%d\n", ret);break;}case 2:{printf("please enter two oprands,separated by a apace:\n");scanf("%d %d", &x, &y);ret = add(x, y);printf("%d\n", ret);break;}case 3:{printf("please enter two oprands,separated by a apace:\n");scanf("%d %d", &x, &y);ret = add(x, y);printf("%d\n", ret);break;}case 4:{printf("please enter two oprands,separated by a apace:\n");scanf("%d %d", &x, &y);ret = add(x, y);printf("%d\n", ret);break;}case 0:{printf("exit the program!\n");break;}defult:{printf("enter error!please re-enter!\n");break;}}}while (input);return 0;
}
通过这段代码可以实现简单的计算问题
但是我们不难发现,这代码也太长了吧,而且很多重复的地方
这个时候就可以用转移表来简化它:
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;
}void mune()
{printf("*************************\n");printf("******** 1.add *******\n");printf("******** 2.sub *******\n");printf("******** 3.mul *******\n");printf("******** 4.div *******\n");printf("******** 0.exit *******\n");printf("*************************\n");
}int main()
{int input = 0;int x, y, ret = 0;int (*(arr[5]))(int x, int y) = { 0,add,sub,mul,div };//创建一个函数指针变量来存放这些函数do{mune();printf("please enter your choice:\n");scanf("%d", &input);if (input > 0 && input < 5){printf("please enter two oprands,separated by a apace:\n");scanf("%d %d", &x, &y);ret = (*arr[input])(x, y);//通过对函数指针数组的应用,我们就能简化这段代码printf("%d\n", ret);}else if(input == 0){printf("exit the program!\n");}else{printf("enter error!please re-enter!\n");}}while (input);return 0;
}
- 本节完…