3

【详解C语言指针】我真的让C指针给我唱征服了~乌拉

 2 years ago
source link: https://blog.csdn.net/weixin_50614301/article/details/123319121
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.



在这里插入图片描述

众所周知,C指针算是C最难的一部分。在这我会将和大家一起深究指针并征服C指针。

在正式学习进阶指针之前,我们先来简单回忆下指针基础

指针定义:指针变量,用于存放地址。地址唯一对应一块内存空间。
指针大小:32位平台下占4个字节,64位平台占8个字节。
指针类型:类型决定指针±整数的步长和指针解引用时访问的大小。
指针运算:指针解引用,指针±整数,指针-指针,指针的关系运算。
指针诞生:
在这里插入图片描述

这些内容在C基础部分已经讲过,铁汁们可以复习一下👉传送门👈,看完记得回来啊~

理解一遍后,就让我们正式起航扬帆吧!乌拉~
注: 我们习惯把指针变量叫作指针,本文指针本质是指针变量


一、字符指针

1.字符指针的定义

字符指针: 指向字符的指针,类型为char*


2.字符指针的作用

  1. 指向单个字符变量
	char ch = "w";
	char* pch = &ch;
  1. 指向字符串首字符
	char* pc = "hello";
	printf("%s\n",pc);

看图说话:
(为了理解简单,地址用201、204表示)
在这里插入图片描述

①比较好理解,pch存有ch的地址,因此可通过解引用操作访问ch
②并不是把字符串"hello"放进指针,而是把字符串首字符的地址放进指针,通过首字符地址,可以找到整个字符串

对于上面的解析,我们可以进行验证:
在这里插入图片描述
为什么靠一个首字符的地址就可以找到整个字符串呢?

下面我们来证明一下:

	char* pc = "hello";
	printf("%c\n", *(pc + 1));//e
	printf("%c\n", *(pc + 2));//l
	printf("%s\n", pc);//hello
	printf("%s\n", pc + 1);//ello

运行结果:
在这里插入图片描述

  1. 字符串中每1个字符占1个字节,且内存的单元是1个字节,为了方便管理,字符串在内存空间上是连续存放的
    即hello是连续的
  2. %s:输出字符串,从所给地址开始,一直打印到\0结束符号(不包括'\0')

3.字符指针的特点

这是一道题面试题:

#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;
}
newCodeMoreWhite.png

运行结果:
在这里插入图片描述

数组名是数组首元素的地址;指针存有字符串首字符的地址
两者有异曲同工之妙,所以
(str1 == str2) ,表示比较存放相同字符串的两个数组的地址是否相同
(str3 == str4),表示比较存放相同字符串的两个指针的值是否相同

我们可以做出分析:

1.str1[]和str2[]是字符数组,在内存上会开辟两块地址不一样空间,但存放相同的内容"hello bit"
2.str3和str4是指向常量字符串的指针,而常量字符串存放在内存的常量区,常量区特点是常量值不可被修改有唯一性(就没有存在2份或多份的必要),所以指针指向的是同一份数据,故地址是相同的
图解:
在这里插入图片描述

注: 常量区的常量不可修改,通常用const来修饰。防止被意外修改


二、指针数组

1.指针数组的定义

我们先看:

int arr[10]; 整型数组
char ch[5];  字符数组
float f[20]; 浮点型数组

整型数组是存放整型的数组。
类比得指针数组是存放指针的数组

int* parr[10];整型指针数组
char* pch[5];字符指针数组
float* pf[20];浮点型指针数组

关于指针数组的数组名:

int arr[10];
int* parr[10];

数组的数组名是首元素的地址:
整型数组的数组名arr,是首元素(整型)的地址,所以arr是一级指针。
整型指针数组的数组名parr,也是首元素(整型指针)的地址,所以parr是二级指针。


2.指针数组的使用

整型指针数组的使用:

#include<stdio.h>
int main()
{
    //int a = 10;
    //int b = 20;
    //int c = 30;
    //int* arr[3] = { &a, &b, &c };//不常用的写法
    
    int arr1[] = { 1,2,3,4,5 };
    int arr2[] = { 2,3,4,5,6 };
    int arr3[] = { 3,4,5,6,7 };
    int* parr[] = { arr1,arr2,arr3 };//常见的写法
    for (int i = 0; i < 3; i++) 
    {
        for (int j = 0; j < 5; j++) 
        {
            //1.
            printf("%d ", parr[i][j]);
            //2.
         // printf("%d ", *(*(parr + i) + j));
        }
        printf("\n");
    }
    return 0;
}
newCodeMoreWhite.png

运行结果:
在这里插入图片描述

通过指针数组访问整型数组的每一个元素
parr[i][j]等价于*(*(parr + i) + j)
在这里插入图片描述

字符指针数组的使用:

#include<stdio.h>
int main()
{
    const char* pch[] = { "abcde", "bcdef", "cdefg" };
    for (int i = 0; i < 3; i++)
    {
        //1.
        printf("%s", pch[i]);
        //2.
    //  printf("%s", *(pch + i));
        printf("\n");
        } 
}

运行结果:
在这里插入图片描述

pch[i]是字符串首字符的地址,%s:打印字符串


三、数组指针

1.数组指针的定义

我们已经知道:
字符指针:指向字符的指针
整型指针:指向整型的指针

C语言语法是有规律性的,也就是说,指针指向的类型<==>指针的类型(相互决定)
所以我们可以类比得:
数组指针:指向数组的指针,即数组的地址存放在数组指针中。

到此,我要说一下,指针部分会出现一些复杂的类型,那如何理解一个复杂的类型?

在这我先给大家抛出一个屡试不爽的技巧总结,有了这些法宝,后面的路就会容易走。

类型显得复杂的原因是它由多种的运算符组成,当我们分清运算符的优先级,理清顺序,自然会柳暗花明又一村~

1.运算符主要有三种,它们的优先级是:* < [ ] < ( )
2.变量名第一次与运算符结合就决定了它的本质。(变量名的处女情结?_?😂)
如,先与*结合是指针;先于[ ]结合是数组;
( ):1.先于( )结合是函数,即p( ) 2.用于改变优先级,如(*p)

下面我们一起来探索一下吧!

int* p[10];
int(*p)[10];
int p(int);
int (*p)(int);
int *(*p(int))[10];

int* p[10];//由优先级知:p先于[]结合,则p是数组;后与*结合,则是指针数组;最后与int结合,则是整型指针数组
int(*p)[10];//()优先级最高,p先与*结合,则p是指针;后[]结合,则是数组指针;最后与int结合,则是整型数组指针
int p(int);//p先与( )结合,则p是函数;参数是整型,返回值是整型,则是参数为整型,返回值是整型的函数
int (*p)(int);//p先与*结合,则p是指针;后与( )结合,则指针指向的是函数,函数参数是整型,返回值是整型,则是一个指向参数为整型,返回值是整型的函数的指针(函数指针)
int *(*p(int))[10];//p先与( )结合,则p是形参为int的函数;后与*结合,则是返回指针的函数;再与[ ]结合,则是返回的指针指向的是一个数组;再与*结合,说明数组里的元素是指针;最后与int结合,指针指向的内容是整型数据。所以p是返回值为整型指针数组指针,形参为int型的函数。


2.细说指针

好了,现在我们已经学会了如何理解一个复杂的类型。那对于一个指针,我们要搞清楚它的什么呢?

指针是一个特殊的变量,它存放着内存中的地址。
要深入了解它从这四方面考虑:
在这里插入图片描述

2.1.指针类型

上一章说过,去掉名字就是类型
同理,若把指针声明语句中的指针的名字去掉,剩下部分就是该指针的类型。

