IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 深入理解C语言之精髓——指针两万字全解! -> 正文阅读

[C++知识库]深入理解C语言之精髓——指针两万字全解!

前言

指针是C语言重要的特色,也是C语言的重难点部分,对于很多人来说也是个痛点。刚接触指针的人,如果没有一个正确的学习方向,学起来是比较痛苦的。而对于接触过的人,有些知识点掌握的可能也是懵懵懂懂的状态。本篇文章共有13节,每一节我都尽可能的去解释好对应的知识点,以及对于一些知识点,我会用图解的方式进行辅助理解。
如果你是初学者,我希望你可以耐心的看完每个知识点,并且一定要理解前面的知识后才去学习后面的知识,千万不可着急。
如果你是接触过指针,可以自行选择想要观看的内容,或者温故而知新即可。

若在阅读过程中,有不理解的地方、或者错误的地方都可以评论或私信我!话不多说,接下来就开始指针之旅!

1、初识指针

我们知道,学校的宿舍楼、酒店的房间通常都有房间号。有了这些房间号,就可以很方便地进行住宿管理。计算机的内存空间就像一栋房,里面有连续的内存空间来存放数据。每个内存空间都有自己的编号,也称为地址。有了这些内存单元的地址,计算机系统就可以快速方便地存储和管理数据。
1.1数据的存储:

了解指针之前,我们须弄清楚数据在内存中是怎样存放的。
计算机中的内存是由很多的单元组成的,而这些单元存储的都是数据,这些单元都是以字节为单位,连续的单元(字节)就是一块内存空间。为了正确的访问这些内存单元(存储的数据),必须给每个内存单元编号,然后通过这些编号即可准确地找到该对应的内存单元。而这些编号也叫作地址,通常也称为指针。如下图:
在这里插入图片描述
上图中,数据20存放在a变量中的,当把a的地址赋给一个p变量,此时p变量就称为指针变量!我们可以通过操作p指针变量来间接操作a变量。

1.2指针变量的定义:

指针变量是专门用来存放地址的,所以我们可以通过&(取地址操作符)取出变量在内存的首地址,然后把该地址放到另一个变量中,那么这个变量就是指针变量,即存放地址的变量。

注意:存放在指针中的值都被当成地址处理

#include<stdio.h>
int main()
{
	int a = 10; //为变量a在内存中开辟一块空间用来存数据10

	int *p = &a;//a为4个字节,有4个地址。
				//这里是把变量a的首地址放到变量p中的,此时p就是一个指针变量。
				//int表示p指向的a类型是int,*告诉我们p是个指针变量。

	//存放在指针中的值都被当成地址处理:
	int *pc = 10;//把一个非地址的值存放进指针变量,
				 //此时就会把10强行改成一个地址,而这个地址
				 //并不属于你本身的,这是极其危险的行为!			 
	return 0;
}

1.3指针变量的使用:

#include<stdio.h>
int main()
{
	int  a = 10; 
	int *p = &a;
	*p = 20;// 指针p存着a的地址,即p == &a,
			// 当*p时,相当于*&a,*&抵消,剩下a,
			// 即*p == a,所以改变*p就是改变a。
			
	printf("%d",a);//结果为20;
						 
	return 0;
}

其中 * 表示间接寻址运算符,如*p就表示找到p所指向的地址,然后对该地址 * 解引用,就找到其内容。
&表示取地址运算符,即取出其地址。可以把 和& 想象成一个是来一个是去,当它们在一起时就会相互抵消。
我们需要注意的是,如果是在声明处 ,
* 只是表示这是一个指针,而不是解引用。如下:

int a = 10;
//声明处的 *
int *p = &a;//这里的*先和p结合,告诉我们p是个指针,而不是对其&a进行*解引用。

1.4指针变量的编址:

一个小的单元是1个字节,且经过计算和权衡我们发现一个字节给一个对应的地址是比较合适的,如下图:
在这里插入图片描述

每个地址标识一个单元(1个字节),那我们就可以给
(2^32Byte == 2^32/1024KB ==2^32/1024/1024MB ==2^32/1024/1024/1024GB == 4GB) 4G的空间进行编址。

所以在32位机器上,一个地址是由32位0或1二进制序列组成的,而1Byte=8bit,那一个地址就需要4个字节的空间来存储。而一个指针变量只能存储一个地址,所以一个指针变量的大小就应该为4个字节。
( 64位机器可自行计算,原理都是一样的,这里就不展示了)。

1.5总结:

指针变量是用来存放地址的,通过地址可以找到对应的内存单元。
指针的大小在32位平台是4个字节,在64位平台是8个字节。

2、指针类型

我们都知道,变量有不同的类型,如整形,浮点型等,而指针同样也有类型。

普通类型

int a;//类型为int
float b;//类型为float
char c;//类型为char

通过观察就会发现,只要把变量名去掉,剩下的就是类型!
所以同样的,指针的类型我们只需要去掉变量名即可:

#include<stdio.h>
int main()
{
	int a = 10;//类型为int
	int b = 20;//类型为int
	int*pa= &a;//类型为int*
	int*pb= &b;//类型为int*
}

2.1指针类型的意义:
2.1.1指针类型决定了在对p解引用操作时可以访问多大字节(即可以修改多大字节的空间)。在这里插入图片描述

2.1.2指针类型决定了地址向前( - ) / 向后( + )一次走多少个字节的空间。

在这里插入图片描述
无论是指针类型还是什么类型,去掉名字剩下的就是类型。当以后遇到指针或者其他变量、数组、函数等时,我们应该问一问自己类型是什么?。只有明白了类型之后,我们才能更好的使用指针去对其操作。并且类型的理解,对后面的内容体现的是很明显的。

3、野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)。

3.1未初始化指针

