【C语言进阶】指针与数组、转移表详解

news2024/11/6 9:58:17

前言

大家好我是程序猿爱打拳,我们在学习完指针的基本概念后知道了指针就是地址,我们可以通过这个地址并对它进行解引用从而改变一些数据。但只学习指针的基础是完全不够的,因此学习完指针的基础后我们可以学习关于指针的进阶,其中包括指针数组、数组指针、函数指针等。这篇文章的末尾也有模拟实现计算器源码及讲解。

目录

1.字符指针

2.指针数组

3.数组指针

3.1&数组名和数组名

3.2数组指针的定义

3.3数组指针的使用

4.数组参数、指针参数

4.1一维数组传参

4.2二维数组传参

4.3一级指针传参

4.4二级指针传参

5.函数指针

6.函数指针数组

6.1函数指针数组定义

7.实现计算器

7.1使用switch实现

7.2使用转移表实现


1.字符指针

经过学习指针的基础后,我们知道了有一种指针类型为字符指针char*。一般这样写代码:

#include<stdio.h>

int main()
{
	char ch = 'a';
	char *p = &ch;
	*p = 'b';
	printf("%c\n", ch);
	return 0;
}

以上代码最终输出的值为b,对指针p进解引用并赋新值从而改变了ch的值我们不难理解。还有一种写代码方式:

#include<stdio.h>

int main()
{
	const char* p = "Hello World";
	printf("%s\n", p);
	return 0;
}

输出结果

以上代码,我们把Hello World的首字符地址赋值给了指针p并不是把整个Hello World赋值给了指针p,因此在输出的时候是从H开始依次往后面输出的。当然我们在指针初阶学过以""初始化一个字符串的时候,字符串末尾会默认生成'\0'(结束标识符)。


 在理解以上程序后,我们来看一组代码:

#include<stdio.h>

int main()
{
	char str1[] = "Hello World.";
	char str2[] = "Hello World.";
	const char* str3 = "Hello World.";
	const char* str4 = "Hello World.";
	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一模一样吗,为啥输出else后面的结果呢。其实是这样的。

str1和str2没有被const修饰的话是分别在内存中占不同的空间,str3和str4两个字符串都被const修饰了因此占用的空间是一致的。所以str1不等于str2,str3等于str4。


2.指针数组

在指针基础知识中我们学到了指针数组是存放指针的数组。如以下代码:

	int* arr1[3];//整型指针的数组
	char *arr2[4];//一级字符指针的数组
	int **arr3[5];//二级字符指针的数组

我们拿整型指针的数组来举例:

#include<stdio.h>

int main()
{
	int arr1[2] = { 1,2 };
	int arr2[2] = { 3,4 };
	int arr3[2] = { 5,6 };
	int* arr4[3] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 2; j++)
		{
			printf("%d", arr4[i][j]);
		}
	}
	return 0;
}

输出结果:

以上代码中int *arry4[3]就是一个存放整型指针的数组,它的每一个元素都存放的是一个地址,这些地址分别是arr1,arr2,arr3的数组名也就是第一个元素的地址。通过这些个地址就能依次访问到这个地址及这个地址以后的内容,如通过arr1的地址访问到了1和2。


3.数组指针

数组指针是什么呢,指针还是数组?其实它是指针。我们在指针初阶知道了整型指针可以这样定义:int * p;说了了p指向的是一个整型。浮点型指针可以这样定义:float * p;说明了p指向的是一个单精度浮点型。


3.1&数组名和数组名

我们在数组学习的时候已经知道了数组名就是数组的首元素地址。那么&数组名到底是什么呢?我们来看一组代码:

#include<stdio.h>

int main()
{
	int arr[2] = { 3,4 };
	printf("arr    = %p\n", arr);
	printf("&arr   = %p\n", &arr);
	printf("arr+1  = %p\n", arr+1);
	printf("&arr+1 = %p\n", &arr+1);
	return 0;
}

输出结果:

以上代码我们可以看到,数组名&数组名还是有很大差异的。前两个结果看不太出,后两个代码我们可以看出单个的数组名+1也就是arr(首元素)+1只是增长了4,而&数组名+1也就是&arr+1却增长了8。我们知道整型是占四个字节而arr数组里面刚好有两个整型数字,因此我们得到的结论是&数组名是&整个数组的地址。 


3.2数组指针的定义

