| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 11657 人关注过本帖, 8 人收藏
标题:[原创]C语言思考——复杂类型的不一致性
只看楼主 加入收藏
StarWing83
Rank: 8Rank: 8
来 自:仙女座大星云
等 级:贵宾
威 望:19
帖 子:3951
专家分:748
注 册:2007-11-16
结帖率:90%
收藏(8)
 问题点数:0 回复次数:60 
[原创]C语言思考——复杂类型的不一致性
  导读:本文透过对C语言中一处看似缺陷的不一致性的思考,提出了两种弥补这一缺陷的方法,并分析了利弊,从而挖掘出C语言设计这样不一致性的原由,以及其中包含的深刻的C哲学思想,希望对大家的编程有所启示。


  不可否认,数组是C语言中设计的最精妙的部分,它递归支持多维数组,向指针的退化都是很精妙而细致的设计。但是从整体来说,我们却可以发现数组和其他的复杂类型相比,有一些不一致性,而这似乎在C语言的完美之上,划上了一层瑕疵。

  首先我们复习一下,数组是由N个元素组成的一个整体,注意这个N也是数组的类型的一部分。也就是说,int[10]和int[20]是完全不同的两种类型,而数组名代表了数组全体元素的整体。注意到这个特性和结构体的相似性。那么我们可以比较一下结构体和数组。
程序代码:
        结构体            数组
typeid        struct{...}        int [N]
变量名        代表整个结构体        代表整个数组
在表达式中使    直接复制        退化为包含类型的指针
用变量名    
解引用操作    成员操作符        索引操作符
指针操作    指向成员操作符        索引操作符
我们可以看出,数组的和结构体最大的不同就是其变量名的使用上。结构体很完美,变量名直接代表整个结构体:赋值操作直接按位拷贝,传参操作也是拷贝。变量名毫无疑问的代表着结构体本身,没有人会怀疑其名称是另外一个东西。而数组呢,众所周知的,初学者会误认为数组名就是一个指针,数组名和指针完全等价等等,而造成这个误会的罪魁祸首就是因为数组名可以自动退化为数组所包含类型的指针。

  我们先忘记C语言的数组语法,来看一个假想的例子:

int f(int a[N]);

  传递了一个数组,凭直觉,如果数组和结构体完全相同,则这里的行为应该是复制整个数组的内容,而且,如果赋值的话,也应该是拷贝整个数组的内容,甚至,如果两个数组大小不一的话,赋值操作的右值应该直接进化为左值的内容,从而隐式拷贝需要的内容。这是多么完美的操作啊。

  让我们从美梦中暂时醒来。这种操作在真正的C语言中是行不通的,上面的函数声明是不合法的,因为作为形参的数组类型不应该带上大小。数组名自动退化为所包含元素的指针,而且和指针共享索引操作符。我们发现了同样作为复合类型,数组和结构体之间惊人的不同。这似乎给高度统一完美的C抹上了一层灰暗。是这样吗?

  继续想象,如果数组和结构体的行为完全相同,那么这种“理想的”数组应该是怎么样的。数组当然可以设计成和结构体相似的复制模型。我们假设,[]操作符的唯一用途就是根据数组名(即整个数组)取出相应元素,而不再支持根据指针取得相应元素(即int *p;p[i]这样的操作不合法),再假设有这样的一种操作符<>,对于int(*)[N],它的操作是取出所指向数组的第N个元素。那么我们可以写出这么个程序来:

void func(int (*a)[N])
{
    for(int i=0;i<N;i++)
        printf("%d ",a<i>);
}

  注意类比一下,[]和.的行为是相同的,而我们假想的操作符<>和->有着相同的行为。这样,数组和结构体有了相同的抽象模式:其名称代表类型整体,通过指向类型的指针可以取出其相应元素。注意,我们这里使用了指向数组的指针,而不是直接传数组,在我们假想的数组模型中int func(int a[])是不合法的,因为数组大小是数组类型的一部分,必须指定数组大小,而int func(int a[N])则会复制整个数组到堆栈中去。注意,现在我们想象中的数组类型,已经和结构体类型有着完全相同的行为了。

  我们看一下嵌套的数组类型类型:

int a[M][N];

  毫无疑问,在这样的类型系统下,上面的声明也是合法的。上面是一个大小为M的数组,其中每个元素是一个大小为N的数组。我们取其中的第一个数组,注意到a是数组名,代表整个数组,那么我们应该使用假想中的[]操作符。

a[1]   //这个表达式的值为int[N]类型

  我们取出了数组的第二个元素,它是一个长度为N的数组,因为还是数组名(而非指向数组的指针!!)所以我们仍然要使用[]操作符:

a[1][2]    //这个表达式的值为int类型

  我们这样就得到了第二个子数组的第三个元素。
  如果是指针呢?假设我们将上面的a传给一个函数:

void func(int (*a)[M][N])
{
    for(int i=0;i<M;i++)
        for(int j=0;j<N;j++)
            printf("%d ",a<i>[j]);
}

  注意这里的写法!a是一个指针,所以这里使用间接索引操作符(也就是假想中的<>操作符)来取得a的第i个元素。然后使用直接索引操作符来取得子数组——它是一个完整整体,而不是一个指针——的第j个元素。这个其实就有点像对结构体的操作了,考虑下面的结构体:

struct x
{
    struct y
    {
         int i,j;
    }a;
    int b;
};

  如果我们有个指向struct x的指针p,我们要引用i,应该怎么样做呢?写法是:

p->a.i

  注意到没有?这种写法和a<i>[j]的解法有惊人的一致性。似乎这已经是一种完美的系统了。

  但是我们没有注意到指针。在这样的假想系统下,指针该如何使用呢?

  假设有这样一个指针
int *a;

  因为假想中的索引操作符只能适用于数组类型,所以我们是无法直接写a[i]的,这样肯定会出错。那么怎么办呢?

  我们可以这样写

(*(int(*)[N])a)[i]

  或者可以稍微简单一点:

((int(*)[N])a)<i>

  因为我们假想中的<>和[]同级,而且优先级比类型转换操作符要高,所以我们不得不加上圆括号。

  于是我们发现了这种貌似完美的类型系统的缺陷:对指针极其的不友好。

  如果我们要使用指针来随机取得元素,我们需要将其转化为数组类型,由于N是数组类型的一部分,所以我们必须提供一个N,而且由于优先级的关系,类型转换可能会变得可怕,这是和C语言“简洁”的设计哲学相悖的。(注意到这种设计的优势了吗?因为即使是指针,也必须提供大小才能进行索引,所以可以强制避免索引越界。)

  于是,我们在权衡了指针以后,只能无奈地放弃了看似完美但却冰冷残酷的整体式数组类型,而使用一种很简单的抽象:

    [bo]数组名代表数组整体,但在表达式中自动退化为其包含元素的指针。[/bo]

  我们拿结构体来打比方,假设结构体拥有相同的性质,那么上面对于x::y::i的引用,会变成这个样子的:

p->a->i;

  很奇怪是不是,为什么第二个解引用操作符会使用->?因为如果结构体具有数组的性质,那么就再也不需要.操作符了——所有的结构体名自动退化为指向结构体的指针(注意,不是指向结构体包含元素的指针,因为结构体通常包含不同类型的元素,这里我们没有办法做到完全的一致性),然后就可以使用->操作符。因为只剩下->操作符可以使用了,我们可以用熟悉的.操作符来取代->操作符,注意到了吗?这和数组只有一种[]索引操作符,而非我们设想的<>[]两种操作符是完全等价的!

  于是上面的操作变为:

p.a.i

  呵呵,是不是有一种Java的味道了呢?注意这里第二个“.”和上面“p->a.i”中第二个点的不同,第一个表达式使用“.”是因为p->a是一个结构体的实体,而实体必须使用.来解引用。而上面第三个表达式的“.”是由于p.a是个实体,自动退化为指针,然后使用.进行解引用,这和a[i]的a自动退化为int*,然后使用[]操作符解引用是相同的行为。

  这里,我们看到了对指针友好的一个一致性解决方案。而我们知道,Java使用了这一套解决方案。问题在于,为什么C语言没有使用这一套解决方案,而是增加了一个->操作符,使结构体和数组的行为变得不统一呢?

  我曾经为这个问题思考了很久,后来有了一个似乎不是答案的答案:为了能实现一种操作——结构体的自动复制。

  我们知道,C++里面的结构体是可以直接赋值的,赋值以后两个结构体会按位拷贝,甚至是在函数形参的结构体也是这样。

  而学过Win32的人还知道,有一些函数,比如CreateFont,参数是非常多的,为了方便,这样的函数会有一个姐妹函数,对于CreateFont是CreateFontIndirect,它将所有参数打包变成一个结构体,然后直接传递结构体的指针。但是因为C语言对结构体独特的设计,我们其实根本不需要姐妹函数,就可以实现这样的功能,如下:

printf("%s %d %c\n","Harry Potter",11,'M');

  上面的调用可以用一个结构体来简化:

struct student
{
    char* name;
    int age,sex;//注意,sex应该是char,后面会提到为什么使用int
}hp={"Harry Potter",11,'M'};
printf("%s %d %c\n",hp);

  上面的代码GCC会给出一些警告,但是可以通过,运行也是正常的,唯一的缺点是会造成额外的拷贝,但是请注意,这里的拷贝结构体的代价,是和直接传参的拷贝代价相同的!!而至于为什么这样行得通,我想大家都能想明白。这里只是解释一下为什么sex用int而不是char。我们知道,为了防止printf的参数“一错百错”,传入不定参数函数的参数,都会自动升级:float会晋级为double,而char,short都会晋级为int。大家可以检查一下,就算你传进去的是'M',实际上也占用了32位的大小,证明它是个int。这一点是我最近才想通的。我写了如下代码:

printf("%hx,%hx",0x12345678,1);

  我料想结果应该是5678,1234,但不是,结果是5678,1,就是因为即使是hx,照样会使参数晋级为int,从而每个参数为32位,而hx取出前16位显示,后面16位丢弃不用。要产生5678,1234的效果,必须这么写:

printf("%hx,%hx",0x1234abcd5678ll,1);

  这样,传入一个64位数,然后hx按照32位读取,就得到料想中的结果了。我们可以看出,数组包含元素类型相同,因此数组可以有特殊的操作:索引。而在类型不同,各元素的大小不同的结构体上面,是很难实现索引操作的(当然也不是不可能,JS就实现了,但是这样会造成效率上的损失,而且你必须承认,结构体上的索引操作除了增加编译的难度以外,并没有什么其他的优点)使用上的不同,造就了实现的不同,也导致了本应该有完美的相似点的两种类型,成了最貌合神离的兄弟。

  于是我们从一个似乎很偏门的小技巧上面得到了真正的结论:C语言故意将结构体和数组设计地不一致,这种近乎残缺的不一致使得C语言有了更强大的功能——对指针的友好支持,同时对结构体功能上的保证。它使得在C语言上不同的类型有着不同的、独特的操作,使得C语言变得强大。我们注意到,Java的引用模型已经完全舍弃了C语言内置的结构体复制功能,导致功能受到限制,这可以说是追求一致性所付出的代价。

  于是我们学到了C的哲学之一——适度的不一致性,会得到更加强大的功能。C试图为每一种类型制造一种与众不同的操作,从而将其与其他功能完全地区分开来,使其功能最大化。这是C的哲学,在C的标准上,这种哲学表现的淋漓尽致,大家可以在很多貌似不一致的地方窥见这种哲学深远的思想。我们也可以将这个结论用到编程上面去,比如,写完全独立的函数,以追求功能的最强化等等。具体的使用,就留给大家思考吧。

  本文是一个系列的一部分,融合了我多年对C语言的思考,大家看了以后给给意见,如果可以接受的话,我会继续写下去的。有异议的地方,也欢迎跟帖讨论。