#include<stdio.h>
int main()
{
	//局部变量指针未初始化,默认为随机值
	int* p;//这里p就是个野指针,因为指向的位置是随机值
	*p =10;//*p对其解引用操作会对把该随机值当成一个地址,
		   //然后在把10存放到该地址,但这个地址不属于我们当前程序的。

	return 0;
}

3.2指针越界访问
在这里插入图片描述

3.3指针指向已释放的空间
在这里插入图片描述

3.4如何规避野指针:

  1. 指针初始化:
#include<stdio.h>
int main()
{
	//(1)
	int  a = 10;
	int *p = &a;//明确初始化(明确指向a)

	//(2)
	int *p = NULL;//把指针p初始化成空指针
				  //当不知道指针指向哪里的时候可以初始化成NULL
				  //NULL就是用来初始化指针的
				  //而NULL的本质就是0

	return 0;
}

  1. 注意指针越界
  2. 指针指向空间释放,及时置NULL(后面会详细讲更多相关内容)
  3. 避免返回局部变量(栈空间)的地址(因为栈空间上的一些地址,出了函数空间就被释放了)
  4. 指针使用之前检查有效性(重):
#include<stdio.h>
int main()
{
	//(1)
	int *p=NULL;
	printf("%d",*p);//强行解引用访问会把NULL(0)强行改成地址,对于NULL(0)指向的空间是不能访问的,此时程序就会崩掉

	//所以我们使用指针前应该检查
	//(2)
	int *p=NULL;
	
	if(p != NULL)//判断指针是个有效指针而不是空指针时才进行操作,前提是要在判断之前要确保指针初始化
	{
		printf("%d",*p);
	}
	
	//(3)
	//也可以这样
	int  a = 10;
	int* p = &a; 
	
	if(p != NULL)//判断指针是个有效指针而不是空指针时才进行操作,前提是要在判断之前要确保指针初始化
	{
		printf("%d",*p);
	}

	//(4)
	//对于空指针,我们是不能直接进行解引用操作的

	//错误写法
	int* p = NULL;
	*p = 10;//error,空指针是不能访问的,解引用修改值程序就会崩掉
	
	//正确写法
	int* p = NULL;
	int  a = 10;
	p = &a;
	*p = 20;
	
	return 0;
}

以上几点在一定程度上可以避免野指针的出现,但关于如何避免野指针的问题不止这几点,只有我们在写代码的过程中不断的积累经验和学习,功底越来越厚之后,才能更好的避免野指针的出现。

4、指针运算

4.1指针± 整数
在这里插入图片描述

4.2指针的关系运算
若指针要进行关系运算,前提是同时指向一块空间。若不是同时指向同一块空间,比较将无意义!

#include<stdio.h>
int main()
{
	//(1)
	//无意义的比较
	int Arr1[5];
	int Arr2[10];
	int* p1 = &Arr1[0];
	int* p2 = &Arr2[9];
	
	if(p1<p2)//条件不成立,因为两个指针没有同时指向一块空间,此时比较的两个指针(地址)也将毫无意义
	{
		printf("0");//不会输出0
	}

	//(2)
	//通常的比较
	//例1
	int a[5];
	int* p1 = &a[0];
	int* p2 = &a[4];

	if(p1 < p2)//&a[0]的地址 < &a[4]的地址,所以条件成立!
	{
		printf("1");//成功输出1
	}

	//例2
	int arr[5]={1,2,3,4,5};
	int *p = &arr[4];
	for(p; p >= &arr[0];p--)
	{
		printf("%d ",*p);
	}
	return 0;
}

上述的例2,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。

标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较

图解:
在这里插入图片描述

4.3指针 - 指针
1、需同时指向一块连续的空间,否则结果是无法预测的(结果取决于编译器)。
2、指针 - 指针的绝对值是指针和指针之间的元素个数。
3、指针 + 指针无意义,且程序会出现错误。
在这里插入图片描述

5、二级指针

指针变量也是变量,它也有自己的地址,而当指针变量的地址存放在另一个变量里时,此时该变量就是二级指针 。

在这里插入图片描述

6、指针数组

指针数组是数组,是用来存放指针的数组(存放地址的数组)。

6.1指针数组的声明:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int* parr[2] = {&a,&b};//[2]先与parr结合成为一个数组,数组有2个元素,
						   //之后的* int表示数组的每个元素是指针,
						   //所以这是个指针数组,去掉数组名,剩下的就是类型
						   //所以该指针数组的类型为int*  [2]
	return 0;
}

6.2指针数组的使用:
在这里插入图片描述

以上是指针初级主题的内容,当我们了解了以上内容之后,接下来才能更好的了解下面的高级主题内容。

7、字符指针

字符指针是个指针,是用来存放字符的指针。

7.1字符指针的声明:
7.1.1一般声明:

#include<stdio.h>
int main()
{
	char  ch = 'h';
	char* pc = &ch;
}

7.1.2另外一种声明:

#include<stdio.h>
int main()
{
	char* pc = "abcdef";
}

7.2字符指针的使用:

7.2.1一般使用:

#include<stdio.h>
int main()
{
	char c = 's';
	char* pc = &c;
	*pc = 't';
	printf("%c",c);//结果为 t
	return 0;
}

7.2.2另外一种使用:

#include<stdio.h>
int main()
{
	const char* pc = "hello";//pc是字符指针,且是字符串常量(不可修改的字符串),
							 //所以我们加上const来修饰,使其不可修改。
							 
	printf("%c\n", *pc);//结果为h
	printf("%s",  pc);//结果为 hello
	return 0;
}

图解:
在这里插入图片描述

不是说指针存放的是地址吗? 为什么这里存的是字符串?如上述的 pc = “hello”,其实C语言中编译器会给字符串常量分配地址,所以pc = "hello"就是个地址,即本质就是pc = “hello” = 0xffffff40所以字符指针指向的是首字符的地址,而不是把整个字符串存放进指针。

