指针的进阶(C语言超详细指针介绍)

前言

关于指针这一主题,我在初阶->C语言指针详解这篇博客中已经介绍过相关的内容:

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

本篇博客,探讨指针的更深层的内容。

字符指针

指针类型有一种为字符指针char*

//demo
char c = 'a';//将常量字符c赋给新创建的字符变量c
char* pc = &c;//将字符变量c的地址赋给新创建的字符指针pc
*pc = 'b';//解引用字符指针pc,然后修改字符变量c的内容

还有一种使用方法如下:

//demo
const char* ps = "hello world!";
printf("%s\n", ps);

注意该方式只是把常量字符串首元素的地址赋给了指针ps,而不是把整个字符串的内容放入了指针ps里,因为指针存放的是地址,并且,字符串中的一个字符占一个字节,而指针只能是4/8个字节。

image-20220531212022685

综上所述:

字符指针可以存放一个字符的地址

字符指针可以存放一个字符串的首元素的地址

来一道经典题目练练手

#include <stdio.h>
int main()
{
    const char* ps1 = "hello world";
    const char* ps2 = "hello world";
    char ps3[] = "hello world";
    char ps4[] = "hello world";
    if(ps1 == ps2){
        printf("ps1 == ps2");
    }
    else{
        printf("ps1 != ps2");
	}
    if(ps3 == ps4){
        printf("ps3 == ps4");
    }
    else{
        printf("ps3 != ps4");
    }
    return 0;
}

image-20220531213035138

原因是:

  1. 由于ps1和ps2只存放地址,而又由于常量字符串相同,所以C/C++会把常量字符串存储到一个单独的一个内存区域,当不同的指针指向该字符串时,实际会指向同一块内存。所以ps1和ps2的值相等。
  2. 但是用相同的常量字符串去初始化不同的数组时,会开辟出不同的内存块,因为数组是真实存放字符串的。由于两个数组分别开辟了内存块,所以ps3和ps4的值不相等。

指针数组

顾名思义,就是一个存放指针的数组

int* arr1[10];	//存放十个整形指针
char* arr2[10];	//存放十个字符指针
char** arr3[10];//存放十个二级字符指针

数组指针

数组指针的定义

指针数组是数组,那么数组指针就是指针啦。

前面已经讲过指向整型数据的整型指针int* pi

指向浮点型数据的浮点型指针float* pf

指向字符数据的字符指针char* pc

那么数组指针就该是指向数组的指针

char* p1[10];//一个数组,存放十个字符指针
char (*p2)[10];//一个指针,指向一个含有十个char型数据的数组
//解释
由于[]的优先级高于*,所以p1先与[]结合,成为数组,存放十个字符指针
由于有括号的存在,p2先与*结合,成为指针变量,然后指向一个含有十个char型数据的数组,所以称为数组指针

&数组名vs数组名

int arr[10];

前面讲过arr是数组名,数组名是首元素的地址(除了两种情况:sizeof(arr)和&arr)

那么&arr究竟是什么呢?

printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);

image-20220531215259418

答案是一样的,继续往下看

printf("arr = %p\n", arr);
printf("arr = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr = %p\n", &arr + 1);

image-20220531215538228

前面讲过,指针+-整数,可以访问该地址的下一个地址(+)或上一个地址(-),而这里我们访问下一个地址,答案却发生了不同,经过计算,arr + 1arr大4,这是因为arr中的数据类型是int,而&arr + 1&arr大40,而该数组的大小就为40。

所以,我们就发现arr&arr看似值相同,但是其背后的意义是不同的

实际上:arr代表的是数组首元素的地址,而&arr代表的是数组的地址

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

arr的地址+1跳过一个数组元素,而&arr + 1跳过一个数组

数组指针的使用

int arr[3][4] = { {1, 2, 3, 4}, {2, 3, 4, 5}, {3, 4, 5, 6} };
int(*pa)[4] = arr;

image-20220531220938871

