函数指针、函数指针数组详解及典型应用_韦东山 函数指针数组-程序员宅基地

技术标签: c语言  嵌入式linux学习笔记  C语言学习  指针  嵌入式  

20200305 杨千嬅唱的《处处吻》真是太好听了,下个他他吻她他吻她吻他吻她… 已沉醉

一、何为函数指针

我们知道指针变量指向内存单元的地址,比如存放普通变量int a;的地址的就是一重指针,存放一重指针变量的地址的就是二重指针,指针变量存地址,以此来实现传址调用,
函数指针,顾名思义,就是指向函数的指针,那么何为指向函数呢?按照上面的逻辑,我们得有一个指针变量,这个指针变量里存放着该函数块在内存中的首地址。
要想理解这个,我们要从指针的层次上理解函数——函数的函数名实际上就是该函数的代码在内存中的首地址的别名,说白了还是个地址,这个有点类似于数组名和数组首元素地址的关系,随便找个反汇编代码就可印证上面所说的,下面的是汇编代码和反汇编调用main函数的例子
1.关于别名:586、587行
2.关于调用:我们可以看看汇编中是如何调用函数的,可以发现,PC指向了文字池中的内容30000848,这不正是mian函数的首地址吗,传址调用就好了
在这里插入图片描述

二、函数指针变量的类型及定义

类型表征着特征,而能够描述一个函数的特征的无非就是入口参数(形参)和返回值了

形式1:返回类型(*函数指针变量名)(参数表)

char*pFun)(int); //定义了一个名为pFun的函数指针变量,要求指向的函数的要有一个int类型的形参并要求该函数返回值为char类型
char glFun(int a)
{
    
	return a;
}
void main()
{
    
	pFun =glFun;
	
	/*函数指针的普通使用,以下两种方式个都可*/
	pFun (2);//与其指向的函数用法无异  
	(*pFun)(2);//此处*pf两端括号必不可少  
}

第1行:pFun是 char (*)(int) 类型的指针
第2行:定义了一个函数glFun().该函数正好是一个以int为参数返回char的函数。即char (int) 类型
第8行:指针变量pFun指向了glFun函数的首地址

形式2:使用typedef更直接

typedef char(*PTRFUN)(int)
PTRFUN pFun;
char glFun(int a)
{
    
	return a;
}
void main()
{
    
	pFun = glFun;
	(*pFun)(2);
}

typedef的功能是定义新的类型。第一句就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。

三、举例说明用途

简单的计算函数回调

#include <stdio.h>
#define SIZE 4
/*typedef的功能是定义新的类型。
下面这句就是定义了一种PFUNC的类型,并定义这种类型为指向某种函数的指针,这种函数以两个int为参数并返回int类型。*/
typedef int (*PFUNC)(int, int);
PFUNC fucp_arr[SIZE] = {
    NULL};
int add(int a, int b)
{
    
	return a + b;
}
int sub(int a, int b)
{
    
	return a - b;
}
int mul(int a, int b)
{
    
	return a * b;
}
int div(int a, int b)
{
    
	return b ? a / b : -1;
}

/*返回函数类型,实现函数回调*/
PFUNC calc_func(char op)
{
    
	switch (op)
	{
    
	case '+':
		return add;
	case '-':
		return sub;
	case '*':
		return mul;
	case '/':
		return div;
	default:
		return NULL;
	}
	return NULL;
}
	int main(void)
{
    

	/*无论是什么类型的指针,在32位系统下都占4字节*/
	printf("sizeof(PFUNC) = %d\n", sizeof(PFUNC));
	/*实现函数回调*/
	printf("%d + %d = %d\n", 13, 14, (calc_func('+'))(13, 14));
	printf("%d - %d = %d\n", 13, 14, (calc_func('-'))(13, 14));
	printf("%d * %d = %d\n", 13, 14, (calc_func('*'))(13, 14));
	printf("%d / %d = %d\n", 13, 14, (calc_func('/'))(13, 14));
	
	return 0;
}


编译运行后得到

