【C语言-指针进阶】挖掘指针更深一层的知识

前言

这篇文章的知识由初阶指针发散,提及更多指针的概念和基础用法,干货超多!快去接杯水!



1. 字符指针

提一种容易犯错的用法:

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

把字符串放到指针里的操作是:把字符串首字符地址放到指针

#include <stdio.h>
int main()
{
    char arr1[] = "bacon";
    char arr2[] = "bacon";
    const char* p1 = "bacon";
    const char* p2 = "bacon";
    if (p1 == p2)
        printf("p1 and p2 are same\n");
    else
        printf("p1 and p2 are not same\n");

    if (arr1 == arr2)
        printf("arr1 and arr2 are same\n");
    else
        printf("arr1 and arr2 are not same\n");

    return 0;
}
结果:
p1 and p2 are same
arr1 and arr2 are not same

解析:p1和p2都存放在 只读数据区,两个相同的常量字符串不重复存储;arr1 和 arr2则分别在栈区创建自己的内存空间。abcdef 是常量字符串,const防止常量字符串被修改

2. 指针数组

提及定义和用法:

2.1 指针数组的定义

指针数组:存放指针的数组

基本形式:

int* arr1[10];
type* 数组名[n]

2.2 指针数组的用法

  • 指针数组用法之一:组合大法!
把不同位置的一维数组组合成二维数组
int main()
{
	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 };

	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			//printf("%d ", parr[i][j]);
			printf("%d ", *(parr[i] + j));
		}
		printf("\n");
	}
	return 0;
}

3. 数组指针

提及定义、数组名和用法:

3.1 数组指针的定义

数组指针:指向数组的指针

来做一个区分

int *p1[10];//指针数组
int (*p2)[10];//数组指针

解析:这里涉及到优先级的问题,[ ] 的优先级高于 *
p1和 [ ] 结合,前面的 int* 是数组元素的类型
p2和 * 结合,代表p2是一个指针,指向的对象类型是:元素个数为10的整形数组

3.2 数组名的事儿

对于

int arr[10];

有这样的说法:

arr 作为数组名,代表首元素的地址
&arr 取出的是整个数组的地址

看看区别

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);//首元素地址
	printf("%p\n", &arr);//整个数组地址
	return 0;
}
结果:
005CF86C
005CF86C

可以看到它们的地址一样。

但即便数值是一样的,意义却不同

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);//首元素地址
	printf("%p\n", &arr);//整个数组地址

	printf("%p\n", arr+1);//首元素+1地址
	printf("%p\n", &arr+1);//整个数组+1地址
	return 0;
}
结果:
0318F61C
0318F61C
0318F620
0318F644

arr+1 跳过了4个字节(一个元素)
&arr+1 跳过了40个字节(整个数组)

这也印证了我上文的说法

3.3 数组指针的用法