既然arr是数组首元素地址,那么在二维数组中,首元素就是一个数组,arr就是一个首数组的地址,所以用数组指针来接收

回顾一下

int a[10];//a是一个整型数组,存放10个int整型
int* b[10];//b是一个指针数组,存放10个int*指针
int(*c)[10];//c是一个数组指针,指向一个数组,该数组存放10个int整型
int(*d[10])[5];//d是一个数组指针数组,该数组存放10个指针,这10个指针分别指向10个数组,这10个数组分别存放5个int整型

数组参数、指针参数

一维数组传参

#include <stdio.h>
void test1(int arr[10]);//可行,形参的类型和实参的类型相同
void test1(int arr[]);	//可行
//实际上我们应该已经清楚数组名传参,传的是首元素地址,而这里的arr实际上是一个指向int的指针,所以写不写10无意义
void test1(int* arr);	//可行

void test2(int* arr[10]);//可行,形参的类型和实参的类型相同,这里的arr实际上是一个指向int*的指针,来接收指针的地址没问题
void test2(int** arr);//可行,首元素是一个指针,一个指针的地址交给一个二级指针没有问题
int main()
{
    int arr1[10] = { 0 };
    int* arr2[10] = { NULL };
    test1(arr1);
    test2(arr2);
	return 0;    
}

二维数组传参

#include <stdio.h>
void test(int arr[3][4]);//可行,形参实参类型相同
void test(int arr[][4]);//可行,二维数组可以省略第一个下标
void test(int arr[][]);//不行,二维数组不可以省略第一个下标
void test(int* arr);//不可行,形参和实参类型不同,形参接收一个int的地址,而实参传的是一个数组的地址
void test(int* arr[4]);//不可行,形参接收二级地址
void test(int(*arr)[4]);//可行,数组指针来接收数组的地址,完全没问题
void test(int** arr);//不可行,形参二级指针来接收二级地址,而实参传的是数组的地址
int main()
{
    int arr[3][4] = { 0 };
    test(arr);
    return 0;
}

一级指针传参

#include <stdio.h>
void print(int* p, int size){
    for(int i = 0; i < size; i++){
        printf("%d ", *(p + i));
    }
}
int main()
{
    int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int size = sizeof(arr) / sizeof(arr[0]);
    int* pa = arr;
    print(arr, size);
    return 0;
}

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

void test(int* p);//能接收一个整型的地址,一个整型数组首元素的地址
void test(char* p);//能接收一个字符的地址,一个字符数组首元素的地址,一个字符串首元素的地址

二级指针传参

#include <stdio.h>
void test(int** p){
    printf("%d\n", **p);
}
int main()
{
    int n = 10;
    int* pn = &n;
    int** ppn = &pn;
    test(ppn);
    test(&pn);
    return 0;
}

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

#include <stdio.h>
void test(char** p);
int main()
{
    char c = 'a';
    char* pc = &c;
    char** ppc = &pc;
    test(&pc);
    test(ppc);
    
    char s[] = "hello world!";
    char* ps = s;
    char** pps = &ps;
    test(&ps);
    test(pps);
    
    char* arr[10];
    test(arr);
    return 0;
}

总结:

写了这么多,其实可以总结出只要形参、实参类型相同,那么就可以进行传参!

函数指针

顾名思义就是指向函数的指针,没错,函数是有地址的,其实在内存中的任何数据都有地址!!!

image-20220602101602048

函数名可以直接输出地址,也可以取地址再输出

怎么样理解这两者呢?可以暂时这样理解:

test是函数的地址,它的类型是void()

&test是指向函数这个对象的地址,它的类型是void(*)()

好了,现在地址有了,那怎么保存呢?

一个整形的地址需要一个整形指针,一个字符的地址需要一个字符指针,那么一个函数的地址,就需要一个函数指针

#include <stdio.h>
int Sub(int x, int y){
	return x + y;
}
int main()
{
    int(*p)(int, int) = &Sub;
    return 0;
}