int* ptr;//指针的类型是int*
char* ptr;//指针的类型是char*
int** ptr;//指针的类型是int**
int (*ptr)[5];//指针的类型是int()[5]
int* (*ptr)[10];//指针的类型是int
(*)[10]

指针类型的意义(C基础篇已经讲过):

1.指针解引用访问几个字节(访问的内存空间大小)
2.指针类型决定了指针±整数跳过几个字节(步长)

2.2.指针所指向的类型

指针所指向的类型决定了编译器看待指针指向内存区的内容的方式
若把指针声明语句中的指针的名字和名字左边的指针声明符号*去掉,剩下部分就是指针所指向的类型。

int* ptr;//指针所指向的类型是int
char* ptr;//指针所指向的类型是char
int** ptr;//指针所指向的类型是是int*
int (*ptr)[5];//指针所指向的类型是int()[5]
int* (*ptr)[10];//指针所指向的类型是int()[10]

通过观察我们可以发现:

二者可互推在这里插入图片描述

2.3.指针的值

指针的值:指针里存放的地址
举个例子:

int *p;	//定义一个指针
int a;	//定义一个int类型的变量
p=&a;	//使用取址运算符(&)将变量a的地址赋给p

指针的值:p本身的值,p里存放这变量a的内存的起始地址
而指针p所指向的内存区就是从a的起始地址开始,长度为size(int)的一片内存区。

2.4.指针大小

指针大小:32位平台下占4个字节,64位平台占8个字节。


3.数组名相关

老生常谈:

①sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
②&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除这两种外,数组名都是数组首元素的地址。

#include<stdio.h>
int main()
{
	int arr[10] = {0};
	int* p1 = arr;//arr是数组首元素地址,为int型
	int (*p2)[10] = &arr;//&arr是整个数组的地址,为int [10]型
	//arr和&arr值一样,但类型不一样
	
	//p1和p2是相同的指向同一位置
	printf("%p\n", p1);//204
	printf("%p\n", p2);//204
	//指针类型决定指针±整数的步长
	printf("%p\n", p1 + 1);//跳过一个整型,208
	printf("%p\n", p2 + 1);//跳过一个数组,244
	return 0;
//为了简单理解,  204,208,244表示地址
}
newCodeMoreWhite.png

4.数组指针的使用

当我们遍历一维数组时,可以这样做:

void Print1(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		//printf("%d ", arr[i]); 
		printf("%d ", *(arr + i));
	}
}
void Print2(int* arr, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
		//printf("%d ", *(arr + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print1(arr, sz);
	Print2(arr, sz);
	return 0;
}
newCodeMoreWhite.png

运行结果:
在这里插入图片描述

把数组名传给函数时,我们有两种接收方式:
①数组接收,编译器会将数组退化为指针
②指针接收
因为数组名是数组首元素的地址,用指针接收是正确的。

当数组指针用来接收并访问一维数组时:

#include<stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,10};
	int (*parr)[10] = &arr;//指针指向数组,数组有10个元素,每个元素为int型
	int i = 0;
	for(i = 0; i < 10; i++)
	{
		printf("%d ", *((*parr) + i));//*parr相当于arr
	}
	return 0;
}

通常这种写法显得小题大作,比较别扭,非常不推荐这种写法。

通常,数组指针用来接收并访问二维数组,会有很好的效果:

void Print1(int arr[3][5], int r, int c)//二维数组传参,用二维数组接收,实际上不会创建二维数组,编译器会将int arr[3][5]退化为int(*pa)[5]
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
			//printf("%d ", arr[i][j]);
			printf("%d ", *(*(arr + i) + j));
		}
		printf("\n");
	}
}
void Print2(int(*pa)[5], int r, int c)//二维数组传参,用数组指针接收
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
            //1.
            printf("%d ", pa[i][j]);
            //2.
		//	printf("%d ", *(*(pa + 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 };
	Print1(arr, 3, 5);//二维数组首元素是首行 
	Print2(arr, 3, 5);//二维数组首元素是首行 
	return 0;
}
newCodeMoreWhite.png

