【C语言进阶】指针与数组、转移表详解
前言
大家好我是程序猿爱打拳,我们在学习完指针的基本概念后知道了指针就是地址,我们可以通过这个地址并对它进行解引用从而改变一些数据。但只学习指针的基础是完全不够的,因此学习完指针的基础后我们可以学习关于指针的进阶,其中包括指针数组、数组指针、函数指针等。这篇文章的末尾也有模拟实现计算器源码及讲解。
目录
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");elseprintf("str1 and str2 are not same\n");if (str3 == str4)printf("str3 and str4 are same\n");elseprintf("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 = #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
相关文章:

【C语言进阶】指针与数组、转移表详解
前言 大家好我是程序猿爱打拳,我们在学习完指针的基本概念后知道了指针就是地址,我们可以通过这个地址并对它进行解引用从而改变一些数据。但只学习指针的基础是完全不够的,因此学习完指针的基础后我们可以学习关于指针的进阶,其中…...
SDN是什么,和SD-WAN有什么关系
SDN全称为“软件定义网络”(Software-Defined Networking),是一种新型的网络架构,通过将网络的控制面和数据面分离,将网络控制集中到控制器中进行统一管理和配置,以提高网络的灵活性和可管理性。传统网络的…...
百度前端高频react面试题(持续更新中)
说说你用react有什么坑点? 1. JSX做表达式判断时候,需要强转为boolean类型 如果不使用 !!b 进行强转数据类型,会在页面里面输出 0。 render() {const b 0;return <div>{!!b && <div>这是一段文本</div>}</div…...

中级嵌入式系统设计师2016下半年下午应用设计试题
中级嵌入式系统设计师2016下半年下午试题 试题一 阅读以下说明,回答问题1至问题3。 【说明】 某综合化智能空气净化器设计以微处理器为核心,包含各种传感器和控制器,具有检测环境空气参数(包含温湿度、可燃气体、细颗粒物等),空气净化、加湿、除湿、加热和杀菌等功能…...
【雅思备考】九分学长写作课笔记
原视频:https://www.bilibili.com/video/BV1FG4y1J7br?p13&vd_source552ac2291179cf9d44088ea168db5531 一、综述 共计1小时 小作文: 描述 图表图(数据图)、流程图(示意图)、地图(示意…...

【源码解析】SpringBoot自动装配的实现原理
什么是SpringBoot的自动装配 SpringBoot在启动的时候会扫描外部jar包中的META-INF/spring.factories文件,将文件中配置的类信息按照条件装配到Spring容器中。 实现原理 核心注解SpringBootApplication Target({ElementType.TYPE}) Retention(RetentionPolicy.R…...
详解ROS时间戳
ROS(Robot Operating System)是一个用于机器人开发的开源软件框架,其中涉及到了一些与时间相关的概念和工具,如时间戳、计时器等。本文将主要介绍ROS中时间戳的概念和应用,并提供一个Python代码案例演示如何处理ROS时间…...
Android Window、WindowManager
1.窗口Window 在Android中显示一个界面,首先想到的是Activity、Dialog或Toast。但是在有些情况下,比如悬浮球,用Activity会显然多余,这个时候可以直接使用窗口来实现。 Android中所有的视图都是通过Window来呈现的,不管是Activity、Dialog还是Toast,它们的视图实际上都…...
【一天一门编程语言】怎样设计一门编程语言?
怎样设计一门编程语言? 确定目标 确定语言的用途: 是一门通用编程语言,还是一门专门面向某个特定目标的语言?是一门面向对象的语言,还是一门过程化的语言?将语言的最终用户定义为谁? 确定语言…...

微服务保护 -- 初识 Sentinel(雪崩问题,快速入门Sentinel)
大家好,今天我们要来学习阿里巴巴开源的流量控制和熔断降级框架 – Sentinel 。 1、雪崩问题及解决方案 首选我们来了解一下雪崩问题及其解决方案,我们学习这个微服务保护,其实就是为了去应对类似于雪崩问题这样的服务故障。 1.1 什么是雪…...

软件测试面试问答
笔试 笔试的话我们需要揣测具体会考什么内容,我们可以通过招聘信息去了解该公司需要什么样的技能,以此来准备笔试。一般必考的内容会有编程,测试用例设计,工作流程,逻辑思维等内容,除此之外每个公司可能还会…...
【架构】架构师的核心能力-抽象能力
文章目录一、通过归纳法找共性二、通过演绎法找关系三、通过归纳法找特性四、最后架构的核心是管理复杂度,架构师的核心能力是抽象能力,什么是抽象能力?抽象能力就是一种化繁为简的能力。何为化繁为简?就是把一种复杂的事情变得简…...
前端一面常见react面试题(持续更新中)
React 组件中怎么做事件代理?它的原理是什么? React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器…...

亥姆霍兹线圈测量系统
亥姆霍兹线圈[Helmholtz线圈]是指由具有相同线圈匝数、相同线圈绕制方式且线圈半径等于线圈间距的一对或者多对线圈构成的线圈组合。 根据线圈的形状,亥姆霍兹线圈可分为圆形亥姆霍兹线圈和方形亥姆霍兹线圈;根据磁场方向,亥姆霍兹线圈可分为…...
JavaScript 类型转换
Number() 转换为数字, String() 转换为字符串, Boolean() 转化为布尔值。JavaScript 数据类型在 JavaScript 中有 5 种不同的数据类型:stringnumberbooleanobjectfunction3 种对象类型:ObjectDateArray2 个不包含任何值的数据类型…...

Spring Batch 综合案例实战-项目准备
目录 案例需求 分析 项目准备 步骤1:新开spring-batch-example 步骤2:导入依赖 步骤3:配置文件 步骤4:建立employee表与employe_temp表 步骤5:建立基本代码体系-domain-mapper-service-controller-mapper.xml …...

STM32CubeMX串口USART中断发送接收数据
本文代码使用 HAL 库。 文章目录前言一、中断控制二、USART中断使用1. 中断优先级设置 :2. 使能中断3. 使能UART的发送、接收中断4. 中断收发函数5. 中断处理函数6. 中断收发回调函数三、串口中断实验串口中断发送数据点亮 led:实验现象:总结…...
JavaScript Web Workers使用流程
背景 Web Workers是一个API,允许在浏览器中运行后台处理任务,而不影响用户界面(UI)线程的稳定性。 Web Workers 可用于消除阻止 UI 的耗时任务,如图表生成,物理模拟或数据分析等: 使用 Web W…...

数据结构与算法(五):优先队列
这节总结一下优先队列的常用实现方法。 一、基本概念 普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级…...

二叉树的前序遍历-java两种方式-力扣144
一、题目描述给你二叉树的根节点 root ,返回它节点值的 前序 遍历。示例 1:输入:root [1,null,2,3]输出:[1,2,3]示例 2:输入:root []输出:[]示例 3:输入:root [1]输出…...

【kafka】Golang实现分布式Masscan任务调度系统
要求: 输出两个程序,一个命令行程序(命令行参数用flag)和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽,然后将消息推送到kafka里面。 服务端程序: 从kafka消费者接收…...

自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...

DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...