仔细看函数指针的定义和函数的定义,可以发现,只需要把函数名称Sub换成(*p),而变量只写类型就可以了。

其实就是要求操作数两边的指针类型一致。

注意:()一定不能少!!!

int(*p)(int,int)//这是一个函数指针
int*p(int,int)//这是一个函数声明

摘自《C陷阱与缺陷》的两处代码

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

解释:

代码1:将0强制转换成函数指针,然后解引用取到该函数。这实际上是一次函数调用。

代码2:signal是函数名,参数类型分别是整形和函数指针,返回类型函数指针。这实际上是一个函数声明。

代码2也可以进行简化,便于更好的理解

typedef void(*pfun_t)(int);//将函数指针类型重命名为pfun_t
pfun_t signal(int, pfun_t);//signal函数的参数类型分别是int和pfun_t,返回类型是pfun_t

函数指针的一个用途:

最基本的计算器的模板是这样的(计算器的功能实现不重要,重要的是函数指针的实现方法)

#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 x = 0, 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;
}

可以看到在case语句那里,有非常多的代码冗余,而函数指针则能解决这一问题。

函数指针的实现方法:

#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;
}
void calc(int(*p)(int, int)) {
	int x = 0, y = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
	printf("%d\n", p(x, y));
}
int main()
{
	int input = 0;
	do
	{
		menu();
		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;
}

函数指针数组

顾名思义就是存放函数指针的数组了

int arr[10];//存放十个int
int* arr[10];//存放十个int*
int(*arr[10])(int, int);//存放十个函数指针,函数指针指向的函数的参数类型分别是int, int, 返回类型是int

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

继续对上面的计算器进行进一步的优化

#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 x = 0, y = 0;
	int(*p[5])(int, int) = { 0, Add, Sub, Mul, Div };
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input >= 1 && input <= 4) {
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			printf("%d\n", p[input](x, y));
		}
		else if(0 == input){
			printf("退出\n");
			break;
		}
		else {
			printf("请重新选择\n");
		}
	} while (input);
    return 0;
}

指向函数指针数组的指针

看名字,最后是指针,那它就是一个指针了。

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

void(*arr[5])();//arr是一个数组,该数组有五个元素,元素类型是函数指针
void(*(*parr)[5])();//parr是一个指针,指向一个数组,该数组有五个元素,元素类型是函数指针

回调函数

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

qsort函数就是一个典型的例子,它是C的一个库函数,该函数可以排序任意类型的数据,但是排序的顺序需要用户自己定义。

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
int cmp(const void* e1, const void* e2) {
    return (*(int*)e1 - *(int*)e2);
}
int main()
{
    int arr[] = { 1,3,2,4,6,5,8,7,0,9 };
    int size = sizeof(arr) / sizeof(arr[0]);
    printf("排序前:\n");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    qsort(arr, size, sizeof(int), cmp);
    printf("排序后:\n");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    system("pause");
    return 0;
}

image-20220602120155110

qsort函数的第一个参数是需要排序的数组的首元素地址,这个没什么解释的

第二个参数是需要排序的元素的数量,这也没什么解释的

第三个参数是需要排序的元素类型的大小,因为qsort函数是用来排序任何类型的,所以为了能够支持这一目的,必须传一下元素类型的大小,能够让qsort函数知道,此时此刻在排序什么类型的数据

第四个参数是cmp函数,这个函数是自定义的,该函数是来确定排序的顺序的。

cmp函数的规则是:

前者-后者为升序排序,结果>0进行排序,<0 || <=0不排序

后者-前者为降序排序,结果>0进行排序,<0 || <=0不排序

cmp函数是用户定义的,但不是用户调用的,而是qsort函数调用的,所以cmp函数就是一个回调函数。

上面是来排序整型数据的,我们接下来使用qsort函数排序结构体数据

