C之诡谲(指针,数组,类型的识别,参数可变的函数)

时间:2006-07-07 17:02:10  来源:本站搜集整理  作者:Eric

四.参数可变的函数
C语言中有一种很奇怪的参数“…”,它主要用在引数(argument)个数不定的函数中,最
常见的就是printf函数。
printf(“Enjoy yourself everyday!\n”);
printf(“The value is %d!\n”, value);
……
你想过它是怎么实现的吗?
1.      printf为什么叫printf?
不管是看什么,我总是一个喜欢刨根问底的人,对事物的源有一种特殊的癖好,一段典故
,一个成语,一句行话,我最喜欢的就是找到它的来历,和当时的意境,一个外文翻译过
来的术语,最低要求我会尽力去找到它原本的外文术语。特别是一个字的命名来历,我一
向是非常在意的,中国有句古话:“名不正,则言不顺。”printf中的f就是format的意思
,即按格式打印【注13】。
注13:其实还有很多函数,很多变量,很多命名在各种语言中都是非常讲究的,你如果细
心观察追溯,一定有很多乐趣和满足,比如哈希表为什么叫hashtable而不叫hashlist?在
C++的SGI STL实现中有一个专门用于递增的函数iota(不是itoa),为什么叫这个奇怪的
名字,你想过吗?看文章我不喜欢意犹未尽,己所不欲,勿施于人,所以我把这两个答案
告诉你:
(1)table与list做为表讲的区别:
table:
-------|--------------------|-------
 item1 |    kadkglasgaldfgl | jkdsfh
-------|--------------------|-------
 item2 |    kjdszhahlka     | xcvz
-------|--------------------|-------
list:
****
***
*******
*****
That's the difference!
如果你还是不明白,可以去看一下hash是如何实现的!
(2)The name iota is taken from the programming language APL.
而APL语言主要是做数学计算的,在数学中有很多公式会借用希腊字母,
希腊字母表中有这样一个字母,大写为Ι,小写为ι,
它的英文拼写正好是iota,这个字母在θ(theta)和κ(kappa)之间!
你可以看看
http://www.wikipedia.org/wiki/APL_programming_language
下面有一段是这样的:
APL is renowned for using a set of non-ASCII symbols that are an extension of  
traditional arithmetic and algebraic notation. These cryptic symbols, some hav
e joked, make it possible to construct an entire air traffic control system in
 two lines of code. Because of its condensed nature and non-standard character
s, APL has sometimes been termed a "write-only language", and reading an APL p
rogram can feel like decoding an alien tongue. Because of the unusual characte
r-set, many programmers used special APL keyboards in the production of APL co
de. Nowadays there are various ways to write APL code using only ASCII charact
ers.
在C++中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任
意数量的函数参数。
在标准C语言中定义了一个头文件<stdarg.h>专门用来对付可变参数列表,它包含了一组宏
,和一个va_list的typedef声明。一个典型实现如下【注14】:
typedef char* va_list;
#define va_start(list) list = (char*)&va_alist
#define va_end(list)
#define va_arg(list, mode)\
    ((mode*) (list += sizeof(mode)))[-1]
注14:你可以查看C99标准7.15节获得详细而权威的说明。也可以参考Andrew Konig的《C
陷阱与缺陷》的附录A。
ANSI C还提供了vprintf函数,它和对应的printf函数行为方式上完全相同,只不过用va_
list替换了格式字符串后的参数序列。至于它是如何实现的,你在认真读完《The C Prog
ramming Language》后,我相信你一定可以do it yourself!
使用这些工具,我们就可以实现自己的可变参数函数,比如实现一个系统化的错误处理函
数error。它和printf函数的使用差不多。只不过将stream重新定向到stderr。在这里我借
鉴了《C陷阱与缺陷》的附录A的例子。
实现如下:
#include <stdio.h>
#include <stdarg.h>
void error(char* format, …)
{
      va_list ap;
      va_start(ap, format);
      fprintf(stderr, “error: “);
      vfprintf(stderr, format, ap);
      va_end(ap);
      fprintf(stderr, “\n”);
      exit(1);
}
你还可以自己实现printf:
#include <stdarg.h>
int printf(char* format, …)
{
      va_list ap;
      va_start(ap, format);
      int n = vprintf(format, ap);
      va_end(ap);
      return n;
}
我还专门找到了VC7.1的头文件<stdarg.h>看了一下,发现各个宏的具体实现还是有区别的
,跟很多预处理(preprocessor)相关。其中va_list就不一定是char*的别名。
typedef struct {
        char *a0;       /* pointer to first homed integer argument */
        int offset;     /* byte offset of next parameter */
} va_list;
其它的定义类似。
 
经常在Windows进行系统编程的人一定知道函数调用有好几种不同的形式,比如__stdcall
,__pascal,__cdecl。在Windows下_stdcall,__pascal是一样的,所以我只说一下__st
dcall和__cdecl的区别。
(1)__stdcall表示调用端负责被调用函数引数的压栈和出栈。函数参数个数一定的函数
都是这种调用形式。
例如:int fun(char c, double d),我们在main函数中调用它,这个函数就只管本身函数
体的运行,参数怎么来的,怎么去的,它一概不管。自然有main负责。不过,不同的编译
器的实现可能将参数从右向左压栈,也可能从左向右压栈,这个顺序我们是不能加于利用
的【注15】。
注15:你可以在Herb Sutter的《More Exceptional C++》中的条款20:An Unmanaged Po
inter Problem, Part 1:Parameter Evaluation找到相关的细节论述。
(2)__cdecl表示被调用函数自身负责函数引数的压栈和出栈。参数参数可变的函数采用
的是这种调用形式。
为什么这种函数要采用不同于前面的调用形式呢?那是因为__stdcall调用形式对它没有作
用,调用端根本就无法知道被调用函数的引数个数,它怎么可能正确工作?所以这种调用
方式是必须的,不过由于参数参数可变的函数本身不多,所以用的地方比较少。
对于这两种方式,你可以编制一些简单的程序,如何反汇编,在汇编代码下面你就可以看
到实际的区别,很好理解的!
重载函数有很多匹配(match)规则调用。参数为“…”的函数是匹配最低的,这一点在A
ndrei Alexandrescu的惊才绝艳之作《Modern C++ Design》中就有用到,参看Page34-35
,2.7“编译期间侦测可转换性和继承性”。

相关文章

文章评论

共有  1  位网友发表了评论 此处只显示部分留言 点击查看完整评论页面