C语言——指针进阶(含例题及详细代码分析)

31 篇文章 2 订阅

导语:

之前有一篇文章给大家讲解了指针的概念(链接:指针的疑难杂症):

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

那本篇文章,讲解深层次的指针概念。

思维导图:

在这里插入图片描述

1.字符指针

字符指针的一般用法:

int main()
{
	char ch = 'q';
	char* pc = &ch;
	//*pc = 'q'
	return 0;
}

还一种使用方法:

int main()
{
	char* p = "abcdef";
	return 0;
}

注意,在这里不是"abcdef"赋给了p,"abcdef"是占7个字节(包含\0),而我们知道,指针变量是4个字节,那p肯定是放不下的。

在这里,"abcdef"是一个常量字符串,当常量字符串出现在表达式里面的时候,其实这个字符串的值是首元素的地址。

在这里插入图片描述
既然刚刚说了"abcdef"是常量字符串,常量是不能修改的,那么上面的这种写法就是不够标准,应该改为:

int main()
{
	const char* p = "abcdef";
	return 0;
}

有了这个知识,我们来看一道例题:

#include <stdio.h>
int main()
{
	char str1[] = "hello world";
	char str2[] = "hello world";
	const char* str3 = "hello world";
	const char* str4 = "hello world";
	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是指针,指向的是一个常量字符串,常量字符串不可被修改,在内存中只会开辟一个空间;
那么在比较的时候,str1和str2比较的是首元素地址,因为是2块空间,所以肯定不一样;str3和str4比较的是首元素地址,指向的是同一块空间。

在这里插入图片描述

2.指针数组

指针数组是存放指针的数组

int main()
{
	int* arr1[10]; //整型数组  存放整型的数组
	char* arr2[10];//字符数组  存放字符的数组
	
	char ch1[] = "qwe";
	char ch2[] = "asd";
	char ch3[] = "zxc";
	char* arr3[3] = { ch1,ch2,ch3 }; //指针数组  存放指针的数组
	//指针数组的访问
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%s\n", arr3[i]);
	}
	return 0;
}

在这里插入图片描述

3.数组指针

3.1 数组指针的定义

数组指针是数组?还是指针?
答案是:指针

int main()
{
	int* pi; //整型指针 -- 存放整型地址的指针 -- 指向整型的指针
	char* pc;//字符指针 -- 存放字符地址的指针 -- 指向字符的指针

	int arr[10];
	int(*pa)[10] = &arr;//数组指针 -- 存放数组地址的指针 -- 指向数组的指针
	return 0;
}

在这里,是需要区分这两端代码p1和p2到底代表着什么?

int main()
{
	int* p1[10];
	int(*p2)[10];
	return 0;
}

首先要知道,[ ] 优先级是要高于 * 号
int* p1[10] ,p1优先和数组结合,那么此时p1就是一个数组,里面存放的内容都是指针类型,所以p1是一个数组,里面存放的内容是指针的地址,叫指针数组。
int(*p2)[10],在这里 *号优先和p2结合,那么p2此时就是一个指针变量,指向的是一个大小为10的整型数组,所以p2是一个指针,指向一个数组,叫数组指针。

3.2 &数组名和数组名

我们知道,数组名就是数组首元素的地址,那么&数组名又是什么意思呢?
在这里插入图片描述
通过编译器,我们可以看出数组名和&数组名指向的都是首元素的地址,那他们的意义是一样的吗?
在这里插入图片描述

将他们的地址都+1,我们发现arr+1是跳过4个字节,而   ~  &arr+1是跳过40个字节

那么我们就可以得出结论:
数组名是数组首元素的地址。
&数组名是整个数组的地址。
数组首元素地址虽然和整个数组的地址从值的角度来看,虽然是一样的,但是意义不相同。

这里还一点就是指针类型决定了指针+1到底偏移的是几,这里数组名和&数组名的类型是不一样的。
在这里插入图片描述

数组名的类型是 int*,首元素的地址也是int*
而&数组名类型是int(*)[10]

3.3 数组指针的使用

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

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pa = arr;
	int(*p)[10] = &arr;
	int i = 0;
	//一般访问
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(pa + i));
	}
	printf("\n");
	//数组指针使用
	for (i = 0; i < 10; i++)
	{
		printf("%d ", (*p)[i]);
	}
	return 0;
}

但我们发现在这里使用数组指针特别的别扭,有点脱裤子放屁的感觉,很鸡肋。其实数组指针在一维数组运用比较少,在二维数组的运用会多一点