运行结果:
在这里插入图片描述
分析:
在C基础数组章节,我们知道二维数组在内存中也是连续存储的。
在这里简单提下,需要复习的,👉C基础数组传送门 👈
连续存储:1.每一行内部的元素连续存放 2.行与行之间连续存放

在这里插入图片描述

  • 因此,二维数组的数组名是首行的地址,类型是int(*)[5]
  • 二维数组首元素地址和数组指针是等价的,即数组指针pa就是数组名。
  • 指针类型是int(*)[5],解一层引用找到的是二维数组的行
  • 指针所指向的类型是int[5],再解一层引用找到的是某行中的元素

综上,正确的做法是:使用数组指针来接收二维数组

正确使用数组指针会有很好的效果,但如果随便用可能会很别扭。
下面是强行使用数组指针的错误用法:

void Print3(int(*pa)[10], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		//printf("%d ", pa[i]);
		printf("%d ", *(pa + i));
	}
}
int main() {
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//一维数组
	int sz = sizeof(arr) / sizeof(arr[0]);
	Print3(&arr, sz);//&arr是整个数组的地址
	return 0;
}

在这里插入图片描述
实参为整个数组的地址,形参是数组指针,*(pa+i)解一层引用后,打印出来的为什么还是地址呢?

  • &是取地址运算符,*是间接运算符。&*互为逆运算
  • *&x 的含义是,先获取变量 x 的地址,再获取地址中的内容,相当于抵消
  • 所以&arr传给pa,执行*pa,相当于*&arr,实质就是arr,还是整个数组的地址。
  • 整个数组的地址的类型是int(*)[10],即指针pa的类型是int(*)[10]
  • 由指针类型决定指针±整数的步长可知,每个地址间相差40字节。

四、数组传参和指针传参

写代码时要把数组和指针传递给函数的情况在所难免,那函数参数该如何设计呢?

1.一维数组传参

一维数组传参,下面的接收方式它合理吗?

void test(int arr[])//合理吗?
{}
void test(int arr[10])//合理吗?
{}
void test(int *arr)//合理吗?
{}
void test2(int *arr[])//合理吗?
{}
void test2(int *arr[20])//合理吗?
{}
void test2(int **arr)//合理吗?
{}
int main()
{
    int arr[10] = {0};
    int* arr2[20] = {0};
    test(arr);
    test2(arr2);
}
newCodeMoreWhite.png

以上的接收方式都合理

分析:
在这里插入图片描述

数组作为函数形参,会退化为指针。
所以一维数组传参,数组和指针都可以作为函数形参。


2.二维数组传参

二维数组传参,下面的接收方式它合理吗?

void test(int arr[3][5])//合理吗?
{}
void test(int arr[][5])//合理吗?
{}
void test(int arr[3][])//合理吗?
{}
void test(int arr[][])//合理吗?
{}
int main()
{
    int arr[3][5] = {0};
    test(arr);//二维数组传参
    return 0;
}

分析:
在这里插入图片描述

为什么二维数组作为函数形参,行可省略,列不可省略。

原因: 二维数组存储的时候是"先行后列",如果不指定列数, 它就不能知道一行放几个数据了。只要知道了列数, 全部放完就可以知道一共能放多少行。
我们C基础数组在已经讲过,C基础数组传送门🤏。

二维数组传参,下面的接收方式它合理吗?

void test(int* arr)//合理吗?
{}
void test(int* arr[5])//合理吗?
{}
void test(int(*arr)[5])//合理吗?
{}
void test(int** arr)//合理吗?
{}
int main()
{
    int arr[3][5] = { 0 };
    test(arr);//二维数组传参
    return 0;
}

分析:
在这里插入图片描述

综上,我们可以总结出二维数组传参时,函数形参的两种设计方法:
①形参给出第二维(列)的长度
②形参为指向数组的指针

