进阶C语言 第二章-------《进阶指针》 (指针数组、数组指针、函数指针、回调指针)知识点+基本练习题+深入细节+通俗易懂+完整思维导图+建议收藏
创始人
2024-05-16 15:12:25
0

绪论

        书接上回,通过对数据类型进阶的认识,你肯定对各种数据类型在内存中如何存储有了了解。虽然说,这方面可能对你的编程能力没什么进步。但是,他是一本内功秘籍,当我们遇到了这方面的问题时我们可以知道可能是哪一方面出了问题。本章进阶指针,可能会有更多的知识点和更复杂的问题,但其实只要你对初阶的知识有很好的认识他也是小ks。

最后祝大家新年快乐,万事如意!

所以安全带系好,发车啦(建议电脑观看)。

        附:红色,部分为重点部分;蓝颜色为需要记忆的部分(不是死记硬背哈,多敲);黑色加粗或者其余颜色为次重点;黑色为描述需要

思维导图:


 

 

要XMind思维导图的话可以私信哈

目录

1.字符指针

2.指针数组

2.1指针数组的定义和使用方法

3.数组指针

3.1  定义

3.2  &数组名与数组名

3.3数组指针的使用

4.数组传参、指针传参

4.1 一维数组和指针数组传参

4.2 二维数组传参

4.3一级指针传参

4.4二级指针传参

5.函数指针

5.1.1函数指针的创建

5.1.2函数指针的使用

6.函数指针数组

6.1函数指针数组定义

7.指向函数指针数组的指针

8.回调指针

8.1.1回调函数的定义:


1.字符指针

1.1

知识点:

字符指针常见的的使用情况:

int main()
{char a = 'a';//字符指针//1.char * pa = &a;//2.char * pa = "abcd";return 0;
}

在第一种情况时:是直接将a的地址存进了一个指针中这个指针叫字符指针

在第二种情况时:是将字符串"abcd"中的首元素的'a'字符的地址存进一个字符指针中 (在常量字符串中他产生的值就是'a'字符的地址即"abcd" == &'a' )

细节(注意点):

常量字符串不能被修改、两相同的常量字符串完全相同

即当char * pa= "abcdef"时

*pa = 'w'(这样时错误的)

再如:

char arr[] = "abcdef";

arr = "cdefg";

这样同样是不行的

练习:

#include
int main()
{char arr1[] = "abcdef";char arr2[] = "abcdef";const char *arr3 = "abcdef";const char *arr4 = "abcdef";if(arr1 == arr2)printf("same\n");elseprintf("no same\n");if(arr3 == arr4)printf("same\n");elseprintf("no same\n");return 0;
}

分析: 

首先,有2个数组变量arr1 arr2存放着字符串,并且还有两个字符指针存放着常量字符串的首元素地址

其次,因为两个数组会创建两个不同的空间即就会导致其数组名表示的首元素地址不同;而对于arr3 和 arr4 来说虽然他们将‘a’字符的地址存放在不同的空间,但是其指针变量的变量名就表示其内存中所存的地址(3、4都常量字符串的首元素地址)

最后,通过上面标红的字就可以推出最终将打印 no same   、 same        

附这里我们需要注意的是arr1 arr2  arr3 arr4 他们分别代表着意思

数组名:首元素的地址 

指针变量的变量名:代表着存放的地址

2.指针数组

2.1指针数组的定义和使用方法

知识点:

指针数组,字如其名是一个存放了多个指针的数组

我们不妨那整形数组、字符数组对应了来看

而我们有知道指针(变量)使用用来存放地址的,所以我们在为int * arr[4] 初始化时应该存放地址进去 

细节(具体):

对于指针数组的使用:

#include
int main()
{char* arr[4] = { "abcd","hehe","blog","nb" };//常量字符串产生的是其首元素的地址for (int i = 0; i < 4; i++){printf("%s ", arr[i]);}return 0;
}

练习:

 通过指针数组来模拟实现二维数组:

为什么可以直接写成arr[][]的形式,是因为你看arr[ i ][ j ]第一个  arr[i] 就是找到了 存放在指针数组中各个数组的地址(即数组名) 当i为0时(arr[ 0 ]  == arr1 ):arr1 [ j ]  是不是就是变成了数组的表示方法。

当然我们也可以吧arr [ i ] [ j ]表示成 *(arr[ i ] + j )


3.数组指针

3.1  定义

知识点:

同整形指针、字符指针一样他是一种存放数组的指针

整形指针:int * p;

字符指针:char * p;

不难发现他们是由 所指向地址的类型     +     * (表示变量是一个指针)  +   变量名  组成的(这点很重要会贯穿后面指针的内容次处称为指针的3步原则)

所以同理,数组指针他也是如此组成的:

int arr[ 4 ] = {1,2,3,4};

int (*parr)[ 4 ] = &arr;

此时的地址数组的类型为int  [ 4 ] 、 *代表其为一个指针,加上括号是因为防止因为[ ] 的优先级比 * 的优先级高而导致变成 指针数组   、 再加上变量名 就成了数组指针

3.2  &数组名与数组名

知识点:

数组名在讲数组内容处已经提过为:这个数组的首元素的地址

&数组名:如int arr[ 10 ] = {0}; arr表示数组名,那么取地址的数组名就真正的表示这个数组

细节:

下面通过代码来更加详细的解释:

int main()
{int arr[10] = { 0 };printf("%p\n", &arr[0]);//首元素的地址printf("%p\n", arr);//数组名printf("%p\n", &arr);//整个数组的地址return 0;
}

此处可以发现他们三个的地址都相同,但是有什么不同地方呢?让我们继续往下

	int main(){int arr[10] = { 0 };printf("%p\n", &arr[0]);//首元素的地址printf("%p\n", &arr[0]+1);//首元素的地址printf("%p\n", arr);//数组名printf("%p\n", arr+1);//数组名printf("%p\n", &arr);//整个数组的地址printf("%p\n", &arr+1);//整个数组的地址return 0;}

此处当我们+1后,可以发现数组名的结果和&arr[0]的效果(加了4byte)是一样的都是,而&arr却加了40(70-48 == 28,十六进制不够接16,转化成十进制就是40byte)

所以,最终得出结论就是

  1. arr(除了两种特别情况外)都表示首元素的地址
  2. &arr 则直接表示整个数组
  3. 他们的不同处在于,代表的意义不一样

最终与上面所讲到的数组指针结合:

int arr[ 4 ] = {1,2,3,4};

int (*parr)[ 4 ] = &arr;

此处的数组指针和指针数组就有着明显的区别:

一个是存整个数组的地址的,一个是存数组中的各个数的地址的。

int * arr[4] = {arr,arr+1,arr+2,arr+3};

int (*p)[4] = &arr;

3.3数组指针的使用

直接通过代码注释的形式来展示;

//对于一维数组时
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int(*p)[10] = &arr;for (int i = 0; i < 10; i++){printf("%d ", (*p)[i]);//通过(*p)找到整个数组 * &arr等于arr再通过下标引用找到对应}//而对于一维数组来说这样写反而有点冗余了,不如直接用指针*(p+i)return 0;
}//对于二维数组来说
void test(int(*p)[4], int r, int c)//将二维数组的首元素传进其表示二维数组的第一行即一整个数组
{int i = 0;for (i = 0 ;i < r; i++)//i表示访问的行{int j = 0;for (j = 0; j < c; j++){printf("%d", (*(p + i))[j]);//访问第i行j列//printf("%d", p[i][j]);p[i] == *(p+i)}printf("\n");}}
int main()
{int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };test(arr, 3, 4);
}

附:当在函数内接收一个二维数组时,方块内放到是二维数组的列宽

注意点:

  1. 数组指针*p = * &arr = arr
  2. int (* parr[10])[5]:

4.数组传参、指针传参

4.1 一维数组和指针数组传参

能够接收一维数组和指针数组的参数有:

对数组的参数进行接收一般有两种情况:

  1. 直接和原类型数据相同
  2. 传过来地址用指针接收;此时注意满足 类型 + * + 变量名 这一指针确定规则即可,如接收指针数组时用到的是二级指针,而不是一级指针的原因是原本传过来的类型就是int * 的类型所以要  类型 + *   所以就应该写成二级的int *  * ,或者还可以理解为传过来的是指针的地址,所以要用二级指针接收。
void test1(int arr[])//括号内加不加数组大小都行
{;
}void test1(int* parr)
{;
}void test2(int* arr2[])
{;
}void test2(int** arr2)
{;
}int main()
{int arr1[10] = { 0 };int* arr2[10] = { 0 };test1(arr1);test2(arr2);return 0;
}

4.2 二维数组传参

知识点:

同上这总结一般只有两种情况:

同上,也就是用原类型/指针类型

分析:

int main()
{int arr[3][4] = {0};test(arr);return 0;
}

test(arr)中的arr是数组名,数组名表示首元素的地址,而此处是一个二级数组,所以数组名就表示成了第一行arr[0]的地址即&arr[0],他是一个数组(第一行),所以我们在用指针时就应该用数组指针来存数组

所以,最终能在test函数中接收的参数类型有:

int arr[][COL]//此处行可以省略但是列不能省略int (*parr)[COL]//数组指针接收数组

可能会感觉二级指针可以接收,但是事实上是不行的因为

二级指针:接收的应该是一级指针的地址(而不是数组的地址,

报错如下:

4.3一级指针传参

知识点:

一级指针传参时:只用相同类型接收(不能用指针接收了此处因为二级指针是接收一级指针的地址的,并且其余的也用不了)

void test(int* p)
{;//code
}int main()
{int arr[] = { 1,2,3,4,5,6,7 };int* p = arr;test(p);return 0;
}

反过来思考:

函数以一级指针接收时传的参数可以是什么?

不难可以想出,首先什么是以指针接收的无非就是地址,而在函数内还要加一个可能就是传过来的值的原本的类型也是指针。

所以最终的可能性就是:

传递一个变量的地址test(&a)、一维数组名(首元素的地址)test(arr)

以及指针变量名test(p)  (和上面的代码一样)


4.4二级指针传参

知识点:

同一级指针:你传的是一级指针的就用一级指针接收、传的是二级指针就用二级指针接收

void test(int** ptr)
{;//code
}int main()
{int arr[] = { 1,2,3,4,5,6,7 };int* p = arr;int** pa = &p;test(pa);//pa 其实可以看成存着 &ptest(&p);//传的是一级指针的地址其原理和二级指针变量名一样都是&preturn 0;
}

反过来:当函数参数是一个二级指针接收

二级指针接收一级指针的地址:

所以可以是:

int * p = NULL;
int **parr = &p;test(&p);test(parr);

还可以存指针数组:因为指针数组内存的都是一个个一级指针

如:int * arr[10] 这里面存了10个int *类型的指针所以当我们把arr数组名传进去的时候就表示把首元素的地址(&arr[0] == & (int *))及第一个一级指针的地址传了进去所以是一级指针就可以用二级指针来接收

5.函数指针

5.1

知识点:

函数指针:一个存储函数地址的指针

其表现形式为:类型  + * +变量名 = 函数的地址

细节:

5.1.1函数指针的创建

下面通过代码来具体的展示:

int Add(int x, int y)
{return x + y;
}int main()
{int a = 3;int b = 2;Add(a, b);//函数指针 类型+ * + 变量名 = 函数的地址//此处Add的类型为 int (int , int)int (*ptr)(int, int) = &Add;int (*ptr)(int, int) = Add;//此处函数名和数组名类似都可以直接表示地址return 0;
}

5.1.2函数指针的使用

和正常的指针一样,我们存了函数的地址所以就可以解应用找到这个函数   (*ptr)(a,b)  ==  Add(a,b)

在平常我们使用函数时是直接Add(a,b) 的 ,所以我们在使用(*ptr)(a,b)时甚至也可以直接写成ptr(a,b)

代码即:

int Add(int x, int y)
{return x + y;
}int main()
{int a = 3;int b = 2;Add(a, b);//函数指针 类型+ * + 变量名 = 函数的地址//此处Add的类型为 int (int , int)int (*ptr)(int, int) = &Add;//上下一样  即:&Add == Addint (*ptr)(int, int) = Add;//此处函数名和数组名类似都可以表示其地址int ret = Add(a, b);// 等于int ret1 = (*ptr)(a, b);// 等于int ret2 = ptr(a, b);//即 Add(a,b) == (*ptr)(a,b) == ptr(a,b)return 0;
}
  1.  Add(a,b) == (*ptr)(a,b) ==  ptr(a,b)  //此处的(*ptr)中的*其实就是个摆设你甚至可以写成(***** ptr)(a,b)他只是便于理解 ,所以在我们用函数指针时可以存好一个函数的地址后可以直接写成 ptr(a,b)来代替Add(a,b),如果要写成(*ptr)(a,b)这种形式的话其中 (*ptr) 的括号不能少(因优先级问题)
  2. &Add ==  Add

附:*arr 可以看成 *(arr+0) 及   arr[0]   所以    **arr -> *(arr[0])  -> (arr[0][0])    (还可以将arr、arr[0] 看成首元素地址  *arr == arr【0】、* (arr[0]) -> arr[0][0]  )

我们在上面所用到的函数指针 ptr  其用法其实并不像上面那种而是一般会用于当把一个函数的地址传进到另一个函数内部时要在另一个函数内使用时可以用来接收这个地址 

练习:

(* (  void  ( * )  ( ) ) 0 )  ( )  : 这段代码的意义是:对0处的地址进行函数访问,不用传参

分析:

首先我们要知道最里面的void( * ) ( )  这是一个函数指针用来存地址 ,所以此处将整形0 用括号 强制转换成函数指针类型  就如 ptr 一样再(*(....)0)() 来访问0所指向的函数,因为函数类型中的参数是空的,所以也不用再进行传参。 

void (* signal(int ,void (*)(int)))(int);这段代码的意义是:对该函数进行声明

分析:

首先我们要知道,函数声明的结构是:返回类型 + 函数名 + 传参的类型  

而此处的返回类型是void (*) ( int )  ;函数名:signal   ;

传参的类型 : (int , void (*)(int))

可能此处不好看但是当写成返回类型为int就会变成:int signal(int , void (*)(int))

只是因为我们需要把函数名写到*的旁边所以看起来比较的奇怪:

现在分颜色来更清晰的写:void ( *  signal (int , void (*)(int)))(int);(注意声明时别漏了分号)

附:

我们还可以将复杂的函数指针给进行typedef陈一个简单点的类型,但是要注意写成(对指针类型要把名字放在星号旁边 ):

typedef void (* v_int) (int) ;//注意应该将想要改成的新类型名写到*的旁边//那么就可以将signal函数的声明写成:
v_int signal (int , v_int);

6.函数指针数

6.1函数指针数组定义

知识点:

函数指针数组还是和其他数组一样遵循3步原则:

首先,在第五节我们已经知道了函数指针的写法如:void (*p) (int)

所以,3步原则已经满足了两:变量名、*  ,此处为函数指针数组不同于函数指针要加上数组所以就可以写成:void (*p[5])(int) (   此处我们可以这样理解为什么要把[ ]放到里面:通过类比的方法看int arr[ ] 、int * arr[ ] 这些数组类型都是将他们的1.方括号放在了其变量名的后面 并且2.其类型写在变量名前面所以又因为3.变量名要写到*旁边所以写在里面,函数指针数组其本质还是个数组,所以类型为void   ...(int)、在加上其* 、变量名、数组 [ ] 就可以容易的想出void ( * p [5]  ) (int)   )

细节:

函数指针数组的用法:其就和其余数组一样,如int arr[] 存的是多组相同类型(int)的元素(即多组整形1,2,3)

所以函数指针数组就是存多组函数指针类型所对应的数(即多组函数Add,test的地址)

练习:

函数指针数组(在某些时候又称为:转移表

通过函数指针数组实现计算器:

其用法和就是数组用法+函数指针的用法:先数组调用后在调用各个函数

#define _CRT_SECURE_NO_WARNINGS 1
#include
void menu()
{printf("********************************\n");printf("**********1.Add   2.Sub*********\n");printf("**********3.Mul   4.Div*********\n");printf("**********   0.exit    *********\n");printf("********************************\n");
}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 input = 0;int a = 0;int b = 0;do{menu();printf("请输入:");scanf("%d", &input);//下面的函数指针数组有一种可以转移的特性所以就可以称其转移表int(*p[5])(int, int) = { NULL,Add,Sub,Mul,Div };if (input == 0){printf("退出计算器\n");break;}else if (input <= 4 && input >= 1){printf("输入两个值:");scanf("%d %d", &a, &b);printf("%d\n", p[input](a, b));}else{printf("输入错误请重新输入\n");}} while (input);return 0;
}

7.指向函数指针数组的指针

7.1

知识点:

指向函数指针数组的指针 如何定义 同样的满足三步法 

首先类型 : 函数指针数组:如int (  *   p  [5]   )(  int   ,    int);  --- 标蓝的表示其类型

其次:他是一个数组,所以大致和数组指针相同:int (  *  p  )[5]    --- ( *p )确保其和*优先结合

最后,变量名 ,设其为pf       :   int (* (*  pf  )[ 5 ]  )(  int   ,  int  )   = &p;

细节(注意点):

  1. 虽然这个名字一大串,但是我们任然要记得他还是一个指针(地址)其大小仍然时4/8byte的
  2. 并且他是一个指向数组的指针所以若+1就是跳过了整个数组,当然若要用也和数组指针类似:(*pf)找到整个数组 再要进行小标访问访问其内部存的内容 (*pf)[1]  == p[1]

在转移表中进行测试Add(  [1]   ) 

练习:

在创建一个.....指针

//函数指针数组
int (*pa [20]) (char ,short);//对于的函数指针数组的指针
int (*   (* ppa )  [20])  (char , short); 

8.回调指针

8.1

知识点:

8.1.1回调函数的定义:

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

细节:上面的大体意思有:

  1. 一个函数的地址(指针)被传递给另一个函数,在另一个函数内被间接性的调用,就称该函数(传地址的函数)为回调函数
  2. 其实回调函数,也就是函数指针所调用的函数
  3. 回调函数不是由自身的名字调用的,而是由另外的一方调用

练习:

下面是再次修改过后的转移表:

通过三个部分来展示其回调函数的特性

     其在第二张中其实就是用了函数指针来调用

本章完。预知后事如何,暂听下回分说。

持续更新大量C语言细致内容,三连关注哈

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
AWSECS:哪种网络模式具有... 使用AWS ECS中的awsvpc网络模式来获得最佳性能。awsvpc网络模式允许ECS任务直接在V...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...