我们可以这样写:int (*p)[10];解释:首先*先和p结合说明p是一个指针,其次int 和[10]结合。因此p指向的是一个有10个整型元素的数组。在3.1中我们知道了,单个的数组名只是数组的首元素地址,而&数组名得到是整个数组的地址,因此我们在初始化的时候应该这样:int (*p)[2]={&arr1,&arr2};以上为两个地址样式。

注意:[]号的优先级要高于*号,所以必须加上()来保证p先和*结合。


3.3数组指针的使用

上面我们说到了,数组指针代表着指针指向的是数组,那么数组指针中存放的就是数组的地址了。比如:

#include<stdio.h>

void print_arr1(int(*arr1)[4],int x,int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{
			printf("%d ", arr[i][j]);
		}
	}
}


void print_arr2(int arr2[3][4], int x, int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{
			printf("%d ", arr2[i][j]);
		}
	}
}

int main()
{
	int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
	print_arr1(arr, 3, 4);
	printf("\n");
	print_arr2(arr, 3, 4);
	return 0;
}

输出结果:

以上代码展示了数组指针的用法,在函数print_arry1(arr,3,4)中数组名arr表示首元素的地址。也就是二维数组的第一行地址。所以int (*arr1)[4]接受的arr其实是第一行的地址,可能有的朋友就有疑问了那为啥[]里面不是3而是4。因为二维数组的每一行有四个元素,因此[]里面是4。


4.数组参数、指针参数

我们在写代码的时候难免会把数组或者指针传给函数,那么函数里面的参数该如何设计呢?下面我们来看四种情况。


4.1一维数组传参

#include<stdio.h>

void test1(int arr[])//函数1
{}
void test1(int arr[10])//函数2
{}
void test1(int* arr)//函数3
{}
void test2(int* arr[20])//函数4
{}
void test2(int** arr)//函数5
{}
int main()
{
	int arr1[10] = { 0 };
	int arr2[20] = { 0 };
	test1(arr1);
	test2(arr2);
	return 0;
}

函数1,没问题,数组传参过去,函数未指定大小的数组接收,可行。

函数2,没问题,数组传参过去,函数指定了大小的数组接收,可行。

函数3,没问题,数组传参过去,函数中未指定大小的指针来接受,可行。

函数4,没问题,数组传参过去,函数中指定大小的指针来接收,可行。

函数5,没问题,数组传参过去,函数中未指定大小指针来接收,可行。


4.2二维数组传参

#include<stdio.h>

void tset(int arr[3][5])//函数1
{}
void test(int arr[][])//函数2
{}
void test(int arr[][5])//函数3
{}
void test(int* arr)//函数4
{}
void test(int(*arr)[5])//函数5
{}
void test(int** arr)//函数6
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
	return 0;
}

函数1,没问题,二维数组传参,函数中二维数组接收。

函数2,有问题,二维数组传参,函数中二维数组不能省略列数。因为对一个二维数组来说可以不知道有多少行,但不能不知道有多少列。这样才能方便计算。

函数3,没问题,二维数组传参,可以省略行数。

函数4,有问题,二维数组传参,函数中用一级指针来接收不可行。

函数5,没问题,二维数组传参,函数中用数组指针来接受,在外面3.3中有讲解到。

函数6,有问题,二维数组传参,函数中用二级指针来接收不不可行,因为二级指针接收的是一级指针,而二维数组传过去的参数是第一个元素也就是第一行的地址。


4.3一级指针传参

我们直接来看一组代码:

#include<stdio.h>

void print(int* p, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
	int* p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(p, sz);
	return 0;
}

以上代码,中print函数就是一级指针的接收。在main函数中我们把arr的地址给了指针p,因此print(p,sz)传参过去的就是arr的首元素地址arr数组元素的个数。因此print函数可以通过首元素地址依次访问到该数组结束。

注意:sizeof操作符和&符号对数组名进行操纵时,此时的数组名代表的是整个数组。


4.4二级指针传参

#include<stdio.h>

void test1(int** ptr1)
{
	printf("num= %d\n", **ptr1);
}

void test2(int** ptr2)
{
	printf("num= %d\n", **ptr2);
}

int main()
{
	int num = 10;
	int* p = &num;
	int** pp = &p;
	test1(pp);
	test2(&p);
	return 0;
}

输出结果:

以上代码,为二级指针的接收。二级指针的接收可以是一级指针的地址也可以是二级指针的地址。但无论是接收那个一种,解引用必须要解引用两次。

我们可以这样理解:p指针里面存放的是num,pp指针里面存放的是p。


