深度刨析程序中的指针

前面我们已经学习过了指针的一下性质:

  • 指针就是个变量,用来存放地址,地址唯一标识的一块内存空间
  • 指针的大小是固定的4/8个字节(32位平台/64位平台)
  • 指针是有类型,指针的类型决定了指针的加减整数的步长,指针解引用操作时的权限。
  • 两个指针相减返回的是两指针间元素的个数(同类型指针)

文章目录

1.字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*

一般使用情况

c 复制代码
#include <stdio.h>
int main()
{
	char c = 'a';
	char* pc = &c;
	*pc = 'y';
	return 0;
}

还有一种情况

c 复制代码
#include <stdio.h>
int main()
{
	const char* str = "hello world";//把"hello world"的首元素的地址给了str
	//但是不能单纯的理解为数组,这里的"hello world"是存放代码区中的不可修改,是常量字符串,所以我们在前面加了const修饰
	printf("%s\n",str);
	return 0;
}

本质就是把常量字符串hello world的首元素的地址放到了str当中,也就是将常量字符串的首元素h的地址放到str中
练习

c 复制代码
#include <stdio.h>
int main()
{
	char str1[] = "hello world";
	char str2[] = "hello world";

	const char* str3 = "hello bit";
	const char* str4 = "hello bit";

	if(str1==str2)
		printf("same\n");
	else
		printf("not same\n");
	if(str3==str4)
		printf("same\n");
	else
		printf("not same\n");
	return 0;
}

//打印结果:
/*
not same
same
*/

这里比较的都是地址。

str1和str2都是数组,当用相同的常量字符串去初始化不同的数组的时候就会开辟不同的空间。而str3和str4指向的同一个常量字符串。在c/c++中会把常量字符串单独存储在一个内存区域(代码段),当我们用几个指针去指向同一个字符串时,它们实际会指向同一块内存的。

可以这么理解:str1和str2是可以修改数组中的元素的,如果不同数组间的修改会相互影响,那岂不是乱遭了。而str3和str4是不可以被修改的,那么让它们两指向同一块空间也是完全没有问题的。

2.指针数组

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

我们可以进行类比:

整型数组是存放整型的数组,字符数组是存放字符的数组。那么指针数组肯定就是存放指针的数组咯。

c 复制代码
int* arr1[10];//整型指针的数组
char* arr2[4];//一级字符指针数组
char** arr3[10];//二级字符指针数组

3.数组指针

3.1 数组指针的定义

我们知道整型指针是指向整型的指针(存放整型变量的地址的指针变量)

还有字符指针是指向字符的指针(存放字符变量的地址的指针变量)

如此类比的话

数组指针就是指向数组的指针(存放数组变量的地址的指针变量)
数组指针的正确写法

c 复制代码
int *p1[10];//错
int (*p2)[10];//对
c 复制代码
int (*p)[10];
//p与*结合,说明p是一个指针变量,然后指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//加()的原因是因为,根据操作符的优先性,[]的优先级是要高于*的,为了保证*与p的结合需要添加括号

3.2 &数组名与数组名

c 复制代码
int arr[10];

arr&arr分别是什么呢?

arr是数组名,数组名又表示数组首元素的地址。

&arr表示的整个数组的地址。

c 复制代码
#include <stdio.h>
int main()
{
	int arr[10];
	printf("%p\n",arr);
	printf("%p\n",&arr);
	return 0;
}
//打印结果:
/*
00DCFBDC
00DCFBDC
*/

打印它们的地址可以发现是一样的。但其实又不完全一样。

c 复制代码
#include <stdio.h>
int main()
{
	int arr[10];
	printf("%p\n",arr);
	printf("%p\n",arr+1);
	
	printf("%p\n",&arr);
	printf("%p\n",&arr+1);
	return 0;
}
//打印结果:
/*
004FF970
004FF974
004FF970
004FF998
*/

arr+1跳过的4个字节的地址。而&arr跳过的是40个字节的地址。

