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

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

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

函数调用的一般实现原理。请參考我的博文C语言中利用setjmp和longjmp做异常处理中的第一段。

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.000000
Segmentation 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 <stdio.h>
#include <stdarg.h>
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的可变參数函数全然不是一个概念。

C语言可变參函数的实现的更多相关文章

  1. C语言可变參实现參数累加返回

    C语言可变參的作用真的是很大,自从发表了可变參怎样实现printf.fprintf,sprintf的文章以来.便有不少博友私信问我实现的机制,我也解释了相关的知识点.今天,我们借着这个机会,再来举一个 ...

  2. C语言可变参数函数实现原理

    一.可变参数函数实现原理 C函数调用的栈结构: 可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈. 本 ...

  3. C语言可变参数函数的编写

    1. 引言 C语言我们接触的第一个库函数是 printf(“hello,world!”);其参数个数为1个. 然后,我们会接触到诸如: printf(“a=%d,b=%s,c=%c”,a,b,c);此 ...

  4. c语言可变参数函数

    c语言支持可变参数函数.这里的可变指,函数的参数个数可变. 其原理是,一般情况下,函数参数传递时,其压栈顺序是从右向左,栈在虚拟内存中的增长方向是从上往下.所以,对于一个函数调用 func(int a ...

  5. C语言可变参数函数详解示例

    先看代码 printf(“hello,world!”);其参数个数为1个. printf(“a=%d,b=%s,c=%c”,a,b,c);其参数个数为4个. 如何编写可变参数函数呢?我们首先来看看pr ...

  6. C语言可变长參数实现原理

    微博:http://weibo.com/u/2203007022             (1)      C语言可变參数 我们能够从C语言的printf得出可变參数的作用.printf函数的原型例如 ...

  7. c 语言函数可变參数的处理

    /************************************************************************* > File Name: va_list.c ...

  8. C语言利用va_list、va_start、va_end、va_arg宏定义可变參数的函数

    在定义可变參数的函数之前,先来理解一下函数參数的传递原理: 1.函数參数是以栈这样的数据结构来存取的,在函数參数列表中,从右至左依次入栈. 2.參数的内存存放格式:參数的内存地址存放在内存的堆栈段中, ...

  9. C中參数个数可变的函数

    一.什么是可变參数 我们在C语言编程中有时会遇到一些參数个数可变的函数,比如printf()函数,其函数原型为: int printf( const char* format, ...); 它除了有一 ...

随机推荐

  1. java中表示二进制、八进制、十进制、十六进制

    1.进制 进制是一种记数方式 ,可以用有限的数字符号代表所有的数值.由特定的数值组成. 2.进制的表现形式 二进制: 由0和1两个数字组成. 八进制: 由0-7数字组成,为了区分与其他进制的数字区别, ...

  2. Python爬虫入门:Urllib库的基本使用

    1.分分钟扒一个网页下来 怎样扒网页呢?其实就是根据URL来获取它的网页信息,虽然我们在浏览器中看到的是一幅幅优美的画面,但是其实是由浏览器解释才呈现出来的,实质它 是一段HTML代码,加 JS.CS ...

  3. 如何运用GitHub来提高生产效率

    这是一篇GitHub的入门级文章,主要针对git的初学者.我们将讨论初学者最关心的一些问题,如:为什么我们要使用GitHub,它的应用有哪些,如何运用它去帮助我们提高工作效率,以及它的基本用法有哪些. ...

  4. java与OC比较

    转自:http://blog.sina.com.cn/s/blog_93742d0d010165qi.html 1.Cocoa是什么?Cocoa是使用OC语言编写的工具包,里面有大量的类库.结构体,说 ...

  5. MyEclipse下安装FatJar打包工具

    方法一:help > software updates > Find and install > add > add remote site name:FatJar url:h ...

  6. [转载] 2 分钟读懂大数据框架 Hadoop 和 Spark 的异同

    转载自https://www.oschina.net/news/73939/hadoop-spark-%20difference 谈到大数据,相信大家对Hadoop和Apache Spark这两个名字 ...

  7. OpenTSDB介绍

    OpenTSDB 2.0, the scalable, distributed time series database可扩展.分布式时间序列数据库 1.背景 一些老的监控系统,它常常会出现这样的问题 ...

  8. 重构手法之Split Temporary Variable(分解临时变量)

    返回总目录 本小节目录 Split Temporary Variable(分解临时变量) Remove Assignments to Parameters(移除对参数的赋值) 6.6Split Tem ...

  9. SDRAM操作说明

    SDRAM是做嵌入式系统中,常用是的缓存数据的器件.基本概念如下(注意区分几个主要常见存储器之间的差异): SDRAM(Synchronous Dynamic Random Access Memory ...

  10. LSA和pLSA的比较

    Comparison   LSA pLSA 1. Theoretical background Linear Algebra Probabilities and Statistics 2. Objec ...