|

楼主 |
发表于 2003-5-28 23:27:44
|
显示全部楼层
C++的前世今生
C的诡谲(下)
三.类型的识别。
基本类型的识别非常简单:
int a;//a的类型是a
char* p;//p的类型是char*
……
那么请你看看下面几个:
int* (*a[5])(int, char*); //#1
void (*b[10]) (void (*)()); //#2
doube(*)() (*pa)[9]; //#3
如果你是第一次看到这种类型声明的时候,我想肯定跟我的感觉一样,就如晴天霹雳
,五雷轰顶,头昏目眩,一头张牙舞爪的狰狞怪兽扑面而来。
不要紧(Take it easy)!我们慢慢来收拾这几个面目可憎的纸老虎!
1.C语言中函数声明和数组声明。
函数声明一般是这样int fun(int,double);对应函数指针(pointer to function)的
声明是这样:
int (*pf)(int,double),你必须习惯。可以这样使用:
pf = &fun;//赋值(assignment)操作
(*pf)(5, 8.9);//函数调用操作
也请注意,C语言本身提供了一种简写方式如下:
pf = fun;// 赋值(assignment)操作
pf(5, 8.9);// 函数调用操作
不过我本人不是很喜欢这种简写,它对初学者带来了比较多的迷惑。
数组声明一般是这样int a[5];对于数组指针(pointer to array)的声明是这样:
int (*pa)[5]; 你也必须习惯。可以这样使用:
pa = &a;// 赋值(assignment)操作
int i = (*pa)[2]//将a[2]赋值给i;
2.有了上面的基础,我们就可以对付开头的三只纸老虎了!:)
这个时候你需要复习一下各种运算符的优先顺序和结合顺序了,顺便找本书看看就够了。
#1:int* (*a[5])(int, char*);
首先看到标识符名a,“[]”优先级大于“*”,a与“[5]”先结合。所以a是一个数组
,这个数组有5个元素,每一个元素都是一个指针,指针指向“(int, char*)”,对,指向
一个函数,函数参数是“int, char*”,返回值是“int*”。完毕,我们干掉了第一个纸
老虎。:)
#2:void (*b[10]) (void (*)());
b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,
函数参数是“void (*)()”【注10】,返回值是“void”。完毕!
注10:这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”。
#3. doube(*)() (*pa)[9];
pa是一个指针,指针指向一个数组,这个数组有9个元素,每一个元素都是“doube(*
)()”【也即一个指针,指向一个函数,函数参数为空,返回值是“double”】。
现在是不是觉得要认识它们是易如反掌,工欲善其事,必先利其器!我们对这种表达
方式熟悉之后,就可以用“typedef”来简化这种类型声明。
#1:int* (*a[5])(int, char*);
typedef int* (*PF)(int, char*);//PF是一个类型别名【注11】。
PF a[5];//跟int* (*a[5])(int, char*);的效果一样!
注11:很多初学者只知道typedef char* pchar;但是对于typedef的其它用法不太了
解。Stephen Blaha对typedef用法做过一个总结:“建立一个类型别名的方法很简单,在
传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”
。可以参看《程序员》杂志2001.3期《C++高手技巧20招》。
#2:void (*b[10]) (void (*)());
typedef void (*pfv)();
typedef void (*pf_taking_pfv)(pfv);
pf_taking_pfv b[10]; //跟void (*b[10]) (void (*)());的效果一样!
#3. doube(*)() (*pa)[9];
typedef double(*PF)();
typedef PF (*PA)[9];
PA pa; //跟doube(*)() (*pa)[9];的效果一样!
3.const和volatile在类型声明中的位置
在这里我只说const,volatile是一样的【注12】!
注12:顾名思义,volatile修饰的量就是很容易变化,不稳定的量,它可能被其它线
程,操作系统,硬件等等在未知的时间改变,所以它被存储在内存中,每次取用它的时候
都只能在内存中去读取,它不能被编译器优化放在内部寄存器中。
类型声明中const用来修饰一个常量,我们一般这样使用:const在前面
const int;//int是const
const char*;//char是const
char* const;//*(指针)是const
const char* const;//char和*都是const
对初学者,const char*;和 char* const;是容易混淆的。这需要时间的历练让你习惯
它。
上面的声明有一个对等的写法:const在后面
int const;//int是const
char const*;//char是const
char* const;//*(指针)是const
char const* const;//char和*都是const
第一次你可能不会习惯,但新事物如果是好的,我们为什么要拒绝它呢?:)
const在后面有两个好处:
A.const所修饰的类型是正好在它前面的那一个。如果这个好处还不能让你动心的话,那
请看下一个!
B.我们很多时候会用到typedef的类型别名定义。比如typedef char* pchar,如果用con
st来修饰的话,当const在前面的时候,就是const pchar,你会以为它就是const char*
,但是你错了,它的真实含义是char* const。是不是让你大吃一惊!但如果你采用const
在后面的写法,意义就怎么也不会变,不信你试试!
不过,在真实项目中的命名一致性更重要。你应该在两种情况下都能适应,并能自如
的转换,公司习惯,商业利润不论在什么时候都应该优先考虑!不过在开始一个新项目的
时候,你可以考虑优先使用const在后面的习惯用法。
四.参数可变的函数
C语言中有一种很奇怪的参数“…”,它主要用在引数(argument)个数不定的函数中
,最常见的就是printf函数。
printf(“Enjoy yourself everyday!\n”);
printf(“The value is %d!\n”, value);
……
你想过它是怎么实现的吗?
1.printf为什么叫printf?
不管是看什么,我总是一个喜欢刨根问底的人,对事物的源有一种特殊的癖好,一段典故
,一个成语,一句行话,我最喜欢的就是找到它的来历,和当时的意境,一个外文翻译过
来的术语,最低要求我会尽力去找到它原本的外文术语。特别是一个字的命名来历,我一
向是非常在意的,中国有句古话:“名不正,则言不顺。”printf中的f就是format的意思
,即按格式打印【注13】。
注13:其实还有很多函数,很多变量,很多命名在各种语言中都是非常讲究的,你如
果细心观察追溯,一定有很多乐趣和满足,比如哈希表为什么叫hashtable而不叫hashlis
t?在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 P
rogramming 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是一样的,所以我只说
一下__stdcall和__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 arameter Evaluation找到相关的细节论述。
(2)__cdecl表示被调用函数自身负责函数引数的压栈和出栈。参数参数可变的函数采用
的是这种调用形式。
为什么这种函数要采用不同于前面的调用形式呢?那是因为__stdcall调用形式对它没
有作用,调用端根本就无法知道被调用函数的引数个数
,它怎么可能正确工作?所以这种调用方式是必须的,不过由于参数参数可变的函数本身
不多,所以用的地方比较少。
对于这两种方式,你可以编制一些简单的程序,如何反汇编,在汇编代码下面你就可
以看到实际的区别,很好理解的!
重载函数有很多匹配(match)规则调用。参数为“…”的函数是匹配最低的,这一点在A
ndrei Alexandrescu的惊才绝艳之作《Modern C++ Design》中就有用到,参看Page34-35
,2.7“编译期间侦测可转换性和继承性”。
后记:
C语言的细节肯定不会只有这么多,但是这几个出现的比较频繁,而且在C语言中也是很重
要的几个语言特征。如果把这几个细节彻底弄清楚了,C语言本身的神秘就不会太多了。
C语言本身就像一把异常锋利的剪刀,你可以用它做出非常精致优雅的艺术品,也可以剪出
一些乱七八糟的废纸片。能够将一件武器用到出神入化那是需要时间的,需要多长时间?
不多,请你拿出一万个小时来,英国Exter大学心理学教授麦克.侯威专门研究神童和天才
,他的结论很有意思:“一般人以为天才是自然而生、流畅而不受阻的闪亮才华,其实,
天才也必须耗费至少十年光阴来学习他们的特殊技能,绝无例外。要成为专家,需要拥有
顽固的个性和坚持的能力……每一行的专业人士,都投注大量心血,培养自己的专业才能
。”【注16】
注16:台湾女作家、电视节目主持人吴淡如《拿出一万个小时来》。《读者》2003.1期。
“不用太努力,只要持续下去。想拥有一辈子的专长或兴趣,就像一个人跑马拉松赛一样
,最重要的是跑完,而不是前头跑得有多快。”
推荐两本书:
K&R的《The C Programming language》,Second Edition。
Andrew Konig的《C陷阱与缺陷》。本文从中引用了好几个例子,一本高段程序员的经验之
谈。
但是对纯粹的初学者不太合适,如果你有一点程序设计的基础知识,花一个月的时间
好好看看这两本书,C语言本身就不用再花更多的精力了。 |
|