C语言的灵魂——指针(4)

前言:上期我们介绍了指针数组,数组名的理解,一维数组传参的本质以及二级指针等。上期侧重于讲数组,而这期我们来介绍字符指针,数组指针,函数指针,侧重讲指针。
往前3期的传送门:
深入理解指针(1)
深入理解指针(2)
深入理解指针(3)


既然本篇是侧重讲指针变量,那我们就从最简单的字符指针变量来讲起!

一,字符指针变量

名词解释:根据我们之前剖析指针数组的时候一样;指针数组——是数组,是存放指针的数组;同理类比过来字符指针也是如此;字符指针(变量)——是指针,是指向字符(存放字符地址)的指针变量。

另外一种理解:指针变量是存放地址的变量,类型在前面指针变量在后面就是存放类型的地址,比如整型指针变量——存放整型变量地址的指针变量,
数组指针变量——存放数组地址的指针变量。
比如:

#include<stdio.h>
int main()
{
    
    
	char ch='a';
	char *p=&ch;//存放字符地址的指针变量
	printf("%c\n",*p);
	return 0;
}

除了这种最基本的字符指针外,我们还介绍一种——常量字符串即指针直接指向一个字符串例如:

#include<stdio.h>
int main()
{
    
    
	char* p = "abcdef";
	//两种打印方式 第一种
	printf("%s\n", p);//%s接受的是地址
	//第二种
	while (*p != '\0')
	{
    
    
		printf("%c", *p);
		p++;
	}
return 0;
}

第二种我们知道是循环打印即一次打印每个字符,而第一种有些人就有疑问了,为什么打印的不是 *P 而是p,第一因为%s接受的是地址,第二 P 这个指针变量拿到的是字符串的的首地址,可以讲其看成是一个数组其中a就是首元素,指针变量p拿到的就是a的地址然后再将其打印。
下面来看一道题:

#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与str2不一样,这个容易理解我们前面说过数组名是数组的首地址数组名都不一样,当然就不一样了。
但str3与str4是一样的这是为什么呢?其实上面也说过了就是两个字符指针指向或存放了相同的字符串,同一字符串首地址是相同的所以str3自然与str4是一样的喽。在这里插入图片描述
了解完字符指针变量紧接着我们来了解数组指针变量。

二,数组指针变量

1,数组指针变量是什么?

前面我们说过字符指针,字符指针是指针;是存储字符地址的指针。
数组指针变量一样,数组指针是指针,是指向(存储)数组地址的指针。

字符指针是 char *p 那么我们可以照葫芦画瓢推测整型指针类型是
int *p 那么我们推测一下数组指针是不是就是 int *arr[10] 呢?

答案其实不是。 我们来拆解一下 int *arr[10] 这段代码的含义你就知道了。
在这里插入图片描述

那么数组指针到底长什么样呢?其实长这样 int (*p)[10] 下面我们给出这个代码的解释:

p先和*结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是 ⼀个指针,指向⼀个数组,叫 数组指针。 这⾥要注意:[]的优先级要⾼于*号的,所以必须加上()来保证p先和*结合。
在这里插入图片描述

2,数组指针变量初始化

前面说了数组指针是指针,是用来存放数组地址的指针。既然是存放数组地址那在我们初始化的时候应该存放的是数组的地址,上一篇文章说了要拿到数组的地址就要取地址&+数组名。例:
对于数组名数组地址还不了解的可以去看指针(3)!

int arr[10]={
    
    0};
int (*p)[10]=&arr;//&arr拿到的是整个数组的地址
//所以数组指针就是专门用来存放数组地址的指针

我们将它放到vs里面调试一下让大家看看:

在这里插入图片描述
我们可以看到 &arrp 的类型是一样的,所以这时候 p 等价于 arrp==arr

数组指针总结:

在这里插入图片描述

三,二维数组传参的本质

了解完了数组指针如果我们强行去使用它,会觉得有些别扭我们举个例子来给大家看看:

#include<stdio.h>
int main()
{
    
    
	int arr[5] = {
    
     1,2,3,4,5 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int (*p)[5] = &arr;//拿出arr数组的整个地址赋给数组指针变量p
	//p=&arr
	//*p==*&arr==arr
	for (int i = 0;i < sz;i++)
	{
    
    
	//printf("%d ", (*p)[i]);//注意*P要加括号,不然p就先跟[]结合了
	printf("%d ", *(*p + i));//*p==arr首地址 所以(*p+i)//地址偏移 *				(*p+i)再解引用

	}
return 0;
}

这样用起来会非常变扭,我们既然已经拿到整个数组的地址了,却又要将它解引用变成首地址再用来打印,这样不是很麻烦吗?而且还不如以前直接用下标的方式来访问数组来得方便。

这就是我为什么要介绍二维数组的传参本质了,数组指针一般用在二维数组的传参中!我们先举个打印二维数组的例子:

#include<stdio.h>
void print(int arr[3][5],int row,int col)
{
    
    
	int i=0;
	for(i=0;i<row;i++)
	{
    
    
		int j=0;
		for(j=0;j<col;j++)
		{
    
    
			printf("%d ",arr[i][j]);
		}
		 printf("\n");
	}
}
int main()
{
    
    
	int arr[3][5]={
    
    1,2,3,4,5,2,3,4,5,6,3,4,5,6,7};
	print(arr,3,5);
	return 0;
}

在这里插入图片描述
我们可以看到结果被很好的打印出来了,但如果我们要写成指针的形式呢?
要写成指针的形式我们先来深入的了解一下二维数组:
不理解数组的可以去看数组篇,传送门:一篇文章搞定数组
在这里插入图片描述
二维数组可以看作由多个一维数组构成;一维数组就是二维数组的元素;二维数组的首地址就是第一个一维数组。有了上面的理解我们将上面的代码写成指针的形式:

#include<stdio.h>
void print(int (*p)[5],int row,int col)
{
    
    
	int i=0;
	for(i=0;i<row;i++)
	{
    
    
		int j=0;
		for(j=0;j<col;j++)
		{
    
    
			//(*p+i) *p是首元素的地址即arr1的地址 +i行偏移
			//(*p+i)==p[i]==arr[i] arr[i]表示的是行 
			
			//*((*p+i)+j)==p[i][j]==arr[i][j] +j是列偏移
			printf("%d ",*((*p+i)+j));
			//printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
    
    
	int arr[3][5]={
    
    1,2,3,4,5,2,3,4,5,6,3,4,5,6,7};
	print(arr,3,5);//实参传的是arr是首元素
	return 0;
}

在这里插入图片描述

解释:

二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据上⾯的例子,第一行的一维数组的类型就是 int [5] ,所以第一行的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址!

到此就介绍完了数组与指针,下面将介绍函数与指针;

四,函数指针变量

1,函数指针的创建和使用

前面我们说过了字符指针是存储字符的指针;数组指针是存储数组的指针;那函数指针自然就是存储函数的指针了。
那既然是存储函数的指针那我们就想办法拿到函数的地址,函数有地址吗?怎么拿到函数的地址呢?我们来写一个加法函数:

#include<stdio.h>
int Add(int x,int y)
{
    
    
	return x+y;
}
int main()
{
    
    
	int a=10,b=20;
	int r =Add(a,b);
	//printf("%d\n",r);
	printf("%p\n",&Add);
	printf("%p\n",Add);
	return 0;
}

在这里插入图片描述
运行结果告诉我们函数也是有地址的,而且函数的地址就是函数名这点我们从 &AddAdd 打印的地址一样就可以看出来!

有了函数的地址接下来就是要怎么储存这个地址,这就要我们创建一个函数指针变量来储存函数的地址,怎么创建呢?还记得上面所说的数组指针的创建吗?函数指针的创建与数组指针的创建比较相似。
在这里插入图片描述

有了上面的理解我们来创建一个函数指针变量来改造一下上面的代码:

#include<stdio.h>
int Add(int x,int y)
{
    
    
	return x+y;
}
int main()
{
    
    
	int a=10,b=20;
	int (*pf)(int ,int )=&Add;//或int (*pf)(int ,int )=Add;
	int r =(*pf)(a,b);//对pf解引用
	//或 int r=pf(a,b);//因为&Add==Add 所以*号写不写都无所谓
	printf("%d\n",r);
	return 0;
}

在这里插入图片描述

有一点要注意就是当使用指针变量去代替函数名时,如果写成解引用的形式就必须要加括号即 (*pf)(a,b) 而不是 * pf(a,b) 这样的话 pf(a,b) 会直接返回30而 *30 就会出现语法错误!

2,两段有趣的代码

1. (*(void (*)())0)();
可以尝试分析一下这段代码表示什么意思呢?

首先我们层层剥开来一探究竟
1,从0入手 首先void(*)()这是一个返回值为void 没有参数的函数指针类型
2,接着 (void(*)())0 在一个数字面前加类型很明显是强制类型转换
3,((void ()())0)() 发生了强转,将0当作一个地址,这个0地址出的函数是:无参返回类型为 void的参数。

所以 **(*(void (*)())0)();**是一次函数调用,调用的是o这个地址处的函数!

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

1,首先我们能看到里面有一个函数signal(int , void(*)(int))且后面有一个分号我们可以初步判定为是一个函数声明。
2,将signal(int , void()(int))去掉留下 void ()(int);我们发现是一个函数指针类型。
所以是怎么样的一个函数声明呢?
void (*signal(int , void(*)(int)))(int); 是一个参数类型为int,和一个参数类型为函数指针,且返回类型也为函数指针的函数声明。

上面两段代码确实让人看起来很费劲,那有没有能简化他们的办法呢?有的,这就是接下来要讲的typedef关键字了。

3,typedef关键字

首先我们要明白typedef是用来干嘛的?typedef 是用来类型重命名的,可以将复杂的类型,简单化。举个例子:

#include<stdiuo.h>
typedef unsigned int uint;//将unsigned int 改名为uint
int main()
{
    
    
	unsigned int a=10;//以前写unsigned int这个类型觉得很长 现在可以重新定义一个新的名字叫uint 来代表unsigned int
	uint b=20;
	printf("%d",a);
	return 0;
}

在这里插入图片描述
我们可以看到正常定义和我们使用typedef改名定义的类型是一样的,说明这种方法有效!
那如果是指针类型呢?其实也是一样的:

#include<stdio.h>
typedef int * p_t;
int main()
{
    
    
	int a=10,b=20;
	int *p1=&a;//int *是类型 p1是指针变量名
	p_t p2=&b;//p_t类型也是int * p2是指针变量名
	return 0;
}

在这里插入图片描述
我们看到使用typedef重命名来定义与传统定义依然一样。那有人就又要问了那typedef来重命名数组指针和函数指针还是一样吗?数组指针和函数指针的重命名与上面还是有些许不一样,我们举例说明:

#include<stdio.h>
//typedef int(*)[6] arr_t;这样改是错的因为我们在定义的时候名字是与*在一起的所以typedef定义的时候也要这么做
typedef int(*arr_t)[6];
int main()
{
    
    
	int arr1[6]={
    
    0};
	int arr2[6]={
    
    0};
	int (*p1)[6]=&arr1;//这是我们传统定义方式
	//去掉名字得类型为int (*)[6] 那么typedef改名字时也要这样改
	arr_t p2=&arr2;
	return 0;
}

在这里插入图片描述
我们看到p1和p2的类型都是 int[6]*int (*)[]。但要特别注意在typedef时重命名的名字要放在 *号 旁边。

函数指针其实也是如此,下面举例:

#include<stdio.h>
int Add(int x,int y)
{
    
    
	return x+y;
}
typedef int (*fun_t)(int ,int);
int main()
{
    
    
	int (*p1)(int ,int)=Add;
	fun_t p2=Add;
	return 0;
}

在这里插入图片描述
我们看到两种定义的类型都为函数指针类型即 int(*)(int ,int) 也要注意在typedef重命名的时候名字的位置一定在 *号 旁边!

现在我们在回看 void (*signal(int , void(*)(int)))(int); 这段让人头疼的代码,我们给他重命名:

#include<stdio.h>
typedef void (*fun_t)(int);
int main()
{
    
    
	//void (*signal(int , void(*)(int)))(int);
	//变成
	fun_t signal(int,fun_t);
	return 0;
}

使用typedef后是不是看起来变得简单了呢?

五,函数指针数组和转移表

1,函数指针数组

经过我们前面的学习,通过看函数指针数组这个后缀我们就知道它是一个数组,用来存储函数指针。那函数指针数组如何来定义呢?非常简单,只需要将函数指针的名字改成数组就可以了:

int(*p)(int,int)//这是一个函数指针,2个参数类型均为int
int(*arr[])(int ,int)//这就是一个函数指针 arr先和[]结合变成数组 再与*结合

有人会问函数指针数组有什么作用呢?其实看了标题你就会知道函数指针数组可以用做转移表。

2,转移表

写一个程序来模拟计算器:

#include <stdio.h> 
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; 
}
int main() 
{
    
     
	int x, y; 
	int input = 1; 
	int ret = 0; 
	do{
    
     
		printf("*************************\n"); 
		printf(" 1:add 2:sub ************\n"); 
		printf(" 3:mul 4:div ************\n"); 
		printf(" 0:exit *****************\n"); 
		printf("*************************\n"); 
		printf("请选择:"); 
		scanf("%d", &input); 
	switch (input) 
	{
    
    
	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; 
	case 0: 
		printf("退出程序\n"); 
		break; 
	default: 
		printf("选择错误\n"); 
		break; 
		} 
	} while (input); 
	return 0; 
}

我们可以看得到代码非常的冗余,我们不妨利用一个数组来存放这些函数指针,使用的时候调用就可以了:

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; 
}
int main() 
{
    
     
	int x, y; 
	int input = 1; 
	int ret = 0; 
	int(*p[5])(int x, int y) = {
    
     0, add, sub, mul, div }; 
	//转移表 
	do
	{
    
     
	printf("*************************\n"); 
	printf("******** 1:add 2:sub ****\n"); 
	printf("******** 3:mul 4:div ****\n"); 
	printf("******** 0:exit *********\n"); 
	printf("*************************\n"); 
	printf( "请选择:" ); 
	scanf("%d", &input); 
	if ((input <= 4 && input >= 1)) 
	{
    
     
		printf( "输入两个操作数:" ); 
		scanf( "%d %d", &x, &y); 
		ret = p[input](x, y); 
		printf( "ret = %d\n", ret); 
	}
	else if(input == 0) 
	{
    
     
		printf("退出计算器\n"); 
	}
	else 
	{
    
     
		printf( "输⼊有误\n" ); 
	} 

 }while (input); 
	return 0; 
}

函数指针数组就像一个表一样能够将函数随时调用出来,大家会发现使用函数指针数组可以让代码更精炼,也可以解决代码冗余的问题。

好了以上就是本章的全部内容啦!
感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!
在这里插入图片描述