C语言中的指针进阶

C语言中的指针进阶

字符指针
数组指针
指针数组
数组传参和指针传参
函数指针
函数指针数组
指向函数指针数组的指针
回调函数

我们之前在初阶的部分就已经了解了一些指针的知识,我们先来回忆一下:

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

接下来,我们继续来深入了解指针的高级主题。

1.字符指针

使用方式:

int main()
{
	char ch = 'w';
	char* pc = &ch;//pc是指向一个字符变量

	return 0;
}

还有一种使用方式如下:

int main()
{
	const char* p = "hello bit";//"hello bit"是一个常量字符串,存放在内存的常量区,const保护了指针指向的内容不能被修改
	//上面表达式的作用是:把常量字符串"hello bit"的第一个字符h的地址赋值给p,因为字符串的存放地址是连续的
	printf("%c\n", *p);//打印结果是h
	printf("%s\n", p);
	return 0;
}

曾经出过这样一道面试题:

#include<stdio.h>
int main()
{
    char str1[]="hello bit.";
    char str2[]="hello bit.";
    char* str3="hello bit.";
    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("str1 and str2 are not same\n");
    
    return 0;
}

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

2.指针数组

使用方式1:

int main()
{
	//char* arr[5];//arr是存放字符指针的数组
	//int* arr2[4];//arr2是存放整型指针的数组
	int a = 0;
	int b = 20;
	int c = 30;
	int d = 40;
	int* arr2[4] = { &a,&b,&c,&d };//arr2就是整型指针的数组
	int i = 0;
	for (i = 0;i < 4;i++)
	{
		printf("%d ", *(arr2[i]));
	}

	return 0;
}

使用方式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]);//p[i] == *(p+i)
            //parr[i][j] == *(parr[i]+j)
		}
		printf("\n");
	}
	return 0;
}

使用方式3:

int main()
{
	const char* arr[5] = { "abcdef","bcdefg","hehe","haha","zhangsan" };
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		printf("%s\n", arr[i]);
	}

	return 0;
}

3.数组指针

3.1数组指针定义

数组指针是数组还是指针?

答案是:指针

我们通过前面的学习已经知道:

整型指针:int* pint;能够指向整型数据的指针。

浮点型指针:float* pf;能够指向浮点型数据的指针。

所以,数组指针应该是能够指向数组的指针。

使用方式:

int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
3.2&数组名VS数组名
int main()
{
	int arr[10] = { 0 };
	//arr;//数组名是首元素的地址
	//&arr[0];//是首元素的地址
	//&arr;//取出数组的地址

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

	printf("%p\n", arr+1);
	printf("%p\n", &arr[0]+1);
	printf("%p\n", &arr+1);
	return 0;
}

数组名是首元素地址

但是有两个例外

  1. sizeof(数组名),这里的数组名是表示整个数组的,sizeof(数组名),计算的是整个数组的大小,单位是字节。
  2. &数组名,这里的数组名不是首元素地址,数组名表示整个数组,所以取出的是整个数组的地址。
3.3数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。

int main()
{
	int* arr[10] = { 0 };
	int* (*p)[10] = &arr;

	int** p2 = arr;//指针数组中首元素是指针,指针的地址是二级指针
	return 0;
}

下面这种数组指针的用法就非常难受了,我们一般不会这样使用:

void print(int(*parr)[10], int sz)
{
	int i = 0;
	printf("%d ", parr[0][i]);
	printf("%d ", (*(parr + 0))[i]);
	printf('%d ', (*parr)[i]);//(*parr)相当于parr指向的数组的数组名

}

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)[5], int r, int c)
{
	int i = 0;
	for (i = 0;i < r;i++)
	{
		int j = 0;
		for (j = 0;j < c;j++)
		{
			//printf("%d ", *(*(p + i) + 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);//arr是数组名,数组名是首元素地址

	return 0;
}

学习了上面的知识,让我们来回顾一下下面这些是什么:

int arr[5];//整型数组
int *parr1[10];//parr1是一个数组,10个元素,每个元素是int*的,所以parr1是一个存放指针的数组
int (*parr2)[10];//parr2是一个数组指针,该指针指向的数组有10个元素,每个元素是int的
int (*parr3[10])[5];//parr3是一个数组,数组有10个元素,每个元素是一个数组指针,该指针指向的数组有5个元素,每个元素是int

4.数组参数、指针参数

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

4.1一维数组传参
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int* arr)
{}
void test2(int* arr[20])
{}
void test2(int** arr)
{}

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[][])//err
{}
void test(int arr[][5])
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样才方便运算。
void test(int(*p)[5])
{}

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\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]);

	print(p, sz);
	return 0;
}
4.4二级指针传参
void test(int** ppa)
{}

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

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

5.函数指针

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

int main()
{
	printf("%p\n", &Add);
	printf("%p\n", &Add);
	int (*pf)(int, int) = &Add;//pf是用来存放函数的地址 - pf就是函数指针变量

	int ret = Add(2, 3);
	printf("%d\n", ret);//5

	ret = (*pf)(4, 5);
	printf("%d\n", ret);//9

	return 0;
}

接下来我们来看两个有意思的代码:

//代码1
int main()
{
	(*(void(*)())0)();
	return 0;
}

代码是一次函数调用
解析:
代码中把0强制类型转换为void(*)()的一个函数的地址。
解引用0地址,就是去0地址处的这个函数,被调用的函数是无参,返回类型是void。

//代码2
int main()
{
	void(*signal(int, void(*)(int)))(int);
	return 0;
}

这个代码是一次函数声明
声明的函数名signal
signal函数有两个参数,第一个是int类型,第二个是void(*)(int)的函数指针类型
signal函数的返回类型依然是:void(*)(int)的函数指针类型

代码2太过复杂,如何简化:

typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);

6.函数指针数组

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

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

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

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

答案是:parr1

parr1先和[]结合,说明parr1是数组,数组的内容是什么呢?

int(*)()类型的函数指针。

函数指针数组的用途:转移表

int main()
{
	int input = 0;
	do
	{
		int x, y;
		int ret = 0;
		menu();
		printf("请选择:>");
		int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div };//前面必须定义这些函数
		if (input == 0)
		{
			printf("退出计算机\n");
		}
		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.指向函数指针数组的指针

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

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

如何定义?

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

int (*pf)(int, int) = Add;//pf是函数指针
int (*pfArr[5])(int, int);//pfArr是一个函数指针的数组
int (*(*ppfArr)[5])(int, int) = &pfArr;//ppfArr就是一个指向函数指针数组的指针

8.回调函数

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

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

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

void Calc(int(*pf)(int, int))
{
	int ret = pf(3, 5);
	printf("%d\n", ret);
}

int main()
{
	Calc(Add);
	return 0;
}

我们来看一下qsort函数的使用:

void qsort(void* base,
	size_t num,//待排序的元素个数
	size_t width,//一个元素的大小,单位是字节
	int(*cmp)(const void* e1, const void* e2)//cmp指向的是:排序时,用来比较2个元素的函数
);

举例1:

#include<stdlib.h>
//比较2个整型函数
int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0;i < sz;i++)
	{
		printf("%d ", arr[i]);
	}
}

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

	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	print_arr(arr,sz);
	return 0;
}

举例2:

#include<stdlib.h>
#include<string.h>
struct Stu
{
	char name[20];
	int age;
};

int cmp_by_name(const void* e1, const void* e2)
{
	strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}

void test2()
{
	struct Stu s[3] = { {"张三",15},{"李四",30},{"王五",10} };
	int sz = sizeof(s) / sizeof(s[0]);
	//按照名字排序
	qsort(s, sz, sizeof(s[0]), cmp_by_name);
}

int main()
{
	//排序结构体数组
	test2();
	return 0;
}

我们也可以使用一个回调函数实现一个通用的冒泡排序函数:

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

int cmp_int(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}

void print_arr(int arr[], int sz)
{
	int i = 0;
	for (i = 0;i < sz;i++)
	{
		printf("%d ", arr[i]);
	}
}

//使用一个回调函数实现一个通用的冒泡排序函数
void BubbleSort(void* base, size_t num, size_t width, int (*cmp)(const void* e1, const void* e2))
{
	size_t i = 0;
	//趟数
	for (i = 0;i < num - 1;i++)
	{
		//比较的对数
		size_t j = 0;
		for (j = 0;j < num - 1 - i;j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}

//测试自定义的BubbleSort():
void test3()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
	//打印
	print_arr(arr, sz);
}

int main()
{
    test3(); 
    return 0;
}

以上就是我们指针高阶部分的全部内容了,我们发现要想实现一些程序,指针的使用是必不可少的,所以熟练指针是我们学习C语言非常重要的一部分,希望大家看了我的文章后可以有所收获。

评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

地火轰雷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值