C之诡谲(下)

类别:编程语言 点击:0 评论:0 推荐:
  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,如果用const来修饰的话,当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而不叫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 have 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 characters, APL has sometimes been termed a "write-only language", and reading an APL program can feel like decoding an alien tongue. Because of the unusual character-set, many programmers used special APL keyboards in the production of APL code. Nowadays there are various ways to write APL code using only ASCII characters.

在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 Programming 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 Pointer Problem, Part 1:Parameter Evaluation找到相关的细节论述。

(2)__cdecl表示调用端负责被调用端引数的压栈和出栈。参数可变的函数采用的是这种调用形式。

为什么这种函数要采用不同于前面的调用形式呢?那是因为__stdcall调用形式对它没有作用,被调用端根本就无法知道调用端的引数个数,它怎么可能正确工作?所以这种调用方式是必须的,不过由于参数参数可变的函数本身不多,所以用的地方比较少。

对于这两种方式,你可以编制一些简单的程序,然后反汇编,在汇编代码下面你就可以看到实际的区别,很好理解的!

重载函数有很多匹配(match)规则调用。参数为“…”的函数是匹配最低的,这一点在Andrei 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语言本身就不用再花更多的精力了。

吴桐写于2003.5.26

最近修改2003.6.19

本文地址:http://com.8s8s.com/it/it29230.htm