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

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

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

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

2 可变參实现思路

2.1 怎样取得兴许实參地址

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

样例代码例如以下。

  1. void f(int x, int y, int z)
  2. {
  3. printf("%p, %p, %p\n", &x, &y, &z);
  4. }
  5. int main()
  6. {
  7. f(100, 200, 300);
  8. return 0;
  9. }

可能的运行结果:

  1. 00FFF674, 00FFF678, 00FFF67C

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

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

  1. void f(int x, int y, int z)
  2. {
  3. char* px = (char*)&x;
  4. char *py = px + sizeof(x);
  5. char *pz = py + sizeof(int);
  6. printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);
  7. }
  8. int main()
  9. {
  10. f(100, 200, 300);
  11. return 0;
  12. }

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

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

  1. void f(int x, ...)
  2. {
  3. char* px = (char*)&x;
  4. char *py = px + sizeof(x);
  5. char *pz = py + sizeof(int);
  6. printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);
  7. }
  8. int main()
  9. {
  10. f(100, 200, 300);
  11. return 0;
  12. }

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

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

  1. void f(char* fmt, ...)
  2. {
  3. char* p0 = (char*)&fmt;
  4. char* ap = p0 + sizeof(fmt);
  5. char* p = fmt;
  6. while (*p) {
  7. if (*p == '%' && *(p+1) == 'd') {
  8. printf("參数类型为int,值为 %d\n", *((int*)ap));
  9. ap += sizeof(int);
  10. }
  11. else if (*p == '%' && *(p+1) == 'f') {
  12. printf("參数类型为double,值为 %f\n", *((double*)ap));
  13. ap += sizeof(double);
  14. }
  15. else if (*p == '%' && *(p+1) == 's') {
  16. printf("參数类型为char*,值为 %s\n", *((char**)ap));
  17. ap += sizeof(char*);
  18. }
  19. p++;
  20. }
  21. }
  22. int main()
  23. {
  24. f("%d,%f,%s", 100, 1.23, "hello world");
  25. return 0;
  26. }

输出:

  1. 參数类型为int,值为 100
  2. 參数类型为double,值为 1.230000
  3. 參数类型为char*,值为 hello world

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

  1. #define va_list char* /* 可变參数地址 */
  2. #define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指针指向第一个可变參数 */
  3. #define va_arg(ap, t) (ap+=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得參数值,同一时候移动指针指向兴许參数 */
  4. #define va_end(ap) ap=0 /* 结束參数处理 */
  5. void f(char* fmt, ...)
  6. {
  7. va_list ap;
  8. va_start(ap, fmt);
  9. char* p = fmt;
  10. while (*p) {
  11. if (*p == '%' && *(p+1) == 'd') {
  12. printf("參数类型为int,值为 %d\n", va_arg(ap, int));
  13. }
  14. else if (*p == '%' && *(p+1) == 'f') {
  15. printf("參数类型为double,值为 %f\n", va_arg(ap, double));
  16. }
  17. else if (*p == '%' && *(p+1) == 's') {
  18. printf("參数类型为char*,值为 %s\n", va_arg(ap, char*));
  19. }
  20. p++;
  21. }
  22. va_end(ap);
  23. }
  24. int main()
  25. {
  26. f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
  27. return 0;
  28. }

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

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

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

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

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

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

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

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

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

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

  1. #include <stdio.h>
  2. #include <stdarg.h>
  3. void f(char* fmt, ...)
  4. {
  5. va_list ap;
  6. va_start(ap, fmt);
  7. char* p = fmt;
  8. while (*p) {
  9. if (*p == '%' && *(p+1) == 'd') {
  10. printf("參数类型为int,值为 %d\n", va_arg(ap, int));
  11. }
  12. else if (*p == '%' && *(p+1) == 'f') {
  13. printf("參数类型为double,值为 %f\n", va_arg(ap, double));
  14. }
  15. else if (*p == '%' && *(p+1) == 's') {
  16. printf("參数类型为char*,值为 %s\n", va_arg(ap, char*));
  17. }
  18. p++;
  19. }
  20. va_end(ap);
  21. }
  22. int main()
  23. {
  24. f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
  25. return 0;
  26. }

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

4 几个须要注意的问题

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. void f(int x);
  2. void f(int x, double d);
  3. 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. 安装MongoDB启动时报错‘发生系统错误2’的解决办法

    安装数据库mongodb启动时报"发生系统错误2". 这个问题是如果你之前已经装过一次,并且两次安装目录不同,就绝对会碰到的,因为你之前安装的路径已经在注册表中生成了,并没有随着你 ...

  2. Linux学习(十九)软件安装与卸载(二)更换yum源

    一.简介 系统自带的源数量有限,而且是国外的源,速度肯定不如国内的.而断网的时候,本地源就可以派得上用处.而RPMForge源是传说中规模最大的一个源.那么接下来我们就来分别配一下本地源,国内源,RP ...

  3. R语言高性能编程(二)

    接着上一篇 一.减少内存使用的简单方法1.重用对象而不多占用内存 y <- x 是指新变量y指向包含X的那个内存块,只有当y被修改时才会复制到新的内存块,一般来说只要向量没有被其他对象引用,就可 ...

  4. Redis安装及使用笔记

    windows下安装Redis 1.下载Redis的软件包 Redis on github; 2.将软件解压到服务器软件目录; 3.在命令行运行此命令: ./redis-server redis.wi ...

  5. 为什么我的子线程更新了 UI 没报错?借此,纠正一些Android 程序员的一个知识误区

    开门见山: 这个误区是:子线程不能更新 UI ,其应该分类讨论,而不是绝对的. 半小时前,我的 XRecyclerView 群里面,一位群友私聊我,问题是: 为什么我的子线程更新了 UI 没报错? 我 ...

  6. 学python3的书

    <Python Cookbook>3rd Edition http://python3-cookbook.readthedocs.io/zh_CN/latest/copyright.htm ...

  7. 《java.util.concurrent 包源码阅读》27 Phaser 第一部分

    Phaser是JDK7新添加的线程同步辅助类,作用同CyclicBarrier,CountDownLatch类似,但是使用起来更加灵活: 1. Parties是动态的. 2. Phaser支持树状结构 ...

  8. 一个RtspServer的设计与实现和RTSP2.0简介

    一个RtspServer的设计与实现和RTSP2.0简介   前段时间着手实现了一个RTSP Server,能够正常实现多路RTSP流的直播播放,因项目需要,只做了对H.264和AAC编码的支持,但是 ...

  9. C++ 将汉字转换成拼音全拼

    #include <string> using std::string; //======================================================= ...

  10. linux 挂在win下文件

    使用mount命令 #mount -t cifs -o username=abc,password=1234 //192.168.1.10/linux /mnt/linux #mount -t cif ...