7.3字符数组和字符指针的区别:

接下来我们来比较一下下面两个看起来很相似的代码。

	char  str1[] = "hello";
	char  *Str2  = "hello";

前者是声明str1是一个字符数组,后者是声明Str2是一个字符指针。
上面的这两个声明都可以用作字符串,但需要注意的是,不能错误地认为上面的str1和Str2可以互换或等价,字符数组和字符指针之间是有很大的差别的。

7.3.1区别1:

在声明数组时,就像任意数组元素一样,可以修改存储在数组中的字符。而在声明为字符指针时,指针指向的是字符串常量,而字符串常量是不可修改的。

	#include<stdio.h>
	int main()
	{	
		//数组可以任意修改数组元素的内容
		char  data[7] = "hello";//data是数组名
		data[0] = 'E';//把数组中的第一个元素改成H
		
		//字符指针
		//因为字符串常量是不可修改的,所以我们为严谨起见最好加上const来修饰。
		const char *Data  = "hello";//Data是指针变量,指向的是字符串常量
		*Data = "ABC";//error,因字符串常量不可修改
		return 0;
	}

7.3.2区别2:

在声明为数组时,data是数组名(数组名是首元素地址)。在声明为指针时,Data是指针变量,而这个变量可以在程序执行期间指向其他字符串。

	#include<stdio.h>
	int main()
	{	
		char  data[] = "hello";//data是数组名
		
		char  *Data  = "hello";//Data是指针变量
		
		Data = "ABC";//指针变量Data重新指向其他字符串:ABC
					 //注意这里是Data而不是*Data
		return 0;
	}

7.3.3区别3:

如果希望可以修改字符串,那么我们可以建立字符数组来存储字符串,或者使字符指针指向一个字符数组,而不是字符指针。

#include<stdio.h>
int main()
{
	//(1)正确写法
	char data[7] = "abcdef";//建立数组来存储字符串
	
	char* Data = data;//使字符指针指向字符数组(数组名是首元素地址)
	*Data = "hello"//这时我们就可以修改字符串了(修改时不能超过指向数组的最大长度)

	//(2)错误写法
	char* DATA = "AAAAA";//指针DATA指向的是字符串常量
	*DATA = "BBBBB";//error,字符串常量不可修改
	
	return 0;
}

7.3.4区别4:

下面的声明虽然使编译器为指针变量p分配了足够的内存空间:

	char* p = NULL

但可惜的是,它不能为字符串分配空间。因为我们没有指明字符串长度,所以在使用指针p作为字符串之前,我们需要先让它指向一个字符数组。

#include<stdio.h>
int main()
{
	char  str[7] = "ABCDEF";
	char* p = str;//数组名str是首元素地址
	return 0;
}

现在指针p指向了str的首字符A的地址,这时可以把指针p作为字符串使用了。另一种是让p指向一个动态分配的字符串(这里就不展示了)。
需要注意的是:使用未初始化的指针变量作为字符串是非常严重的错误。如下:

#include<stdio.h>
int main()
{
	char* p;
	
	p[0] = 'A';//erro	p[0]等价于*(p+0)
	p[1] = 'B';//erro	……
	p[2] = 'C';//erro
	p[3] = '\0'//erro	p[3]等价于*(p+3)
	return 0;
}

因为指针p没有被初始化,不知道指向了哪里,所以p是个野指针,而用野指针p把字符a、b、c、\0写入内存会导致未定义的行为。

8、数组指针

我们先来了解以下内容:

8.1数组名VS&数组名:

#include<stdio.h>
int main()
{
	int arr[5] = {1,2,3,4,5};
	printf("%p\n",arr);//arr表示的是首元素的地址
	printf("%p",&arr);//&arr表示的是数组的地址
	return 0;
}

在这里插入图片描述

注:数组名除了以下两种情况外,剩下的都表示首元素地址。

#include<stdio.h>
int main()
{	
	int arr[10];
	&arr;//表示取出数组的地址
	sizeof(arr);//表示计算数组的大小
}	
	

8.2数组指针的声明:

我们已经熟悉:整型指针、字符指针以及指针数组:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int * i;//这是整型指针,能够指向整型的指针
	char* c;//这是字符指针,能够指向字符的指针
	int* parr[2]={&a,&b};//这是指针数组,存放地址(指针)的数组。
	return 0;
}

那数组指针就是: 能够指向数组的指针,即存放数组地址的指针。(注:不要和指针数组搞混了)

#include<stdio.h>
int main()
{
	int arr[5];//一维数组
	int (*pa)[5] = &arr;// pa先和*结合,表示pa是个指针
					   // 然后与[5]结合,表示pa指向一个一维数组,该数组有5个元素,每个元素是int类型
					   //该数组指针的类型去掉名字,类型就是int (*)[5]。
	return 0;
}

8.3数组指针的使用

8.3.1对一维数组的使用:

#include<stdio.h>

void test(int(*p)[5], int sz)//需要类型一样,所以这里指针类型就为int *[5]
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ",(*p)[i]);// 这里传参传的是&arr,所以p==&arr,那*p就等价于*&arr,而*和&抵消,剩下arr,
							  // 所以*p==arr,而arr[i]我们访问的就是数组下标。
							  // 如果传参传arr,arr首元素地址,而*p
	}
}
int main()
{
	
	int arr[5]= {1,2,3,4,5};//去掉名字,该数组类型为int [5]。
	int sz = sizeof(arr) / sizeof(arr[0]);
	test(&arr,sz);//&arr是传数组的地址,去掉名字,剩下&,而有&,那么类型就也要有*,所以该数组类型为int *[5]
				  //既然类型是int *[5],那么我们传参的时候就需要一个类型为int *[5]的指针来接收

	return 0;
}

