c指针进阶

news2025/1/13 15:38:13

目录

char* 指针

指针数组

 数组指针

应用

接收一维数组(不常用)

接收二维数组

 存放数组指针的数组

 一维数组传参

二维数组传参

函数指针

两段有趣的代码

函数指针数组

 应用——计算器

指向函数指针数组的指针 

回调函数

qsort

修改冒泡排序


char* 指针

一个char*类型的指针可以接收下面类型的地址

 //接收字符 
    char ch = 'w';
	char* pc = &ch;
//接收常量字符串
    const char* pa = "abcdef";
//接收数组首地址
    char arr[] = "abcdef";
	char* p = arr;

需要注意字符串常量不可改变,所以要用const修饰,正因如此,在遇到一组相同的字符串常量时,编译器会给他们分配相同的地址空间(存放在代码段)

上面比较的是二者地址大小,可以用strcmp比较字符串大小。

指针数组

int* arr1[ ];//整形指针数组
char* arr2[ ];//一级字符指针数组
char** arr3[ ];//二级字符指针数组

结合优先级为数组名和[]结合,然后*与类型结合表示数组里存放指针类型。 

前面我们用指针数组模拟了一个二维数组,具体做法就是将多个元素相同的数组首元素地址依次存放到指针数组里,再逐个打印。我们也可以用它来输出多个字符串

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

 数组指针

数组指针是指针,所以我们要用()将*与变量名结合使之成为一个指针,后面跟[ ]内表示指向对象的下标大小(不可省略

    int arr[10] = {1,2,3,4,5};
	int (* pa)[10] = &arr;//取出的是数组的地址存放到pa中,pa是数组指针变量
	//int(*)[10] -> 数组指针类型
    int a = 0;
    int* p = &a;
    //int* 指针类型

那数组指针与其他指针有什么区别码?答案当然是有的,我们可以对比一下它们的地址: 

 

可以发现并没有什么不同,但又是合理的,数组指针和整形指针指向起始位置才能保证访问数据的完整性。但是,起点相同,并不代表走的路相同。

前二者+1跳过一个整形(4个字节),而&arr直接跳过了40个字节,其实很好理解,因为它的类型是int (*) [10]类型,我们可以把*理解为乘号,他访问字节的能力是 4*10 = 40个字节。 

 arr作为首元素地址有两个例外:

>>>sizeof(arr) 注意arr传参后为指针类型(4/8字节)

>>>&arr

应用

接收一维数组(不常用)

void print1(int *arr, int sz)//一维数组(指针)传参
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr+i));
	}
	printf("\n");
}
void print2(int(*p)[10], int sz)//数组指针
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", (*p)[i]);//arr;
	}
	printf("\n");
}

区别在于是否接收&,对&arr解引用就可以得到数组首地址啦。

接收二维数组

void print3(int arr[3][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 ", arr[i][j]);
		}
		printf("\n");
	}
}
void print(int (*p)[5],int l,int r)//数组指针
{
	for (int i = 0; i < l; i++)
	{
		for (int j = 0; j < r; j++)
		{
			printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
}
int main(){
    int arr[3][5] = { {1,2,3,4,5},{2},{2,3} };
    print(arr,3,5);
    return 0;
}

和一维数组不同,二维数组的数组名表示第一行的地址,也就是说二维数组的首元素就是第一行地址。

 存放数组指针的数组

int (*parr3[10])[5];

我们先来拆分一下:去掉数组名,我们可以看到,int (*)[5]是一个数组指针,而parr3是一个包含10个元素的数组

 

 一维数组传参

我们来分析下面的传参是否合法:

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

}

可能最后一个不好理解,因为指针数组的每个元素都是int*类型,而接收一级指针的地址(首元素地址)需要用二级指针。

二维数组传参

void test(int arr[3][5])//正确
{}
void test(int arr[][])//错误,可省略行,但必须知道一列有多少个数字,方便计算
{}
void test(int arr[][5])//正确
{}
void test(int *arr)//错误,接收的是arr(整形地址),而非&arr(数组地址)
{}
void test(int* arr[5])//错误,存放的是指针
{}
void test(int (*arr)[5])//数组指针,正确
{}
void test(int **arr)//错误,用于接收一级指针地址,而非数组地址
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);

函数指针

顾名思义,函数指针可以通过接收函数名或函数地址将函数的地址用指针保存起来。

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf) (int x,int y) = Add;//可省略参数名,*必须与变量名结合
    return 0;
}

声明类型:int (*) (int,int) 

 对于一个函数,不存在首元素地址这样的概念,所以取地址与否都将直接拿到函数的地址。

