博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C语言可变參函数的实现
阅读量:6935 次
发布时间:2019-06-27

本文共 4788 字,大约阅读时间需要 15 分钟。

1 C语言中函数调用的原理

函数是大多数编程语言都实现的编程要素。调用函数的实现原理就是:运行跳转+參数传递。对于运行跳转,全部的CPU都直接提供跳转指令;对于參数传递,CPU会提供多种方式。最常见的方式就是利用栈来传递參数。

C语言标准实现了函数调用。可是却没有限定实现细节。不同的C编译器厂商能够依据底层硬件环境自行确定实现方式。

函数调用的一般实现原理。请參考我的博文中的第一段。

2 可变參实现思路

2.1 怎样取得兴许实參地址

我们以X86架构上的VC++编译器为例进行举例说明。

样例代码例如以下。

void f(int x, int y, int z){    printf("%p, %p, %p\n", &x, &y, &z);}int main(){    f(100, 200, 300);    return 0;}

可能的运行结果:

00FFF674, 00FFF678, 00FFF67C

VC++中函数的參数是通过堆栈传递的,參数依照从右向左的顺序入栈。调用f时參数在堆栈中的情况例如以下图所看到的:

这里写图片描写叙述

可见,我们仅仅要知道x的地址,就能够推算出y,z的地址。从而通过其地址取得參数y,z的值,而不用其參数名称取值。例如以下代码所看到的。

void f(int x, int y, int z){    char* px = (char*)&x;    char *py = px + sizeof(x);    char *pz = py + sizeof(int);    printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);}int main(){    f(100, 200, 300);    return 0;}

可见依据函数的第一个參数。以及兴许參数的类型。就能够依据偏移量计算出兴许參数的地址。从而取得兴许參数值。

于是能够把上述代码改写成可变參数的形式。

void f(int x, ...){    char* px = (char*)&x;    char *py = px + sizeof(x);    char *pz = py + sizeof(int);    printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);}int main(){    f(100, 200, 300);    return 0;}

2.2 怎样标识兴许參数个数和类型

尽管写成了可变參形式。可是函数怎样推断兴许实參的个数和类型呢?这就须要在固定參数中携带这些信息,如printf(char*, …)使用的格式化字符串方法。通过第一个參数来携带兴许參数个数以及类型的信息。我们实现一个简单点的,仅仅能识别%s,%d,%f三种标志。

void f(char* fmt, ...){    char* p0 = (char*)&fmt;    char* ap = p0 + sizeof(fmt);    char* p = fmt;    while (*p) {        if (*p == '%' && *(p+1) == 'd') {            printf("參数类型为int,值为 %d\n", *((int*)ap));            ap += sizeof(int);        }        else if (*p == '%' && *(p+1) == 'f') {            printf("參数类型为double,值为 %f\n", *((double*)ap));            ap += sizeof(double);        }        else if (*p == '%' && *(p+1) == 's') {            printf("參数类型为char*,值为 %s\n", *((char**)ap));            ap += sizeof(char*);        }        p++;    }}int main(){    f("%d,%f,%s", 100, 1.23, "hello world");    return 0;}

输出:

參数类型为int,值为 100參数类型为double,值为 1.230000參数类型为char*,值为 hello world

为简化分析參数代码,定义一些宏来简化,例如以下。

#define va_list char*   /* 可变參数地址 */#define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指针指向第一个可变參数 */#define va_arg(ap, t)   (ap+=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得參数值,同一时候移动指针指向兴许參数 */#define va_end(ap)  ap=0 /* 结束參数处理 */void f(char* fmt, ...){    va_list ap;    va_start(ap, fmt);    char* p = fmt;    while (*p) {        if (*p == '%' && *(p+1) == 'd') {            printf("參数类型为int,值为 %d\n", va_arg(ap, int));        }        else if (*p == '%' && *(p+1) == 'f') {            printf("參数类型为double,值为 %f\n", va_arg(ap, double));        }        else if (*p == '%' && *(p+1) == 's') {            printf("參数类型为char*,值为 %s\n", va_arg(ap, char*));        }        p++;    }    va_end(ap);}int main(){    f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);    return 0;}

3 正确的变參函数实现方法

上面的样例中,我们没有使用不论什么库函数就轻松实现了可变參数函数。

别高兴太早,上述代码在X86平台的VC++编译器下能够顺利编译、正确运行。可是在gcc编译后。运行却是错误的。

可见GCC对于可变參数的实參传递实现与VC++并不同样。

gcc下编译运行:[smstong@cf-19 ~]$ ./a.out參数类型为int,值为 0參数类型为double,值为 0.000000Segmentation fault

可见,上述代码是不可移植的。为了在使得可变參函数能够跨平台、跨编译器正确运行,必须使用C标准头文件stdarg.h中定义的宏,而不是我们自定义的。