根据年龄来排序

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
typedef struct Student {
	char name[20];
	int age;
}Student;
int cmpStudentAge(const void* e1, const void* e2) {
	return (((Student*)e1)->age - ((Student*)e2)->age);
}
int main()
{
	Student s[] = { { "zhangsan", 20 }, { "lisi", 60 }, { "wangwu", 40 } };
	int sSize = sizeof(s) / sizeof(s[0]);
	printf("排序前:\n");
	for (int i = 0; i < sSize; i++) {
		printf("%s %d\n", s[i].name, s[i].age);
	}
	printf("\n");
	qsort(s, sSize, sizeof(Student), cmpStudentAge);
	printf("排序后:\n");
	for (int i = 0; i < sSize; i++) {
		printf("%s %d\n", s[i].name, s[i].age);
	}
	printf("\n");
	system("pause");
	return 0;
}

image-20220602121012186

根据姓名来排序

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
#include <string.h>
typedef struct Student {
	char name[20];
	int age;
}Student;
int cmpStudentName(const void* e1, const void* e2) {
	return strcmp(((Student*)e1)->name, ((Student*)e2)->name);
}
int main()
{
	Student s[] = { { "zhangsan", 20 }, { "lisi", 60 }, { "wangwu", 40 } };
	int sSize = sizeof(s) / sizeof(s[0]);
	printf("排序前:\n");
	for (int i = 0; i < sSize; i++) {
		printf("%s %d\n", s[i].name, s[i].age);
	}
	printf("\n");
	qsort(s, sSize, sizeof(Student), cmpStudentName);
	printf("排序后:\n");
	for (int i = 0; i < sSize; i++) {
		printf("%s %d\n", s[i].name, s[i].age);
	}
	printf("\n");
	system("pause");
	return 0;
}

image-20220602121206933

综上所述:

qsort函数需要调用一个cmp函数,cmp函数就是回调函数。

qsort函数的使用很简单,只需要自定义一个cmp函数,前-后是升序,后-前是降序。

用冒泡排序模拟实现qsort函数

#include <stdio.h>
#include <Windows.h>
int cmp(const void* e1, const void* e2) {
	return (*(int*)e1 - *(int*)e2);
}
void sort(char* e1, char* e2, int width) {
	for (int i = 0; i < width; i++) {
		char tmp = *e1;
		*e1++ = *e2;
		*e2++ = tmp;
	}
}
void bsort(void* base, int size, int width, int(*cmp)(const void* e1, const void* e2)) {
	for (int i = 0; i < size - 1; i++) {
		int flag = 1;
		for (int j = 0; j < size - 1 - i; j++) {
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
				sort((char*)base + j * width, (char*)base + (j + 1) * width, width);
				flag = 0;
			}
		}
		if (1 == flag) {
			break;
		}
	}
}
int main()
{
	int arr[] = { 1,3,2,4,6,5,8,7,0,9 };
	int size = sizeof(arr) / sizeof(arr[0]);
	printf("排序前:\n");
	for (int i = 0; i < size; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");
	bsort(arr, size, sizeof(int), cmp);
	printf("排序后:\n");
	for (int i = 0; i < size; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");
	system("pause");
	return 0;
}

关于qsort函数的参数介绍,上面段落已经讲到了,这里讲一下函数内部的核心部分:交换数据

void sort(char* e1, char* e2, int width) {
	for (int i = 0; i < width; i++) {
		char tmp = *e1;
		*e1++ = *e2;
		*e2++ = tmp;
	}
}
if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
	sort((char*)base + j * width, (char*)base + (j + 1) * width, width);
}

为使qsort函数能够排序任意类型数据,只能将第一个参数设置成char型,然后配合传入的数据的width一起来确定每个数据的大小,从而间接确定数据的类型。

真正在交换数据时,实际上时以char为单位来进行交换的,假设交换两个int型,只需要连续交换四次char型就可以了。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

云朵c

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

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

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

打赏作者

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

抵扣说明:

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

余额充值