直接看实例

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++)
		{
			printf("%d ", *(*(p + 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;
}

解析:这里用一个一维数组指针来接受二维数组的数组名(第一行的地址)。相当于,p接收了第一行的地址;p+1,就是第二行而*(p+1)是每一行一维数组的数组名,又表示首元素地址,给它+1,*(p+1)+1,相当于第二行的第二个元素

3.4 分辨指针数组和数组指针

仔细分析,才能体味到…

int arr[5];//整形数组
int *parr1[10];//存放整形指针的数组
int (*parr2)[10];//指向 有5个元素的整形数组 的指针
int (*parr3[10])[5];//存放 5个指向整型数组的数组指针 的数组

!!!

分析类型我有个办法:

为了方便理解,我把类型分成两层:第一层是跟名字结合的类型第二层是 指向的/存放的数据的类型
在这里插入图片描述

  1. 根据优先级判断第一层类型:( ) > [ ] > *
  2. 去名,得到第二层类型
  3. 分析第二层类型:关注 * 和 (*)
    * :表示最近的类型是指针,紧贴最近的类型
    (*):表示是第二层整体是指针类型,这颗 * 放到最后

在这里插入图片描述

单独的一颗 * 则直接紧贴最近的类型

在这里插入图片描述

(*) 则放到第二层类型最后,表示第二层整体是指针类型

int arr[5];
arr跟 [] 结合,是数组,5个元素,拿掉arr[5]int就是数组存放的元素的类型
arr的类型是 int[5]

int *parr1[10];
parr1跟 [] 结合,是数组,10个元素,拿掉parr[10]int* 就是数组存放的元素的类型
parr的类型是 int* [10]*int 结合,表示是数组类型,每个元素是 int*

int (*parr2)[10];
parr2和 * 结合,是指针,拿掉*parr2,int[10]就是指向的元素类型——存放10个整型的数组
parr2的类型是 int[10] *

int (*parr3[10])[5];
parr3和 [] 结合,是数组,10个元素,拿掉parr3[10]int(*)[5]就是数组存放的元素的类型
int(*)[5]*() 括起来了,标准表示是:int[5] *,说明是指针类型,指向5个整型的指针类型

在这里插入图片描述
int (*) [5]中的 * 有括号,放到存放的数据的类型的最后,所以数组存放的类型是 int[5] *——指向存放5个整型的数组的指针

4. 形参:数组和指针

数组和指针是怎样传参的?

4.1 一维数组传参

:一维数组做实参,本质上 传过去的是首元素地址
看实例

//传过去的是首元素地址,用一维数组作形参接收,没问题
void test1(int arr[])
{}

//传过去的是首元素地址,拿一个指针接收首元素地址,没问题
void test1(int* parr)
{}

//传过去的是首元素地址,却用指针数组接收,错
void test2(int* arr[])
{}

//传过去的是首元素地址,却用二级指针接收
//二级指针是用来存放一级指针变量的地址的,错
void test2(int** arr)
{}

int main()
{
	int arr1[10] = { 0 };
	int* arr2[10] = { 0 };
	test1(arr1);
	test2(arr2);
	return 0;
}

4.2 二维数组传参

:二维数组作实参,本质上 传过去的是二维数组首元素(第一行)的地址
看实例

二维数组做实参,形参的设计只能省略第一个[ ]的数字

//错
void test(int arr[][])
{}
//错
void test(int arr[3][])
{}
//对
void test(int arr[][5])
{}

//-------------------------------------------- 


传过去的是二维数组首元素(第一行)的地址,不能简单地用整型指针接收 ,错
void test(int* arr)
{}

传过去的是二维数组首元素(第一行)的地址,却用整型指针数组接收, 错
void test(int* arr[5])
{}

传过去的是二维数组首元素(第一行)的地址,用数组指针接收数组名(第一行的地址),arr可以指向每一行
也就是二维数组的每一个元素,没问题
void test(int(*arr)[5])
{}

传过去的是二维数组首元素(第一行)的地址,却用二级指针接收
二级指针是用来存放一级指针变量的地址的,错
void test(int** arr)
{}


int main()
{
	int arr[3][5] = { 0 };
	test(arr);
	return 0;
}

4.3 一级指针传参

看实例:

  • 一级指针传参实例
void print(int* p, int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);
	return 0;
}

适时的逆向思维,能让我们更进一步

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

void print(int* p)
{}

int main()
{
	int a = 0;
	int* pa = &a;
	int arr[10] = { 0 };
	print(&a);
	print(pa);
	print(arr);
	return 0;
}

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

void print(int** p)
{}

int main()
{
	int a = 0;
	int* pa = &a;
	int** ppa = &pa;

	print(ppa);
	print(&pa);
	return 0;
}

总结:只要传过去的实参本质上是地址/一级指针变量的地址,就没问题

5. 函数指针

先了解一下:

对于函数来说, &函数名 和 函数名 ,都是函数的地址

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

int main()
{
	printf("%p\n", Add);
	printf("%p\n", &Add);
	return 0;
}
结果:
00ED13B6
00ED13B6

5.1 函数指针的定义

*返回类型 (指针名)(形参类型,形参类型,…)
int(*pf)(int, int)

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

int main()
{
	int (*pf)(int, int) = &Add;
	//int ret = (*pf)(10, 20);
	int ret = pf(10, 20);
	printf("%d\n", ret);
	return 0;
}
结果:
30

5.2 函数指针的用法

int Add(int x, int y)
{
	return x + y;
}
int calc(int(*pf)(int, int), int x, int y)
{
	return pf(x, y);
}
int main()
{
	int ret = calc(Add, 3, 5);
	printf("%d\n", ret);
	return 0;
}

!!!

函数指针的意义:道理和普通指针一样,可以通过指针调用函数,也就是说,可以用函数作实参

!!!

6. 函数指针数组

6.1 函数指针数组的定义

定义一个存放10个函数指针的数组,每个函数指针指向的函数返回类型为int无参数

int(*parr[10])()

  • 如何理解?

parr 先和 [ ] 结合,我们再拿掉 parr[10] ,就得到数组的类型: int(*)() - 函数指针

6.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;
	int x = 0;
	int y = 0;
	int ret = 0;

	int (*pfarr[])(int, int) = { Add,Sub,Mul,Div };
	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-1](x, y);//通过input来访问具体函数
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

函数指针数组在这省去了冗余的代码,不信来看看普通实现:

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:");
			scanf("%d%d", &x, &y);
			printf("%d\n", Add(x, y));
			break;
		case 2:
			printf("请输入两个操作数:");
			scanf("%d%d", &x, &y);
			printf("%d\n", Sub(x, y));
			break;
		case 3:
			printf("请输入两个操作数:");
			scanf("%d%d", &x, &y);
			printf("%d\n", Mul(x, y));
			break;
		case 4:
			printf("请输入两个操作数:");
			scanf("%d%d", &x, &y);
			printf("%d\n", Div(x, y));
			break;
		case 0:
			printf("退出...\n");
			break;
		default:
			printf("请正确选择!\n");
			break;
		}

	} while (input);
	return 0;
}

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

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

