C语言发展史 作者:李彦锋,腾讯 IEG 运营开发工程师
C语言的特点 作者:海岛Blog
C语言标准 作者:Alexxinlu
C语言编程机制 作者:丑心疼
资料整理:mooc芝麻狐
目录制作:mooc芝麻狐
作者:李彦锋,腾讯 IEG 运营开发工程师
一直想写一篇关于C语言的文章,里面包含C语言的发展史、创始人等相关事迹。但是却迟迟未写,主要原因是因为:在我看来,这个语言太过于伟大、耀眼。作为一个仅仅使用过C语言的普通开发来说,完全没资格去写。但是,最近在看过一篇丹尼斯.里奇写的《C语言发展史》之后,坚定了我写这篇文章的决心。不是歌功颂德,仅仅是以一种客观的视角去欣赏。
任何一种新事物的出现都不是来自于偶然,而是时代所驱使的必然结果。
如果你问我:C语言有多伟大。那么,我可能会想一下,说:多伟大我不知道,但是我知道很伟大。
这里,我想说一句可能有点片面的话,就是:如今这世界上,凡是带电的地方,可能都会有她(C语言)或者她的子孙的影子。
任何比C语言更低级的语言,都不足以完整地抽象一个计算机系统;任何比C高级的语言,都可以用C来实现。
Ritchie 贝尔实验室的个人主页地址 https://www.bell-labs.com/usr/dmr/www/index.html
丹尼斯·麦卡利斯泰尔·里奇(英语:Dennis MacAlistair Ritchie,1941年9月9日-2011年10月12日),美国计算机科学家。黑客圈子通常称他为“dmr”。他是C语言的创造者、Unix操作系统的关键开发者,对计算机领域产生了深远影响,并与肯·汤普逊同为1983年图灵奖得主。
丹尼斯.里奇 生平时间线
麻省理工大学计算机系的马丁教授评价说:"如果说,乔布斯是可视化产品中的国王,那么里奇就是不可见王国中的君主。乔布斯的贡献在于,他如此了解用户的需求和渴求,以至于创造出了让当代人乐不思蜀的科技产品。然而,却是里奇先生为这些产品提供了最核心的部件,人们看不到这些部件,却每天都在使用着。"
克尼汉评价道:牛顿说他是站在巨人的肩膀上,如今,我们都站在里奇的肩膀上。
为了简洁起见,我(Dennis M.Ritchie)省略了对C本身,其父级B [Johnson 73]和其祖父母BCPL [Richards 79]的完整描述,而只关注每种语言的特征元素以及它们如何演变。
This paper is about the development of the C programming language, the influences on it, and the conditions under which it was created. For the sake of brevity, I omit full descriptions of C itself, its parent B [Johnson 73] and its grandparent BCPL [Richards 79], and instead concentrate on characteristic elements of each language and how they evolved.
https://www.bell-labs.com/usr/dmr/www/chist.html
这段文字出自C语言之父丹尼斯.M.里奇
所写的一篇关于《C语言发展史》的文章,文中明确指出C语言源自于B、BCPL两种语言。可以把C语言看做是站在巨人的肩上,顺应时代潮流的后浪。
Martin Richards
Martin Richards's BCPL Reference Manual, 1967 https://web.archive.org/web/20080622171914/http://cm.bell-labs.com/cm/cs/who/dmr/bcpl.html
马丁·理察德(英语:Martin Richards,1940年7月21日-),生于英国,计算机科学家,为BCPL编程语言的发明者,发展了TRIPOS操作系统。
1966年,马丁·理察德在剑桥大学,以CPL编程语言为基础,发明了BCPL编程语言。
Kenneth Lane Thompson
肯尼斯·蓝·汤普逊(英语:Kenneth Lane Thompson,1943年2月4日-)小名肯·汤普逊(英语:Ken Thompson),美国计算机科学学者和工程师。黑客文化圈子通常称他为“ken”。在贝尔实验室工作期间,汤普逊设计和实现了Unix操作系统。他创造了B语言(基于BCPL) — C语言的前身,而且他是Plan 9操作系统的创造者和开发者之一。与丹尼斯·里奇同为1983年图灵奖得主。
2006年,汤普逊进入Google公司工作,与他人共同设计了Go语言。
日益精进
站在巨人的肩上
坐着的是Ken 站着的是Dennis
与优秀之人为伍
不畏得失,做有趣的事
左Ken 右Dennis | 右上角:Unix标识牌
言传身教
感谢丹尼斯.里奇留给了这世界一本“C语言圣经”
可惜的是,当年笔者大学学的是谭浩强谭老师的C语言
Dennis与《The C Programming Language》
互相成就
终成正果
你做了什么,最终会被世人看到
1999年获得美国国家技术奖 [左一:Ken | 左二:Dennis | 右一:克林顿]
从这些老照片中,我隐隐约约看到了几行小字,写着:
1.与优秀之人为伍
2.互相成就
试想,有多么重要?
Computer Languages History https://www.levenez.com/lang/
从图中时间线,可以明显的看出C语言的起源以及时间节点。
有时候不得不说时势造英雄,在1969~1971年之间著名的操作系统Unix从肯.汤普逊手中诞生,作为一种大型的系统性软件来说,极其需要一种可靠的高级语言的出现(当时的低级语言指的是汇编,因为之前的操作系统是用汇编写的)。这个时候的丹尼斯.里奇也没闲着,在对B语言改良之后,就诞生了带有类型的C语言(据里奇自己说,有一段时间称这种改良的语言为NB
。即:new B
。不过,在我们这些吃瓜群众眼中看来也确实NB)。
In 1971 I began to extend the B language by adding a character type and also rewrote its compiler to generate PDP-11 machine instructions instead of threaded code. Thus the transition from B to C was contemporaneous with the creation of a compiler capable of producing programs fast and small enough to compete with assembly language. I called the slightly-extended language NB, for `new B.'
Unix的诞生与C语言被广泛的传播、使用,有着密切的联系。
上图时间线只显示前几个与C语言在相同时间段内诞生的Unix版本(当然,感兴趣的话,可以查询Unix相关发展史,绝对会让你大吃一惊。其中最著名的几个分支:BSD、minix、Linux...)。
下面,通过在网上找得到的部分Unix内核源码,来追溯一下C语言出现的时机。
PDP-Unix系统内核代码文件
PDP-7 Unix https://minnie.tuhs.org/cgi-bin/utree.pl?file=PDP7-Unix
可以看到基本都是用汇编写的(文件名后缀.s)。为什么用基本
这个词呢?因为,在系统里面有一部分命令是用B语言写的。
First Edition Unix系统内核代码文件
https://minnie.tuhs.org/cgi-bin/utree.pl?file=V1
可以看到,还是用汇编写的(文件名后缀.s)。
Second Edition Unix系统内核代码文件
Second Edition Unix The second edition of Unix was developed for the PDP-11 at Bell Labs by Ken Thompson, Dennis Ritchie and others. It extended the First Edition with more system calls and more commands. This edition also saw the beginning of the C language, which was used to write some of the commands.
https://minnie.tuhs.org/cgi-bin/utree.pl?file=V2
到这个版本,已经可以看到C语言的身影了。
从C语言在Unix V2版本出现之后,Unix的V3版本开始,已经可以在Unix内核中见到大量C语言编写的代码。
据里奇所说:到1973年初,现代C的基本知识已经完成。C语言和编译器强大到,足以让我们在当年夏天用C重写PDP-11的Unix内核 (也就是Unix的V3版本)。
By early 1973, the essentials of modern C were complete. The language and compiler were strong enough to permit us to rewrite the Unix kernel for the PDP-11 in C during the summer of that year.
到了这个时间节点,基本可以肯定的是C语言、Unix的大部分核心都已经完善。剩下要做的就是,可移植性、标准化。
后面的故事,大家可能也听说过:后来学术和政府组织中都在使用Unix,也正是由于Unix的风靡与兴盛,带动了C语言被广泛的传播、使用。
在1980年代,C语言的使用广泛传播,并且几乎所有机器体系结构和操作系统都可以使用编译器。尤其是,它已成为个人计算机的编程工具,无论是用于这些机器的商业软件制造商,还是对编程感兴趣的最终用户,都非常受欢迎。
During the 1980s the use of the C language spread widely, and compilers became available on nearly every machine architecture and operating system; in particular it became popular as a programming tool for personal computers, both for manufacturers of commercial software for these machines, and for end-users interested in programming.
这也就是所谓的互相成就。
一句话概括就是:不畏得失、日渐精进,最终互相成就。
不知道你有没有想过,大家都用C语言或基于C语言的语言来写编译器,那么世界上第一个C语言编译器又是怎么编写的呢?这不是一个“鸡和蛋”的问题……
回顾一下C语言历史:Tomphson在BCPL的基础上开发了B语言,Ritchie又在B语言的基础上成功开发出了现在的C语言。在C语言被用作系统编程语言之前,Tomphson也用过B语言编写过操作系统。可见在C语言实现以前,B语言已经可以投入实用了。因此第一个C语言编译器的原型完全可能是用B语言或者混合B语言与PDP汇编语言编写的。
我们现在都知道,B语言的执行效率比较低,但是如果全部用汇编语言来编写,不仅开发周期长、维护难度大,更可怕的是失去了高级程序设计语言必需的移植性。
所以早期的C语言编译器就采取了一个取巧的办法:先用汇编语言编写一个C语言的一个子集的编译器,再通过这个子集去递推,进而完成完整的C语言编译器。
详细的过程如下:先创造一个只有C语言最基本功能的子集,记作C0语言,C0语言已经足够简单了,可以直接用汇编语言编写出C0的编译器。依靠C0已有的功能,设计比C0复杂,但仍然不完整的C语言的又一个子集C1语言,其中C0属于C1,C1属于C,用C0开发出C1语言的编译器。在C1的基础上设计C语言的又一个子集C2语言,C2语言比C1复杂,但是仍然不是完整的C语言,开发出C2语言的编译器 …… 如此直到CN,CN已经足够强大了,这时候就足够开发出完整的C语言编译器的实现了。至于这里的N是多少,这取决于你的目标语言(这里是C语言)的复杂程度和程序员的编程能力。简单地说,如果到了某个子集阶段,可以很方便地利用现有功能实现C语言时,那么你就找到N了。下面的图说明了这个抽象过程:
https://kknews.cc/tech/bx2r3j.html 介绍一个概念,“自编译”Self-Compile,也就是对于某些具有明显自举性质的强类型(所谓强类型就是程序中的每个变量必须声明类型后才能使用,比如C语言,相反有些脚本语言则根本没有类型这一说法)编程语言,可以借助它们的一个有限小子集,通过有限次数的递推来实现对它们自身的表述,这样的语言有C、Pascal、Ada等等,至于为什么可以自编译,可以参见清华大学出版社的《编译原理》,书中实现了一个Pascal的子集的编译器。
https://zhuanlan.zhihu.com/p/136102461
而这个过程也在Unix V2版本中找到了证据。
肯恩·汤普森,丹尼斯·里奇和其他人在贝尔实验室为PDP-11开发了Unix的第二版。它通过更多的系统调用和更多的命令扩展了第一版。此版本还看到了C语言的开始,该语言用于编写一些命令。
此处的代码仅是某些命令,某些库函数和C编译器的源代码。c /中的文件来自 last1120c.tar.gz 磁带,并构成了第二版Unix的有效C编译器。
下载地址:http://minnie.tuhs.org/Archive/Applications/Early_C_Compilers/last1120c.tar.gz
The second edition of Unix was developed for the PDP-11 at Bell Labs by Ken Thompson, Dennis Ritchie and others. It extended the First Edition with more system calls and more commands. This edition also saw the beginning of the C language, which was used to write some of the commands.
The code here is only the source to some of the commands, some of the library functions, and the C compiler. The files in c/ come from the last1120c.tar.gz tape, and form a working C compiler for Second Edition Unix.
https://minnie.tuhs.org/cgi-bin/utree.pl?file=V2
下载源码解压缩之后,目录结构如下:
感兴趣的小伙伴可以下载下来研究一下。
如果想要找到一种好的方式,来进行编程语言之间比较的话,那么非代码莫属。
下面分别使用BCPL、B、C三种语言实现一个简单的程序:程序将三个数字a、b、c相加,并将结果赋值给sum,最后打印总和。
BCPL https://zh.wikipedia.org/wiki/BCPL
- GET "libhdr"
-
- LET start() = VALOF
- { LET a, b, c = 1, 2, ,3
-
- sum := a + b + c
- writen(sum)
- }
LET 声明变量
:=
符号为赋值符号 Go中也有该符号,表示函数内部局部变量。这里感觉很有意思的一点是:最初B语言之父肯.汤普逊
把:=
符号改成了=
符号。现在,也作为Go语言之父之一,又把:=
符号请回来了(冥冥之中的命运~)。
从BCPL到B的过渡中,决定使用单个字符 = 代替赋值 :=
Other fiddles in the transition from BCPL to B were introduced as a matter of taste, and some remain controversial,
for example the decision to use the single character = for assignment instead of :=
. Similarly, B uses /**/ to enclose comments, where BCPL uses //, to ignore text up to the end of the line. The legacy of PL/I is evident here. (C++ has resurrected the BCPL comment convention.) Fortran influenced the syntax of declarations: B declarations begin with a specifier like auto or static, followed by a list of names, and C not only followed this style but ornamented it by placing its type keywords at the start of declarations.https://www.bell-labs.com/usr/dmr/www/chist.html
A TUTORIAL INTRODUCTION TO THE LANGUAGE B https://web.archive.org/web/20070807110157/http://cm.bell-labs.com/cm/cs/who/dmr/btut.html
B语言的语言结构
- main() {
- -- statements --
- }
-
- newfunc(arg1, arg2) {
- -- statements --
- }
-
- fun3(arg) {
- -- more statements --
- }
B语言代码示例
- main() {
- auto a, b, c, sum;
-
- a = 1; b = 2; c = 3;
- sum = a+b+c;
- putnumb(sum);
- }
语句auto ...
是一个声明。即,它定义了要在函数内使用的局部变量
putnumb 是一个带参数的库函数,它将在终端上打印一个数字
- #include
-
- void main(){
- int a,b,c,sum;
-
- a=1; b=2; c=3;
- sum = a+b+c;
- printf("%d", sum);
- }
通过上面例子可以三者的区别:
C语言写法更接近于B语言
BCPL、B语言都是无类型的语言,用word/cell表示一个固定长度的bit。C语言是有类型的
有一些地方,你可能感兴趣:
++
、--
符号是Thompson发明的
&&
、||
是在C语言引入的
说明:
查了好久只找到了BCPL、B语言的部分代码片段,至于能不能跑起来,我也不知道 ^_^
如果想要知晓三者的具体区别的话,建议阅读丹尼斯.里奇关于《C语言发展史》的文章
BCPL、B语言也有经历过若干次版本迭代 (因为,网上找到的代码片段有很多写法不一样的地方。比如说,维基百科中找到的B代码片段,与在Unix内核前几个版本中找到的B代码片段写法就不一样。个人推测是版本问题,不同的版本不同的写法)
C语言更接近与B语言,或着说是在B的基础上不断的添加了很多新特性 (抛出2个问题:1.里奇起名字时为什么不像C++一样,起名叫B++ ?
2.为什么C++用了2个加号,而不是一个加号,叫C+ ?
欢迎脑洞够大的同学在评论留言!)
如果有高手觉得上面的代码片段有问题或者知道怎么跑起来的话,可以私下交流
1960s年代后期,贝尔实验室对计算机系统的研究进入繁盛时期。MIT、General Electric、Bell实验室合作的Mutlics项目以失败而告终(1969年左右)。就是在这个时期,Ken Tompson开始写Mutlics的替代品,他希望按照自己的设计构造一个令人舒服的计算系统(也就是Unix)。后来在写出第一个版本的Unix时,觉得Unix上需要一个新的系统编程语言,他创造了一个B语言。B语言是没有类型的C,准确说B语言是Tompson把BCPL挤进8K内存,被其个人大脑过滤后的产生的语言。
由于B语言存在的一些问题,导致其只是被用来写一些命令工具使用。恰好在这个时期,Ritchie在B语言的基础上,进行了重新的设计改良,从而诞生了C语言。
1973年,C语言基本上已经完备,从语言和编译器层面已经足够让Tompson和Ritchie使用C语言重写Unix内核。后来,Unix在一些研究机构、大学、政府机关开始慢慢流行起来,进而带动了C语言的发展。
1978年,K&R编写的《The C Programming Language》出版,进一步推动了C语言的普及。
用一句话总结就是:对的时间、对的地点,出现了对的人以及工具 (Unix与C语言的关系,有点像GNU与Linux kernel的关系,都是互相成就)。
C语言及其标准经过若干次迭代之后,就成了今天大家看到的样子。其标准中指定了很多C标准库,而不同的系统都有自己不同的代码实现。
当然,Linux内核中也有实现了标准C库的代码,下面一起欣赏她的美。
ANSI C共包括15个头文件。1995年,Normative Addendum 1(NA1)批准了3个头文件(iso646.h、wchar.h和wctype.h)增加到C标准函数库中。C99标准增加6个头文件(complex.h、fenv.h、inttypes.h、stdbool.h、stdint.h和tgmath.h)。C11标准中又新增了5个头文件(stdalign.h、stdatomic.h、stdnoreturn.h、threads.h和uchar.h)。
至此,C标准函数库共有29个头文件:
https://www.wikiwand.com/zh-sg/C%E6%A8%99%E6%BA%96%E5%87%BD%E5%BC%8F%E5%BA%AB#/%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE
linux kernel版本:4.18.13
lnux kernel 地址 https://www.kernel.org/
下面列出3个字符串处理函数 strcpy()、strncpy()、strncat()。代码出自Linus Benedict Torvalds之手,为什么这么说?看代码头部注释,还是那个熟悉的味道stupid
。看过git源代码的人应该也会知道,git源码中也有类似注释。
- // SPDX-License-Identifier: GPL-2.0
- /*
- * linux/lib/string.c
- *
- * Copyright (C) 1991, 1992 Linus Torvalds
- */
-
- /*
- * stupid library routines.. The optimized versions should generally be found
- * as inline code in
- *
- * These are buggy as well..
- *
- * * Fri Jun 25 1999, Ingo Oeser
- * - Added strsep() which will replace strtok() soon (because strsep() is
- * reentrant and should be faster). Use only strsep() in new code, please.
- *
- * * Sat Feb 09 2002, Jason Thomas
, - * Matthew Hawkins
- * - Kissed strtok() goodbye
- */
-
- // .......omit other......
- // ...... here is my love code .....
-
- #ifndef __HAVE_ARCH_STRCPY
- /**
- * strcpy - Copy a %NUL terminated string
- * @dest: Where to copy the string to
- * @src: Where to copy the string from
- */
- #undef strcpy
- char *strcpy(char *dest, const char *src)
- {
- char *tmp = dest;
-
- while ((*dest++ = *src++) != '\0')
- /* nothing */;
- return tmp;
- }
- EXPORT_SYMBOL(strcpy);
- #endif
-
- #ifndef __HAVE_ARCH_STRNCPY
- /**
- * strncpy - Copy a length-limited, C-string
- * @dest: Where to copy the string to
- * @src: Where to copy the string from
- * @count: The maximum number of bytes to copy
- *
- * The result is not %NUL-terminated if the source exceeds
- * @count bytes.
- *
- * In the case where the length of @src is less than that of
- * count, the remainder of @dest will be padded with %NUL.
- *
- */
- char *strncpy(char *dest, const char *src, size_t count)
- {
- char *tmp = dest;
-
- while (count) {
- if ((*tmp = *src) != 0)
- src++;
- tmp++;
- count--;
- }
- return dest;
- }
- EXPORT_SYMBOL(strncpy);
- #endif
-
- #ifndef __HAVE_ARCH_STRLCPY
-
- #ifndef __HAVE_ARCH_STRNCAT
- /**
- * strncat - Append a length-limited, C-string to another
- * @dest: The string to be appended to
- * @src: The string to append to it
- * @count: The maximum numbers of bytes to copy
- *
- * Note that in contrast to strncpy(), strncat() ensures the result is
- * terminated.
- */
- char *strncat(char *dest, const char *src, size_t count)
- {
- char *tmp = dest;
-
- if (count) {
- while (*dest)
- dest++;
- while ((*dest++ = *src++) != 0) {
- if (--count == 0) {
- *dest = '\0';
- break;
- }
- }
- }
- return tmp;
- }
- EXPORT_SYMBOL(strncat);
- #endif
-
- #ifndef __HAVE_ARCH_STRLCAT
-
- // .......omit other......
- // ...... here is my love code .....
第一次跟同学一起看这些代码的时候,他说了一句话:这才叫代码,其他的都是s-h-X-t
。现在回想起,自己在实现这些代码时,写了一坨不知道是什么的东西。哎,代码比代码要扔~
阅读他人代码, 也是一种进步、成长
《史记·廉颇蔺相如列传》记载,廉颇被免职后,跑到魏国,赵王想再用他,派人去看他的身体情况,廉颇之仇郭开贿赂使者,使者看到廉颇,廉颇为之米饭一斗,肉十斤,被甲上马,以示尚可用。使者回来报告赵王说:"廉颇将军虽老,尚善饭,然与臣坐,顷之三遗矢(通假字,即屎)矣。"赵王以为廉颇已老,遂不用。
经历过几十年的风雨洗礼,C语言可谓风光无数,这世界上随处可见它的身影。但是,同时在一些人眼里,可能觉得C语言已是暮年(将近50岁)、老矣。如同下图:
如果你真这样想,那你就错了。
TIOBE Index for September 2020
TIOBE 2020-09 编程语言排行榜告诉你,C语言宝刀未老,还是那个风采耀眼的少年。
个人想说的是,只要计算机还是基于冯诺依曼体系结构,芯片还是基于物理制程。那么,都会有一片C的天空。因为,她知道一个最接近天空的地方(C是最接近汇编、机器语言的高级语言之一)。
任他上层应用改朝换代,我(C语言)自岿然不动。这就是C,我心中的C语言。
猛然间发现已经到了总结,但是还觉得仍旧意犹未尽,这并不是我心目中最真实的那个她。但是,我还是希望你看完本文之后,能够多少了解与熟悉C的美与真实。
最后想说的是:纵使千言万语也说不尽C语言的重要性,这些文字也仅仅只是冰山一角。
鉴于个人能力有限,如有问题或者缺陷,欢迎指正。
在整理的过程中,部分参考、引用下面链接地址内容:
[1] https://www.bell-labs.com/usr/dmr/www/index.html 里奇贝尔实验室主页
[2] https://www.bell-labs.com/usr/dmr/www/chist.html C语言发展史
[3] https://www.bell-labs.com/usr/dmr/www/1stEdman.html Unix Programmer's Manual
[4] https://www.bell-labs.com/usr/dmr/www/bcpl.html Martin Richards's BCPL Manual
[5] https://www.levenez.com/lang/ Computer Languages History
[6] https://www.levenez.com/unix/ Unix History
[7] https://minnie.tuhs.org/cgi-bin/utree.pl The Unix Tree(可以看到很多老系统的源代码)
[8] https://zh.wikipedia.org/wiki/丹尼斯·里奇
[9] https://www.tiobe.com/tiobe-index/ TIOBE
[10] http://web.eah-jena.de/~kleine/history/ Historic Documents in Computer Science
1. C语言简洁、紧凑、灵活。C语言的核心内容很少,只有32个关键字,9种控制语句;程序书写格式自由,压缩了一切不必要的成分。
2. 表达方式简练、实用。C语言有一套强有力的运算符,达44种,可以构造出多种形式的表达式,用一个表达式就可以实现其它语言可能要用多条语句才能实现的功能。
3. 具有丰富的数据类型。数据类型越多,数据的表达能力就越强。C语言具有现代语言的各种数据类型,如:字符型、整型、实型、数组、指针、结构体和共用体等。可以实现诸如链表、堆栈、队列、树等各种复杂的数据结构。其中指针使参数的传递简单、迅速,节省内存。
4. 具有低级语言的特点。具有与汇编语言相近的功能和描述方法,如地址运算、二进制数位运算等,对硬件端口等资源直接操作,可充分使用计算机资源。因此,C语言既具有高级语言便于学习和掌握的特点,又具有机器语言或汇编语言对硬件的操作能力。所以,C语言既可以作为系统描述语言,又可以作为通用的程序设计语言。
5. 是一种结构化语言,适合于大型程序的模块化设计。C语言提供了编写结构化程序的基本控制语句,如if~else语句、switch语句、while语句、do~while语句等。C程序是函数的集合,函数是构成C程序的基本单位,每个函数具有独立的功能,函数之间通过参数传递数据。除了用户编写的函数外,不同的编译系统、操作系统都相伴还提供了大量的库函数供用户使用,如输入输出函数、数学函数、字符串处理函数等,灵活使用库函数可以简化程序的设计。
6. 各种版本的编译系统都提供了预处理命令和预处理程序。预处理扩展了C语言的功能,提高了程序的可移植性,为大型程序的调试提供了方便。
7. 可移植性好。程序可以从一个环境不经改动或稍加改动就可移植到另一个完全不同的环境中运行。这是因为系统库函数和预处理程序将可能出现的与机器有关的因素与源程序隔开,这就容易在不同的C编译系统之间重新定义有关内容。
8. 生成的目标代码质量高。由C源程序得到的目标代码的运行效率比用汇编语言写的也不过只低10%到20%,可充分发挥机器的效率。
9. C语言语法限制不严,程序设计自由度大。C程序在运行时不做诸如数组下标越界和变量类型兼容性等检查,而是由编程者自己保证程序的正确性。C语言几乎允许所有的数据类型的转换,字符型和整型可以自由混合使用,所有类型均可作逻辑型,可自己定义新的类型,还可以把某类型强制转换为指定的类型。实际上,这使编程者有了更大的自主性,能编写出灵活、优质的程序,同时也给初学者增加了一定的难度。所以,只有在熟练掌握C语言程序设计后,才能体会出其灵活的特性。
通过上述的介绍,已经了解了C语言的若干特点。C语言虽然是一种优秀的计算机程序设计语言,但也存在以下的一些缺点,了解这些缺点,才能够在实际使用中扬长避短。
1. C程序的错误更隐蔽。C语言的灵活性使得用它编写程序时更容易出错,而且C语言的编译器不检查这样的错误。与汇编语言类似,需要程序运行是才能发现这些逻辑错误。C语言还会有一些隐患,需要程序员重视,比如将比较的"=="写成赋值"=",语法上没有错误,这样的逻辑错误不易发现,要找出来往往十分费时。
2. C程序有时会难以理解。C语言语法成分相对简单,是一种小型语言。但是,其数据类型多,运算符丰富且结合性多样,使得对其理解有一定的难度。有关运算符和结合性,人们最常说的一句话是“先乘除,后加减,同级运算从左到右”,但是C语言远比这要复杂。发明C语言时,为了减少字符输入,C语言比较简明,同时也使得C语言可以写出常人几乎无法理解的程序。
3. C程序有时会难以修改。考虑到程序规模的大型化或者说巨型化,现代编程语言通常会提供“类”和“包”之类的语言特性,这样的特性可以将程序分解成更加易于管理的模块。然而C语言缺少这样的特性,维护大型程序显得比较困难。
早期的计算机语言有BASIC语言、Fortran语言、ALGOL语言、COBOL语言和Pascal语言等,近来使用的人相对要少得多,除非是既有的软件系统使用这些语言,或者一些人想使用现成的程序或软件,才会使用这些语言。
现在的软件开发中更多地使用C++语言和Java语言,在开发Web应用软件时则会使用JSP语言和PHP语言等。随着面向对象技术的广泛普及,Java语言受到很多人的青睐,这是由于Java语言具有编程效率高,降低软件开发成本,不需要考虑存储的分配与回收等程序细节,编写出来的程序更具有健壮性,但是也一定程度上付出了运行效率的代价。C++语言则介于C语言和Java语言之间,也是面向对象的计算机语言,同时具有编程效率高和运行速度快的特点。
C语言是一种过程性的语言,职业的程序员或软件开发人员应该学习一下该计算机语言。这是因为,C语言可以代替机器语言或汇编语言编写运行速度快的程序;对于单片机应用、嵌入式系统和通信软件等是不可替代的;C语言的指针与计算机硬件的地址具有异曲同工之处,是了解计算本质的钥匙;通过C语言相关的存储分配函数,可以深入了解计算机存储分配的原理。
C语言标准的发展主要分为以下几个阶段:
1978年,丹尼斯·里奇(Dennis Ritchie)和布莱恩·科尔尼干(Brian Kernighan)出版了一本书,名叫《The C Programming Language》。这本书被C语言开发者们称为“K&R”,很多年来被当作C语言的非正式的标准说明。人们称这个版本的C语言为“K&R C”。
为统一C语言版本,1983年美国国家标准局(American National Standards Institute,简称ANSI)成立了一个委员会,来制定C语言标准。1989年C语言标准被批准,被称为ANSI X3.159-1989 “Programming Language C”。这个版本的C语言标准通常被称为ANSI C。又由于这个版本是 89年完成制定的,因此也被称为C89。
后来ANSI把这个标准提交到ISO(国际化标准组织),1990年被ISO采纳为国际标准,称为ISO C。又因为这个版本是1990年发布的,因此也被称为C90。所以ANSI C、ISO C、C89、C90这4个标准的内容其实是一样的。
在ANSI C标准确立之后,C语言的规范在很长一段时间内都没有大的变动。1995年C程序设计语言工作组对C语言进行了一些修改,成为后来的1999年发布的ISO/IEC 9899:1999标准,通常被成为C99。但是各个公司对C99的支持所表现出来的兴趣不同。当GCC和其它一些商业编译器支持C99的大部分特性的时候,微软和Borland却似乎对此不感兴趣。
在2011年12月,ANSI采纳了ISO/IEC 9899:2011标准,这个标准通常即C11。
2018年6月发布的ISO/IEC 9899:2018标准,这个标准被称为C18,是目前最新的C语言编程标准,该标准主要是对C11进行了补充和修正,并没有引入新的语言特性。
下一个版本的C语言标准,预计将于2022年12月1日完成。
C (programming language)
The Standard
C语言程序从源代码到二进制行程序都经历了那些过程?本文以Linux下C语言的编译过程为例,讲解C语言程序的编译过程。
编写hello world C程序:
- // hello.c
- #include
- int main(){
- printf("hello world!\n");
- }
编译过程只需:
- $ gcc hello.c # 编译
- $ ./a.out # 执行
- hello world!
这个过程如此熟悉,以至于大家觉得编译事件很简单的事。事实真的如此吗?我们来细看一下C语言的编译过程到底是怎样的。
上述gcc命令其实依次执行了四步操作:1.预处理(Preprocessing), 2.编译(Compilation), 3.汇编(Assemble), 4.链接(Linking)。
为了下面步骤讲解的方便,我们需要一个稍微复杂一点的例子。假设我们自己定义了一个头文件mymath.h
,实现一些自己的数学函数,并把具体实现放在mymath.c
当中。然后写一个test.c
程序使用这些函数。程序目录结构如下:
- ├── test.c
- └── inc
- ├── mymath.h
- └── mymath.c
程序代码如下:
- // test.c
- #include
- #include "mymath.h"// 自定义头文件
- int main(){
- int a = 2;
- int b = 3;
- int sum = add(a, b);
- printf("a=%d, b=%d, a+b=%d\n", a, b, sum);
- }
头文件定义:
- // mymath.h
- #ifndef MYMATH_H
- #define MYMATH_H
- int add(int a, int b);
- int sum(int a, int b);
- #endif
头文件实现:
- // mymath.c
- int add(int a, int b){
- return a+b;
- }
- int sub(int a, int b){
- return a-b;
- }
预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc
的预处理是预处理器cpp
来完成的,你可以通过如下命令对test.c
进行预处理:
gcc -E -I./inc test.c -o test.i
或者直接调用cpp
命令
$ cpp test.c -I./inc -o test.i
上述命令中-E
是让编译器在预处理之后就退出,不进行后续编译过程;-I
指定头文件目录,这里指定的是我们自定义的头文件目录;-o
指定输出文件名。
经过预处理之后代码体积会大很多:
X | 文件名 | 文件大小 | 代码行数 |
---|---|---|---|
预处理前 | test.c | 146B | 9 |
预处理后 | test.i | 17691B | 857 |
预处理之后的程序还是文本,可以用文本编辑器打开。
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。编译的指定如下:
$ gcc -S -I./inc test.c -o test.s
上述命令中-S
让编译器在编译之后停止,不进行后续过程。编译过程完成后,将生成程序的汇编代码test.s
,这也是文本文件,内容如下:
- // test.c汇编之后的结果test.s
- .file "test.c"
- .section .rodata
- .LC0:
- .string "a=%d, b=%d, a+b=%d\n"
- .text
- .globl main
- .type main, @function
- main:
- .LFB0:
- .cfi_startproc
- pushl %ebp
- .cfi_def_cfa_offset 8
- .cfi_offset 5, -8
- movl %esp, %ebp
- .cfi_def_cfa_register 5
- andl $-16, %esp
- subl $32, %esp
- movl $2, 20(%esp)
- movl $3, 24(%esp)
- movl 24(%esp), %eax
- movl %eax, 4(%esp)
- movl 20(%esp), %eax
- movl %eax, (%esp)
- call add
- movl %eax, 28(%esp)
- movl 28(%esp), %eax
- movl %eax, 12(%esp)
- movl 24(%esp), %eax
- movl %eax, 8(%esp)
- movl 20(%esp), %eax
- movl %eax, 4(%esp)
- movl $.LC0, (%esp)
- call printf
- leave
- .cfi_restore 5
- .cfi_def_cfa 4, 4
- ret
- .cfi_endproc
- .LFE0:
- .size main, .-main
- .ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
- .section .note.GNU-stack,"",@progbits
请不要问我上述代码是什么意思!-_-
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。gcc
汇编过程通过as
命令完成:
$ as test.s -o test.o
等价于:
gcc -c test.s -o test.o
这一步会为每一个源文件产生一个目标文件。因此mymath.c
也需要产生一个mymath.o
文件
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
命令大致如下:
$ ld -o test.out test.o inc/mymath.o ...libraries...
经过以上分析,我们发现编译过程并不像想象的那么简单,而是要经过预处理、编译、汇编、链接。尽管我们平时使用gcc
命令的时候没有关心中间结果,但每次程序的编译都少不了这几个步骤。也不用为上述繁琐过程而烦恼,因为你仍然可以:
- $ gcc hello.c # 编译
- $ ./a.out # 执行
1.https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html
2.http://www.trilithium.com/johan/2005/08/linux-gate/
3.https://gcc.gnu.org/onlinedocs/gccint/Collect2.html