1965年时,贝尔实验室(Bell Labs)加入一项由通用电气(General Electric)和麻省理工学院(MIT)合作的计划;该计划要建立一套多使用者、多任务、多层次(multi-user、multi-processor、multi-level)的MULTICS操作系统。直到1969年,因MULTICS计划的工作进度太慢,该计划被停了下来。当时,Ken Thompson(后被称为UNIX之父)已经有一个称为"星际旅行"的程序在GE-635的机器上跑,但是反应非常慢,正巧被他发现了一部被闲置的PDP-7(Digital的主机),Ken Thompson和Dernis Ritchie就将"星际旅行"的程序移植到PDP-7上,并为此编写了一个操作系统。
在1970年时,那部PDP-7却只能支持两个使用者,当时,Brian Kernighan就开玩笑地称他们的系统其实是:"UNiplexed Information and Computing System",即复杂信息系统,缩写为"UNICS",后来,大家取其谐音,就称其为"UNIX"了。1970年可称为"UNIX元年"。由于该系统是用汇编实现的,难以调试,难以理解,Thompson想用一些高级语言实现UNIX,他在Fortran语言上尝试了几次后,最后简化了BCPL语言,创建出B语言。然而,B语言由于硬件系统的内存限制,只能在内存中放置解释器,而不是编译器,这种限制产生的低效阻碍了B语言编写UNIX系统。Thompson在B语言中发明了++,--操作,但是B语言保持了BCPL的语言无类型的特点,所以当1972年开发平台转移到PDP-11上后,由于该处理器的特色即为硬件支持几种不同长度的数据类型,而B语言又是无类型语言。因此,效率便是一个问题,且Thompson又不得不在PDP-11上重新编写UNIX系统。同年Dennis Ritche利用PDP--11强大的性能,创立了能同时解决多种数据类型和效率的New B语言。它采用了编译模式,而不是解释模式,并引入了类型系统。从此诞生了C。
增加类型系统主要目的是帮助编译器设计者区分PDP-11机器所拥有的不同数据类型,不仅如此,由于早期C语言的客户都是编译器设计者,因此,为了方便编译器设计者而增加了语言特性,除了类型系统,还有例如数组下标,基本数据类型直接与底层硬件相对应,auto关键字,表达式中的数组名可当做指针,register关键字,不允许函数嵌套定义等特性。
还有个语言特性,也是为编译器设计者而增加的,那就是float自动扩展为double类型,但ANSIC中已不在如此。因为pdp-11中,float转换double的代价非常小,只要在后面增加一个每位都为0的字即可,如果要转换回float,则去掉第二个字即可,其次,在某些pdp-11的浮点硬件表示形式中有一个运算模式位,你可以只进行float运算,也可以只进行double运算,但如果要在这两个模式中切换,则要修改该模式位,在早期的UNIX中float用的不是太多,所以运算模式固定为double比较方便,省的编译器还要跟踪它的变化。
1978年,C语言经典名著The C Programming Language出版,这本书受到了广泛的赞誉,作者Brian Kernighan和Dennis Ritchie也因此名声大噪,所以这个版本的C语言就成为K&R C语言。
1983年美国国家标准化组织ANSI 成立了C语言工作小组,开始C语言标准化工作,1989年12月,C语言标准化草案最终被ANSI委员会接纳,随后ISO也接纳了ANSI C标准,但她删除了Rationale一节,并调整了文档格式,1990年初,ANSI重新接纳了ISO C,同样删除了Rationale,取代了原先的版本,因此从原则上说,ANSIC是ISOC,也是我们今天所说的ISO C。ANSIC定义了下面一些术语,用于描述某种编译器的特点。理解这些术语有助于你理解什么东西能被语言所接收,什么东西不能被语言接收。
A.设计不可移植代码的术语:
1.由编译器定义的,即有编译器设计者觉得采取何种行动,并做好记录文档。
2.未确定的,在某些正确情况下的做法,标准并未明确规定应该怎么样做。
B.坏代码
1.未定义的,在某些不正确情况下的做法,标准并未明确规定应该怎么样做。
2.约束条件,
C.可移植的代码
1.严格遵循标准的。
一个严格遵循标准的程序应该是:
只使用已确定的特性
不突破任何由编译器实现的限制
不产生任何依赖由编译器定义的或未确定的或未定义的特性的输出
2.遵循标准的。
关于编译原理和ANSIC,以后一定拜读。。。
ANSI C必须能够支持:
1.在函数定义中形参数量的上限至少可以达到31个
2.在函数调用时实参数量的上限至少可以达到31个
3.在一条源代码行里至少可以有509个字符
4.在表达式中至少可以支持32层嵌套的符号
5.long 型整数不得低于32位。
ANSIC 和K & R C区别有:
增加了原型:把形参作为函数声明的一部分。方便编译器根据函数定义检查函数用法、在K &R C中,原型的检查被推迟到链接时,或者干脆不检查。
增加新的关键字:加入了enum,const,volatile,signed,void等关键字,而entry则弃之不用。
安静的改变:悄悄修改了原来某些语言特性,例如下面说的寻常算数转换。
最后一条,就是除上面区别外的所有区别。
这里有个例子,感觉蛮好,说的是const限定符的问题。
首先:
void foo(const char *p)
{}
int _tmain(int argc, _TCHAR* argv[])
{
char *pArray;
char Array[10];
pArray = Array;
foo(pArray);
return 0;
}这种情况下,程序正确运行。但是当形参修改为二级指针时,程序便报错。如下:
void foo(const char **p)
{}
.......
foo(&pArray);
error C2664: “foo”: 不能将参数 1 从“char **”转换为“const char **”
理由如下:
在ANSI C标准中,函数的传参过程类似于赋值,那么,要是赋值操作合法,则必须满足如下条件:
每个实参都应该具有自己的类型,这样它的值就可以赋值给与它所对应的形参类型的对象(该对象的类型不能含有限定符)。
两个操作数都是指向有限定符或者无限定符的相容类型的指针,左边指针所指向的类型必须具有右边指针所指向的全部限定符。也就是说,左边的限定符要比右边的严格。因此:
char *cp;
const char *cpp;
cpp = cp;
这里,cpp,cp都是指向char类型数据的指针。加限定符表示cpp指向的是一个const限定的char数据,也就是说,对于操作数cpp,cp来讲,他们都是无限定符的,都是char类型的指针,这是相容的。同时,左边cpp所指向的类型有const限定符,比右边(无)严格,所以,这种传参是可以的。
然而:
char **cp;
const char **cpp;
cpp = cp;
此时cp指向一个char *指针的指针,cpp则是一个指向const char*的指针的指针,两者不是相容类型的指针。因此,不能传参。
寻常算数转换
在K & R C中,
首先任何类型为char和short的操作数被转换成int,任何float的操作数被转换为double类型,
其次,如果其中一个操作数为double/long/unsigned类型,则另一个操作数被转换成double/long/unsigned类型,计算结果也是double/long/unsigned类型。
如果不符合上面几种情况,则操作数类型都是int,计算结果也是int.
ANSI C中,
首先,其中一个操作数类型是long double /double/float,则另外一个操作数也被转换成long double /double/float。
其他情况,则两个操作数执行整形升级,即char,short,int或者int类型位段,包括他们的有符号,或者无符号变形,以及枚举类型,如果int能够完整表示源类型的所有值,那么该源类型就转换成int,否则转换为unsigned int.也就是执行下面的规则:
其中一个是unsigned long int则另外一个转换为unsigned long int,如果其中一个是unsigned int,而另外一个是long int ,则如果long int 能够完整表示unsigned int 所有值,则unsigned int 被转换为long int,否则,两个操作数都转换为unsigned long int。
如果其中一个是long int / unsigned int ,则另外一个转换为long int / unsigned int,除此之外,都转换为int.
也就是说,ANSIC 的寻常算数转换,是指当执行算数运算时,操作数的类型如果不同,就会发生转换,数据类型一般朝着浮点精度更高,长度更长的方向转换,整型如果转换为signed不会丢失信息,则转换为signed,否则转换为unsigned。
K & R C所才采用的是无符号保留,即简单的规定,无符号数和有符号数混合运算时,结果类型时无符号数,这与硬件无关,它会使一个负数丢失符号位。
ANSIC 采用值保留原则,即取决于操作数的类型的相对大小。
多做之过:
所谓多做之过,就是语言中存在某些不应该存在的特性,包括容易出错的switch语句,相邻字符串常量的自动连接,缺陷全局范围。
关于switch语句
对于case可能出现的值,标准的C编译器应该支持一条switch语句至少257个case标签,它对case可能出现的值太过于纵容了,另外,它内部的任何语句都可以加上标签,并执行时跳转到哪里,这就有可能破坏程序流的结构化。
另外,switch语句的case标签,必须要加上break才可以终止跳出,如果全部不加break,就是fall through现象,实际中,97%的fall through的利用都是错误的。
关于缺陷的全局范围
即如果不添加static类型限定,默认情况的函数声明,是所有地方都可见的,您在其他外部引用时,加不加extern限定修饰,都是一样的。C语言对于信息可见性的选择很有限,一个符号,要吗所有人可见,要吗都不可见。
关于相邻字符串常量的自动连接,这里有个例子:
char *ptr_rev[] = {
"string0",
"string1",
"string2"
"string3",
}
如上,string2和string3会自动连接,导致当修改ptr_rev[3]时,实际已经越界,修改了其他变量。顺便提一句,最后那个字符串末尾的逗号,并不是打字错误,而是从最早的的C语法中继承下来的东西,不管存在与否都没有什么意义。ANSIC对它的辩护理由是,称它使C语言在自动生成时,更加容易。实际上,这会使得代码的可读性变差了。
误做之过:
这里涉及到运算符的重载,以及相关关键字的多重意义。另外,还有优先级问题等。这里要记住的一点是:
在优先级和结合性规则告诉你在哪些符号组成一个意群的同时,这些意群内部如何进行计算的次序始终是未定义的。如果编写程序时依赖这些意群的先后计算顺序,那就不是很好的编程风格。
结合性只用于表达式中出现两个以上相同优先级的操作符情况下,用于消除歧义。
对于大部分表达式里各个操作数计算的顺序是不确定的,它的目的是让编译器设计者选取最合适的方法来产生最快的代码,但&& || 等操作数的计算顺序是规定的,严格按照从左到右的顺序计算,当提前得到计算结果时,便结束剩余的计算。但是在函数调用时,各个参数的计算顺序是不确定的。