正如前面所说&arr是整个数组的地址,整个数组大小就是40个字节。

本例中&arr的类型就是int(*)[10],是一种数组指针类型。

数组地址+1,跳过整个数组的大小,所以&arr+1相对于&arr的差值就是40.

3.3 数组指针的使用

了解到数组指针指向的数组,那么数组指针中存放的就是数组的地址。

c 复制代码
#include <stdio.h>
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int(*p)[10] = &arr;
	//把整个数组的地址存放在数组指针变量当中
	//但是很少这么写
	return 0;
}

数组指针的使用

c 复制代码
#include <stdio.h>
void print1(int arr[3][5],int row,int col)
{
	for(int i = 0;i<row;++i)
	{
		for(int j = 0;j<col;++j)
		{
			printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
}

void print2(int (*arr)[5],int row,int col)
{
	for(int i = 0;i<row;++i)
	{
		for(int j = 0;j<col;++j)
		{
			printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = {1,2,3,4,5,6,7,8,9,0};
	print1(arr,3,5);
	//arr是数组的数组名,表示数组首元素的地址。而这又是一个二维数组,二维数组的首元素地址就是第一行的地址,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址,可以利用数组指针接收。
	print2(arr,3,5);
	
	return 0;
}

区分

c 复制代码
int arr[5];//整型数组
int* parr1[10];//整型指针数组
int (*parr2)[10];//数组指针
int (*parr3[10])[5];//数组指针数组

4.数组传参、指针参数

在写代码时不可避免的要把【数组】或者【指针】传递给函数,那么函数的参数设计要怎么做呢?

4.1 一维数组传参

c 复制代码
#include <stdio.h>
void test(int arr[])//可行,最容易理解的写法([]内的数字可以随便写,不影响系统的判断)该传参的本质就是int* arr
{}
void test(int arr[10])//可行,最容易理解的写法。([]内的数字可以随便写,不影响系统的判断)该传参的本质就是int* arr
{}
void test(int* arr)//可行,实参传递的是arr代表数组首元素的地址,利用整型指针接收合情合理
{}
void test2(int *arr[20])//可行,本质是int** arr
{}
void test2(int **arr)//可行,实参传递的是arr2也代表首元素的地址,因为arr2是指针数组,一维指针的地址要有二级指针接收,合情合理
{}

int main()
{
	int arr[10] = {0};
	int* arr2[20] = {0};
	test(arr);
	test2(arr2);
	return 0;
}

4.2 二维数组传参

c 复制代码
#include <stdio.h>

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])//可行
{}
void test(int** arr)//不可行,二维数组的数组名代表的是数组第一行的地址,是数组指针.这里是二级指针,不一致。
{}
int main()
{
	int arr[3][5] = {0};
	test(arr);
	return 0;
}

4.3 一级指针传参

c 复制代码
#include <stdio.h>
void print(int* arr,int sz)
{
	for(int i = 0;i<sz;++i)
	{
		printf("%d ",*(arr+i));
	}
}
int main()
{
	int arr[10] = {1,2,3,4,5,6,7,8,9,0};
	int sz = 10;
	int* p = arr;
	print(arr,sz);
	return 0;
}
//打印结果:1 2 3 4 5 6 7 8 9 0

当函数的参数部分为1级指针的时候,函数能接受的的参数为该一级指针对应类型的地址。

4.4 二级指针传参

c 复制代码
#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.函数指针

其实函数也是有地址的。

c 复制代码
#include <stdio.h>
void test()
{
	printf("hello\n");
}
int main()
{
	printf("%p\n",test);
	printf("%p\n",&test);
	return 0;
}
//打印结果
/*
00A41267
00A41267
*/

从这里可以看出,函数不仅有地址,而且函数的函数名就代表了函数的地址,&函数名同样也表示函数的地址。

既然函数有地址,那么也就说明可以利用变量来存储。这个存储函数地址的变量就是函数指针。
函数指针的正确写法

c 复制代码
#include <stdio.h>
void test()
{
	printf("hello\n");
}
int main()
{
	void (*pf1)() = test;//正确写法
	void *pf2() = test;//错误写法
	return 0;
}

pf1可以存储,和数组指针类似,这里的*要先和pf1结合,确定pf1是一个指针,()的优先级有比较高。因此需要用()将*pf1括起来。pf1指向的是一个函数,指向函数无参数,返回类型为void。

c 复制代码
#include <stdio.h>
int Add(int x,int y)
{
	return x+y;
}
int main()
{
	int (*pf)(int int) = Add//指向有参数的函数指针
	return 0;
}

练习

c 复制代码
//代码1
(*(void(*)())0)();
/*
解释:
先看void(*)()这是一个函数指针类型。再往外看,这个函数指针类型被括号括住了(void(*)())
一个类型被()住就是表示强制类型转换的意思。也是说明0被强制类型转换成了函数指针类型。然后*表示对一个函数指针类型进行解引用取出指向的函数*(void(*)())0,最后再调用这个函数。
总结:调用0地址处的函数(实际是无法调用的)
*/

//代码2
void (*signal(int,void(*)(int)))(int);//signal为函数名
/*
解释:signal是函数名,那signal()中的就是函数的参数类型,类型分别为整型和函数指针类型,现在一个函数有了函数名和函数的参数,就差函数的返回类型,如果我们把signal(int,void(*)(int))删除就得到了void (*)(int)这不就是函数指针类型吗,那也就是说signal的函数的返回类型就是void(*)(int)
总结:这是一个函数的声明,找到其函数参数和函数返回类型就可以了。
*/

简化代码2

c 复制代码
typedef void (*pf)(int);
pf signal (int,pf);//利用typedef将类型重命名,来简化代码

6.函数指针数组

数组是存放相同类型数据的存储空间

前面我们已经学习了指针数组

c 复制代码
int* arr[10];
//数组的每个元素类型是int*

同样的我们也可以把函数指针存放进数组,就叫做函数指针数组,那函数指针数组的是如何定义的呢?

c 复制代码
int (*pf)();//这是一个函数指针
//我们将[]添加到变量名后面就可以了
int (*pf[10])();//这就是函数指针数组

pf[]结合说明pf是一个数组,然后数组存放的类型就是int(*)()
函数指针数组的运用

下面以实现一个简单的计算器为例

c 复制代码
#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;
	int a = 0,b = 0;
	int ret = 0;
	do
	{
		menu();
		printf("选择你所要用到的功能>\n");
		scanf("%d",&input);
		switch(input)
		{
			case 1:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Add(a,b);
				printf("ret = %d\n",ret);
				break;
			case 2:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Sub(a,b);
				printf("ret = %d\n",ret);
				break;
			case 3:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Mul(a,b);
				printf("ret = %d\n",ret);
				break;
			case 4:
				printf("请输入两数>");
				scanf("%d %d",&a,&b);
				ret = Div(a,b);
				printf("ret = %d\n",ret);
				break;
			case 0:
				printf("退出\n");
				break;
			default:
				printf("输入错误\n");
				break;
		}
	}while(input);
	return 0;
}

这样写的话其实是很繁琐的。实现这种简单的功能都写了这么长的代码,而且如果后续在添加什么函数功能的话,代码又要增加好的。所以我们要简化。

通过观察,这4个函数的参数和返回类型都是相同的,那么不就说明了可以写成函数指针数组吗。数组中存放这个函数指针类型就可以了。

c 复制代码
#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;
	int a = 0,b = 0;
	int ret = 0;
	do
	{
		menu();
		printf("选择你所要用到的功能>\n");
		scanf("%d",&input);
		int(*pf[5])(int,int) = {NULL,Add,Sub,Mul,Div};//存入NULL是为了可以和菜单对应
		if(input>0&&input<5)
		{
			printf("请输入两数>");
			scanf("%d %d",&a,&b);
			ret = pf[input](a,b);
			printf("ret = %d\n",ret);
		}
		else if(input == 0)
			printf("退出\n");
		else
			printf("输入有误\n");
	}while(input);
	return 0;
}

利用函数指针数组我们将该程序充分简化,而且如果后续还要添加类似的函数功能的话,我们只需要将新写的函数添加进数组,在改变一下判断条件即可。

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

指向函数指针数组的指针是一个指针。

指针指向一个数组,数组的元素都是函数指针;

c 复制代码
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的指针ppfunarr
	void(*(*ppfunarr)[5])(const char*) = &pfunarr;
}

还可以再绕下去的

8.回调函数

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

首先演示一下qsort函数的使用:

c 复制代码
//对整型数组进行排序
#include<stdio.h>
int int_cmp(const void* a,const void* b)
{
	return (*(int*)a) - (*(int*)b);
}
int main()
{
	int arr[10] = {1,3,5,7,9,2,4,6,8,0};
	qsort(arr,10,sizeof(int),int_cmp);
	for(int i = 0;i<10;++i)
	{
		printf("%d ",arr[i]);
	}
	printf("\n");
	return 0;
}
//打印结果:
//0 1 2 3 4 5 6 7 8 9

//对结构体数组进行排序
#include <stdio.h>
#include <string.h>
struct stu
{
	int age;
	char name[10];
};
int struct_cmp_age(const void* a, const void* b)//利用年龄排序
{
	return ((struct stu*)a)->age - ((struct stu*)b)->age;
}
int struct_cmp_name(const void* a, const void* b)//利用名字排序,因为字符串无法相减
//所以这里利用了strcmp进行字符串的比较
{
	return strcmp(((struct stu*)a)->name,((struct stu*)b)->name);
}
int main()
{
	struct stu s[3] = { {17,"yui"},{14,"anna"},{20,"hua"} };
	qsort(s, 3, sizeof(s[0]), struct_cmp_age);
	printf("调用struct_cmp_age\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	qsort(s, 3, sizeof(s[0]), struct_cmp_name);
	printf("调用struct_cmp_name\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	return 0;
}
//打印结果:
/*
调用struct_cmp_age
14 anna
17 yui
20 hua
调用struct_cmp_name
14 anna
20 hua
17 yui

*/

qsort

打开cplusplus网站->qsort

c 复制代码
void qsort(void* base,//需要排序的数组首元素地址
		  size_t num,//需要排序的数组的元素个数
		  size_t size,//需要排序数组的单个元素的大小
		  int (*compar)(const void*,const void*)//传递函数指针,需要自己写
		  }

可以看到的时,这里接受数组首元素的地址是用void*来接收。

提问:为什么呢?

回答:Void*指针 是无具体类型的指针。Void* 类型的指针可以接任意类型的地址(这种类型的指针是不能直接解引用操作的,也不能直接进行指针运算的)。

所以用void*接收是没问题的。然后,这个qsort函数不仅可以对整型数组排序,还可以对字符数组,浮点型数组,甚至是结构体数组。这也就造成了不能使用特定类型指针来接收的情况,如果使用了特定的类型,那其它类型就不能被接收了,所以才会选择使用void*来接收。
模拟实现qsort,但是因为目前还没有学快速排序,所以这里我们利用冒泡排序替代。

c 复制代码
//主要逻辑
void swap(char* a, char* b, int size)
{
	char tmp = 0;
	for (int i = 0; i < size; ++i)//交换的实质其实就是指针所指向内容的交换.
	//因为char只能指向一个字节,所以我们需要传递size了解到要交换的字节大小,然后一个字节一个字节的交换。
	{
		tmp = *a;
		*a = *b;
		*b = tmp;
		a += 1;
		b += 1;
	}
}
void bubble_sort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
	for (int i = 0; i < num - 1; ++i)
	{
		for (int j = 0; j < num - i - 1; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size)>0)//因为void类型的指针是不能直接解引用操作的,也不
			//能直接进行指针运算的。为了拿到比较位置的地址,我们需要将base强转为(char*),因为char*的加减整数时只会跳过一个字节,
			//这是最小的位移距离了。所以我们可以通过强转后的base拿到base[j]和base[j+1]的地址进行比较。
			{
				swap((char*)base + j * size, (char*)base + (j + 1) * size, size);//开始交换
			}
		}
	}
}