root@youngfar-PC:/mnt/hgfs/Linux教程/mytest# gcc -o exe test.c -m32
root@youngfar-PC:/mnt/hgfs/Linux教程/mytest# ./exe
sizeof(PFUNC) = 4
13 + 14 = 27
13 - 14 = -1
13 * 14 = 182
13 / 14 = 0

四、利用函数指针数组进行函数的注册和调用

#include <stdio.h>
#define SIZE 4
/*typedef的功能是定义新的类型。
下面这句就是定义了一种PFUNC的类型,并定义这种类型为指向某种函数的指针,这种函数以两个int为参数并返回int类型。*/
typedef int (*PFUNC)(int, int);
PFUNC fucp_arr[SIZE] = {
    NULL};
int add(int a, int b)
{
    
	return a + b;
}
int sub(int a, int b)
{
    
	return a - b;
}
int mul(int a, int b)
{
    
	return a * b;
}
int div(int a, int b)
{
    
	return b ? a / b : -1;
}

/*返回函数类型,实现函数回调*/
PFUNC calc_func(char op)
{
    
	switch (op)
	{
    
	case '+':
		return add;
	case '-':
		return sub;
	case '*':
		return mul;
	case '/':
		return div;
	default:
		return NULL;
	}
	return NULL;
}

/* 
 不使用typedef,直接让函数返回一个函数指针,写起来比较麻烦,不直观,所以不推荐

 定义了一个函数,s_calc_func是函数名,
 s_calc_func这个函数需要char类型的形参,
 且最终返回一个函数指针,
 且要求这个函数指针指向的函数有两个int类型的形参和一个int类型的返回值
 */
int (*s_calc_func(char op))(int, int) /* 注意:这个函数的用途与上一个名为calc_func的函数的作业和调用方式完全相同*/
{
    
	return calc_func(op);
}

/*我们要从指针的层次上理解函数-函数的函数名实际上就是一个指针,函数名指向该函数的代码在内存中的首地址。*/
void Fp_Register(int position, PFUNC fp)
{
    
	fucp_arr[position] = fp;
}

int main(void)
{
    
/*无论是什么类型的指针,在32位系统下都占4字节*/
	printf("sizeof(PFUNC) = %d\n", sizeof(PFUNC));
	// /*实现函数回调*/
	// printf("%d + %d = %d\n", 13, 14, (calc_func('+'))(13, 14));
	// printf("%d - %d = %d\n", 13, 14, (calc_func('-'))(13, 14));
	// printf("%d * %d = %d\n", 13, 14, (calc_func('*'))(13, 14));
	// printf("%d / %d = %d\n", 13, 14, (calc_func('/'))(13, 14));

	/*函数注册*/
	Fp_Register(0, add);
	Fp_Register(1, sub);
	Fp_Register(2, mul);
	Fp_Register(3, div);

	/*函数调用*/
	printf("%d + %d = %d\n", 13, 14, fucp_arr[0](13, 14));
	printf("%d - %d = %d\n", 13, 14, fucp_arr[1](13, 14));
	printf("%d * %d = %d\n", 13, 14, fucp_arr[2](13, 14));
	printf("%d / %d = %d\n", 13, 14, fucp_arr[3](13, 14));

	return 0;
}

运行结果和上边的一样

函数指针数组应用实例