另外,还有一种方式:形参声明为指针的指针void test(int**a){},该方式比较复杂,到后期学到CPP在详谈(关注我不迷路哦~)


3.一级指针传参

一级指针作为实参时,函数形参该如何设计?

void print(int* ptr, int sz)//一级指针传参,用一级指针接收
{
    int i = 0;
    for(i=0; i<sz; i++)
    {
        printf("%d ", *(ptr + i));
    }
}
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(p, sz);//p是一级指针,传给函数
    return 0;
  //1 2 3 4 5 6 7 8 9 10
}
newCodeMoreWhite.png

一级指针作为实参,函数形参可以是一级指针,也可以是一维数组(不推荐)

反向思考: 若函数形参是一级指针,实参该如何设计?

void test(int* p)
{}
int main()
{
	int a = 10;
    int* pa = &a;
    test1(&a);
    test1(pa);
	return 0;
}

函数形参为一级指针,实参可以是地址(一级指针),也可以是数组


4.二级指针传参

二级指针作为实参时,函数形参该如何设计?

void test(int** pp)//二级指针接收
{
	printf("%d\n", **pp);
}
void test(int* arr[])//指针数组,会退化为二级指针,不推荐该写法
{
	printf("%d\n", *arr[0]);
}
int main() {
	int a = 10;
	int* p = &a;
	int** pp = &p;
	test(&p);//一级指针的地址,类型为二级指针
	test(pp);//实参二级指针
	return 0;
}
newCodeMoreWhite.png

二级指针作为实参时,函数形参可以是二级指针,也可以是指针数组(不推荐)

反向思考:若函数形参是二级指针,实参该如何设计?

void test(int** pp)
{
	printf("%d\n", **pp);
}
int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	int* arr[10] = { &a };

	test(&p);//一级指针p的地址,类型为二级指针
	test(pp);//实参二级指针
	test(arr);//数组名是数组首元素的地址,首元素是一级指针,它的地址是二级指针
	return 0;	
}
newCodeMoreWhite.png

函数形参为二级指针时,实参可以是二级指针(一级指针的地址),也可以是指针数组的元素的地址(特殊的为数组名,因为它是数组首元素的地址嘛)。

我们为什么要学习如何设计实参和函数形参?

1.调用别人设计好的函数时,我们需要清楚传什么实参。
2.根据函数的目的,来设计函数形参。


五、函数指针

1.函数指针的定义

类比得,函数指针:指向函数的指针,它存放着函数的地址。

没错,函数也是有地址的,可以看下面一组代码:
在这里插入图片描述

表示函数地址的方式:

注:这里需要与数组名区别开来:

  • 函数名 == &函数名 (都是函数地址,相同意义)
  • 数组名 != &数组名 (不同意义)

2.函数指针的类型

函数的地址需要存放在函数指针里,那么函数指针的类型该如何写呢?

其实很简单,上面我们学习了如何去理解一个负责的类型,相信我们也可以自己写出来:

//以Add函数为例
int (*pf)(int, int) = &Add;

解析:
在这里插入图片描述
自我检测:如何保存viod test(char* str){}的地址

答案:void (*pt)(char*) = &test;


3.函数指针的使用

计算机硬件程序经常通过调用地址的方式来调用函数,因此需要使用函数指针调用函数。

int Add(int x, int y)
{
    return x + y;
}
int main()
{

    //int(*pf)(int, int) = &Add;
    int(*pf)(int, int) = Add;

    int ret0 = pf(2, 3);
    int ret1 = (*pf)(2, 3);
    int ret2 = (**pf)(2, 3);
    int ret3 = (***pf)(2, 3);

    printf("ret1 = %d\n", ret0);
    printf("ret2 = %d\n", ret1);
    printf("ret3 = %d\n", ret2);
    printf("ret3 = %d\n", ret3);
    return 0;
}
newCodeMoreWhite.png

在这里插入图片描述
因为pf = Add; 所以pf(2,3)相当于Add(2,3),但pf是函数指针。
(*pf)(2,3)中的*并没意义,可加可不加。

