深析C语言的灵魂 -- 指针(2)

简介: 深析C语言的灵魂 -- 指针(2)

4、数组参数、指针参数

我们在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?下面我们来探讨这个问题。

一位数组传参

void test(int arr[]) {};
# true 数组传参,用数组接受
void test(int arr[10]) {};
# true 数组传参,用数组接受,数组元素个数可写可不写
void test(int* arr) {};
# true 整形的地址,用整形指针来接收,上面的两种传参方式本质上是这种
void test2(int* arr[20]) {};
# true 数组传参,用数组接受
void test2(int** arr) {};
# true 整形指针的地址,用二级指针来接收
int main()
{
  int arr[10] = { 0 };
  int* arr2[20] = { 0 };
  test(arr);  //数组名,首元素地址,整形的地址
  test2(arr2); //数组名,首元素地址,整形指针的地址
  return 0;
}

二维数组传参

void test(int arr[3][5]) {};
# true 二维数组传参,用二维数组来接收
void test(int arr[][]) {};
# false 二维数组传参可以不指定行,但必须指定列
void test(int arr[][5]) {};
# true 二维数组传参,用二维数组来接收,行号可写可不写
void test(int* arr) {};
# false 一位数组的地址不能用整形指针来接收
void test(int* arr[5]) {};
# false 一维数组的地址不能用指针数组来接收
void test(int(*arr)[5]) {};
# true 一维数组的地址用数组指针来接收,数组里面有5个元素
void test(int** arr) {};
# false 一位数组的地址不能用整形二级指针来接收
int main()
{
  int arr[3][5] = { 0 };
  test(arr);  //二维数组的数组名代表第一行的地址,即一维数组的地址
}

一级指针传参

void print(int* p, int sz)  //一级指针用指针变量来接收
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d\n", *(p + i));
  }
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9 };
  int* p = arr;  //数组名代表首元素地址,即整形的地址
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(p, sz);  //将一级指针p传给函数
  return 0;
}

二级指针传参

void test(int** ptr)
{
  printf("num = %d\n", **ptr);
}
int main()
{
  int n = 10;
  int* p = &n;  //一级指针变量
  int** pp = &p;  //二级指针变量
  test(pp);  //二级指针
  test(&p);  //一级指针的地址,即二级指针
  return 0;
}

5、函数指针

什么是函数指针

我们知道,任何类型的变量都是要在内存中占用空间的,而每个内存空间都会有自己的编号,也就是地址,那么对于函数来说,函数也会在内存中占用空间,所以函数也是有地址的;而函数指针就是用来存放函数地址的指针。

int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int a = 10;
  int b = 20;
  int c = Add(a, b);
  printf("%p\n", &Add);
  printf("%p\n", Add);
  return 0;
}

2020062310470442.png

函数指针的定义

int (*p1)(int, int) = &Add;
int (*p1)(int, int) = Add;
# 我们上面已经知道了,函数名和&函数名都代表函数的地址,所以上面这两种写法其实是一样的,都是把Add函数的地址赋给了函数指针p1;
# p1的类型:int (*)(int, int)  //去掉变量名剩下的就是变量类型
# p1首先和*结合,表示它是一个指针,然后和(int, int)结合,表示它指向的是一个函数,函数的参数是int,int,返回值也是int;

函数指针的使用

int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int (*p1)(int, int) = &Add;
  //int (*p1)(int, int) = Add;
  int ret1 = Add(2, 3);
  int ret2 = (*Add)(2, 3);
  printf("%d %d\n", ret1, ret2);
  int ret3 = p1(2, 3);
  int ret4 = (*p1)(2, 3);
  int ret5 = (*********p1)(2, 3);
  printf("%d %d %d\n", ret3, ret4, ret5);
  return 0;
}

2020062310470442.png

我们日常在调用函数的时候,都是直接函数名 + 函数参数,但是在学习了函数指针之后我们可能会有一个疑惑,既然函数名也代表函数的地址,那我们在调用函数的时候是不是应该先对函数进行解引用,然后再进行传参等操作?


其实对于函数来说,调用是不需要解引用的 (C语言就是这样设计的,大家当作一个特例记住就行,不用深究) ,当然,我们对它解引用编译器也不会报错,说白了对函数就行解引用只是为了让我们能够更好的理解指针,而编译器会自动忽略掉函数前面的*号;


就像我们上面例子中的 ret1 和 ter2,ret3 和 ret4 ,其实对于编译器来说他们都是一样的,甚至我们像 ret5 那样在函数前面加上若干个*号编译器也不会报错,因为编译器会将其忽略。

两段有趣的代码

第一段代码:

(*(void (*)())0)();

(void (*)())0:首先,0前面括号中的 void (*)() 是一个函数指针类型,该指针指向的函数的参数为空,返回值也为空;其次,我们知道,(int*)0 是把0强制类型转换为int*类型,即把0当作一个地址,该地址存放的是一个整数;所以 (void (*)())0 是把0强制类型转换为函数指针类型,即把0当作一个参数为空,返回值也为空的函数的地址。

(*(void (*)())0)():现在我们知道了(void (*)())0 代表的是0地址处的函数,且该函数的参数为空,所以我们现在调用该函数;首先用*对函数(不加*也行)解引用,然后传参(参数为空);

所以实际上上面的代码完成的是一次函数调用,调用0地址处的函数。

上面这段出自于《C陷阱与缺陷》这本书的第二章,该书中对此问题的描述如下:

2020062310470442.png

所以说,上面这段是有着实际意义的,并不是我们为了炫技而设计出的无用的代码。(上面提到的子例程是函数的意思)

第二段代码:

void (*signal(int, void(*)(int)))(int);

signal ( int, void (*) (int) ):对于这种复杂的语句,我们一般从函数名开始分析,我们发现,signal 是一个函数的函数名,该函数有两个参数,第一个参数是一个整形,第二个参数是一个函数指针,该指针指向的函数的参数是 int,返回值是 void;

void (*) (int):我们把 signal ( int, void (*) (int) ) 从代码中抽离出去就得到了函数的返回值,可以看到,该函数的返回值也是一个函数指针,该指针指向的函数的参数是 int,返回值是 void;

所以实际上上面这段代码是一个函数声明,声明的函数的第一个参数是整形,第二个参数是参数为 int,返回值为 void 的函数指针,返回值也是参数为 int,返回值为 void 的函数指针。

上面这段也出自于《C陷阱与缺陷》这本书的第二章,紧挨着我们上面的函数调用:

2020062310470442.png

《C陷阱与缺陷》这本书是十分经典的一本C语言书籍,里面提到了许多C语言中可能会出现的一些错误,特别是指针方面的错误,希望大家都能抽时间看看这本书,我把这本书的电子版放到了阿里云盘中,有需要的可以自取。

阿里云盘链接:https://www.aliyundrive.com/s/WHvpjWmFqDo
提取码: zg83

6、函数指针的用途

当我们学习了函数指针的相关知识过后,可能大家会有这样一个疑惑,既然我们可以在代码中直接来调用函数,那为什么还要通过函数指针来间接调用函数呢?这不是多此一举吗?我们说,存在即合理,其实只是我们没有还见过函数指针的真正用途而已,而并不能说函数指针没用;


实际上,函数指针是C语言中一种特别高明的存在,我们在用C语言完成比较大型的工程项目的时候,函数指针会被经常用到;而函数指针数组最常用的两个用途就是回调函数和转移表;回调函数是指通过函数指针来间接调用函数,转移表其实就是函数指针数组。


回调函数


回调函数就是一个通过函数指针调用的函数;如果我们把函数的指针(地址)作为参数传递给另一 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数;回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。(C语言中的库函数 qsort 就是回调函数使用的经典例子)


下面我通过一个简易的计算器来具体体现函数指针的回调函数的用法:

int add(int a, int b)
{
  return a + b;
}
int sub(int a, int b)
{
  return a - b;
}
int mul(int a, int b)
{
  return a * b;
}
int div(int a, int b)
{
  return a / b;
}
void menu()
{
  printf("****************************\n");
  printf("*****   1. Add   2. Sub*****\n");
  printf("*****   3. Mul   4. Div*****\n");
  printf("*****      0. Exit     *****\n");
  printf("****************************\n");
}
int main()
{
  int x, y;
  int input = 1;
  int ret = 0;
  do
  {
    menu();
    printf("请选择:");
    scanf("%d", &input);
    switch (input)
    {
    case 0:
      printf("退出程序\n");
      break;
    case 1:
      printf("输入操作数:");
      scanf("%d %d", &x, &y);
      ret = add(x, y);
      printf("ret = %d\n", ret);
      break;
    case 2:
      printf("输入操作数:");
      scanf("%d %d", &x, &y);
      ret = sub(x, y);
      printf("ret = %d\n", ret);
      break;
    case 3:
      printf("输入操作数:");
      scanf("%d %d", &x, &y);
      ret = mul(x, y);
      printf("ret = %d\n", ret);
      break;
    case 4:
      printf("输入操作数:");
      scanf("%d %d", &x, &y);
      ret = div(x, y);
      printf("ret = %d\n", ret);
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}

上面我们完成了一个简易的计算器,但是我们发现它存在一个问题,那就是case 1 、case 2、case 3、case 4 中的代码除了调用函数的那句不一样之外,其他三句完全一样,造成了代码冗余,我们设想能不能设计一个函数,把这句代码都放入一个函数中去,从而实现代码的复用,这时候就要用到我们的回调函数了。

2020062310470442.png

经过改造后的代码如下:

int add(int a, int b)
{
  return a + b;
}
int sub(int a, int b)
{
  return a - b;
}
int mul(int a, int b)
{
  return a * b;
}
int div(int a, int b)
{
  return a / b;
}
void menu()
{
  printf("****************************\n");
  printf("*****   1. Add   2. Sub*****\n");
  printf("*****   3. Mul   4. Div*****\n");
  printf("*****      0. Exit     *****\n");
  printf("****************************\n");
}
void calc(int(*pf)(int, int))
{
  int x = 0;
  int y = 0;
  int ret = 0;
  printf("输入操作数:");
  scanf("%d %d", &x, &y);
  ret = pf(x, y);
  printf("ret = %d\n", ret);
}
int main()
{
  int input = 1;
  do
  {
    menu();
    printf("请选择:");
    scanf("%d", &input);
    switch (input)
    {
    case 0:
      printf("退出程序\n");
      break;
    case 1:
      calc(add);
      break;
    case 2:
      calc(sub);
      break;
    case 3:
      calc(mul);
      break;
    case 4:
      calc(div);
      break;
    default:
      printf("选择错误\n");
      break;
    }
  } while (input);
  return 0;
}

上面代码中我们把前面冗余的代码全部封装到了 calc 函数中,通过把 calc 函数的参数设置为函数指针来实现了回调函数。

7、函数指针数组

什么是函数指针数组

顾名思义,函数指针数组就是用来存放函数指针的数组。

函数指针数组的定义

int (*parr1[10])(int);
# parr1的类型:int (*[10])(int)  //去掉变量名剩下的就是变量类型
# parr1和[10]结合,表示它是一个数组,数组里面有10个元素,每个元素的类型是一个函数指针,该指针指向的函数的参数为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 (*parr[4])(int, int) = { Add, Sub, Mul, Div };  //将函数地址放入数组中
  int a = 10;
  int b = 20;
  int i = 0;
  for (i = 0; i < 4; i++)
  {
    printf("%d\n", (parr[i])(a, b));  //知道需要函数地址,调用函数
  }
  return 0;
}

2020062310470442.png

8、函数指针数组的用途

上面我们已经提到,函数指针数组用于转移表。

下面我们继续通过对计算器的改造来体现转移表的作用:经过改造后的代码如下

int add(int a, int b)
{
  return a + b;
}
int sub(int a, int b)
{
  return a - b;
}
int mul(int a, int b)
{
  return a * b;
}
int div(int a, int b)
{
  return a / b;
}
void menu()
{
  printf("****************************\n");
  printf("*****   1. Add   2. Sub*****\n");
  printf("*****   3. Mul   4. Div*****\n");
  printf("*****      0. Exit     *****\n");
  printf("****************************\n");
}
int main()
{
  int x, y;
  int input = 1;
  int ret = 0;
  int(*p[5])(int x, int y) = { 0, add, sub, mul, div };  //转移表
  while (1)
  {
    menu();
    printf("请选择:");
    scanf("%d", &input);
    if ((input > 0 && input <= 4))
    {
      printf("输入操作数:");
      scanf("%d %d", &x, &y);
      ret = (*p[input])(x, y);  //回调函数
      printf("ret = %d\n", ret);
    }
    else if (input == 0)
    {
      printf("退出程序\n");
      break;
    }
    else
    {
      printf("输入错误\n");
    }
  }
  return 0;
}

上面我们把各个函数的地址存放到一个函数指针数组中去,实现了一个转移表,然后通过访问数组里面的元素,配合回调函数,从而达到了我们简化代码的目的。

9、指向函数指针数组的指针

指向函数指针数组的指针就是一个指针,指针指向的是一个数组,数组里面的每个元素是函数指针。

其定义和使用如下:

void test(const char* str)
{
  printf("%s\n", str);
}
int main()
{
  //函数指针pfun
  void (*pfun)(const char*) = test;
  //函数指针的数组pfunArr
  void (*pfunArr[5])(const char* str);
  pfunArr[0] = test;
  //指向函数指针数组pfunArr的指针ppfunArr
  void (*(*ppfunArr)[5])(const char*) = &pfunArr;
  return 0;
}


相关文章
|
3天前
|
编译器 C语言
【C语言初阶】指针篇—下
【C语言初阶】指针篇—下
|
3天前
|
存储 C语言
【C语言初阶】指针篇—上
【C语言初阶】指针篇—上
|
14天前
|
搜索推荐 程序员 C语言
指针赋值与引用传递:C语言的基础知识与实践技巧
指针赋值与引用传递:C语言的基础知识与实践技巧
|
13天前
|
搜索推荐 程序员 C语言
指针赋值与引用传递:C语言的基础知识与实践技巧
指针赋值与引用传递:C语言的基础知识与实践技巧
|
17天前
|
Java 程序员 Linux
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
探索C语言宝库:从基础到进阶的干货知识(类型变量+条件循环+函数模块+指针+内存+文件)
19 0
|
18天前
|
C语言
C语言中的函数指针、指针函数与函数回调
C语言中的函数指针、指针函数与函数回调
11 0
|
2月前
|
C语言
C语言---指针进阶
C语言---指针进阶
29 0
|
7月前
|
C语言
C语言指针进阶(下)
C语言指针进阶(下)
47 1
|
7月前
|
编译器 C语言
C语言指针进阶(上)
C语言指针进阶(上)
93 1