而数组指针一般很少用于一维数组,更多的是对二维数组的使用。

8.3.2对二维数组的使用:

#include<stdio.h>

void test(int (*p)[5],int r,int c)	//假设有int a[5]是个一维数组,一维数组的地址就相当于&a,
									//去掉名字,剩下&,而有&,类型就需要有*,所以我们需要一个int * [5]类型的指针来接收。
{
	for(int i=0; i<r,i++)
	{
		for(int j=0; j<c,j++)
		{
			printf("%d ",*(*(p+i)+j));// p指向的是行,p+i表示指向第i行,然后*(p+i)表示对该行解引用,
									  // *(p+i)+j表示拿到i行里所存元素的第j的地址,
									  // *(*(p+i)+j)表示找到对应的行和列的元素
		}
	}
}
int main()
{
	int arr[3][5] = {{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};//3行5列的一个二维数组
	
	test(arr,3,5)	//数组名arr是首元素地址,而二维数组的首元素地其实是第一行的地址
					//而每行其实都是一个一维数组,所以这里传的是一个一维数组的地址(是一维数组的地址,不是一维数组的首地址)
				 
}

图解:
在这里插入图片描述

以上就是关于数组指针的内容。接下来我们就来了解数组、指针参数的传参。

9、数组、指针参数的传参

我们在写代码的时候难免要把【数组】或者【指针】传给函数,而函数的参数又该如何设计?

9.1一维数组传参:

#include<stdio.h>

void test_1(int arr_1[3])//函数以整型数组形式接收:数组是什么样的,函数形参的设计可以写成跟数组一样,且这里的3可以省略。
{
}
void test_1(int *arr_1)//函数以指针形式接收:arr[0]这个元素的类型是int,
{					   //而实参&arr_1[0]表示对该元素取地址,出现&,那么类型就为int*,所以我们形参可以用一个类型为int*的指针来接收。
}


void Test_2(int* Arr_2[5])//函数以指针数组形式接收:数组是什么样的,函数形参的设计可以写成跟数组一样,且这里的5可以省略。
{}
void Test_2(int **Arr_2)//函数以二级指针来接收:arr[0]这个元素的类型是int*
{						//而实参&Arr_2[0]表示对该元素取地址,出现&,那么类型就为int **,所以我们形参可以用一个类型为int **的指针来接收。
}
int main()
{
	int  arr_1[3] = {0};//整型数组:该数组有3个元素,每个元素是int类型
	int* Arr_2[5] = {0};//指针数组:该数组有5个元素,每个元素是int*类型
	test_1(arr_1);//数组名是首元素地址,即&arr_1[0]
	Test_2(Arr_2);//数组名是首元素地址,即&Arr_2[0]
	return 0;
}

如果你还不是很理解上面的内容,不妨格外注意:数组元素的类型,以及对该元素&(取地址)后,需要在该元素原本的类型上加个* 。理解了这句话之后在看一遍试试?

9.2二维数组传参:

#include<stdio.h>

//函数形参部分用数组接收
void test(int arr[2][4])//数组是什么样的,函数形参的设计就可以写成什么样的。
{
}
void test(int arr[][4])//对于二维数组而言,行可以省略,但是列不能省略(列是多少必须与数组一样),
{					   //即可以不知道多少行,但是必须知道多少个元素,所以这里函数形参的设计也是没问题的
}
void test(int arr[][])//error,不能两个都省略,只能省略行,不能省略列。
{
}

//函数形参部分用指针接收
void test(int *arr)//error,整型指针:存放的是整型的地址,而实参是一维数组的地址,所以这里的设计是错误的。
{
}
void test(int* arr[4])//error,指针数组:是用来存放指针的数组,而实参是一维数组的地址,所以这里的设计是错误的。
{
}
void test(int (*arr)[4])//数组指针:是用来存放数组地址的指针,所以这里函数形参的设计是没问题的。
{
}
void test(int **arr)//error,二级指针:存放的是一级指针的地址,而实参是一维数组的地址,所以这里的设计是错误的。
{
}
int main()
{
	int arr[2][4]={0};//2行4列的二维数组,每行有4个元素。
	test(arr);//数组名是首元素地址,即第一行的地址,而每一行又都是一个一维数组,所以二维数组名arr是一个一维数组地址。
	return 0;
}

9.3一级指针传参:

#include<stdio.h>

void test_1(int *p)//函数形参部分为一级指针接收。
{
}
int main()
{
	int n = 10;
	int arr[5];
	int* p1 = &n;
	int* p2 = arr;
	
	//函数形参部分为一级指针时,函数可以接收以下的参数:
	test_1(&n);//n的类型为int,那&n的类型就问int*,所以可以用一级指针接收。
	test_1(arr);//传的是数组首元素地址,元素类型为int,对元素&取地址那类型就为int*。
	test_1(p1);//去掉名字,指针p1的类型为int*,所以可以用一级指针接收。
	test_1(p2);//传的是数组首元素地址,元素类型为int,对元素&取地址那类型就为int*。
	
	return 0;
}

9.4二级指针传参:

#include <stdio.h>
void test(int** ptr)//函数形参部分为二级指针接收。
{
}
int main()
{
	int a = 20;
	int*p = &a;
	int **pp = &p;
	int *arr[5];//指针数组:存放指针的数组,数组每个元素的类型为int*
	
	
	//函数形参部分为二级指针时,函数可以接收以下的参数:
	test(pp);//二级指针pp类型为int**
	test(&p);//去掉名字,指针p的类型为int*,&p那类型就问int**
	test(arr);//传的是数组首元素地址,而每个元素类型为int*,对元素&取地址那类型就为int**
	
	return 0;
}

以上就是关于数组、指针传参的内容,如果看完觉得很难理解,建议回到前面的知识在复习复习。

10、函数指针

我们已经知道,数组指针是用来存放数组地址的指针,那函数指针呢?如果从名字上去分析,那么函数指针就是用来存放函数地址的指针, 其实函数指针就是如此,就是用来存放函数地址的指针。

10.1函数指针的声明:

我们知道一个数组,它的数组名表示首元素地址,&数组名表示数组的地址,那一个函数它的函数和&函数名又是怎样的呢?

#include<stdio.h>

void test(int x, int y)
{
}
int main()
{
	int arr[10];
	printf("arr  = %p\n", arr);//arr表示数组首元素地址
	printf("&arr = %p\n", &arr);//&arr表示数组的地址
	printf("\n");
	printf("test = %p\n", test);//test表示函数地址
	printf("&test= %p\n", &test);//&test也表示函数地址

	return 0;
}

图解:
在这里插入图片描述
因为函数不像数组会有多少元素,所以函数名是直接表示函数的地址的,所以我们就知道对于函数来说函数名和&函数名意义是一样的,只不过我们写&函数名只是为了让人更容易理解这是函数的地址而已,但两者其实意义都一样。所以我们就可以这样定义:

#include<stdio.h>
int test(char x,char y)//去掉名字,该函数的类型就是int  (char x,char y),这里的x和y可以省略
{					   //也就是int  (char x,char y)等价于int  (char ,char)
}
int main()
{
		//函数指针的定义
	int (*p1)(char x,char y) = test;//*号先和p1结合,告诉我们p是一个指针,在与()结合,告诉我们指向的是函数
								    //函数的参数是(char x,char y),最后的int则表示函数的返回类型是int
								    //去掉p1,剩下的int(*)(int,int)就是函数指针的类型,函数指针的类型只关注:参数类型和返回类型,所以这里的x,y是可以省略的。

		//也可这样定义						  
	int (*p2)(char,char) = test;
	int (*p3)(char a,char b) =&test;
	int (*p4)(char,char) = &test;
	
	return 0;
}

我们观察就会发现,其实去掉指针名字,剩下的就是指针类型。去掉指针名字和*,那么剩下的就是函数类型(即所指向对象的类型)。

10.2函数指针的使用:

#include<stdio.h>

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

	int sum = 0;
	//一般函数的使用
	sum = test(2,3);//函数名+参数

	//函数指针的使用
	int (*p)(int,int) = test;//定义函数指针

	//使用
	sum = (*p)(2,3);//我们知道test(2,3)就可以调用函数,而p存放test,
					//也就是p == test,所以这里的*其实也就是个摆设(甚至多加几个*都一样),是无意义的。
					
	//所以我们就可以这样写
	sum = p(2,3);//指针p存放test,所以p(2,3)是等价于test(2,3)的。
	
	printf("%d",sum);

	return 0;
}

学习了以上内容后,那下面的函数,函数指针该如何定义?

int test (const int *x,float y)
{
}

解答:

#include<stdio.h>
int main()
{
	//可以是这样
	int (*p)(const int*,float) = test;
	//也可以是这样
	int (*p)(const int* x,float y) = test;
}

11、函数指针数组

数组是一个存放相同类型数据的存储空间,去掉:“ 数组名[ ] ” ,剩下的就是数组元素的类型。
我们知道,指针数组是一个数组是:存放指针的数组。

#include<stdio.h>
int main()
{
	int  a = 10;//整型变量
	int* p = &a;//p是整型指针
	int* parr[1] = {p};//整型指针数组
	
	return 0;
}

当我们把整型指针数组中的 parr[1](数组) 去掉后,就剩下 int *(指针),而 int *,就是该数组“元素的类型”。而函数指针数组也是如此。

我们需要知道,数组名和&数组名[0]虽然都表示首元素地址,这一特点和:函数名和&函数名都表示函数地址是一样的。虽然都表示是地址,它们的类型是有所不同的。千万不要以为它们都表示地址,那么类型就一样。如下所示:

#include<stdio.h>
int test(int x,int y)//去掉名字函数名test,剩下int (int,int)就是类型
{					 //而&test,去掉test,剩下&,而*和&是成对出现的,所以&test的类型就为
					 //int (*)(int,int)
	
}
int main()
{
	int arr[10] = {0};//去掉名字arr,剩下int  [10]就是类型。
					  //&arr[0],去掉arr[0],剩下&,*和&成对出现的,所以&arr[0]的类型就为int*
	return 0;
}

图解:
在这里插入图片描述
学习了上面的内容之后,我们接下来就可以很好的理解函数指针数组了。

11.1函数指针数组的声明:

前面我们说过,指针数组是一个数组是:存放指针的数组。而函数指针数组,其实也是一个数组,是:存放函数指针的数组。
函数指针数组:是存放函数指针的数组,可以存放多个【参数相同、返回类型相同】的函数的地址。

#include<stdio.h>
int test_1(int x,int y)
{
	return x + y;
}

int test_2(int n,int m)
{
	return n - m;
}

int main()
{
	int (*p1)(int x,int y) = test_1;//p1是函数指针,去掉名字,类型为int (*)(int x,int y)
	int (*p2)(int n,int m) = test_2;//p2是函数指针,去掉名字,类型为int (*)(int n,int m)

	int (*parr_1[2])(int x,int y) = {p1,p2};//parr先和[2]结合,表示parr是个数组,
										    //去掉parr[2](数组),剩下的int (*)(int,int)就是数组 “元素的类型”。
										    //即元素的类型是函数指针。
	
	//也可以这样定义
	int(*parr_2[2])(int,int) = {test_1,test_2};//去掉parr_2[2],剩下的int (*)(int,int)就是数组 “元素的类型”,即元素类型为函数指针。
											   //而test_1 == &test_1,而&test_1的类型就是int (*)(int,int)。

	return 0;
}

我们需要理清一点:函数指针数组 parr_2[2],是数组类型是int (*)(int ,int),而决定的元素只能是函数指针。而不是因为数组的元素是函数指针,而决定的数组类型。

11.2函数指针数组的使用:

函数指针数组是数组,所以我们就按照数组的使用方式去访问函数指针就可以了。

#include<stdio.h>
int test_1(int x,int y)
{
	return x + y;
}
int test_2(int x,int y)
{
	return x - y;
}
int main()
{
	int sum = 0;
	int(*parr[1])(int,int)={test_1,test_2};
	sum = parr[0](3,3);//这里的parr[0]就相当于test_1,传参为3和3。
	sum = parr[1](3,1);//这里的parr[1]就相当于test_2,传参为3和1
	printf("%d",sum);//输出6
	return 0;
}

知道了如何使用后,这里我们就使用函数指针数组来实现一个小的案例:计算器

#include<stdio.h>

int add(int a, int d)
{
	return a + d;
}
int sub(int s, int u)
{
	return s - u;
}
int mul(int m, int u)
{
	return m*u;
}
int div(int d, int i)
{
	return d / i;
}

int main()
{
 	int x, y;
 	int input = 1;
    int sum = 0;
	int(*parr[5])(int x,int y) = { 0, add, sub, mul, div };
	
	while (input)
    {
    	printf( "*************************\n" );
        printf( " 1:add           2:sub   \n" );
        printf( " 3:mul           4:div   \n" );
        printf( "*************************\n" );
        printf( "请选择:" );
        
      	scanf( "%d", &input);//用来表示数组下标
          if ((input <= 4 && input >= 1))
          {
          		printf( "输入操作数:" );
                scanf( "%d %d", &x, &y);//输入想要传参的值
                sum = (*parr[input])(x, y);//调用下标所对应的函数,并且传参。
          }
          else
          {
               printf( "输入有误\n" );
          }
          printf( "sum = %d\n", sum);
    }
    
	return 0;
}

上述的案例我们通过函数指针数组来实现会方便和高效许多。以上就是关于函数指针数组的内容。我们接下来就简单的来了解,指向函数指针数组的指针。

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

还记得我们前面所学的数组指针吗?数组指针,也叫指向数组的指针,是存放数组的指针。
而我们知道,函数指针数组也是一个数组,当把函数指针数组存放进一个指针时,就称之为:指向函数指针数组的指针。
我们回顾一下数组指针的声明:

#include<stdio.h>
{
	int arr[10];//arr是数组,去掉arr,剩下的int [10]表示数组类型
				//去掉arr[10],剩下的int表示数组元素的类型为int。
				
	int(*p1)[10] = &arr;//p1是指针,p1的类型为int(*)[10]。
						//去掉*p1,剩下的int [10]就是所指向的数组的类型。
						//去掉p1,剩下的就表示指针所指向的类型。
	return 0;
}

我们需要理清楚:
去掉 p,剩下的就表示指针所指向的数组的类型。
去掉p1,剩下的就表示指针所指向的类型。

清楚之后我们就可以来定义,指向函数指针数组的指针了。

12.1指向函数指针数组的指针的声明:

#include<stdio.h>
int test_1(int x,int y)
{
	return x + y;
}
int test_2(int x,int y)
{
	return x - y;
}
int main()
{
	//函数指针数组
	int(*parr[1])(int ,int) = { &test_1, &test_2 }; //parr是数组,去掉parr,剩下的int (* [1])(int,int)表示数组类型。
									    			//去掉数组parr[1],剩下的int (*)(int,int)表示数组元素的类型。
	
	//指向函数指针数组的指针
	int (*(*p)[1])(int,int) = &parr;//p是指针,p的类型为int (*(*)[1])(int,int)
									//去掉*p,剩下的int(* [1])(int,int)就是所指向的数组的类型
	return 0;
}

如果还是看的不太明白,很大原因可能是因为对类型还不够理解,建议去前面的知识复习复习!

12.2指向函数指针数组的指针的使用:

#include<stdio.h>
int test_1(int x,int y)
{
	return x + y;
}
int test_2(int x,int y)
{
	return x - y;
}
int main()
{
	//函数指针数组
	int(*parr[2])(int ,int) = { &test_1, &test_2 }; //parr是个数组
	
	//指向函数指针数组的指针
	int (*(*p)[2])(int,int) = &parr;//这里只能是&parr,而不是parr,注意这两个的区别。

	int sum = (*p)[0](2, 6);//p存着&parr,即p==&parr,所以*p就相当于*&parr,*&抵消,剩下parr。
							//所以*p等价于parr,所以(*p)[0]就是找到数组的元素,因为parr数组元素是函数,所以我们需要传参。
	int ret = (*p)[1](7, 4);

	printf("%d\n", sum);//结果为8
	printf("%d\n", ret);//结果为3
	return 0;
}

以上就是关于本节内容,如果对于本节内容不是很理解(当然能理解最好),其实关系也不大,因为我们对指向函数指针数组的指针使用是很少的,也用不着太深究,所以我们这里就大概的知道有这个东西,以后见到不陌生就可以了。
不知道你有没有发现,我们连着的这3节都离不开:函数指针。对于前面函数指针的使用,我们只是简单的介绍和使用而已,而对于函数指针的真正使用,其实是回调函数!

13、回调函数

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

这里我们举个简单的计算器例子:

#include<stdio.h>

int add(int a, int d)
{
	return a + d;
}
int sub(int s, int u)
{
	return s - u;
}
int mul(int m, int u)
{
	return m * u;
}
int div(int d, int i)
{
	return d / i;
}