阅读两端有趣的代码:(注: 出自 《C指针和陷阱》 )

代码1:

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

解析:

在这里插入图片描述
代码2:

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

解析:
在这里插入图片描述
或者我们可以通过类型重定义来理解:

int main()
{
	typedef void (*PFUN)(int);//把void(*)(int)类型重定义为PFUN
	PFUN signal(int, PFUN);//等效于void (*signal(int, void(*)(int)))(int);
	return 0;
}

六、函数指针数组

1.函数指针数组的定义

整型指针数组:存放整型指针的数组。
类比得,函数指针数组:存放函数指针的数组。

int Add(int x, int y)//int(*)(int,int)
{
    return x + y;
}
int Sub(int x, int y)//int(*)(int,int)
{
    return x - y;
}
int main()
{
    int (*pf1)(int, int) = Add;
    int (*pf2)(int, int) = Sub;

    int (*pfArr[2])(int, int) = { pf1, pf2 };
  //等效于int (*pfArr[2])(int, int) = { Add, Sub };
			 //pfArr 就是函数指针数组
    return 0;
}
newCodeMoreWhite.png
  • 类型相同的函数指针,放在同一个数组中。
  • 数组去掉数组名和[ ],剩下部分就是数组元素类型。pfArr的元素类型为int(*)(int,int)

2.函数指针数组的使用

实现一个简易计算器,来进行加减乘除运算。

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;
    do{
        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);
        printf("请输入2个操作数:> ");
        scanf("%d %d", &x, &y);
        switch (input)
        {
        case 1:
            ret = Add(x, y);
            break;
        case 2:
            ret = Div(x, y);
            break;
        case 3:
            ret = Mul(x, y);
            break;
        case 4:
            ret = Div(x, y);
            break;
        case 0:
            printf("退出程序\n");
            break;
        default:
            printf("重新选择\n");
            break;
        }
        printf("ret = %d\n", ret);
    } while (input);

    return 0;
}
newCodeMoreWhite.png

运行并测试发现:
在这里插入图片描述

input = 0input = 5放进程序就很容易发现问题所在。

我们对main主函数部分修改:

int main()
{
    int input = 0;
    do{
        menu();
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);
        switch (input)
        {
        case 1:
            printf("请输入2个操作数:> ");
            scanf("%d %d", &x, &y);
            ret = Add(x, y);
            printf("ret = %d\n", ret);
            break;
        case 2:
            printf("请输入2个操作数:> ");
            scanf("%d %d", &x, &y);
            ret = Div(x, y);
            printf("ret = %d\n", ret);
            break;
        case 3:
            printf("请输入2个操作数:> ");
            scanf("%d %d", &x, &y);
            ret = Mul(x, y);
            printf("ret = %d\n", ret);
            break;
        case 4:
            printf("请输入2个操作数:> ");
            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;
}
newCodeMoreWhite.png

运行并测试
在这里插入图片描述
经测试,虽然bug得到了解决,但代码仍存在如下缺陷:

1.代码冗余,出现大量重复的代码(case中)
2.可维护性低(后期需要增加其他功能,就得多写一个case)
3.代码可读性差

针对缺陷,我们利用函数指针数组进行优化,达到通过数组下标"跳转"来调用不同的函数的目的。

#include <stdio.h>
 
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;
    do {
        menu();
        int (*pfArr[5])(int, int) = {0, Add, Sub, Mul, Div};//pfArr就是函数指针数组
        int x = 0;
        int y = 0;
        int ret = 0;
        printf("请选择:> ");
        scanf("%d", &input);
        if(input >= 1 && input <= 4)
        {
            printf("请输入2个操作数:> ");
            scanf("%d %d", &x, &y);
            ret = (pfArr[input])(x, y);
            printf("ret = %d\n", ret);  
        }
        else if(input == 0)
        {
            printf("退出程序\n");
            break;
        }
        else
        {
            printf("选择错误\n");
        }
    } while(input);   
    return 0;
}
newCodeMoreWhite.png