也就是说,我们通过函数指针访问函数的时候可以选择性地使用*操作符。 

两段有趣的代码

(*(void (*)())0)();//*表示解引用可以省略

对于void(*) ( )表示一个函数指针(返回类型为空,参数为空),也就是说,这段代码先将0转换成函数指针类型,再执行(*0)()*必须与0结合)意为调用0地址处的函数

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

 根据分号我们可以该代码是一个函数声明。声明对象是signal,我们将它分成两部分——

signal(int,void (*)(int))//内部
void (*)(int);//外部

内部:signal函数第一个参数为int,第二个参数为返回类型为void,参数为int函数指针

外部:signal函数的返回类型也是一个函数指针类型,该函数指针指向的函数参数是int,返回类型是void

我们可以对这段代码稍作修改,增强其可读性:

typedef void (*pf_t)(int);//规定:pf_t只能放在括号里
pf_t(signal(int,pf_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;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		
		switch (input)
		{
		case 1:
			printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:	
			printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
    return 0;
}

函数指针数组:

利用它们函数返回类型和参数一样的性质,我们可以创建一个有函数指针数组来存放这些函数。

int main()
{
	int input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	int (*pfarr[])(int, int) = {0, Add,Sub,Mul,Div };//指针数组,0表示0地址
	do {
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		if (input >= 1 && input < 5)
		{
			scanf("%d %d", &x, &y);
			int ret = pfarr[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("输入错误\n");
		}
	} while (input);

运行结果: 

指向函数指针数组的指针 

int (*parr)(int,int);//函数指针
int (*pfarr[4])(int,int);//函数指针数组
int (*(*ptr)[4])(int,int);//指向函数指针数组的指针

简单了解一下这个指针:*ptr为指针,它指向外面的数组[4],每个元素都是函数指针类型,像这样的指针被称为指向函数指针数组的指针

回调函数

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

对于刚才实现加法器的第一种方法存在重复代码和类型相同的函数,我们可以实现一个回调函数简化代码:

void cal(int(*pf)(int, int))
{   int x = 0;
	int y = 0;
	int ret = 0;
	printf("请输入2个操作数:>");
	scanf("%d %d", &x, &y);
	ret = pf(x, y);
	printf("%d\n", ret);
}

我们用一个函数指针作为参数去接收函数的地址,再通过这个指针在函数体内部进行函数调用这个过程就叫做回调函数。(当调用Add时,Add函数就被称为回调函数)

//简化后的switch case:
switch (input)
		{
		case 1:
			cal(Add);
			/*printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);*/
			/*ret = Add(x, y);*/
			/*printf("%d\n", ret);*/
			break;
		case 2:	
			/*printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);*/
			cal(Sub);
			break;
		case 3:
			/*printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);*/
			cal(Mul);
			break;
		case 4:
			/*printf("请输入2个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);*/
			cal(Div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;

区别cal(ADD(2,3))和cal(ADD),前者传递的是函数返回值,后者传递的是函数地址。 

qsort

qsort函数是c语言内置的排序函数。它的作用是对任意类型数据进行快速排序。它的函数声明如下:

void* base 

指向第一个元素的地址,将其转换成void*类型(可以接收任意类型的指针)。在具体使用void*类型的指针时,需要注意:

不要通过解引用访问指针或者对指针进行加减操作,因为不知道访问多少字节合适。

int a = 0;
void* p = &a;
*p = 10;//err
p++;//err

如需访问,需先强制转换成具体类型。

size_t num

数组元素个数

size_t size

获取单个元素所占字节大小。void*类型指针不知道向后偏移多少字节去遍历数组,通过该参数可对不同类型的不同偏移量的数据进行访问。

int (*compar)(const void*,const void*))

比较两个元素大小的函数指针。通过自己实现两个数的比较规则来对不同数据类型(字符串,结构体等)体等)进行排序,以达到对多种数据排序的功能。

当函数返回值>0时,表示p1大于p2,当函数返回值<0时,表示p1小于p2,当函数返回值为0时,表示p1等于p2。此时的排序顺序默认为升序。

对整形排序:

#include <stdlib.h>//qsort所属库
int cmp_int(void const* p1, void const* p2)//比较整形
{
	return *(int*)p1 - *(int*)p2;
}
void print(int arr[], int sz)
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}
void test1()
{
	int arr[] = { 2,1,3,7,5,9,6,8,0,4 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_int);
	print(arr, sz);
}

对结构体排序:

struct student 
{
	char name[20];
	int age;
};
?*int cmp_s_name( const void* p1,  const void* p2)//结构体名字
{
	return strcmp(((struct student*)p1)->name, ((struct student*)p2)->name);
}*/
int cmp_s_age(const void* e1, const void* e2)//结构体整形
{
	return ((struct student*)e1)->age - ((struct student*)e2)->age;
}
void test2()
{
	struct student s[] = { {"zhangsan", 20}, {"lisi", 55}, {"wangwu", 40} };
	//按照名字比较
	int sz = sizeof(s) / sizeof(s[0]);
	
}

注意:(强制类型转换) 优先级低于->操作符。

qsort(s, sz, sizeof(s[0]), cmp_s_name);//name

qsort(s, sz, sizeof(s[0]), cmp_s_age);//年龄

修改冒泡排序

我们知道冒泡排序只能对整形进行排序,通过qsort函数函数传参我们能否得到一点启发,将冒泡排序修改成可以对任意类型进行排序呢?答案是可以的。 

void bubble_sort(void* base, int sz, int width, int (*cmp)(const void* e1, const void* e2))
{
	int i = 0;
	//趟数
	for (i = 0; i < sz - 1; i++)
	{
		//一趟冒泡排序的过程
		int j = 0;
		for (j = 0; j < sz - 1 - i; j++)
		{
			//排序逻辑
		}
    }
}

对于int*类型的数据,我们可以通过它的偏移量遍数据,而void*类型的数据无法获取具体偏移量,我们又想到,数据在内存中存放的单位是字节,我们可以通过数据所占字节数获取到每个元素。然后对满足要求的数据进行交换我们可以一个字节一个字节交换保证数据的完整性。

void Swap( char* e1, char* e2,int width)
{
	int i = 0;
	for ( i = 0; i < width; i++)
	{
		char tmp = *e2;
	    *e2 = *e1;
		*e1 = tmp;
		e1++;
		e2++;
	}
}
//内部逻辑
    if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				//交换
				Swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}

cmp函数指针通过接收不同类型的数据调用相应的回调函数对数据进行比较判断。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1029981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android:创建jniLibs的步骤

一、前言&#xff1a; android libs&#xff0c;jniLibs库的基本使用 libs&#xff0c;jniLibs用来存放各种.so库文件。如果没有jniLibs目录需要自己手动创建&#xff0c;并且库名称也不能随便更改。 二、解决方案&#xff1a; 我之前弄了好久也弄不出来&#xff0c;之前有说…

ubuntu20.04下源码编译colmap3.9

由于稠密重建需要CUDA&#xff0c;因此先安装CUDA&#xff0c;我使用的是3050GPU&#xff0c;nvidia-smi显示最高支持CUDA11.4。 不要用sudo apt安装&#xff0c;版本较低&#xff0c;30系显卡建议安装CUDA11.0以上&#xff0c;这里安装了11.1版本。 下载&#xff1a; cuda_1…

程序员接单实现财富自由?原来是用了这十大良心平台!!!

后疫情时代下,经济复苏缓慢&#xff0c;处于下行阶段。同时&#xff0c;由于强大的生活压力&#xff0c;社会内卷日益严峻 各行各业的打工人&#xff0c;都在公司里“阴暗扭曲爬行”。从“996”到“007”&#xff0c;工作强度简直是苦不堪言。尤其对咱们IT行业&#xff0c;本来…

Python 打印文本进度条

""" 打印文本进度条知识点&#xff1a;1、字符串运算&#xff0c;注意只能适用于加法、乘法&#xff0c;例如&#xff1a;123 123 123123例如&#xff1a;123 * 3 1231231232、循环语句while、for3、条件语句if4、重点&#xff1a;转义字符\r&#xff0c;可以…

浙大mpa项目提前批面试如果拿不到A资格怎么办?

2024年浙江大学MPA项目提前批面试申请已经结束&#xff0c;至今来看总的申请人数跟去年2023届基本相当&#xff0c;超过四百名学员报名提面&#xff0c;按照去年1923人报考的体量来看&#xff0c;大多数人恐怕还是把录取的希望保留到常规批复试中。那么&#xff0c;400提面考生…

Linux 爱好者线下沙龙:LLUG 2023 深圳硬核来袭 | 第三站

导读&#xff1a;2023 年 9 月 24 日下午&#xff0c;我们将在深圳举行 LLUG 2023 深圳场。本文转自 Linux 中国&#xff0c;以下为本次活动介绍。本文字数&#xff1a;1629&#xff0c;阅读时长大约&#xff1a;2分钟 经历过 6 月北京场、7 月上海场&#xff0c;一个月的休整…

关于物联网技术的水电厂电气开关柜测温系统设计应用

摘要&#xff1a;针对洪江水电厂电气开关柜温度无法在线监测的问题,本文提出了一种基于物联网技术的水电厂开关柜温度测量系统。该系统部署简单高效&#xff0c;能快速采集设备温度数据&#xff0c;通过4G或者WiFi无线信号将数据送入物联网云平台&#xff0c;实现开关柜温度远程…

常见的项目进度管理失败的原因及应对建议

在项目管理中&#xff0c;进度管理是核心环节之一。然而&#xff0c;许多项目经理在实施过程中面临失败的困境。为了改善这一状况&#xff0c;本文将分析我们常见三种的进度管理失败的原因&#xff0c;并分享一些有效的提升效率的方法。希望对大家有所帮助。 进度管理失败的原…

ReclerView的多种条目布局

一、效果图 模仿QQ看点的布局&#xff1a; 二、代码 MoreTypeAdapter &#xff1a; package com.example.qq.ThirdFragment.Adapter;import android.content.Context; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import an…

[杂谈]-十六进制数

十六进制数 文章目录 十六进制数1、概述2、十六进制数字3、以十六进制计数4、二进制数补零5、十六进制到十进制转换6、十进制到十六进制转换7、二进制到十六进制转换示例8、十六进制转二进制和十进制示例9、总结 1、概述 十六进制 数字是一种以16为基数的计数系统&#xff0c;…

pcl--第八节 点云配准数学原理

学习目的&#xff1a; 能够大致看懂技术算法的论文&#xff08;理论理解能力&#xff09;能够将论文和代码对应起来&#xff08;代码追踪能力&#xff09;知道常见算法函数的代码实现方式&#xff08;算法实现能力&#xff09; 注意&#xff01;不要求能够推导、证明算法论文…

导航菜单布局

制作包含logo、菜单、按钮的3分离布局菜单 完成效果 准备html <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0">…

Multisim14.0仿真(二十)74LS161 4位同步二进制加法计数器

一、仿真原理图&#xff1a; 二、仿真效果图&#xff1a;

C/C++内存管理相关知识点

1.内存分布 C/C将内存大体上分为四个区域&#xff1a;栈区、堆区、静态区&#xff08;数据段&#xff09;、常量区&#xff08;代码段&#xff09;。 栈区&#xff1a;用来存储函数调用时的临时信息的结构&#xff0c;存放为运行时函数分配的局部变量、函数参数、返回数据、返…

解码癌症预测的密码:可解释性机器学习算法SHAP揭示XGBoost模型的预测机制

一、引言 癌症是全球范围内健康领域的一大挑战&#xff0c;早期预测和诊断对于提高治疗效果和生存率至关重要。机器学习在癌症预测中发挥了重要作用&#xff0c;可以从临床数据中学习并构建癌症预测模型&#xff0c;帮助医生进行早期检测和干预&#xff0c;提高患者的生活质量和…

kubernetes集群证书过期启动失败问题解决方法

1、问题现象 执行kubectl命令异常报告 [rootk8s-master1 ~]# kubectl get node The connection to the server 192.168.227.131:6443 was refused - did you specify the right host or port? [rootk8s-master1 ~]# 查看etcd的日志&#xff0c;报错信息如下 {"level&…

服务器性能测试监控平台export+prometheus(普罗米修斯)+grafana搭建

1. export 数据采集工具 简介&#xff1a; export是prometheus是的数据采集组件的总称&#xff0c;它可以将采集到的数据转为prometheus支持的格式 node_export: 用来监控服务器硬件资源的采集器&#xff0c;端口号为9100mysql_export: 用来监控mysql数据库资源的采集器&…

【Java 基础篇】Java线程:volatile关键字与原子操作详解

在多线程编程中&#xff0c;确保线程之间的可见性和数据一致性是非常重要的。Java中提供了volatile关键字和原子操作机制&#xff0c;用于解决这些问题。本文将深入讨论volatile关键字和原子操作的用法&#xff0c;以及它们在多线程编程中的重要性和注意事项。 volatile关键字…

vue项目升级webpack

vue项目升级webpack 目录 1. vue项目中影响webpack版本的是什么 2.理解package.json中库前缀^和~区别 3.升级webpack4到5操作 1. vue项目中影响webpack版本的是什么 答案是&#xff1a;vue/cli-service版本 2.理解package.json中库前缀^和~区别 x.y.z x代表大版本&#xf…

ES修改字段的数据类型

-- mysql修改字段数据类型语句 ALTER TABLE 表名 MODIFY COLUMN 列名 修改的字段类型;-- hive 修改字段数据类型语句 ALTER TABLE 表名 CHANGE COLUMN 列名 修改的字段类型;--es修改字段数据类型语句无法通过一个语句进行修改。思路&#xff1a; 1、对修改字段重新建修改类型的…