【C进阶】指针

楔子

在文章开始前,我们先复习一下什么是指针:

  1. 指针实际上是一个变量,是用来存储地址的。
  2. 指针的大小为4/8字节(32位/64位)。
  3. 指针也是有类型的,它决定了在加减操作中所移动的步长。

字符指针

字符指针,顾名思义是一个指向字符的指针,我们可以通过*p间接修改ch中的内容。

int main()
{
	char ch = 'W';
	char* p = &ch;//p中存放的是ch的地址
	*p = 'T';
	char *p1 = "abcdef"//p1存放的是“abcdef”字符串的首元素地址
	return 0;
}

那如果是一个字符串呢?C语言是没有字符串类型的,但我们可以通过字符数组来存放它,例如:

int main ()
{
	char *str = "abcdef";//该字符串是存放在字符常量区,str是一个字符指针变量,里面存放的是字符串的首元素地址
	*str = "qwe";//对指针解引用就找到了其地址所指向的内容(即“abcdef”),又因为常量不可以被赋值,所以这个赋值是错误的。
}

那么我们再来看看用这种方式存储的和以数组方式存储的区别

int main()
{
	char str1[] = "hello";
	char str2[] = "hello";
	const char* str3 = "hello";
	const char* str4 = "hello";
	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中内容是相同的,但是在压栈时顺序不同,导致其存放的地址不同,而数组名是首元素地址所以不同,而“hello”是在字符常量区,str3和str4都是指向其首元素地址所以相同

指针数组

存放指针的数组,其基类型为指针

  • int *arr[3]定义了一个指针数组,里面元素类型为int *[],一共有3个。
    使用方法:
int main()
{
	int arr1[3] = { 1,2,3 };
	int arr2[3] = { 4,5,6 };
	int arr3[3] = { 7,8,9 };
	int* arr[3] = { arr1,arr2,arr3 };
	printf("%d\n", *(*(arr + 0) + 2));//输出3
	printf("%d\n", *(*(arr + 1) + 2));//输出6
	printf("%d\n", *(*(arr + 2) + 2));//输出9
	return 0;
}
  • char*arr[4]定义一个指针数组,里面元素类型为char *[],一共有3个.
    使用方法
int main()
{
	char* arr[4] = { "zheng","xin","shong","jiu" };
	printf("%c\n", arr[0][1]);//我们可以通过访问数组下标的方式来访问
	printf("%c\n", arr[1][1]);
	printf("%c\n", arr[2][1]);
	return 0;
}

数组指针

数组指针本质是指针,我们知道int *pint为整形指针,char *pchar为字符指针,那么int (*p)[3]为数组指针。

  1. 我们先通过一段代码来认识:
int main ()
{
	int arr[5] = {0};
	printf("%p\n",arr);
	printf("%p\n",&arr);
	printf("%p\n",&arr[0]);//三者的结果完全相同,但是它们真的相同吗?我们在看一段代码
	return 0;
}
int main()
{
	int arr[10] = { 0 };

	printf("arr = %p\n", arr);
	printf("arr+1 = %p\n", arr + 1);//arr是首元素地址,类型为int*,加1实际上是加上前类型的大小,所以指向下一个元素的地址。

	printf("&arr= %p\n", &arr);
	printf("&arr+1= %p\n", &arr + 1);//&arr是整个数组的地址,类型为int*[],同上,指向下一个数组首元素。
	return 0;
}
  1. 如何使用数组指针?
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*p)[10] = &arr;
	for (int i = 0;i < 10;i++)
	{
		printf("%d ", *((*p) + i));
	}
	return 0;
}

这个是一维数组,如果是二维数组呢?

void print(int(*p)[4], int row, int col)
{
	for (int i = 0;i < row; i++)
	{
		for (int j = 0;j < col;j++)
		{
			printf("%d ", *((*p + i) + j));
			//首先(*p + i)我们拿到的是二维数组的第i行,实际上就是第i行的地址
			//然后加j就是访问第i行数组中的第j个元素
	}
}
int main()
{
	int arr[3][4] = { {1,2,3,4},{2,3,4,5},{4,5,6,7} };
	print(arr, 3, 4);
	return 0;
}
  • int arr[5];
    arr是一个整型数组,有5个元素,每个元素是int类型的

  • int * parr1[10];
    parr是一个数组,数组有10个元素,每个元素的类型是int *,所以parr1是指针数组。

  • int ( * parr2)[10];
    parr2和 * 结合,说明parr2是一个指针,该指针指向一个数组,数组是10个元素,每个元素是int类型的。所以它是数组指针.

  • int ( * parr3[10])[5];
    parr3和[]结合,说明parr3是一个数组,数组是10个元素
    数组的每个元素是什么类型呢?是一种数组指针,类型是int(*)[5]该类型的指针指向的数组有5个int类型的元素。

数组参数,指针参数

一维数组传参

void test(int arr[])//以数组形式传参,数组形式接受参数是🆗
{}
void test(int arr[10])//[]里面的数字不会影响,同上
{}
void test(int* arr)//数组名基类型是int* ,二者类型相同可以接受
{}
void test2(int* arr[20])//以指针数组形式传参,接受方式相同是🆗
{}
void test2(int** arr)//指针数组里面的元素为指针(即int*),所以以数组名传参其基类型为int**,所以🆗
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

总结: 传参看前后要确保其基类型是否相同

二维数组传参

	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)
	{}
	//总结 arr的基类型为int(*)[],第一,第二为int*,第四为int**,只有第三其基类型相同;
	int main()
	{
		int arr[3][5] = { 0 };
		test(arr);
	}

