很多C语言初学者都非常好奇的问题,怎样定义可以可变参数函数?
liebian365 2024-10-20 09:57 30 浏览 0 评论
如果一定要说哪段C语言代码最“著名”,我想非“hello world”莫属了。大多数初学者人生中编写的第一段C语言代码就是这段“里程碑”式的代码:
#include <stdio.h> int main() { printf("hello world\n"); return 0; }
也正因为这段著名的程序,printf() 函数成为大多数C语言初学者接触到的第一个标准库函数。
C语言中的可变参数函数
随着学习的推进,初学者逐步学会调用别的C语言函数,以及定义自己的函数,观察力敏锐的会注意到 printf() 函数似乎与其他函数不太一样——printf()函数没有固定数目的参数,它似乎可以接收任意多的参数。
而其他C语言函数则不同,它们大都有固定数量的参数(0个,3个等),调用这些函数必须传递对应数目的参数。
有些持有“特殊论”的初学者认为像 printf() 这样的“可变参数”函数是“特殊的”,是系统定义的,我们程序员只能定义固定参数的函数,其实不是的,C语言是有手段定义自己“可变参数”函数的。
printf() 究竟是不是只能由系统定义的“特殊”函数呢?
怎样定义自己的可变参数函数?
事实上,标准库 <stdarg.h>就是方便C语言程序员定义自己的“可变参数”函数的。如果读者和我一样使用的是 Linux 系统,则可以方便的通过 man 命令查询到相关库函数:
头文件<stdarg.h>声明了 va_list 类型用于描述可变参数,并且定义了上述 4 个方法解析。这里不打算介绍过多枯燥的理论知识,我们直接看实例,请看相关C语言代码:
上述代码定义了可变参数函数 foo(),它可以接收类似于 printf() 的函数,并且将 fmt 中的 s 解析为字符串,d 解析为整数,c 解析为字符,因此编译并执行这段C语言代码,可得到如下输出:
# gcc t.c # ./a.out string hello int 12 char m
通过这段实例,可以看出使用C语言定义可变参数函数并不复杂,在处理可变参数时,只需先调用 va_start() 将参数序列加载到 va_list 结构的变量中,然后调用 va_arg() 依次解析。解析完毕后,再调用 va_end() 结束解析。
va_start -> va_arg -> va_end。
唯一需要注意的是使用 va_arg() 解析参数时,需要指定类型。但是这个过程也很简单,可变参数函数的实现者可以指定一套规则,用于约束函数调用者传递参数,这样就知道接下来需要解析的参数是何种类型。例如上面的C语言代码就约定了 fmt 中的 s 表示接下来的要解析的参数是字符串,d 表示整数等。
计算机是如何处理可变参数函数的?
C语言定义可变参数函数的过程并不复杂,借助于<stdarg.h>,我们能够轻易的定义接收任意多参数的函数,不过到这里,有读者发现问题了:我们人类可以按照规则写出可变参数函数,但是计算机是如何理解这一套规则的呢?或者换句话说,计算机是如何处理“可变参数”的?
以Linux为例,看过我之前文章的读者应该明白,每个C语言程序进程都有属于自己的栈,进程中的每个函数则有属于自己的栈帧,当有函数调用时,例如:
foo("%d%d%d", 3,2,1);
C语言编译器会产生类似于下面这样的汇编代码:
push 1 push 2 push 3 push "%d%d%d" call foo
也即将 foo() 函数的参数先压入栈中,然后再调用 foo() 函数。鉴于栈这种数据结构“先进后出”的特点,一般函数参数的入栈顺序是从右至左的。
[] // 空栈 ------------------------------- push 1: [1] ------------------------------- push 2: [1] [2] ------------------------------- push 3: [1] [2] [3] // 参数 1 2 3 被压入栈中 ------------------------------- push "%d%d%d":[1] [2] [3] ["%d%d%d"] ------------------------------- call foo ... // foo 函数开始使用参数
按照这样的参数入栈顺序,foo() 函数使用参数很方便,依次从栈中将参数取出就可以了。至于如何解析栈中的参数,则可以根据可变参数实现者指定的规则,例如在格式化字符串 fmt 中遇到 s 就解析为字符串等。
如果可变参数 foo() 接收到其他数目的参数,对于最终程序来说,也仅仅只需要修改压栈的参数数目,其他并无太多不同。
小结
本文主要讨论了C语言中可变参数函数的定义方法,以及计算机如何处理可变参数函数的过程,其实并不复杂。C语言不像C++那样支持函数重载,但是借助于可变参数函数和宏,我们可以像定义“伪类”那样,定义自己的“伪函数重载”,这是一种编程技巧,以后有机会再讨论了。
欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。
未经许可,禁止转载。
相关推荐
- 月薪 4K 到 4W 的运维工程师都经历了什么?
-
运维工程师在前期是一个很苦逼的工作,在这期间可能干着修电脑、掐网线、搬机器的活,显得没地位!时间也很碎片化,各种零碎的琐事围绕着你,很难体现个人价值,渐渐的对行业很迷茫,觉得没什么发展前途。这些枯燥无...
- 计算机专业必须掌握的脚本开发语言—shell
-
提起Shell脚本很多都有了解,因为无论是windows的Dom命令行还是Linux的bash都是它的表现形式,但是很多人不知道它还有一门脚本编程语言,就是ShellScript,我们提起的Shel...
- Linux/Shell:排名第四的计算机关键技能
-
除了编程语言之外,要想找一份计算机相关的工作,还需要很多其他方面的技能。最近,来自美国求职公司Indeed的一份报告显示:在全美工作技能需求中,Linux/Shell技能仅次于SQL、Java、P...
- 使用Flask应用框架在Centos7.8系统上部署机器学习模型
-
安装centos7.8虚拟环境1、镜像链接...
- shell编程
-
简介:Shell是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。...
- 14天shell脚本入门学习-第二天#脚本和参数#排版修正
-
脚本是一种包含一系列命令的文本文件,通常用于自动化任务。Shell脚本是用Shell命令编写的脚本,可以在命令行中执行。掌握脚本的基础知识和变量的使用是编写高效脚本的关键。...
- 嵌入式Linux开发教程:Linux Shell
-
本章重点介绍Linux的常用操作和命令。在介绍命令之前,先对Linux的Shell进行了简单介绍,然后按照大多数用户的使用习惯,对各种操作和相关命令进行了分类介绍。对相关命令的介绍都力求通俗易懂,都给...
- 实现SHELL中的列表和字典效果
-
大家好,我是博哥爱运维。编写代码,很多情况下我们需要有种类型来存储数据,在python中有列表和字典,golang中有切片slice和map,那么在shell中,我们能否实现列表和字典呢,答案是肯定的...
- 14天shell脚本入门学习-第二天#脚本和变量
-
脚本是一种包含一系列命令的文本文件,通常用于自动化任务。Shell脚本是用Shell命令编写的脚本,可以在命令行中执行。掌握脚本的基础知识和变量的使用是编写高效脚本的关键。...
- shell常用命令之awk用法介绍
-
一、awk介绍awk的强大之处,在于能生成强大的格式化报告。数据可以来自标准输入,一个或多个文件,或者其他命令的输出。他支持用户自定义函数和动态正则表达式等先进功能,是Linux/unix一个强大的文...
- Linux编程Shell之入门——Shell数组拼接与合并
-
在Shell中,可以使用不同的方式实现数组拼接和合并。数组拼接指将两个数组中的元素合并成一个数组,而数组合并指将两个数组逐个组合成一个新数组。以下是关于Shell数组拼接和合并的详细介绍:数...
- shell中如何逆序打印数组的内容,或者反转一个数组?
-
章节索引图首先请注意,有序的概念仅适用于索引数组,而不适用于关联数组。如果没有稀疏数组,答案会更简单,但是Bash的数组可以是稀疏的(非连续索引)。因此,我们需要引入一个额外的步骤。...
- 如何学好大数据开发?---shell基本语法
-
昨天我们初步了解到了shell的一些基本知识,比如shell的分类,常用的shell类型。今天就带来大数据开发之shell基本语法,掌握好基础才是最重要的,那接下来就开始学习shell的基本语法。一、...
- Linux编程Shell之入门——Shell关联数组
-
关联数组是Shell中一种特殊的数组类型,它使用字符串作为下标。在关联数组中,每个元素都被标识为一个唯一的字符串键值,也称为关联数组的索引。在Shell中,可以使用declare或typeset命令...
- 从编译器视角看数组和指针
-
虽然有单独的文章描述数组和指针,但二者的关系实在值得再写一篇文章。...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)