5.函数指针

首先我们来看一组代码:

#include<stdio.h>

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

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

输出结果:

我们发现函数也是有地址的,因此我们可以把函数的地址存起来形成一个函数指针!

以上个代码为例:

#include<stdio.h>

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

int main()
{
	int (*p)(int, int) = Add;
	return 0;
}

以上代码为例,我们可以这样存放函数的地址,首先我们要用一个指针p来接受,指针p的类型跟函数的返回类型一致(Add返回类型为int,因此p的类型为int),其次指针p后面紧接着要说明函数的参数类型(Add参数为两个int,因此p后面要说明参数类型为int,int),最后把函数的地址赋值给指针p。

注意:

1.函数名等同于&函数名,如Add=&Add

2.函数指针中指针的类型根据函数的返回类型来定

3.函数指针后面要说明函数的参数类型


函数指针怎么用呢?还是根据以上代码进行修改:

#include<stdio.h>

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

int main()
{
	int (*p)(int, int) = Add;
	int sum = (*p)(4, 6);
	printf("sum=%d\n", sum);
	return 0;
}

输出结果:

 

我们已经知道了,函数指针如何去赋值。用法也并不难,只是把对应的数据放在函数指针后面的()里面即可实现功能。 


6.函数指针数组

函数指针数组的作用是:转移表,转移表是什么呢?

我们在写代码的时候,会遇到使用switch语句的情况。当我们使用switch来编写代码的时候,会发现得使用成千甚至上万条代码。但经过转移表的使用,代码的篇幅将会大大减少。


6.1函数指针数组定义

我们在前几节学到了指针数组的用法,如:char* arr[10]存放的是字符指针,此时arr数组的每个元素为char*int* arr[10]存放的是整型指针,此时arr数组的每个元素为int*

那我们可不可以把函数指针存放在数组里面呢?是可以的!所以函数指针数组是存放函数指针的数组。它的定义方法如下:

我们在定义函数指针数组的时候,需要要在函数指针的基础上加上一个[]。使得函数指针变为函数指针数组。[]里面为函数指针的个数。以下代码演示了如何在函数指针基础上改变方法。

#include<stdio.h>

int Add(int x, int y)
{}

int main()
{
	int (*p)(int, int) = Add;//这是一个函数指针

	int (*p[5])(int, int) = { Add };//这是一个函数指针数组
	return 0;
}

以上代码中,要注意的是:

1.函数指针数组定义时只是比函数指针多了一个[],[]的个数代表着函数指针的个数。

2.函数指针数组在赋值的时候只能是地址。

3.函数名等同于&函数名。


7.实现计算器

我们在认识道具函数指针数组的含义以及定义方式后,我们可以用转移表的方式来实现计算器。


7.1使用switch实现

#include<stdio.h>

void menu()
{
	printf("************************\n");
	printf("*    1.Plu  2.Sub      *\n");
	printf("*    3.Mul  4.Div      *\n");
	printf("*    0.Exit            *\n");
	printf("************************\n");
}

int Plu(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;
	int y = 0;
	int key = 0;
	do
	{
		menu();
		printf("请输入你的选项:>");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("你已退出程序!");
			break;
		case 1:
			printf("请输入两个整数:>");
			scanf("%d %d", &x, &y);
			key = Plu(x, y);
			printf("两数之和为:%d\n",key);
			break;
		case 2:
			printf("请输入两个整数:>");
			scanf("%d %d", &x, &y);
			key = Sub(x, y);
			printf("两数之差为:%d\n", key);
			break;
		case 3:
			printf("请输入两个整数:>");
			scanf("%d %d", &x, &y);
			key = Mul(x, y);
			printf("两数之积为:%d\n",key );
			break;
		case 4:
			printf("请输入两个整数:>");
			scanf("%d %d", &x, &y);
			key = Div(x, y);
			printf("两数之商为:%d\n",key);
			break;
		default:
			printf("请输入正确的选项!\n");
			break;
		}
	} while (input);

	return 0;
}

效果展示:

如果我们使用switch语句来实现这样一个简易的计算器我们会发现,每当我要添加一个功能的时候。都需要增加一个case语句,比如我要增加一个&运算,我得再加上一个case语句。因此我们可以使用函数指针数组(转移表)来实现,会简易很多。


7.2使用转移表实现

#include<stdio.h>