一级指针传参

void test(int* p)
{}
int main()
{
	int a = 10;
	int* ptr = &a;
	int arr[10] = {0};
	test(&a);
	test(ptr);
	test(arr);
	return 0;
}

注:arr[i] == *(arr+i) == p[i] == *(p+i);访问方式相同

总结: 保证形参与实参类型相同

二级指针传参

void test(char** p)
{}
int main()
{
	char ch = 'w';
	char* p = &ch;
	char** pp = &p;
	char* arr[5];

	test(&p);
	test(pp);
	test(arr);

	return 0;
}

函数指针

在数组中数组名为首元素地址,那么函数名是不是也是其地址呢?

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	printf("%p\n", Add);
	printf("%p\n", &Add);
	return 0;
}

输出的地址是相同的,那么我们就可以把函数地址存放在指针中,比如上面的Add函数的声明为int (*pAdd)(int,int)= &Add*先与pAdd结合表明是一个指针,返回类型为int,函数参数类型为两个int类型参数。

那么我们怎么使用它呢?例如:int sum = (*pAdd)(2,3)

注: 这里我们省略某些部分,比如
int (* pf)(int, int) = &Add <==> int (* pf)(int, int) = Add,
int sum = (*pf)(2,3) <==> int sum = pf(2, 3)
这里的& 和* 可以省略,不影响使用。

在《C陷阱和缺陷》有两段关于函数指针的代码:

  • ( *( void (*)() )0 )()
    我们一层一层的分析,首先把0强制类型转换为void (*)()类型的函数指针,再去调用0地址处这个参数为无参,返回类型是void的函数。
  • void ( *signal( int, void(*)(int) ) )(int)
    首先void( * )(int) 该函数指针和int作为参数,传给signal函数,剩余的部分为void ( * )(int),表明它是一个返回类型为函数指针的函数。
    我们可以用typedef来重定义
 typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

函数指针数组

函数指针数组-存放函数指针的数组,每个元素都是函数指针类型
比如我们写一个简单的计算器:

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[5])(int, int) = { 0, Add, Sub, Mul, Div };//pfArr是一个函数指针的数组,也叫转移表

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

	return 0;
}

回调函数

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

  • qsort函数中的cmp函数即为回调函数,这里我们用冒泡排序的思想来实现
#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[20];
	float score;
	int age;
};
void print(struct Stu arr[], int len)
{
	for (int i = 0; i < len; i++)
	{
		printf("%-5s %.3f %d\n", arr[i].name, arr[i].score, arr[i].age);
	}
}
int cmp_name(const void* e1, const void* e2)
{
	return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int cmp_age(const void* e1, const void* e2)
{
	return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_score(const void* e1, const void* e2)
{
	if (((struct Stu*)e1)->score > ((struct Stu*)e2)->score)
		return 1;
	else if (((struct Stu*)e1)->score < ((struct Stu*)e2)->score)
		return -1;
	else
		return 0;
}
void Swap(char* buf1, char* buf2, int width)
{
	for (int i = 0;i < width;i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void My_qsort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
{
	for (int i = 0;i < sz - 1;i++)
	{
		for (int j = 0;j < sz - i - 1;j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
int main()
{
	struct Stu arr[] = { {"zhang",81.2f,20},{"ning",90.2f,32},{"shang",76.2f,45} };
	int len = sizeof(arr) / sizeof(arr[0]);
	My_qsort(arr, len, sizeof(struct Stu), cmp_name);
	//My_qsort(arr, len, sizeof(struct Stu), cmp_score);
	//My_qsort(arr, len, sizeof(struct Stu), cmp_age);
	print(arr, len);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Zzt.opkk

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

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

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

打赏作者

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

抵扣说明:

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

余额充值