运行并测试成功:
在这里插入图片描述

  • 这是函数指针数组的一个应用。函数指针数组的元素是函数形参同类型,返回值也同类型的函数指针,我们通过数组下标找到对应的函数指针,可直接调用函数。
  • 通常我们把这样的数组叫作转移表(《C和指针》中有所提及)。

七、(函数指针数组)指针

1.(函数指针数组)指针的定义

(函数指针数组)指针:本质是一个指针,指针存放着函数指针数组的地址。

给出一个函数,结合我们所学,请写出函数指针数组指针:

int Add(int x, int y)
{
    return x + y;
}

答案:
在这里插入图片描述
用咱们上面说过的方法来写,这岂不是洒洒水的事情~😁


2.(函数指针数组)指针的使用

我们还是以Add函数,来说明(函数指针数组)指针的使用:

int Add(int x, int y)
{
    return x + y;
}
int main()
{

	int (*pa)(int ,int ) = Add;	//函数指针pa

	int (*pArr[5])(int ,int );	//函数指针的数组pArr

	int (*(*ppArr)[5])(int ,int ) = &pArr;	//(函数指针数组)指针ppArr
	
	return 0;
}

八、回调函数

1.回调函数的定义

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

用图来解析:

在这里插入图片描述


2.回调函数的使用

用刚才的 switch 版本的计算器为例:

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;
}
void Calc(int (*pf)(int, int))//Calc形参为函数指针
{
    int x = 0;
    int y = 0;
    printf("请输入2个操作数:>");
    scanf("%d %d", &x, &y);
    printf("%d\n", pf(x, y));
}
int main()
{
    int input = 0;
    do
    {
        menu();
        printf("请选择:>");
        scanf("%d", &input);

        switch (input)
        {
        case 1:
            Calc(Add);
            break;
        case 2:
            Calc(Sub);
            break;
        case 3:
            Calc(Mul);
            break;
        case 4:
            Calc(Div);
            break;
        case 0:
            printf("退出\n");
            break;
        default:
            printf("选择错误\n");
            break;
        }
    } while (input);
    return 0;
}
newCodeMoreWhite.png

把冗余的代码封装成一个Calc函数。
把所需要用的函数地址传给Calc函数,Calc函数通过传进来的地址,找到所需要用的函数。
图解:
在这里插入图片描述


3.qsort函数(quick sort快速排序函数)

回顾冒泡排序(我们在C基础数组谈过):

