C语言底层学习(3.指针、函数与数组)(超详细) - 详解

  • 这里是say-fall分享,感兴趣欢迎三连与评论区留言
  • 专栏:《C语言入门知识点》、《C语言底层》
  • 格言:今天多敲一行代码,明天少吃一份苦头

前言:

在之前发布的指针和数组的关系里,我们已经蛮详细地介绍了他们之间的关系,尤其是一维数组传参的本质,相信大家都已经比较熟悉了,那么对于更深的指针、函数和数组关系,我们本篇就来介绍一下



正文:

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");
  else
  printf("str1 and str2 are not same\n");
  if(str3 == str4)
  printf("str3 and str4 are same\n");
  else
  printf("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是一个函数名(也是地址)
那里面就是他的参数了呗,所以它的参数是intvoid(*)(int)
所以这段代码意思就是:声明一个函数signal,它的返回值是void (*)(int)类型的函数指针,参数是intvoid(*)(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;
}
  • 本节完…
posted on 2025-09-26 17:49  ljbguanli  阅读(68)  评论(0)    收藏  举报