函数指针数组一般用于有相同操作流程但细节却不能使用一个函数统一描述的的函数的调用,比如在2440处理中断时的一些操作,下面贴出代码,举例说明,先来看2440中断的处理流程
在这里插入图片描述
The S3C2440A has two interrupt pending registers: source pending register (SRCPND) and interrupt pending register (INTPND). These pending registers indicate whether an interrupt request is pending or not. When the interrupt sources request interrupt the service, the corresponding bits of SRCPND register are set to 1, and at the same time, only one bit of the INTPND register is set to 1 automatically after arbitration procedure. If interrupts are masked, then the corresponding bits of the SRCPND register are set to 1. This does not cause the bit of INTPND register changed. When a pending bit of INTPND register is set, the interrupt service routine will start whenever the I-flag or F-flag is cleared to 0. The SRCPND and INTPND registers can be read and written, so the service routine must clear the pending condition by writing a 1 to the corresponding bit in the SRCPND register first and then clear the pending condition in the INTPND registers by using the same method.
S3C2440A有两个中断暂挂寄存器:源暂挂寄存器(SRCPND)和中断暂挂寄存器(INTPND)。这些挂起的寄存器指示中断请求是否挂起。当中断源请求中断服务时,SRCPND寄存器的相应位被设置为1,同时,经过仲裁程序,INTPND寄存器中只有1位被设置为1。如果中断被屏蔽,那么SRCPND寄存器的相应位被设置为1。这不会导致INTPND寄存器的位发生变化。当设置一个暂挂的INTPND寄存器位时,中断服务例程将在I-flag或F-flag被清除为0时启动。SRCPND和INTPND寄存器可以读写,因此服务例程必须先通过将1写入SRCPND寄存器中相应的位来清除挂起条件,然后使用相同的方法清除INTPND寄存器中的挂起条件。
在这里插入图片描述
在这里插入图片描述
下面是2440代码的按键中断处理函数

#include "s3c2440_soc.h"

/* SRCPND 用来显示哪个中断产生了, 需要清除对应位
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTMSK 用来屏蔽中断, 1-masked
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTOFFSET : 用来显示INTPND中哪一位被设置为1
 */

/* 初始化中断控制器 Initial State 为1, 0 = Service available*/
void interrupt_init(void)
{
    
	INTMSK &= ~((1 << 0) | (1 << 2) | (1 << 5)); /*按键中断源配置*/
	INTMSK &= ~(1 << 10);						 /* timer0 中断源配置*/
}

/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{
    
	/* 配置GPIO为中断引脚 */
	GPFCON &= ~((3 << 0) | (3 << 4));
	GPFCON |= ((2 << 0) | (2 << 4)); /* S2,S3被配置为中断引脚 */

	GPGCON &= ~((3 << 6) | (3 << 22));
	GPGCON |= ((2 << 6) | (2 << 22)); /* S4,S5被配置为中断引脚 */

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7 << 0) | (7 << 8); /* S2,S3 */
	EXTINT1 |= (7 << 12);			/* S4 */
	EXTINT2 |= (7 << 12);			/* S5 */

	/* 设置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1 << 11) | (1 << 19));
}

/* 读EINTPEND分辨率哪个EINT产生(eint4~23)
 * 清除中断时, 写EINTPEND的相应位
 */

void key_eint_irq(int irq)
{
    
	unsigned int val = EINTPEND;
	unsigned int val1 = GPFDAT;
	unsigned int val2 = GPGDAT;

	if (irq == 0) /* eint0 : s2 控制 D12 */
	{
    
		if (val1 & (1 << 0)) /* s2 --> gpf6 */
		{
    
			/* 松开 */
			GPFDAT |= (1 << 6);
		}
		else
		{
    
			/* 按下 */
			GPFDAT &= ~(1 << 6);
		}
	}
	else if (irq == 2) /* eint2 : s3 控制 D11 */
	{
    
		if (val1 & (1 << 2)) /* s3 --> gpf5 */
		{
    
			/* 松开 */
			GPFDAT |= (1 << 5);
		}
		else
		{
    
			/* 按下 */
			GPFDAT &= ~(1 << 5);
		}
	}
	else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
	{
    
		if (val & (1 << 11)) /* eint11 */
		{
    
			if (val2 & (1 << 3)) /* s4 --> gpf4 */
			{
    
				/* 松开 */
				GPFDAT |= (1 << 4);
			}
			else
			{
    
				/* 按下 */
				GPFDAT &= ~(1 << 4);
			}
		}
		else if (val & (1 << 19)) /* eint19 */
		{
    
			if (val2 & (1 << 11))
			{
    
				/* 松开 */
				/* 熄灭所有LED */
				GPFDAT |= ((1 << 4) | (1 << 5) | (1 << 6));
			}
			else
			{
    
				/* 按下: 点亮所有LED */
				GPFDAT &= ~((1 << 4) | (1 << 5) | (1 << 6));
			}
		}
	}

	EINTPEND = val; //写1清除中断标志
}

void handle_irq_c(void)
{
    
	/* 分辨中断源 */
	int bit = INTOFFSET;

	/* 调用对应的处理函数 */
	if (bit == 0 || bit == 2 || bit == 5) /* eint0,2,eint8_23 */
	{
    
		key_eint_irq(bit); /* 处理中断, 清中断源EINTPEND */
	}
	else if (bit == 10)
	{
    
		timer_irq();
	}

	/* 清中断 : 从源头开始清 */
	SRCPND = (1 << bit);
	INTPND = (1 << bit);
}

每次编写中断相关函数需要

汇编中:
1.中断发生前:

01.设置中断向量表
02.调用c中的mian函数

2.中断发生时:

跳转到中断向量表,去执行相应的处理(硬件自动)
01.设置 sp_irq
02.保存现场 :
在irq异常处理函数中有可能会修改r0-r12, 所以先保存
lr-4是异常处理完后的返回地址, 也要保存
05. 汇编中调用c中断处理函数处理中断
06. 恢复现场

C语言中 :
3.中断发生前

01.main函数由汇编调用,初始化各个中断控制器,配置中断源

4.中断发生时:

由汇编调用中断处理函数,该函数应包含:
01.中断产生时分辨中断源
02.调用对应的处理函数
03.清中断标志

函数指针数组主要用来解决上述 4 中断处理函数的流程调用,未使用函数指针数组时,每配置中断,都需要改变handle_irq_c来完成 4 中的01、02、03,改动interrupt_init来配置对应的MASK位,而使用之后我们就不需要改动了,由上述 2 中的05调用 后可自动配置,也是一种优化,下面是使用函数指针数组改进后的代码:

#include "s3c2440_soc.h"

typedef void(*irq_func)(int) ;
irq_func irq_array[32];


/* SRCPND 用来显示哪个中断产生了, 需要清除对应位
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTMSK 用来屏蔽中断, 1-masked
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTPND 用来显示当前优先级最高的、正在发生的中断, 需要清除对应位
 * bit0-eint0
 * bit2-eint2
 * bit5-eint8_23
 */

/* INTOFFSET : 用来显示INTPND中哪一位被设置为1
 */

/* 初始化中断控制器 */
// void interrupt_init(void)
// {
    
// 	INTMSK &= ~((1<<0) | (1<<2) | (1<<5));
// 	INTMSK &= ~(1<<10);  /* enable timer0 int */
// }

/* 读EINTPEND分辨率哪个EINT产生(eint4~23)
 * 清除中断时, 写EINTPEND的相应位
 */


void key_eint_irq(int irq)
{
    
	unsigned int val = EINTPEND;
	unsigned int val1 = GPFDAT;
	unsigned int val2 = GPGDAT;
	
	if (irq == 0) /* eint0 : s2 控制 D12 */
	{
    
		if (val1 & (1<<0)) /* s2 --> gpf6 */
		{
    
			/* 松开 */
			GPFDAT |= (1<<6);
		}
		else
		{
    
			/* 按下 */
			GPFDAT &= ~(1<<6);
		}
		
	}
	else if (irq == 2) /* eint2 : s3 控制 D11 */
	{
    
		if (val1 & (1<<2)) /* s3 --> gpf5 */
		{
    
			/* 松开 */
			GPFDAT |= (1<<5);
		}
		else
		{
    
			/* 按下 */
			GPFDAT &= ~(1<<5);
		}
		
	}
	else if (irq == 5) /* eint8_23, eint11--s4 控制 D10, eint19---s5 控制所有LED */
	{
    
		if (val & (1<<11)) /* eint11 */
		{
    
			if (val2 & (1<<3)) /* s4 --> gpf4 */
			{
    
				/* 松开 */
				GPFDAT |= (1<<4);
			}
			else
			{
    
				/* 按下 */
				GPFDAT &= ~(1<<4);
			}
		}
		else if (val & (1<<19)) /* eint19 */
		{
    
			if (val2 & (1<<11))
			{
    
				/* 松开 */
				/* 熄灭所有LED */
				GPFDAT |= ((1<<4) | (1<<5) | (1<<6));
			}
			else
			{
    
				/* 按下: 点亮所有LED */
				GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
			}
		}
	}

	EINTPEND = val;
}


void handle_irq_c(void)
{
    
	/* 分辨中断源 */
	int bit = INTOFFSET;

	/* 调用对应的处理函数 */
	irq_array[bit](bit);
	
	/* 清中断 : 从源头开始清 */
	SRCPND = (1<<bit);
	INTPND = (1<<bit);	
}

void register_irq(int irq, irq_func fp)
{
    
	irq_array[irq] = fp;

	INTMSK &= ~(1<<irq);
}


/* 初始化按键, 设为中断源 */
void key_eint_init(void)
{
    
	/* 配置GPIO为中断引脚 */
	GPFCON &= ~((3<<0) | (3<<4));
	GPFCON |= ((2<<0) | (2<<4));   /* S2,S3被配置为中断引脚 */

	GPGCON &= ~((3<<6) | (3<<22));
	GPGCON |= ((2<<6) | (2<<22));   /* S4,S5被配置为中断引脚 */
	

	/* 设置中断触发方式: 双边沿触发 */
	EXTINT0 |= (7<<0) | (7<<8);     /* S2,S3 */
	EXTINT1 |= (7<<12);             /* S4 */
	EXTINT2 |= (7<<12);             /* S5 */

	/* 设置EINTMASK使能eint11,19 */
	EINTMASK &= ~((1<<11) | (1<<19));

	register_irq(0, key_eint_irq);
	register_irq(2, key_eint_irq);
	register_irq(5, key_eint_irq);
}


只需在初始化时注册一下相应的处理函数
register_irq(0, key_eint_irq);
register_irq(2, key_eint_irq);
register_irq(5, key_eint_irq);
待到中断到来时即可自动完成 分辨中断源 、调用对应的处理函数 、清中断这一系列操作,时代码更加便于维护

注:本文参考了
typedef函数指针的用法(C++)

C/C++ 函数指针使用总结

和韦东山老师的讲解,转载请注明出处,本人初学Linux,如有错误,欢迎评论区批评指出,共同进步

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_28816873/article/details/104684373

智能推荐

报错: Could not build wheels for pillow, which is required to install pyproject.toml-based projects的解决_error: could not build wheels for pillow, which is-程序员宅基地

文章浏览阅读8.2k次,点赞6次,收藏4次。刚开始按报错的提示安装pillow==4.0.0版本的,就报上面的错误,上网上找解决方法,都未能解决,然后试着安装更高版本的pillow==10.0.0版本的,结果就成功了,我用的python版本是3.8的,估计是和python的版本与pillow版本里引用的库的兼容有关系,升级到相对应的版本就可以解决以上问题。以上供大家参考,讨论。_error: could not build wheels for pillow, which is required to install pypro

win10+uefi+gpt安装一键GHOST之后无法重启的快速解决方法(无需重装系统无需修复引导)_一键ghost不支持uefi+gpt-程序员宅基地

文章浏览阅读5.8w次,点赞2次,收藏7次。今天装完常用软件后,准备一键备份一下系统,就从一键GOST官网下载安装了一键GHOST,安装完成之后提示不支持UEFI+GPT格式系统,但仍可以使用,就没在意,也没卸载,接着就重启电脑试一试,结果就出现了无法进入系统引导的情况。 折腾一番BIOS之后,发现只需要按F7(我的笔记本主板选择启动方式或者说选择启动盘的快捷键是F7键,具体哪个按键根据自己电脑主板选择),选择windows_一键ghost不支持uefi+gpt

JVM之三大性能调优参数_jvm三大性能调优参数-程序员宅基地

文章浏览阅读339次。—Xss:规定了每个线程虚拟机栈的大小,会影响此进程中并发的线程数。—Xms:堆得初始值。—Xmx:堆能达到的最大值。一般Xms与Xmx的值设置的一样大小,当堆不够用进行扩容时会发生内存抖动,影响程序运行的稳定性。..._jvm三大性能调优参数

AndroidStudio_安卓原生开发_Android中调用摄像头拍照_并剪裁图片---Android原生开发工作笔记138_android studio如何调用摄像头-程序员宅基地

文章浏览阅读884次。写一个按钮,点击事件,然后:1.调用代码: openCamera(this);2.粘贴以下代码实现拍照 public void openCamera(Activity activity) { //獲取系統版本 int currentapiVersion = android.os.Build.VERSION.SDK_INT; // 激活相机 Intent intent = new Intent(MediaStore.ACT.._android studio如何调用摄像头

DragonBones龙骨发布后在Egret中的位置-程序员宅基地

文章浏览阅读321次。DragonBones发布后的动画,加载到Egret中场景中,原点的位置在哪呢?DragonBones中的图片位置导出加载到Egret中。可见DragonBones中的图片位置原点左下方(0,0)对应着Egret中的左上角(0,0)将DragonBones中的图片位置调整到左上导出发布到Egret中,圆球..._龙骨动画里如何更改中心点

poj3660 Cow Contest_poj 3660 并查集-程序员宅基地

文章浏览阅读1.6k次。Cow ContestTime Limit: 1000MSMemory Limit: 65536KTotal Submissions: 8986Accepted: 5045DescriptionN (1 ≤ N ≤ 100) cows, conveniently numbered 1..N, are partici_poj 3660 并查集

随便推点

MVP模式研究与实践_前端 mvppersistent模式-程序员宅基地

文章浏览阅读450次。虽然有那么多资料介绍MVP了,但是还是想把自己的实践经验分享一下。MVP简介相信大家对MVC都是比较熟悉了,:M-Model-模型、V-View-视图、C-Controller-控制器,MVP作为MVC的演化版本,那么类似的MVP所对应的意义:M-Model-模型、V-View-视图、P-Presenter-表示器。 从MVC和MVP两者结合来看,Controlller/Presenter..._前端 mvppersistent模式

VMware Vsphere 6.0安装部署 (三) vCenter Server安装-程序员宅基地

文章浏览阅读132次。安装准备环境需求:安装vcenter需要域环境,因此要先安装域控(有些功能比如horizon view需要用到域环境),学习环境可以用一台虚拟机做域控。建议将域控和vcenter服务器分别装在不同的服务器上,生产环境应该尽量在物理服务器上安装vcenter服务器,这里学习环境我们就在虚拟机上安装vcenter管理程序。安装vcenter需要最低2核CPU,8G内存本例中采用一..._使用命令行从vcenter 6.0或更高版本的服务器设备或外部平台服务控制器中收集支持

计算机原理 - 第五章 存储器_内存储器是按字长编址-程序员宅基地

文章浏览阅读1.9k次。5.1 存储器基本概念存储器的分类按存取方式分:随机、顺序、直接、相联按存储介质分:半导体、磁表面、激光盘按信息可更改性:可读可写、只读按断电后可否保存:易失、非易失按功能/容量/速度分:寄存器、Cache、主存(内存)、辅存(外存)解决内存访问速度慢的措施:1、提高主存芯片本身的速度2、在主存和CPU之间加入Cach..._内存储器是按字长编址

误删libstdc++.so.6文件导致Ubuntu系统无法正常启动_linux libstdc++.so.文件-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏29次。错误。看了网上的一些博客,并follow了解决方案,结果导致误删文件。误删该文件以后,因为很多功能不可用,所以又脑残的把系统重启了,然后,,,,,,,就进不了系统了。文件误删并重启后,在进入系统的过程中出现一个白色光标在屏幕左上角闪动,然后就一直闪动而无法进入系统,(在出现白色光标之前,可以打开BIOS)。_linux libstdc++.so.文件

【allegro等长走线】_等长线路径上有元器件-程序员宅基地

文章浏览阅读487次。allegro等长走线_等长线路径上有元器件

Typora官方Markdown教程翻译-程序员宅基地

文章浏览阅读410次。目录 Typora官方Markdown教程翻译 概述 块级元素 段落和换行符 标题 引文区块 列表 任务列表 (隔绝的)代码区块 数学区块 表格 脚注 ..._typora翻译

推荐文章

热门文章

相关标签