(这些宏的名字和作用与我们自定义的宏全然同样,这绝不是巧合!)每一个不同的C编译器所附带的stdarg.h文件里对这些宏的定义都不同样。

再次重申一下这几个宏的使用范式:

va_list ap;va_start(ap, 固定參数名); /* 依据最后一个固定參数初始化 */可变參数1类型 x1 = va_arg(ap, 可变參数类型1); /* 依据參数类型,取得第一个可变參数值 */可变參数2类型 x2 = va_arg(ap, 可变參数类型2); /* 依据參数类型。取得第二个可变參数值 */...va_end(ap);     /* 结束 */

这次。把我们自己的宏定义去掉,换成#include

#include 
#include
void f(char* fmt, ...){ va_list ap; va_start(ap, fmt); char* p = fmt; while (*p) { if (*p == '%' && *(p+1) == 'd') { printf("參数类型为int,值为 %d\n", va_arg(ap, int)); } else if (*p == '%' && *(p+1) == 'f') { printf("參数类型为double,值为 %f\n", va_arg(ap, double)); } else if (*p == '%' && *(p+1) == 's') { printf("參数类型为char*,值为 %s\n", va_arg(ap, char*)); } p++; } va_end(ap);}int main(){ f("%d,%f,%s,%d", 100, 1.23, "hello world", 200); return 0;}

代码在VC++和GCC下均能够正确运行了。

4 几个须要注意的问题

4.1 va_end(ap); 必须不能省略

或许在有些编译器环境中,va_end(ap);确实没有什么作用,可是在其它编译器中却可能涉及到内存的回收,切不可省略。

4.2 可变參数的默认类型提升

《C语言程序设计》中提到:

在没有函数原型的情况下。char与short类型都将被转换为int类型,float类型将被转换为double类型。实际上。用...标识的可变參数总是会运行这样的类型提升。

引用《C陷阱与缺陷》里的话:

**va_arg宏的第2个參数不能被指定为char、short或者float类型**。由于char和short类型的參数会被转换为int类型,而float类型的參数会被转换为double类型 ……比如,这样写肯定是不正确的:c = va_arg(ap,char);由于我们无法传递一个char类型參数,假设传递了,它将会被自己主动转化为int类型。上面的式子应该写成:c = va_arg(ap,int);

4.3 编译器无法进行參数类型检查

对于可变參数。编译器无法进行不论什么检查。仅仅能靠调用者的自觉来保证正确。

4.4 可变參数函数必须提供一个或很多其它的固定參数

可变參数必须靠固定參数来定位,所以函数中至少须要提供固定參数,f(固定參数,…)。

当然,也能够提供很多其它的固定參数,如f(固定參数1,固定參数2。…)。

注意的是,当提供2个或以上固定參数时。va_start(ap, x)宏中的x必须是最后一个固定參数的名字(也就是紧邻可变參数的那个固定參数)。

5 C的可变參函数与C++的重载函数

C++的函数重载特性,同意反复使用同样的名称来定义函数,仅仅要同名函数的參数(类型或数量)不同。比如,

void f(int x);void f(int x, double d);void f(char* s);

尽管源码中函数名字同样,事实上编译器处理后生成的是三个具有不同函数名的函数(名字改编name mangling)。

尽管在使用上有些相似之处。但这显然与C的可变參数函数全然不是一个概念。

你可能感兴趣的文章
【转】PHP date("Y-m-d H:i:s");获取当前时间 差8小时解决办法
查看>>
System.Security.Cryptography.CryptographicException,密钥集不存在
查看>>
敏捷团队中的QA由来
查看>>
gdb调试报错:Missing separate debuginfos, use: debuginfo-install glibc-XXX
查看>>
根据百度API获得经纬度,然后根据经纬度在获得城市信息
查看>>
mariadb 10.1查看per connection内存消耗
查看>>
重装MAC系统 “安装器有效负载签名检查失败” 解决方法
查看>>
(转) Supercharging Style Transfer
查看>>
JMeter性能测试,验证请求数据的准确性(wc命令)
查看>>
Python学习札记(二十三) 函数式编程4 sorted
查看>>
跟着百度学PHP[14]-PDO-优化驱动
查看>>
mysql的.sql文件头部 /*!32312 IF NOT EXISTS*/;
查看>>
ONVIF测试方法及工具
查看>>
《ArcGIS Runtime SDK for Android开发笔记》——数据制作篇:发布具有同步能力的FeatureService服务...
查看>>
Oracle快速克隆安装
查看>>
Spring Boot中使用JdbcTemplate访问数据库
查看>>
struts2的核心和工作原理
查看>>
一种快速统计SQL Server每个表行数的方法
查看>>
(zhuan) How to Train Neural Networks With Backpropagation
查看>>
MHA快速搭建
查看>>