int main()
{
	int arr[3][4] = { {1,2,3,4 },{2,3,4,5 },{3,4,5,6} };
	int(*p)[4] = &arr;
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 4; j++)
		{
			printf("%d ", (*(p+i))[j]);
			//printf("%d ", p[i][j]);  写出这种形式也行
		}
		printf("\n");
	}
	return 0;
}

在这里插入图片描述
既然了解了指针数组和数组指针的概念,那我们再来看看下面这段代码的意思:

int main()
{
	int arr[5]; //整型数组 -- 元素是5个
	int* parr1[10];//指针数组 -- 可存放10个整型指针
	int(*parr2)[10];//数组指针 -- 指向一个数组 -- 该数组元素为10个,每个元素为整型
	
	//[]的优先级高于*,使用parr3先和[]结合
	//parr3是数组,数组有10个元素
	//数组的每个元素是int(*)[5]的数组指针类型
	int(*parr3[10])[5];
	return 0;
}

在这里插入图片描述

4.数组参数、指针参数

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

4.1 一维数组传参

#include <stdio.h>
void test(int arr[])//形参部分写出数组,可以不指定大小(√)
{}
void test(int arr[10])//形参部分是数组接收,10个元素,与传递过来的数组一致(√)
{}
void test(int* arr)//传过来的是数组名,数组名是首元素地址,用整型指针接收(√)
{}
void test2(int* arr[20])//形参与传递过来的保持一致(√)
{}
void test2(int** arr)//传的是数组名,首元素地址,传过来的类型是int*一级指针,用二级指针接收(√)
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr); //数组传参,传的数组名,也是首元素的地址
	test2(arr2);//数组传参,传的数组名,也是首元素的地址
	return 0;
}

4.2 二维数组传参

//数组形式
void test(int arr[3][5])//形参与传递过来的参数保持一致,标准写法 (√)
{}
void test(int arr[][])//二维数组行可以省略,但列不能省略,该处都省略了 (×)
{}
void test(int arr[][5])//二维数组可以省略,但列不能省略 (√)
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
//指针形式
void test(int* arr)//形参写成了整型一维数组的接收方式 (×)
{}
void test(int* arr[5])//形参是指针数组,是数组,但不是二维数组;能存指针,但不是指针 (×)
{}
void test(int(*arr)[5])//形参是数组指针,数组5个元素,每个元素是int类型(√)
{}
void test(int** arr)// 传过来的是首行的地址,而二级指针是接收一级指针的地址 (×)
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);//传的是二维数组
}

4.3 一级指针传参

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

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

void test(int* p)
{}
int main()
{
	int a = 10;
	int* p = &a;
	int arr[5] = { 1,2,3,4,5 };
	test(&a);//整型变量的地址
	test(p);//一级指针
	test(arr);//一维数组的数组名
	return 0;
}

4.4 二级指针传参

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

那我们再思考,当一个函数的参数部分为二级指针的时候,函数又能接收什么参数呢?

void test(int** p)
{}
int main()
{
	int** ptr;
	int* pp;
	int* arr[5];
	int arr[3][3];
	test(ptr);//二级指针
	test(&pp);//一级指针的地址
	test(arr);//指针数组的数组名
	return 0;
}

5.函数指针

在上面我们讲了,数组指针是指向数组的指针;那么函数指针顾名思义就是指向函数的指针
函数还有地址吗?我们通过编译器,看看是否能将函数的地址取出:
在这里插入图片描述
我们发现确实可以拿到函数的地址,那么函数的地址是否存起来呢?答案是肯定的,是地址,我们就可以存起来,语法规则如下:

int Add(int x,int y)
{

	return x + y;
}
int main()
{
	int(*pf)(int, int) = &Add;
	//pf是存放一个函数地址的变量 -- 函数指针
	
	//int(*pf)(int, int) = Add;
	//&函数名和函数名都代表函数的地址,这里 没 有 区 别!
	return 0;
}

函数指针与数组指针的书写十分类似

数组指针书写:
int arr[10];
int(*pa)[10] = &arr;
函数指针的书写:
int(*pf)(int, int) = &Add;

在这里插入图片描述
既然能拿到函数的地址,也能将函数的地址存起来,那是否能通过地址调用这块函数呢?答案也是肯定的。
请添加图片描述
通过调试,可以观察到确实可以通过pf指针去调用这个函数。
另外,既然&函数名和函数名一样,都代表着函数的地址,那么其实我们的写法可以更加简便一点:

int Add(int x, int y)
{

	return x + y;
}
int main()
{
	int(*pf)(int, int) = Add;
	//Add赋给pf,那pf就和Add其实是一回事
	//int ret = Add(1,2); //原先调用Add函数的写法
	//那么pf也可以这样写
	int ret = pf(1, 2);
	printf("%d\n", ret);
	return 0;
}

注意:(*pf)这里的 * 可以不写,但是如果要写上,一定要加上括号(*pf)

阅读(坑X)两段有趣的代码:

//代码1
(*(void (*)())0)();

在这里插入图片描述

该代码是一次函数调用;
首先将0先强制类型转换成一个函数指针类型,把0当成某个函数的地址,那个函数没有参数,返回类型为void;
然后*解引用调用函数,函数是无参的,所以最后面直接写括号,没有参数。

//代码2
void (*signal(int , void(*)(int)))(int);

在这里插入图片描述

该代码是一次函数声明;
声明的函数名字叫signal;
signal有2个参数,一个是int类型,一个是函数指针类型,该函数指针指向的函数参数为int,返回类型为void;
signal函数的返回类型是一个函数指针,该函数指针指向的函数参数为int,返回类型为void。

其实在这里,代码还是十分的难以理解,我们可以将其简化:

//将函数指针类型改名为pf_t
typedef void(*pf_t)(int);
//signal一个参数为int,另一个为pf_t,返回类型也是pf_t
pf_t signal(int, pf_t);

6.函数指针数组

函数指针数组,见名知意,是存放函数指针的数组,既然了解了函数指针的概念,那么这个写法,在函数指针的基础上再改造一下:

void test1()
{}
void test2()
{}
void test3()
{}
int main()
{
	//函数指针
	void(*pf1)() = &test1;
	void(*pf2)() = &test2;
	void(*pf3)() = &test3;
	//函数指针数组
	//pf先与[]结合,说明是一个数组,该数组里面存放的内容是函数指针类型
	void(*pf[3])() = { pf1,pf2,pf3 };//存放了3个函数的地址
	return 0;
}

函数指针数组的用途:转移表
我们在这里模拟一个简易计算器的程序,这是一般写法:

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

这个写法其实是十分的冗余,我们了解函数指针数组之后,就可以简化很多:

void menu()
{
	printf("*********************\n");
	printf("*   1.add   2.sub   *\n");
	printf("*   3.mul   4.div   *\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()
{
	menu();
	int input = 0;
	int a = 0;
	int b = 0;
	//将函数的地址全部放入函数指针中去
	int(*pf[5])(int, int) = { 0,add,sub,mul,div };
	do
	{
		printf("请选择:");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input >= 1 && input <= 4)
		{
			printf("请输入两个操作数:");
			scanf("%d %d", &a, &b);
			int ret = pf[input](a,b);
			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;
}

将这段代码拆分就会更好理解一点,这里要一步一步的写,这样不容易出错,思路也会更清晰一点。
在这里插入图片描述

8.回调函数

前面的函数指针、函数指针数组的内容,其实基本上会运用于回调函数的实现。

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

还是以简易计算器为例:
在这里插入图片描述
红框里面的代码,相似度非常高,逻辑也是十分的类似,那么其实可以将其封装为一个函数:

void menu()
{
	printf("*********************\n");
	printf("*   1.add   2.sub   *\n");
	printf("*   3.mul   4.div   *\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))
{
	int a = 0;
	int b = 0;
	printf("输入2个操作数:");
	scanf("%d %d", &a, &b);
	printf("%d", pf(a, b));
}
int main()
{
	menu();
	int input = 0;
	do
	{
		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;
}

回调逻辑:
在这里插入图片描述
在这里确实并没有直接去调用add函数,而是把函数的地址传递给另一个函数,然后在这个函数内部通过函数指针去调用add函数。
换言之,回调函数是一个中介,现实生活中,通过找中介办事,这样自己会轻松很多,但同时会被赚差价。那么运用到这里,就是代码的通用性更加高,但是调来调去,运行速度,自然会降低。

结语

   ~~   指针是C语言的重点内容,不要想的太复杂,也不要不敢面对,指针它就是个地址,没什么高科技,将内容梳理一下,采用零敲牛皮糖战术,一点一点的消化。
   ~~   那么本期方向就到这里,有帮助的话,三连支持一下,谢谢,再见!

  • 16
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加法器+

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值