int Plu(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 = 1;
	int x = 0;
	int y = 0;
	int key = 0;
	int (*p[5])(int x, int y) = { 0,Plu,Sub,Mul,Div };
	while (input)
	{
		printf("************************\n");
		printf("****  1.Plu  2.Sub  ****\n");
		printf("****  3.Mul  4.Div  ****\n");
		printf("****  0.Exit        ****\n");
		printf("************************\n");
		printf("请输入你的选项:>");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("请输入两个整数:>");
			scanf("%d %d",&x,&y);
			key = (*p[input])(x, y);
			printf("得到的结果为:%d\n", key);
		}
		else
		{
			if (input != 0)
			{
				printf("请输入正确的选项!\n");
			}
			else
			{
				printf("您已退出程序!");
				break;
			}
		}
	}
	return 0;
}

效果显示:

以上代码如果我们想要增加程序的功能,只需要添加函数、增加菜单栏内容、if语句的判断条件即可。


以上就是本篇博客的内容,感谢你的阅读

 Never Give Up

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

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

相关文章

WireShark如何进行USB包协议分析

USB协议学习的步骤之一就是从抓包看协议通信,进而学习usb设备开发是怎么回事。这里发现一个工具就是wireshark。 WireShark如果要抓取usb设备的包,需要在安装的时候,选择usbpcap一并进行安装。

惠普星14Pro电脑开机不了显示错误代码界面怎么办?

惠普星14Pro电脑开机不了显示错误代码界面怎么办&#xff1f;有用户电脑开机之后&#xff0c;进入了一个错误界面&#xff0c;里面有一些错误代码。重启电脑之后依然是无法进入到桌面中&#xff0c;那么这个情况怎么去进行解决呢&#xff1f;我们可以重装一个新系统&#xff0c…

Android自定义View实现打钩签到动画

效果图实现原理我们看实现的动画效果&#xff0c;其实是分为1. 绘制未选中状态图形&#xff08;圆弧和对号&#xff09;2. 绘制选中状态圆弧的旋转的动画3. 绘制选中状态圆弧向中心收缩铺满动画4. 绘制选中状态对号5. 绘制选中状态下圆的放大回弹动画6. 暴露接口接口回调传递选…

FEBC2022|打造VR内容生态闭环 佳创视讯持续加码轻量化内容建设

2月24日&#xff0c;由陀螺科技主办的未来商业生态链接大会作为 2023 癸卯兔年开年率先召开的行业重要影响力盛会在深圳成功召开。今年大会云集了科技、软件、游戏、XR等元宇宙领域的世界500强、上市公司及行业独角兽企业&#xff0c;围绕游戏、元宇宙、XR、数字营销等多项热门…

ZigBee基本概念

本文主要介绍zigbee中profile、cluster、attribute、command的概念&#xff0c;以及zigbee的一些基本思想。zigbee联盟为了不同厂商的zigbee设备之间能够互联互通&#xff0c;于是制订了的zigbee协议标准&#xff0c;到今天&#xff08;2016.3.28&#xff09;已经到了版本3.0。…

【数据结构】知识点总结(C语言)

线性表、栈和队列、串、数组和广义表、树和二叉树、图、查找、排序线性表线性表&#xff08;顺序表示&#xff09;线性表是具有相同特性元素的一个有限序列&#xff0c;数据元素之间是线性关系&#xff0c;起始元素称为线性起点&#xff0c;终端元素称为线性终点。线性表的顺序…

python线程通信

在现实生活中&#xff0c;如果一个人团队正在共同完成任务&#xff0c;那么他们之间应该有通信&#xff0c;以便正确完成任务。 同样的比喻也适用于线程。 在编程中&#xff0c;要减少处理器的理想时间&#xff0c;我们创建了多个线程&#xff0c;并为每个线程分配不同的子任务…

线上监控诊断神器arthas

目录 什么是arthas 常用命令列表 1、dashboard仪表盘 2、heapdump dumpJAVA堆栈快照 3、jvm 4、thread 5、memory 官方文档 安装使用 1、云安装arthas 2、获取需要监控进程ID 3、运行arthas 4、进入仪表盘 5、其他命令使用查看官方文档 什么是arthas arthas是阿…

【自然语言处理】BERT GPT

BERT & GPT近年来&#xff0c;随着大规模预训练语言模型的发展&#xff0c;自然语言处理领域发生了巨大变革。BERT 和 GPT 是其中最流行且最有影响力的两种模型。在本篇博客中&#xff0c;我们将讨论 BERT 和 GPT 之间的区别以及它们的演变过程。 1.起源 201820182018 年&a…

