深析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;
}


相关文章
|
29天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
48 0
|
28天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
17 2
|
28天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
28天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
1月前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
1月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
1月前
|
C语言
C语言指针(3)
C语言指针(3)
14 1
|
1月前
|
C语言
C语言指针(2)
C语言指针(2)
15 1
|
28天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
19 0