测试

c 复制代码
#include <stdio.h>
#include <string.h>
struct stu
{
	int age;
	char name[10];
};
int int_cmp(const void* a, const void* b)
{
	return (*(int*)a) - (*(int*)b);
}
int struct_cmp_age(const void* a, const void* b)//利用年龄排序
{
	return ((struct stu*)a)->age - ((struct stu*)b)->age;
}
int struct_cmp_name(const void* a, const void* b)//利用名字排序,因为字符串无法相减
//所以这里利用了strcmp进行字符串的比较
{
	return strcmp(((struct stu*)a)->name,((struct stu*)b)->name);
}
void swap(char* a, char* b, int size)
{
	char tmp = 0;
	for (int i = 0; i < size; ++i)//交换的实质其实就是指针所指向内容的交换,因为char只能指向一个字节,所以我们需要传递size了解到要交换的字节大小,然后一个字节一个字节的交换。
	{
		tmp = *a;
		*a = *b;
		*b = tmp;
		a += 1;
		b += 1;
	}
}
void bubble_sort(void* base, int num, int size, int(*cmp)(const void*, const void*))
{
	for (int i = 0; i < num - 1; ++i)
	{
		for (int j = 0; j < num - i - 1; ++j)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size)>0)//因为void类型的指针是不能直接解引用操作的,也不能直接进行指针运算的。为了拿到比较位置的地址,我们需要将base强转为(char*),因为char*的加减整数时只会跳过一个字节,这是最小的位移距离了。所以我们可以通过强转后的base拿到base[j]和base[j+1]的地址进行比较。
			{
				swap((char*)base + j * size, (char*)base + (j + 1) * size, size);
			}
		}
	}
}
int main()
{
	int arr[10] = { 1,3,5,7,9,2,4,6,8,0 };
	bubble_sort(arr, 10, sizeof(int), int_cmp);//冒泡排序
	printf("对arr排序:\n");
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	struct stu s[3] = { {17,"yui"},{14,"anna"},{20,"hua"} };
	bubble_sort(s, 3, sizeof(s[0]), struct_cmp_age);
	printf("调用struct_cmp_age:\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	bubble_sort(s, 3, sizeof(s[0]), struct_cmp_name);
	printf("调用struct_cmp_name:\n");
	for (int i = 0; i < 3; ++i)
	{
		printf("%d %s\n", s[i].age, s[i].name);
	}
	return 0;
}
//打印结果:
/*
对arr排序:
0 1 2 3 4 5 6 7 8 9
调用struct_cmp_age:
14 anna
17 yui
20 hua
调用struct_cmp_name:
14 anna
20 hua
17 yui
*/

相关推荐
zhaoyang03015 分钟前
css3笔记 (1) 自用
前端·javascript·css·vue.js·笔记·html·css3
struggle20252 小时前
RushDB开源程序 是现代应用程序和 AI 的即时数据库。建立在 Neo4j 之上
数据库·typescript·neo4j
小柯博客4 小时前
从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(十二)
c语言·stm32·单片机·嵌入式硬件·php·嵌入式
伤不起bb4 小时前
Redis 哨兵模式
数据库·redis·缓存
卑微的Coder4 小时前
Redis Set集合命令、内部编码及应用场景(详细)
java·数据库·redis
2501_915373884 小时前
Redis线程安全深度解析:单线程模型的并发智慧
数据库·redis·安全
呼拉拉呼拉4 小时前
Redis知识体系
数据库·redis·缓存·知识体系
霖檬ing4 小时前
Redis——主从&哨兵配置
数据库·redis·缓存
CrissChan4 小时前
Pycharm 函数注释
java·前端·pycharm
moxiaoran57534 小时前
uni-app学习笔记二十九--数据缓存
笔记·学习·uni-app