void cale(int(*p)(int, int))//p存放着函数的地址,即p的类型是int(*)(int,int),即函数指针。
{
	int x, y;
	scanf("%d %d",&x,&y);//输入要计算的两个值
	int ret = p(x, y);//p == 所存放的函数的地址。当p调用时,p所指向的函数,我们就称之为回调函数!
	printf("%d\n",ret);
}
int main()
{
	int input = 0;
	scanf("%d", &input);
	switch (input)//input决定要实现什么功能
	{
	case 1:
		cale(add);//add == &add,把add函数的地址作为cale函数的参数。
		break;	  //&add的类型为int(*)(int,int),所以cale函数的参数是个函数指针。
	case 2:
		cale(sub);
		break;
	case 3:
		cale(mul);
		break;
	case 4:
		cale(div);
		break;
	}

	return 0;
}

例如这里的add,cale函数的参数是把add的地址传给指针函数p,我们在cale函数内部,调用p函数时,其实就是调用add函数,而此时add函数就称之为回调函数,其它函数也是同理。
而当我们了解了回调函数之后,我们就来学习一个函数:qsort函数。

13.1psort函数:

qsort是C语言标准库提供的排序函数, 它采用的是快速排序的思想。
我们所熟悉的冒泡排序(不熟悉的可以百度下),它只能排整形数据。而对于qsort函数它任何数据都可以排。这也是两者最大的区别。

我们需要明白,既然是排序,那数据肯定需要先放进一个数组里。 所以我们下面来了解qsort函数的参数和返回类型。

void qsort ( void* base,  size_t num,  size_t size, int (*compar)(const void*,const void*) );

图解:
在这里插入图片描述

qsort函数有4个参数,无返回类型。因为是排序函数,所以前三个参数都是关于数组的。而最后一个参数是函数指针,即函数的地址。所以是把compar函数的地址作为qsort函数的参数,当我们调用qsort函数时,会在qsort函数内部调用compar函数,此时的compar就是回调函数!

因为qsort函数可以排序任意类型,因为设计者不知道我们会排序什么类型的数据,所以在设计之初就设计一个指针来存放任意类型的地址以便我们使用,而void *就是该指针,void * 表示它是个无类型的指针,该指针可以存放任意类型的地址,当想要使用时,强制类型转换后,在解引用即可!如下:

一般情况:只存类型匹配的地址

#include<stdio.h>
int main()
{	
	int   n = 10;
	char  c = 'r';
	int* pa = &n;//pa只能存类型匹配的地址(不匹配编译器会报警告)。
	char*pc = &c;//pc只能存类型匹配的地址。
	
	//如下的类型不匹配会报警告
	pa =&c;//pa类型为int*,&c类型为*char
	pc =&n;//pc类型为char*,&a类型为int*
	return 0;
}

void * 情况:可存不同类型的地址

#include<stdio.h>
int main()
{	
	int  n = 10;
	char ch = 'w';
	double d = 1.11;

	//void* 指针可存任意类型的地址
	void* p1 = &n;
	p1 = &ch;
	p1 = &d;
	
	//不能这样使用
	*p1 = 3.33;//error

	//需要类型转换后才能使用
	*(double*)p1 = 2.10;//注意最后存的是double*类型的地址,所以我们这里才强制类型转换成该类型。
	
	return 0;
}

了解完每个参数之后,我们就可以来学习qsort函数的定义和使用了。

13.1.1qsort函数的声明和使用:

#include<stdio.h>

int cmp(const void* p1,const void* p2)//定义cmp函数,参数中的p1和p2存放着两个元素的地址
{
	return (*(int*)p1 - *(int*)p2); // 如果(*(int*)p1  > *(int*)p2) ,则return  1;
								 	// 如果(*(int*)p1 == *(int*)p2) ,则return  0;
									// 如果(*(int*)p1  < *(int*)p2) ,则return -1;
}
int main()
{

	int arr[10] = {2,9,6,3,7,1,0,4,8,5};
	int sz = sizeof(arr)/sizeof(arr[0]);//求数组有多少个元素
	
	qsort(arr, sz, sizeof(int), cmp);//cmp == &cmp(即函数地址),类型为int(*)(const void*,const void*),即函数指针,
									 //当cmp调用时,就进入cmp函数。

	for (int i = 0; i < sz; i++)
		{
			printf("%d ", arr[i]);//结果为:0 ~ 9
		}

	return 0;
}

图解:
在这里插入图片描述

以上就是关于int类型的数据的排序,接下来我们来实现对其他类型的数据进行排序。

对char类型排序:

#include<stdio.h>
#include<stdlib.h>//qsort所需的头文件
#include<string.h>
int cmp(const void* p1, const void* p2)
{
	return  (*(char*)p1 - *(char*)p2);
}
int main()
{
	char ch[7] = "yzxbac";

	int len = strlen(ch);//数组长度

	qsort(ch, lne, sizeof(char), cmp);

	for (int i = 0; i < len; i++)
	{
		printf("%c ", ch[i]);//结果为 abcxyz
	}

	return 0;
}

这里只是字符排序。想字符串排序的可以自己试试,原理都是一样的。

对结构体类型排序:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[15];
	int age;

};
int cmp(const void* p1, const void* p2)
{
	return  strcmp(((struct Stu*)p1)->name , ((struct Stu*)p2)->name);//按名字排序
	//return  (((struct Stu*)p1)->age - ((struct Stu*)p2)->age);//按年龄排序
}
int main()
{
	struct Stu s[3] = { { "zhangsan",15}, {"lisi",20}, {"wangwu",18}};//结构体数组

	int sz = sizeof(s) / sizeof(s[0]);//sz == 3

	qsort(s,sz,sizeof(s[0]),cmp);//s是首元素地址。sizeof(s[0])表示每个元素的大小。

	for (int i = 0; i < sz; i++)
	{
		printf("%s\n",s[i].name);//按名字排序
		//printf("%d\n",s[i].age);//按年龄排序
	}

	return 0;
}

图解:
在这里插入图片描述

以上内容只是举了我们都比较熟悉的类型进行排序,qsort可以排序的数据类型还有很多,但原理都是一样的,如果能理解上面内容,对未来遇到的任意类型的排序,就可以大胆去实现了。

理解了qsort的实现,接下来我们就来模拟实现qsort函数,让它也如同qsort一样对任意类型进行排序。

13.2模拟qsort函数:

对int类型排序:

#include<stdio.h>
int cmp_int(const void* p1, const void* p2)//定义cmp_int函数,类型为int (const void* , const void* )
{
	return *(int*)p1 - *(int*)p2 ;

}
void Swap(char* ps1,char* ps2,int sz)//用ps1和ps2接收两个元素的地址,sz表示元素的大小,即sz == 4
{
	for (int i = 0; i < sz; i++)
	{
		char tmp = *ps1;//ps1和ps2类型是char*
		*ps1 = *ps2;	//所以交换的时候是一个字节一个字节交换的
		*ps2 = tmp;		//而元素的大小是4,所以需要交换4次,也就是循环4次
		ps1++;//向后走1个字节
		ps2++;
	}
}
void My_qsort(void* pa, int All_ele, int sz, int cmp(const void* e1, const void* e2))//cmp 接收参数时类型要和传参时的cmp_int一样,
{																					 //此时使用cmp就是使用cmp_int函数,即cmp == cmp_int

	for (int i = 0; i < (All_ele - 1); i++)//趟数
	{
		for (int j = 0; j < (All_ele - 1 -i); j++)//一趟的过程
		{
			if ( cmp( (char*)pa + j * sz ,(char*)pa + (j + 1) * sz) > 0 )//调用cmp_int函数,然后传数组的两个元素的地址过去比较。然后对返回值进行判断是否 > 0
			{
				Swap( (char*)pa + j * sz, (char*)pa + (j + 1) * sz, sz);//刚刚 return > 0,所以这里调用Swap函数,将这两个元素的地址传过去,以进行交换。
			}
		}
	}
}
void test()
{
	int arr[] = { 4,2,9,8,10 };
	int All_ele = sizeof(arr) / sizeof(arr[0]);//求数组共有多少元素

	My_qsort(arr, All_ele, sizeof(arr[0]), cmp_int);//我们在最前面已经定义了cmp_int函数,类型为int (const void*,const void*)
													//所以是把该函数的地址(函数指针)作为参数的。

	for (int i = 0; i < All_ele; i++)
	{
		printf("%d ",arr[i]);
	}
}
int main()
{
	test();
	return 0;
}

图解:
在这里插入图片描述
对结构体类型排序:

#include<stdio.h>
#include<string.h>
struct stu//先定义结构体,才能使用结构体。
{
	char name[15];
	int age;
};
int cmp_name(const void* p1, const void* p2)//定义cmp_int函数,类型为int (const void* , const void* )
{
	return strcmp( ((struct stu*)p1)->name,((struct stu*)p2)->name );//字符串比较

}
void Swap(char* ps1, char* ps2, int sz)//用ps1和ps2接收两个元素的地址,sz表示元素的大小
{
	for (int i = 0; i < sz; i++)
	{
		char tmp = *ps1;//ps1和ps2类型是char*
		*ps1 = *ps2;	//所以交换的时候是1个字节1个字节交换的
		*ps2 = tmp;		//所以需要交换sz次,也就是循环sz次
		ps1++;//向后走1个字节
		ps2++;
	}
}
void My_qsort(void* pa, int All_ele, int sz, int cmp(const void* e1, const void* e2))//cmp 接收参数时类型要和传参时的cmp_name一样,
{																					 //此时使用cmp就是使用cmp_name函数,即cmp == cmp_name

	for (int i = 0; i < (All_ele - 1); i++)//趟数
	{
		for (int j = 0; j < (All_ele - 1 - i); j++)//一趟的过程
		{
			if (cmp ((char*)pa + j * sz, (char*)pa + (j + 1) * sz) > 0)//调用cmp_name函数,然后传数组的两个元素的地址过去比较。然后对返回值进行判断是否 > 0
			{
				Swap((char*)pa + j * sz, (char*)pa + (j + 1) * sz, sz);//刚刚 return > 0,所以这里调用Swap函数,将这两个元素的地址传过去,以进行交换。
			}
		}
	}
}
void test()
{
	struct stu s[3] = { {"zhangsan",16},{"lisi",14},{"wangwu",18} };
	int All_ele = sizeof(s) / sizeof(s[0]);//求数组共有多少元素

	My_qsort(s, All_ele, sizeof(s[0]), cmp_name);//我们在最前面已经定义了cmp_name函数,类型为int (const void*,const void*)
												 //所以是把该函数的地址(函数指针)作为参数的。
	for (int i = 0; i < All_ele; i++)
	{
		printf("%s\n", s[i].name);
	}
}
int main()
{
	test();
	return 0;
}

图解:
在这里插入图片描述
以上就是关于模拟qsort的内容,还有很多类型可以模拟实现,原理都是大同小异的。只有当我们掌握了其核心内容,才能游刃有余的使用。关于指针的内容还有很多,当理解了本篇文章的指针后,遇到关于指针的内容,也大概可以看得懂什么意思。当然后续我也会更新一些关于指针的内容。
本篇文章到这里也就结束了,只有我们不断的积累知识,今后才能在在知识的海洋中畅游!

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-10-22 20:56:54  更:2022-10-22 20:57:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 3:42:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码