搜索更多相关主题的帖子: C语言 一致性 多维 类型 思考 
2008-05-26 13:54
StarWing83
Rank: 8Rank: 8
来 自:仙女座大星云
等 级:贵宾
威 望:19
帖 子:3951
专家分:748
注 册:2007-11-16
收藏
得分:0 
发现居然没人顶贴……伤心,沙发自己坐吧~~~~

专心编程………
飞燕算法初级群:3996098
我的Blog
2008-05-26 14:05
mqh21364
Rank: 1
等 级:新手上路
帖 子:642
专家分:0
注 册:2008-2-28
收藏
得分:0 
我来顶!!!!!!!!!

顶了再看看。

前不见古人,后不见来者。念天地之悠悠,独怆然而涕下。
2008-05-26 16:38
雨中飛燕
Rank: 1
等 级:新手上路
帖 子:765
专家分:0
注 册:2007-10-13
收藏
得分:0 
还没看完。。。。有待验证。。。

[color=white]

[[it] 本帖最后由 雨中飛燕 于 2008-5-26 17:07 编辑 [/it]]
2008-05-26 16:41
mqh21364
Rank: 1
等 级:新手上路
帖 子:642
专家分:0
注 册:2008-2-28
收藏
得分:0 
说实话,看着有点迷糊。估计我功力不够的原因。

不过支持楼主,一来写这么多不容易;二来谢谢你愿意和大家一起分享你的学习心得.

继续!!期待!!

前不见古人,后不见来者。念天地之悠悠,独怆然而涕下。
2008-05-26 16:50
StarWing83
Rank: 8Rank: 8
来 自:仙女座大星云
等 级:贵宾
威 望:19
帖 子:3951
专家分:748
注 册:2007-11-16
收藏
得分:0 
热切盼望飞燕验证…………估计是最好的鼓励了,无论是赞成还是反对~~~

专心编程………
飞燕算法初级群:3996098
我的Blog
2008-05-26 18:03
StarWing83
Rank: 8Rank: 8
来 自:仙女座大星云
等 级:贵宾
威 望:19
帖 子:3951
专家分:748
注 册:2007-11-16
收藏
得分:0 
看情况吧,如果有下次,准备结合指针讲讲C的哲学——信任哲学。这个应该可以叫做不一致哲学吧。有一个很简单的应用:就是尽量不写功能一样的代码,如果你写了这种代码,就要想办法优化自己程序的结构了。这也是一种不一致哲学:争取自己代码最大的不一致性,从而在有限的代码中获得最大的功能。

专心编程………
飞燕算法初级群:3996098
我的Blog
2008-05-26 18:08
jxt598598
Rank: 1
等 级:新手上路
帖 子:149
专家分:0
注 册:2007-6-13
收藏
得分:0 
很晕啊!

qq:304742297
2008-05-26 20:03
zhuwei168
Rank: 1
来 自:东软信息学院
等 级:新手上路
帖 子:180
专家分:0
注 册:2008-2-13
收藏
得分:0 
看了
脑子里有点乱了
不是很懂的样子
顶你了~~~~~~~~~

做一个自由的人,飞到蔚蓝的天空里。
2008-05-26 20:25
中学者
Rank: 16Rank: 16Rank: 16Rank: 16
等 级:版主
威 望:20
帖 子:3554
专家分:80
注 册:2007-9-14
收藏
得分:0 
我来说一下,关于翅膀兄说的参数压栈类型晋升的问题..
我所了解的是:float会扩展成double;short,char会扩展成int进行压栈....是因为内存对齐....这样可以加速内存的读取...

樱花大战,  有爱.
2008-05-26 21:29
快速回复:[原创]C语言思考——复杂类型的不一致性
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.027410 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved