0基础C语言自学教程——第八节 函数指针数组的各种关系

简介: const int* 是指向一个常量整数的指针,所以说,const int*所修饰的指针变量,其指针变量本身(即指向元素的地址)是可以被修改的,但是其指针所指向的值是不允许被修改的。

0基础C语言自学教程——第八节 函数指针数组的各种关系


目录


1、const int*        int const*         int* const      const int* const       int const* const 的区别和联系


2、指针数组


3、数组指针


4、数组名和&数组名所表示的地址区别


5、数组元素访问的两种形式


6、*p++和(*p)++的区别


7、指针和数组的关系表述


8、函数传参的传址调用


9、函数传参传数组


一维数组传参


多维数组传参(以二维数组为例)


10、函数指针


11、返回指针的函数——注意要避免野指针


12、函数指针数组


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


14、回调函数


1、const int*        int const*         int* const      const int* const       int const* const 的区别和联系

const int* 是指向一个常量整数的指针,所以说,const int*所修饰的指针变量,其指针变量本身(即指向元素的地址)是可以被修改的,但是其指针所指向的值是不允许被修改的。


这里的const 不会限制指针变量本身,也就是说,其指针变量是允许指向其他的地址的。


const的位置可以放在int 的前面,也可以放在int  的后面,不会产生影响。


也就是说,const int* 和int const* 等价。


int* const 是一个指向可修改的整型的常量指针,也就是说,其指针变量本身所指向的地址是不可以被改变的,但是可以改变其指针所修饰的值。


也就是说,这里的指针变量所指向的地址是不允许修改的,但是其地址所对应的值是可以被修改的。


const int* const 是指向一个常量整数的常量指针。也就是说,其指针变量所指向的地址和其地址所对应的整型值都是不允许改变的。


第一个const的位置可以放在int 的前面,也可以放在int 的后面。


也就是说,const int* const 和 int const* const 等价。


2、指针数组

我们先来看这么一个例子:

int* p[20] = {0};


这里的数组p,实际上就是一个指针数组。


什么意思呢?


就是一个数组p,里面有20个元素,每一个元素的类型都是int*。


这里的int*表示的是元素的类型。


就好像我们当初我们定义数组一样,之前我们会写

int a[20] = {0};


这样的代码,这里的int表示数组里有20个类型为int的元素。


那么如果现在是int*类型,依此类推,其数组里就是含有20个int*类型的指针(变量)元素


而这个数组p,就是 叫指针数组。


我们来举一个例子,便于读者理解:



OK,上面实际上就是其一个实例,仅做理解该知识点使用,实际应用比这稍复杂。


3、数组指针

数组指针,本质上为一个指针。实际上,它就是一个指向数组的指针。


我们还是来通过之前的类型来引入。


我们如果是这样

int* p1 = NULL;


这是的定义了一个指向整型的指针p1


如果是

char* p2 = NULL;


这是定义了一个指向字符类型的指针p2


依此类推...


那么,我如果想定义一个指向数组的指针,我该怎么写?


比如,我有这么个数组

int a[3] = {0,1,2};
___  p = &a;


那么问题来了,我该怎么去定义这个p呢?


这个时候,我们就引进了数组指针这样一个概念:


它的定义方式是这个样子的:

int (*p)[3] = &a;


为什么要加那一个小括号呢?


理由很简单,因为[]的优先级是高于*的,会首先与p结合。如果不加括号,那么就变成了我们上面所说的指针数组了。


所以总结一下:数组指针是用来存放数组的地址的,它的定义方式为 type_name (*__)[num_arr]


按照上面的例子来看, 就是int (*p)[3]


我们在下面讲解函数传参的时候会详细地讲解这个东西的实际作用。


4、数组名和&数组名所表示的地址区别

数组名实际上就是数组的首元素地址。


但是有两种情况例外:


一种是用sizeof()去求数组的大小的时候;


还有一种情况是在&整个数组的时候(就是取整个数组的地址)


这两种情况下,数组名代表的是整个数组的地址。


当然,也许在你看来数组首元素的地址和整个数组的地址并没有什么区别。如图:



但是,其表示的含义不一样。何以见得?


我们来继续看:

为什么会这样?


我们知道,指针加减一个常数表示的是下一个元素。


那么,直接一个a表示的是数组首元素的首地址;其下一个元素就是数组a的第二个元素。


而&a表示的是整个数组的首地址;其下一个元素是数组后面的地址。


如图:

image.png



至此,我们就解释清楚了这两者的关系


5、数组元素访问的两种形式

有两种形式,它们本质上是一致的:


一种是用数组的下标访问,还有一种是用指针访问。


我们来看下面这一段代码:


#include<stdio.h>
int main()
{
  char arr[3] = { 'a','b','c' };
  char* p = arr;
  for (int i = 0; i < 3; i++)
  {
  printf("%c ", arr[i]);
  }
  printf("\n");
  for (int i = 0; i < 3; i++)
  {
  printf("%c ", *(p + i));
  }
  return 0;
}


可以看到,这两种方式所得到的结果是一样的。


不仅一维数组这样,二维甚至多维数组也可以这样。


我们再来举一个二维数组的例子:

#include<stdio.h>
int main()
{
  int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
  for (int i = 0; i < 3; i++)
  {
  for (int j = 0; j < 4; j++)
  {
    printf("%d ", a[i][j]);
  }
  printf("\n");
  }
  printf("\n");
  printf("\n");
  for (int i = 0; i < 3; i++)
  {
  for (int j = 0; j < 4; j++)
  {
    printf("%d ", *(*(a + i) + j));
  }
  printf("\n");
  }
  return 0;
}



在这个例子当中,我们主要对比了a[ i ][ j ]和 *(*(a + i) + j)


它们都是访问数组的形式,用到了不同的操作符。但代表的含义是一样的,都是下标为i,j的元素。


6、*p++和(*p)++的区别

其实这个很简单,需要注意的是这两个不是同一个东西。


*p++的含义是对p进行解引用,然后指针 p自增。


(*p)++的含义是先对p进行解引用,然后对其解引用的值自增。


7、指针和数组的关系表述

指针提供一种以符号形式是以哦那个地址的方法,因为计算机的硬件指令非常依赖地址,指针某种程度上把程序员想要传达的指令以更接近机器的方式表达。使用指针能够更有效率,并且,指针能够有效地处理数组,数组表示法实际上是变相地使用指针。


8、函数传参的传址调用

这个详见之前的博客,说的已经很详细了,我比较懒,不想再重复了,直接拿来用了。


0基础C语言保姆教程——第4节 函数_jxwd的博客-CSDN博客


在什么位置我给标注一下

image.png



9、函数传参传数组

对于一个函数,想要传参传数组,


我们在实参中,就直接是传一个数组名就可以了。


但是,在形参中,我们有很多种写法,其中一些写法是对的,还有一些写法注意是错误的。


一维数组传参

我们来看这么写写法:


(这里的test(a)可以被修改为test1(a),test2(a),test3(a))

int test1(int a[])
{}
int test2(int a[3])
{}
int test3(int* a)
{}


这三种方法到底可不可以呢?


答案是:都是可以的。(只考虑传参,不考虑返回值以及函数的实现方式什么的)


因为这里int a[ ]就是等价于int* a。


a[ ]里的数可以为任何数(必须是大于0的整数)。


这里的a[ ]只是用来便于理解,并不具有实际意义,其本质就是一个指针。


就是说,其本质就是第三种形式。一个指针。


那下面的可不可以呢?



先看第四个:我们看到,a是一个指针数组,那么其调用函数的形参也是一个指针数组的形式。肯定是可以的。


再看第五个:由于在传参的过程中,传递的是数组a首元素的地址,而数组a是一个指针数组,所以其首元素的地址就是一个指针的地址。而指针的地址实际上就是一个二级指针。所以,第五种写法也是对的。


多维数组传参(以二维数组为例)

#include<stdio.h>
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok
{}
int main()
{
  int* a[3][4] = { 0 };
  test(a);
  return 0;
}



先来看第一个:void test(int arr[3][5]){}


这个肯定是可以的。我们之前在写形参的时候都是这样写的。它的形式与二维数组一致,所以是可以的。


再看第二个:void test(int arr[][]){}


这个看上图都已经挂红了,肯定是不行的。原因很简单,和我们在定义二维数组的时候是一样的,就是说,必须要指出其列数。可以省略其行数,但是不可以省略其列数。


再看第三个:void test(int arr[][5]){} 同理第二个,这个是可以的。


我们4、5、6、7一起来看


void test(int *arr){}

void test(int* arr[5]){}

void test(int (*arr)[5]){}

void test(int **arr){}


在弄清楚这个问题之前,我们需要来搞清楚一个问题:二维数组所传的首元素的地址到底是谁的地址?


我们知道,二维数组长这样:

image.png



如图,我们表示一个四行四列的二维数组。


那么,我们所传的二维数组a到底是谁的地址?是第一行的1?还是什么?


这里笔者就不浪费时间了,直接给出答案。(也是经历了千辛万苦才得到的答案....呜呜呜~~~)


其传递的是第一行的地址。


为什么是这样呢?


我们可以这样来试着理解一下:


类比一维数组,如果把二维数组想象成一个一维数组,那么二维数组就是一个数组元素为数组的一维数组。


举个例子:

int a[4][4] = {{1,0,1,0},{0,1,0,1},{1,0,1,0},{0,1,0,1}};


里层的{ }看成是外层{ }的一个元素。那么a就是一个以数组为元素的数组。


这个时候,传递时,所传的就是首元素的地址;而其首元素就是{1,0,1,0}


所以,其传递的就是数组{1,0,1,0}的地址。


所以说,其传递时一个数组的地址!


那么,就必须要用一个接收数组地址的变量来去接收。而谁可以来做这个事情?


就是我们刚刚说的数组指针。


所以,4,5,6,7里只有6是正确的。


提醒一下:千万不要受到一维数组的影响,以为一维数组是用一级指针接收,那么二维数组就是用二级指针接收!如果用二级指针,编译器会报警告(严格的会报错),并且会让你的程序的执行结果超出你的掌控。


10、函数指针

我们同上面所说的数组指针,


数组指针是指一个指向数组的指针;


而函数指针就是一个指向函数的指针。


怎么去写呢?


int Add(int x, int y)
{
  return x + y;
}
int main()
{
  int (*p)(int, int) = &Add;
  return 0;
}


如图,这里的p就是一个函数指针。


我们的写法是:

image.png



当然,这里的参数可以不止有两个。


而这里的指针p, 存放的就是函数的地址。


我们可以通过函数的地址来找到这个函数,从而是实现调用。


那么,函数指针如何使用呢?



就像这样。p是存储着函数的地址,对p进行解引用,就相当于这个函数Add,后面跟上参数。


也就是说,(*p)(2,3)就相当于(Add)(2,3),从而实现函数的调用。


而这里的*实际上可以省略,就是是说,实际上可以直接写p(2,3)。理由是什么呢?


理由是函数名本身就是一个地址。


函数名本身就是一个地址!


我们来看:

你不论怎么样对Add去取地址,结果都是这个地址。


同样的道理,不论你怎么对p去解引用,你得到的都还是函数的地址。就是说,哪怕你这样写: (******p)(2,3),还是相当于一个Add(2,3)


需要注意的是,如果我们这样去写:

int* p(int,int);


那么这个就是一个普通的函数声明,p表示的是一个函数。因为这里的p会优先于()结合。函数的返回值为int* 。也就是我们下面所要说的————返回值为指针的函数。


我们来看一看这两个实例:

//实例1(*(void (*)())0)();
//实例2void (*signal(int , void(*)(int)))(int);


先说实例1:


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


什么意思?


我们一步一步拆解分析:


void (* )( )是一个函数指针类型,指向的函数的返回类型为void。


0先与左边的括号结合,也就是先是(void (* )( ) ) 0,那么它的意思就是将数字0强转为void(*)()的函数指针。


然后*(void (* )( ) ) 0,表示对这个函数指针解引用。


然后 (*(void (* )( ) ) 0 ) ( );表示调用这个函数。


总结来说,就是将0先强转为类型为void (* )( )的函数指针,然后去调用这个函数。


实例2:

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



这个实际上是一个函数声明。


signal 是一个函数名,(int , void(*)(int))是其两个参数,一个是int ,一个是void(*)(int)(函数指针,参数为int,返回类型为void)其返回类型也是一个函数指针,类型为void (*)(int) (指向参数为int,返回值为void 的函数指针)


我们这里为了便于理解,再来解释一下:


我们如果typedef一下这个函数指针,


我们能不能这样写?

typedef void(*)(int) fun_t;


很遗憾,这样写法是不行的。


下面的写法是允许的:

typedef void(*pfun_t)(int);


这两个是一个意思。这里的pfun_t就是类型名


有了这个typedef,我们就可以把这个代码改成:

pfun_t signal(int, pfun_t);


这个代码与上述代码一个意思。


11、返回指针的函数——注意要避免野指针

一个函数如果返回值是一个指针类型,尤其需要避免野指针的出现。


为什么?


我们来通过一个例子来讲解:


#include<stdio.h>
int* test(int x, int y)
{
  int a[2] = { x,y };
  return a;
}
int main()
{
  int* p = test(1,2);
  printf("%d %d", *p, *(p + 1));
  return 0;
}


让上述代码执行,我们会发现:


这好像没有什么问题。


但是,我们可以在底部看到一个警告:

为什么不能这样做?


我们先来看看其危害吧:


我在函数和printf之间再加上一个printf

这个时候,*p和 *(p+1)便不再是我们想要的1,2了。而是变成了一个个随机值。


为什么会这样?


原因很简单。在test函数里的a,是一个临时变量,临时变量在出函数test的时候,会被销毁。那么a所指向的内容也会被销毁。就是说,a[0]和a[1]的内存会还给操作系统。不再被用户所使用。


p确实接收到了a的地址的值,但是a所指向的内容,也就是其地址所指向的内容,就像我们刚刚说的那样,该地址以及后面的地址的内存会被还给操作系统。


这有什么危害呢?


当内存还给系统,不再被我们所使用的时候,在下一次函数调用其他函数的时候,就有可能会使用。


这也就是我加了一个printf后会什么会变成这个样子的原因:


在我直接用printf去打印的时候,其内容并未被覆盖或者使用,编译器并未改变其值。需要注意的就是虽然值是这个,但是已经不是用户的了。


当我再加上了一个printf的时候,由于printf也是函数,函数在调用的时候会创建和销毁栈帧,这部分的内存被系统使用,原来的值就会被覆盖。


所以说,这里的p是一个野指针。这也就是为什么会出现这种情况的原因。


因此,我们一定在书写的时候要避免这种情况。



12、函数指针数组

我们在函数指针的基础上再引申一下:

如果我们这样:

这个在上述函数指针基础上就加了一个[ ]。


我们来理解一下:我们知道,这里的[ ]会先与p结合,那么p就是一个数组。数组的每个元素都是指向int (*)(int , int)的函数指针


这个有什么用呢?计算器就可以通过这个来实现。可以来看一下这篇文章:


用函数数组指针来实现计算器的功能(简易模式)(不过稍微改改也能算比较复杂的)_jxwd的博客-CSDN博客


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

这个有点绕。它什么意思呢?


首先,有这么一个函数指针数组

int test(int x,int y)
{}
int main()
{
    int(*p)(int,int) = test;//这是一个函数指针
    int(*p1[2])(int,int);//这是一个函数指针数组
    p1[0] = test;
    int(*(*p2)[2])(int, int) = &p1;//这里的p2就是一个指向函数指针数组的指针。
}


上面的p2,就是一个指向函数指针数组的指针。


如果在p2后面再加[ ]变成数组,那么又变成了一个函数,就又变成了一个指向函数指针数组的指针数组.....无穷尽套娃下去,这就有点离谱了。。。。。。


14、回调函数

简单来说,回调函数就是一个通过函数指针调用的函数。


我们依照着刚刚用函数指针数组写的计算器程序来看,实际上,这就是一个回调函数的实例

#include <stdio.h>
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 a = 0;
  int (*output[4])(int, int) = { add,sub,mul,div };
  printf("是否进入四则运算模式\n");
  printf("*****1       是*****\n");
  printf("*****0       否*****\n");
  scanf("%d", &a);
  int b = 0;
  int c = 0;
  int d = 0;
  while (a)
  {
  printf("**************************\n");
  printf("*1  add**********2  sub***\n");
  printf("*3  mul**********4  div***\n");
  printf("**************************\n");
  printf("请选择\n");
  scanf("%d", &b);
  printf("请输入计算的两个值\n");
  scanf("%d%d", &c, &d);
  int e = 0;
  e = (output[b - 1])(c, d);// 回调函数实例
  printf("%d\n", e);
  printf("是否继续\n");
  printf("*****1       是*****\n");
  printf("*****0       否*****\n");
  scanf("%d", &a);
  }
  return 0;
}


好啦,本节内容就到此结束啦。



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