【进阶C语言】指针的进阶(万字图文详解)

在这里插入图片描述

指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

这个章节,我们继续探讨指针的高级主题。

1. 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:

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

还有一种使用方式如下:

#include <stdio.h>
int main()
{
    
    
	const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

代码 const char* pstr = “hello bit.”;

特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。

在这里插入图片描述
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
那就有可这样的面试题:

#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;
}

这里最终输出的是:
在这里插入图片描述

这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同。

2. 指针数组

《指针》章节 我们也学了指针数组,指针数组是一个存放指针的数组。
这里我们再复习一下,下面指针数组是什么意思?
类比一下:

  1. 字符数组 - 存放字符的数组
    int arr1 [ 10 ] ;
  2. 整型数组 - 存放整型的数组
    int arr2 [ 5 ] ;
  3. 指针数组 - 存放的就是指针
    存放字符指针的数组 - 字符指针数组
    char* arr3 [ 5 ] ;
    存放整型指针的数组 - 整型指针数组
    int* arr4 [ 6 ] ;
    举一个生动的例子:
int main()
{
    
    
	char* arr[] = {
    
     "abcdef", "hehe", "qwer" };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
    
    
		printf("%s\n", arr[i]);
	}
	return 0;
}

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

再举一个例子:

int main()
{
    
    
	int arr1[] = {
    
     1,2,3,4,5 };
	int arr2[] = {
    
     2,3,4,5,6 };
	int arr3[] = {
    
     3,4,5,6,7 };

	//arr[i] == *(arr+i)
	//arr是一个存放整型指针的数组
	int* arr[] = {
    
     arr1, arr2, arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
    
    
		int j = 0;
		for (j = 0; j < 5; j++)
		{
    
    
			//printf("%d ", arr[i][j]);
			printf("%d ", *(arr[i]+j));
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述

3. 数组指针

3.1 数组指针的定义

数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
类比:

  1. 整型指针 - 指向整型的指针
    int a = 10 ;
    int* p = &a ;
  2. 字符指针 - 指向字符的指针
    char ch = ’ w ’ ;
    char* pc = &ch ;
  3. 数组指针 - 指向数组的指针
    int arr [ 10 ] ;
    int (*pa ) [ 10 ] = &arr ; //取出的是数组的地址
    char arr[ 10 ] ;
    char (*pc) [ 10 ] &arr ;
    int * arr [ 5 ] ;
    int * (*p ) [ 5 ] = &arr ;
    下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

解释:

int (p)[10];
//解释:p先和
结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[ ]的优先级要高于号的,所以必须加上( )来保证p先和结合。

总结:

指针数组 - 是数组 - 是一种存放指针的数组。
数组指针 - 是指针 - 是一种指向数组的指针 - 存放的是数组的地址。

//指针数组 - 是数组 - 是一种存放指针的数组
//数组指针 - 是指针 - 是一种指向数组的指针 - 存放的是数组的地址

int main()
{
    
    
	//指针数组
	char* arr[4];
	//数组指针
	int arr[5];
	int (*p)[5] = &arr;

	return 0;
}

3.2 &数组名VS数组名

对于下面的数组:

int arr[10];

arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:

int main()
{
    
    
	int arr[10] = {
    
     0 };
	
	printf("%p\n", arr);

	printf("%p\n", &arr[0]);
	
	printf("%p\n", &arr);

	return 0;
}

运行结果如下:
在这里插入图片描述
可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:

//数组名绝大部分情况下是数组首元素的地址
//但是有2个例外:
//1. sizeof(数组名) - sizeof内部单独放一个数组名的时候,数组名表示的整个数组,计算得到的是数组的总大小
//2. &arr  - 这里的数组名表示整个数组,取出的是整个数组的地址,从地址值的角度来讲和数组首元素的地址是一样的,但是意义不一样
//

int main()
{
    
    
	int arr[10] = {
    
     0 };
	//printf("%d\n", sizeof(arr));
	printf("%p\n", arr);//int * 
	printf("%p\n", arr+1);//4

	printf("%p\n", &arr[0]);//int* 
	printf("%p\n", &arr[0]+1);//4

	printf("%p\n", &arr);//int(*)[10]
	printf("%p\n", &arr+1);//40
	int (*p)[10] = &arr;//p是一个数组指针
	//int(*)[10]
	return 0;
}

在这里插入图片描述
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40。
注意:

数组名绝大部分情况下是数组首元素的地址
但是有2个例外:

  1. sizeof(数组名) - sizeof内部单独放一个数组名的时候,数组名表示的整个数组,计算得到的是数组的总大小
  2. &arr - 这里的数组名表示整个数组,取出的是整个数组的地址,从地址值的角度
    讲和数组首元素的地址是一样的,但是意义不一样

3.3 数组指针的使用

那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:

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

	int sz = sizeof(arr) / sizeof(arr[0]);
	int (* p)[10] = &arr;
	int i = 0;
	//p  --- &arr
	//*p --- *&arr
	//*p --- arr
	//虽然对,但是不推荐
	for (i = 0; i < sz; i++)
	{
    
    
		printf("%d ", (*p)[i]);
	}
	//虽然对,但是不推荐
	for (i = 0; i < sz; i++)
	{
    
    
		printf("%d ", *((*p) + i));
	}

	//使用指针来访问
	int* p = arr;
	for (i = 0; i < sz; i++)
	{
    
    
		printf("%d ", *(p + i));
	}

	//下标的形式访问数组
	for (i = 0; i < sz; i++)
	{
    
    
		printf("%d ", arr[i]);
	}

	return 0;
}

一个数组指针的使用:

#include <stdio.h>
void print(int arr[3][5], int r, int c)
{
    
    
	int i = 0;
	for (i = 0; i < 3; i++)
	{
    
    
		int j = 0;
		for (j = 0; j < 5; 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(*arr)[5], int r, int c)
{
    
    
	int i = 0;
	for (i = 0; i < 3; i++)
	{
    
    
		int j = 0;
		for (j = 0; j < 5; j++)
		{
    
    
			//printf("%d ", *(*(arr + i) + j));//arr[i]
			printf("%d ", arr[i][j]);//arr[i]
		}
		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;
}

图片讲解:
在这里插入图片描述
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

1. int arr[5];
2. int *parr1[10];
3. int (*parr2)[10];
4. int (*parr3[10])[5];

第一个是数组
第二个是指针数组
第三个是数组指针
第四个原理:parr3是数组,数组中存放的指针,该指针指向的又是数组,所以是指针数组里面存放的数组指针

第四个图片讲解:
在这里插入图片描述

4. 数组参数、指针参数

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

4.1 一维数组传参

#include <stdio.h>
void test(int arr[])//ok?
{
    
    }
void test(int arr[10])//ok?
{
    
    }
void test(int* arr)//ok?
{
    
    }
void test2(int* arr[20])//ok?
{
    
    }
void test2(int** arr)//ok?
{
    
    }
int main()
{
    
    
	int arr[10] = {
    
     0 };
	int* arr2[20] = {
    
     0 };
	test(arr);
	test2(arr2);
}
}

结果是都可以运行。

结论:
一维数组传参,传参可以是数组,也可以是指针的,当形参是指针的时候,要注意类型。

4.2 二维数组传参

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 arr[3][5] = {
    
     0 };
	test(arr);
}

结论:
二维数组传参
传参可以是指针,也可以是数组
如果是数组,行可以省略,但是列不能省略
如果是指针,传过去的是第一行的地址,形参就应该是数组指针

4.3 一级指针传参

#include <stdio.h>
void print(int* p, int sz)
{
    
    
	int i = 0;
	for (i = 0; i < sz; i++)
	{
    
    
		printf("%d\n", *(p + i));
	}
}
int main()
{
    
    
	int arr[10] = {
    
     1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}

思考:

当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

在这里插入图片描述

4.4 二级指针传参

#include <stdio.h>
void test(int** ptr)
{
    
    
	printf("num = %d\n", **ptr);
}
int main()
{
    
    
	int n = 10;
	int* p = &n;
	int** pp = &p;
	test(pp);
	test(&p);
	return 0;
}

思考:

当函数的参数为二级指针的时候,可以接收什么参数?

在这里插入图片描述

5. 函数指针

类比方法:
整型指针 - 指向整型的指针 int*
字符指针 - 指向字符的指针 char*
数组指针 - 指向数组的指针 int arr[ 10 ]* ; int( p )[ 10 ] = & arr ;
函数指针 - 指向函数的指针 int

数组指针中存放的是数组的地址
函数指针中存放的应该是函数的地址
函数有地址吗?
首先看一段代码:

#include <stdio.h>
void test()
{
    
    
	printf("hehe\n");
}
int main()
{
    
    
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

输出的结果:
在这里插入图片描述
输出的是两个地址,这两个地址是 test 函数的地址。
数组:
数组名
&数组名

函数名和&函数名 都是函数的地址,没有区别。
那我们的函数的地址要想保存起来,怎么保存?
下面我们看代码:

void test()
{
    
    
	printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void* pfun2();

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?
答案是:

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。

举一个例子:

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

//&函数名得到就是函数的地址

int main()
{
    
    
	//printf("%p\n", &Add);
	//printf("%p\n", Add);
	//pf就是函数指针
	int (* pf)(int, int) = Add;//函数的地址要存起来,就得放在【函数指针变量】中
	
	int ret = (*pf)(3, 5);
	//int ret = Add(3, 5);
	//int ret = pf(3, 5);

	printf("%d\n", ret);

	return 0;
}

在练习一个函数指针的例子

char* test(int c, float* pf)
{
    
    

}
int main()
{
    
    
	char* (*pt)(int, float*) = test;

	return 0;
}

阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
//代码1
int main()
{
    
    
	//1. 将0强制类型转换为void (*)() 类型的函数指针
	//2. 这就意味着0地址处放着一个函数,函数没参数,返回类型是void
	//3. 调用0地址处的这个函数
	//下面代码是一次函数调用
    (*( void (*)() ) 0)();
	return 0;
}
  1. 将0强制类型转换为void (*)() 类型的函数指针
  2. 这就意味着0地址处放着一个函数,函数没参数,返回类型是void
  3. 调用0地址处的这个函数
typedef void(*pf_t)(int);//将void(*)(int)类型重新起个别名叫pf_t

//
typedef void(*pf_t2)(int);//pf_t2是类型名
void(*pf)(int);//pf是函数指针变量的名字
	//代码2
int main()
{
    
    
	void (* signal(int, void(*)(int) ) )(int);
	//
	pf_t signal(int, pf_t);
	//上述的代码是一个函数的声明
	//函数的名字是signal
	//signal函数的参数第一个是int类型,第二个是void(*)(int)类型的函数指针
	//该函数指针指向的函数参数是int,返回类型是void
	// 
	//signal函数的返回类型也是一个函数指针
	//该函数指针指向的函数参数是int,返回类型是void
	//
	//void (* signal(int, void(*)(int)))(int)
	return 0;
}

上述的代码是一个函数的声明
函数的名字是signal
signal函数的参数第一个是int类型,第二个是void(*)(int)类型的函数指针
该函数指针指向的函数参数是int,返回类型是void
signal函数的返回类型也是一个函数指针
该函数指针指向的函数参数是int,返回类型是void

6. 函数指针数组

数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组,
比如:

int *arr[10];
//数组的每个元素是int*

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

答案是:parr1
parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。

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 (* pf[4])(int, int) = {
    
    Add, Sub, Mul, Div};
	//0 1 2 3
	int i = 0;
	for (i = 0; i < 4; i++)
	{
    
    
		int ret = pf[i](8, 4);
		printf("%d\n", ret);
	}

	return 0;
}

函数指针数组的用途:转移表
例子:(计算器)
先写一个冗余的计算器

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 menu()
{
    
    
	printf("******************************\n");
	printf("****   1. add    2.sub   *****\n");
	printf("****   3. mul    4.div   *****\n");
	printf("****   0. exit           *****\n");
	printf("******************************\n");
}
int main()
{
    
    
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do
	{
    
    
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
    
    
		case 1:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

使用函数指针数组的实现:

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 menu()
{
    
    
	printf("******************************\n");
	printf("****   1. add    2.sub   *****\n");
	printf("****   3. mul    4.div   *****\n");
	printf("****   0. exit           *****\n");
	printf("******************************\n");
}
int main()
{
    
    
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;

	//转移表 - 函数指针的数组
	int (*pfArr[])(int, int) = {
    
     NULL, Add, Sub, Mul, Div };
	//0    1    2    3    4

	do
	{
    
    
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
    
    
			printf("退出计算器\n");
			break;
		}
		else if (input >= 1 && input <= 4)
		{
    
    
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = pfArr[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
    
    
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

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

指向函数指针数组的指针是一个 指针
指针指向一个 数组 ,数组的元素都是 函数指针 ;
如何定义?

void test(const char* str)
{
    
    
	printf("%s\n", str);
}
int main()
{
    
    
	//函数指针pfun
	void (*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void (*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void (*(*ppfunArr)[5])(const char*) = &pfunArr;
	return 0;
}

再举了一个例子:

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

int main()
{
    
    
	int (*pf)(int, int) = Add;
	//函数指针数组
	int (* pfArr[4])(int, int) = {
    
    Add, Sub};
	//
	int (*(* ppfArr)[4])(int, int) = &pfArr;//ppfArr是一个指向函数指针数组的指针变量

	return 0;
}

8. 回调函数

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

拿一下刚刚的计算器代码来优化讲解一下回调函数:

#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;
}
//写一个计算器能完成整数的+-*/
//增加: << >> & | ^ && ||
//
void menu()
{
    
    
	printf("******************************\n");
	printf("****   1. add    2.sub   *****\n");
	printf("****   3. mul    4.div   *****\n");
	printf("****   0. exit           *****\n");
	printf("******************************\n");
}

void Calc(int(*pf)(int, int))
{
    
    
	int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}

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;
}

此时代码上面标注的就是回调函数。

图片展示过程:

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

如果这份博客对大家有帮助,希望各位给恒川一个免费的点赞作为鼓励,并评论收藏一下,谢谢大家!!!
制作不易,如果大家有什么疑问或给恒川的意见,欢迎评论区留言。
下一期要用到sqort库函数来讲解回调函数,希望大家期待一下!

猜你喜欢

转载自blog.csdn.net/m0_75058342/article/details/129691340