int main()
{
	int(*pf)(int, int) = Add;//函数指针
	int(*parr[1])(int, int) = { pf };//函数指针数组
	int(*(*pparr)[1])(int, int) = &parr;//指向函数指针数组的指针
	return 0;
}

我算是找到规律了:

  1. balabala指针:把指针变量和*括起来 —— bala (*p) bala
  2. balabal数组: 让数组名和 [ ] 挨在一起 —— bala arr[10] bala

8. 回调函数

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

  1. 不是由实现方调用
  2. 由函数指针调用

库函数 qsort 的使用,就用到了回调函数的知识

并且 qosrt 的设计逻辑,有很多值得学习的地方

由此,咱们来研究研究 qsort !

8.1 qsort

要研究一个函数,不得先研究函数的 使用场景、返回类型、参数 嘛!

  1. 使用场景:也许研究了才知道?
  2. 返回类型:void,人家就是排个序嘛
  3. 参数:这个须得好好看看!

8.1.1 qsort的参数

qsort 的参数是这样:

void qsort
( 
void *base,//要排序的数据的起始位置
size_t num, //元素个数
size_t width,//每个元素的大小(单位:字节)

int (__cdecl//C语言的函数调用约定,不用深究
 *compare )//“比较函数”的指针
(const void *elem1, const void *elem2 ) 
);


现在就来解析为什么这么设计:

  1. 为什么要起始位置、元素个数、元素大小…这么多参数,不麻烦嘛?

这样的参数,能使 qsort 更通用,管你什么类型,咱都能排

  1. void* ?

void* ,无确切类型的指针,也因此可以把不同类型的指针传给他,也是为了 通用性
但是,正因为没有确切类型,无法 解引用 和 +- 整数 (不知道访问多大空间,也不知道跳过多少字节)

  1. num?width?

想想,只知道要排序的数据的 起始位置,你能找到每个元素嘛?

起始位置 + 元素个数 + 元素大小 —> 精准找到每个元素

  1. 比较函数?

还是一样,服务于 通用性, qsort 的使用者,根据自己要排序的数据,自实现一个比较函数
int 可以用 > 比较,字符串呢?结构体呢?所以根据需要自己实现更妥

  • 要求:e1>e2 返回一个>0的数,e1=e2 返回0,e1<e2 返回一个 <0的数

哇!指针在我心中的地位直线上升,妙!

分析了 qsort 的参数,再来尝试使用一下:

8.2 qsort 的使用& qsort 中的回调函数

#include<stdlib.h>
#include<stdio.h>
int cmp(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}
结果:
0 1 2 3 4 5 6 7 8 9
  • cmp 的实现不是确定的,因此,我们可以按照自己的想法实现,得以比较不同数据

哟?这里的 cmp?哟?qsort调用了cmp?

  1. cmp是指针

  2. qosrt 在排序过程中调用了 cmp指针 指向的比较函数 —— 不是实现方调用哦!

这不就和回调函数的定义没差嘛!通过 qsort , 我们不仅学到 指针的妙用 , 还学到回调函数

qsort总结:

  1. 利用void* 指针可以接收各种指针的特性,提升通用性
  2. 把部分功能作为参数,通过函数指针,灵活地传递函数,进一步提升通用性

8.3 取qsort之长,补bubble之短

  • 我们已经学过基础的冒泡排序:
void BubbleSort(int arr[], int sz)
{
	int i = 0;
	int j = 0;
	int flag = 1;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (arr[i] > arr[i + 1])
			{
				int tmp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = tmp;
				flag = 0;
			}
		}
		if(1 == flag)
			break;
	}
}

但是,已经吸收了qsort的精华后,我们可不会满足于此了:

只能排整型,这个冒泡太笨了!

仿照着 qsort 的参数设计,看看能不能也让笨笨冒泡通用起来

  • 改良冒泡排序
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)//根据类型交换每个元素的字节
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void BubbleSort(void* base, int sz, int width, int(*cmp)(const void*, const void*))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;//如果排好序了
		for (j = 0; j < sz - 1 - i; j++)
		{
			//base是void*:不能+-整数 ;
			// 如果强制转换成 int* 可能一下跳过太多,无法达到预期效果
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0
			{
				//交换
				//这里传个width过去,直接交换字节,不用考虑类型了
				//为什么交换字节? 如果交换变量,临时变量不好创建(不知道什么类型)
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				flag = 0;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}int main()
{
	int arr[10] = { 9,8,7,6,5,4,3,2,1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	//qsort(arr, sz, sizeof(arr[0]), cmp);
	BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

结果:
0 1 2 3 4 5 6 7 8 9

吸取了精华的 Bubblesort ,已经聪明多了!

  • 不信咱们再让他排个结构体数据:
struct stu
{
	char name[20];
	int id;
};

int cmp_stu_by_name(const void* e1, const void* e2)
{
	return strcmp(((struct stu*)e1)->name, ((struct stu*)e2)->name);
}

int cmp_stu_by_id(const void* e1, const void* e2)
{
	return ((struct stu*)e1)->id - ((struct stu*)e2)->id;
}


void Swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}

void BubbleSort(void* base, int sz, int width, int(*cmp)(const void*, const void*))
{
	int i = 0;
	int j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int flag = 1;//如果排好序了
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)//base是void*:不能+-整数 ; 如果强制转换成 int* 可能一下跳过太多,无法达到预期效果
			{
				//交换
				//这里传个width过去,直接交换字节,不用考虑类型了
				//为什么交换字节? 如果交换变量,临时变量不好创建(不知道什么类型)
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				flag = 0;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}
}

void test()
{
	struct stu s[] = { {"zhangsan",110},{"lisi",120},{"wangwu",911} };
	int sz = sizeof(s) / sizeof(s[0]);
	BubbleSort(s, sz, sizeof(s[0]), cmp_stu_by_name);
	BubbleSort(s, sz, sizeof(s[0]), cmp_stu_by_id);
}
int main()
{
	test();
	return 0;
}
调试发现,s数组先以name排成升序,再以id排成升序

*复杂的指针类型

  • 判断:

根据优先级,把 变量名/数组名 拿掉,剩下的就是类型

  • 定义

从最基础的指针/数组开始,一层层套娃


奇怪又有趣?

你能试着分析这两句代码是什么意思吗?

1.
(*(void (*)())0)();

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

分析1:

  1. 把 0 强制类型转换成:无参,返回类型是void的函数的指针
  2. 解引用 这个奇怪的函数指针
  3. 调用这个函数指针指向的函数

把 0 这个值,转换成指针,再解引用,找到这个地址处的函数并调用它

分析2:

  1. 首先根据优先级:signal是函数名
  2. signal后的圆括号里放的是类型,所以这句代码是一次函数声明
  3. 用咱们的经典手法:把signal(int, void(*)(int))拿开
  4. 剩下 void(*)(int)还能是什么呢?不就是函数的返回类型嘛

: 声明一个函数signal,函数的返回类型是 void(*)(int),参数是 int 和 void(*)(int)


9. 已知指针用法总结

这里的用法,会不断增加

好的用法希望大家大方分享,共同进步;拙劣的地方希望大家指出,并帮助我优化,感谢

9.1 普通指针

  1. 指针可以用来找到 数据、地址…
  2. void* 可以提升通用性

9.2 指针数组

  1. 可以把不同内存区域的数据组合在一起(有点链表的感觉?)

9.3 数组指针

  1. 一维数组指针接收二维数组名

9.4 函数指针

  1. 可以实现 函数传参,提高通用性
  2. 回调函数

9.5 函数指针数组

  1. 转移表

未完……


笔者水平有限,一定有不妥不恰、有待提升的地方,希望大家指出

这里是培根的blog,和你共同进步!

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

周杰偷奶茶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值