2023软件测试现状,点工如何破局成为卷王····

近几年来的特殊情况&#xff0c;综合过去的大形势变化&#xff0c;所有行业都会自下而上的进行一轮技术“大清洗”&#xff0c;技术停滞不前的“点工”或将被逐步取代。 软件测试现状 测试行业在十几年间发生了翻天覆地的变化&#xff0c;从早期站在风口上的快速发展&#xf…

【mybatis】 01- mybatis快速入门

数据库创建(注意&#xff1a;最好先创建好数据库设置utf8再进行表创建) create database mybatis; use mybatis;drop table if exists tb_user;create table tb_user(id int primary key auto_increment,username varchar(20),password varchar(20),gender char(1),addr varch…

【数据结构】初识二叉树(二叉树的入门知识)

初识二叉树一、树概念及结构1、树的概念2、树的相关概念3、树的表示4、树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09;二、二叉树概念及结构1、概念2、特殊的二叉树3、二叉树的性质4、二叉树的存储结构三、结语一、树概念及结构 1、树的概念 树是一种非线…

【win10网络重置后,网络适配器消失或者不能使用】

注&#xff1a;此文章为重新整理的版本&#xff0c;旧版本&#xff1a;https://blog.csdn.net/Viwise/article/details/123263847?spm1001.2014.3001.5502 目录 一、问题描述 【总结】 【过程】 二、方法 1、针对设备问题代码为56的解决方法&#xff1a;安装CCleaner &am…

建模算法整理

优化 改进的蝙蝠算法 2022 E 2218144 使用背景&#xff1a;将每年砍伐树木比例定义为n&#xff0c;每年树木种植比例定义为m&#xff0c;设置一系列指标以及指标的加权计算方法&#xff08;即函数F(X)的设定&#xff09;&#xff0c;寻找最优的n,m。 蝙蝠算法主要用于目标函数…

算法leetcode|38. 外观数列(多语言实现)

文章目录38. 外观数列&#xff1a;样例 1&#xff1a;样例 2&#xff1a;提示&#xff1a;分析&#xff1a;题解&#xff1a;rustgocpythonjava38. 外观数列&#xff1a; 给定一个正整数 n &#xff0c;输出外观数列的第 n 项。 「外观数列」是一个整数序列&#xff0c;从数字…

【C++】map和set的封装(红黑树)

map和set的封装一、介绍二、stl源码剖析三、仿函数获取数值四、红黑树的迭代器五、map的[]5.1 普通迭代器转const迭代器六、set源码七、map源码八、红黑树源码一、介绍 首先要知道map和set的底层都是用红黑树实现的 【数据结构】红黑树 set只需要一个key&#xff0c;但是map既…

分布式-分布式消息笔记

消息队列应用场景 消息队列 消息队列是进程之间的一种很重要的通信机制。参与消息传递的双方称为生产者和消费者&#xff0c;生产者和消费者可以只有一个实例&#xff0c;也可以集群部署。 消息体是参与生产和消费两方传递的数据&#xff0c;消息格式既可以是简单的字符串&am…

MYSQL安装部署 - Linux 本地安装及卸载

声明 &#xff1a;# 此次我们安装的 MYSQL 版本是 8.0.32 版本我们本次安装 MYSQL 总共要介绍 四种方式# 仓库安装# 本地安装# 容器安装# 源码安装我们本篇介绍的是 本地安装 我们还是去官网下载 &#xff1a;我们就是找着 .bundle.tar 这个包&#xff0c;里面就包含了所有 mys…

功率信号源有什么作用和功能呢

功率信号源是指集信号发生器与功率放大器为一体的电子测量仪器&#xff0c;它具有高电压、大功率的特点&#xff0c;在电子实验室中能够帮助用来驱动压电陶瓷、换能器以及电磁线圈等&#xff0c;可以有效的帮助电子工程师解决驱动负载和放大功率的问题。功率信号源和功率放大器…

过滤器,监听器,拦截器的原理与在Servlet和Spring的应用

在Java Web的开发中&#xff0c;最原始和初期的学习都是从Servlet开始的&#xff0c;Servlet是Java最为耀眼的技术&#xff0c;也是Java EE的技术变革。目前大火主流的框架spring boot也的spring mvc部分也是基于拓展servlet完成的。回到之前的文章spring 实现了对servlet的封装…