void bubble_sort(int arr[], int sz)
{
    int i = 0;
    // 确认趟数
    for (i = 0; i < sz - 1; i++)
    {
        //一趟冒泡排序
        int j = 0;
        for (j = 0; j < sz - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                //交换
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

void print_arr(int arr[], int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
    int sz = sizeof(arr) / sizeof(arr[0]);

    print_arr(arr, sz);
    bubble_sort(arr, sz);
    print_arr(arr, sz);

    return 0;
}
newCodeMoreWhite.png

我们实现的冒泡排序函数bubble_sort只能对整型数组排序,当我们想对其他数组或字符串等排序时,bubble_sort就显得很鸡肋。

因此,我们来了解一下在这方面无所不能的qsort函数:
说明:qsort函数是C库函数中的快速排序函数,可处理多种类型数组。
在这里插入图片描述
(注: 给大家推荐一个学C的网站——菜鸟教程👍)

为什么qsort函数可以处理多种类型数组、字符串、结构体等呢?

我们对比自己写的bubble_sort函数和qsort函数,可以从中找到答案:
在这里插入图片描述
因为void* base指针和nitems(待排序数组的元素个数)、size(待排序数组的元素大小)可以描述出任意类型。

为什么将参数base的类型是viod*呢?
	int a;
	char* pa = &a;//从int* 到char* 类型不兼容

确定类型的地址赋值给不同类型的指针会警告类型不兼容,强制转化还可能精度丢失。

而viod*:无(具体)类型,又称通用类型。它可以接收任意类型的指针,但无法进行指针运算(解引用,指针±整数等)

知道了待排序数组的元素个数待排序数组的元素大小遍历它们的指针void* base,但是按升序还是降序的顺序还得依靠一个比较函数。

这个比较函数指定元素的比较方式,而且需要我们自行定义,所以qsort可以说是一个半库函数半自定义函数。
compar函数:

  • elem1小于elem2,返回值小于0
  • elem1大于elem2,返回值大于0
  • elem1等于elem2,返回值为0
    (注:elem1,elem2:进行比较的两个元素的地址作参数。)

现在我们把qsort函数内裤都摸透了,那我们一起来用一用吧~
在这里插入图片描述

3.1.qsort函数的使用

qsort函数对整型数组排序:

int int_cmp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;//e1 - e2:升序//将void* 强转为int*
								 //e2 - e1降序 
}
void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	qsort(arr, sz, sizeof(arr[0]), int_cmp);
	print_arr(arr, sz);
	return 0;
}
newCodeMoreWhite.png

运行结果:
在这里插入图片描述

qsort函数实质是回调函数,int_cmp函数把地址传给qsort函数,qsort函数通过地址找到int_cmp函数。
qsort函数对结构体排序:

#include<stdlib.h>
#include<string.h>
#include<stdio.h>
struct Stu
{
	char name[20];
	int age;
};
int sort_by_age(const void* e1, const void* e2)
{
	//return ((struct Stu*)e1) -> age - ((struct Stu*)e2) -> age;//升序
	return ((struct Stu*)e2)->age - ((struct Stu*)e1)->age;//降序
}
int sort_by_name(const void* e1, const void* e2)
{
	//return strcmp(((struct Stu*)e1) -> name, ((struct Stu*)e2) -> name);//升序
	return strcmp(((struct Stu*)e2)->name, ((struct Stu*)e1)->name);
}
int main()
{
	struct Stu stu[] = { {"zhangsan", 30}, {"lisi", 34}, {"wangwu", 20} };
	//按照年龄来升序
	qsort(stu, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_age);
	//按照名字来排序
	qsort(stu, sizeof(s) / sizeof(s[0]), sizeof(s[0]), sort_by_name);
	return 0;
}
newCodeMoreWhite.png

e1 - e2为升序;e2 - e1为降序

3.2.qsort函数的模拟实现

//打印函数
void print_arr(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
//交换函数
void Swap(char* buf1, char* buf2, size_t size)
{
	for (size_t i = 0; i < size; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
//比较函数
int cmp(const void* e1, const void* e2) 
{
	return *(int*)e1 - *(int*)e2;
}
//排序函数
void my_bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void* e1, const void* e2))
{
	for (size_t i = 0; i < num - 1; i++)
	{
		for (size_t j = 0; j < num - 1 - i; j++) 
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)//以字节为单位
			{
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print_arr(arr, sz);
	my_bubble_sort(arr, sz, sizeof(arr[0]), cmp);
	print_arr(arr, sz);
	return 0;
}
newCodeMoreWhite.png

运行结果:
在这里插入图片描述

  • 对于base + j * size,若basevoid*类型 ,语法不支持(指针±整数),而char*类型是任意类型中的最小单元。所以(char*)base + j * size可以确定元素地址。
  • Swap函数是以最小字节进行比较和交换,是代码具有普遍性。

指针进阶内容丰富,比较难理解,我们一定要多看多想多敲代码~

你听到了吗?经过我们跋山涉水,走过了千沟万壑,我们终于让C指针唱响了征服
最后,各位老铁看了文章,请给我点赞关注评论吧,你的支持是我坚持的动力~
……未完待续
在这里插入图片描述
笔试题链接:【这些题我一拿到手就会】指针和数组笔试题详解(上)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK