Last Updated: 2020-07-01
在阅读本文档前,请先阅读本教程的整体介绍,详见 Introduction 。
大部分编程语言都会有自己的标准,这其中就包括 C 语言。当然,编程语言的初学者无需非常深入地去了解语言的标准,只需要有个大概的认识即可,当你对这门语言有了足够的领悟之后再去深入了解也不迟。
C 语言标准在几十年中没有经历过巨大的变革,毕竟和五花八门的新兴语言不同, C 语言是一门传统的、接近底层的、面向过程的语言,这使得 C 语言标准的制定者无需去考虑那些复杂的面向对象相关的问题。
C 语言被绝大多数大学选择作为本科计算机专业的入门语言,并且归功于那本广为流传的毒瘤教材(《C程序设计》,作者谭浩强),大多数初学者,甚至是一些教师,都搞不清楚 C 语言标准,甚至是弄错弄混。这篇文章将帮助你弄清楚 C 语言标准到底是怎么一回事,也会对几个常见的主流标准之间的差异进行详细的介绍。
不要认为这是没有意义的,即便你对这些东西不感兴趣(其实我自己也不怎么感兴趣),你也至少应当对 C89/C90 和 C99 有一个基本的认识,因为它们最为常见,否则你在以后的学习和编程中可能会陷入困惑。
我们首先对 C 语言标准的变迁历程进行一个大致的介绍,帮读者确立一个基本的认识。
本节中的内容主要是对 Wikipedia 的 C 语言条目中的内容的翻译和精简,如果读者有兴趣可以前往本文底部的链接自行查阅。
下表列出了 C 语言标准所经历的主要时间阶段。不过我并不打算和读者探讨这门语言的详细的发展史,毕竟这没什么意义。这张表只是为了提供一个总览而已。
时间节点 | 创立的 C 语言标准 |
---|---|
1972 | 最初的诞生 |
1978 | K&R C |
1989/1990 | C89/C90 |
1999 | C99 |
2011 | C11 |
2017/2018 | C18 |
C 语言诞生于1972年,它是从 B 语言发展衍生出来的。这其中的历史故事我不打算赘述。
1978年, Brian Kernighan 和 Dennis Ritchie 发布了《The C Programming Language》的第一版,这本书被大众认为是 C 语言的母书之一,也是被极力推荐的 C 语言入门教材之一。这本书的第一版中所介绍的 C 语言被称为 K&R C ,这是最早的、经过精心设计的 C 语言标准。
很多在今天看来非常普通的语法都是在 K&R C 中被首次定义的,但我不打算在本文中介绍具体是哪些,因为现在几乎不会使用比 K&R C 更早的 C 语言标准了,所以初学者没必要特地去区分出 K&R C 中追加的特性。
时至今日, K&R C 仍然被认为是 C 语言标准的“最低公分母”。如果你在以后的开发中使用 C 语言,并希望你的程序具有最大的可移植性,你就应当使用 K&R C 标准,以使得程序能在绝大多数运行环境下正常工作。当然,如果你对可移植性没有那么高的要求,就不必使用古老的 K&R C 了,可能 C89 就足以满足要求,这要根据实际情况来判断。
不过在初学阶段你不需要在意这些,毕竟大多数人将来并不会从事 C 语言的开发,而本科教学通常也不会使用这么古老的标准,你只需要能够知道 K&R C 的存在即可。
正如其名, C89/C90 标准诞生于1989/1990年。
1989年, 美国国家标准协会 (American National Standards Institute, ANSI) 建立了 C 语言的标准规范,并被批准为 ANSI X3.159-1989 ,通常称为 ANSI C ,有时也称为 Standard C 或 C89 。
1990年, 国际标准化组织 (International Organization for Standardization, ISO) 采用了 ANSI C 标准,对其格式有所更改。该标准的正式名称为 ISO/IEC 9899:1990 ,简称为 ISO C ,有时也称为 C90 。
国际标准化组织仅对 C89 的格式进行了一些更改,而技术内容本身没有任何区别,因此 C89 和 C90 指的是相同的编程语言标准。我们常常见到形如 C89/C90 的记法,就是因为 C89 和 C90 在本质上是一样的。
从 C90 以后, ANSI 与其他国家标准机构一样,不再单独开发 C 标准,而是遵循由工作组 ISO/IEC JTC1/SC22/WG14 维护的国际 C 标准。
后来 ANSI 和 ISO 分别撤销了 C89 和 C90 的标准,但是它们至今仍然被广泛使用。
大多数现代 C 语言代码都是基于 C89 的。因此我也不打算在这篇面向初学者的文章里介绍 C89 相比于 K&R C 的新特性,这对初学者而言不是必要的内容。
1995年, ISO 针对 C90 标准发布了一个扩展,称为修订1,它的正式名称为 ISO/IEC 9899:1990/AMD1:1995 ,简称为 C95 。
除此以外, ISO 还针对 C90 发布了两个技术勘误,包括1994年的 ISO/IEC 9899:1990/Cor 1:1994 TCOR1 和1996年的 ISO/IEC 9899:1990/Cor 2:1996 。
C95 主要是更正了一些 C90 中的细节,并为国际字符集增加更多的支持。由于 C95 相对于 C89/C90 而言并没有大的变化,多数情况下我们不会提及 C95 ,甚至有时只把它当作是 C90 的一个修订版。
1999年, ISO 发布了 ISO/IEC 9899:1999 ,通常称为 C99 。
后来 ISO 针对 C99 发布了三个技术勘误,包括 ISO/IEC 9899:1999/Cor 1:2001(E) 、 ISO/IEC 9899:1999/Cor 2:2004(E) 和 ISO/IEC 9899:1999/Cor 3:2007(E) 。
C99 相对于 C89 而言引入了不少新特性,算得上是 C 语言标准的一次大规模变化,其中有些是为了适应计算机软件的发展,有些则是引入了 C++ 中出现的一些有益的内容。
事实上,有些 C99 引入的新特性已经被许多编译器无形地实现在 C89 中,不需要在编译器中启用 C99 就能使用。例如 //
的单行注释事实上是 C99标准才引入的,这一特性来自 C++ 语言,在最初的 C89 中并没有这种语法,但是在许多编译器中无需启用 C99 也可以合法使用单行注释。也有些新特性被实现为扩展功能。
由于 C89 和 C99 的差异相对较大,你需要对 C99 引入的新特性有所了解,知道哪些语法和功能是 C89 不支持的。
考虑到 C11 和 C18 的普及程度远不如 K&R C 、 C89 和 C99 ,并且本科课程中也通常不会使用,我们就不在本文中过多地介绍了。
如果需要设置标准为 C89 ,可以使用编译参数 -std=c89
。如果需要设置标准为带有 GNU 扩展的 C89 ,可以使用编译参数 -std=gnu89
。
如果需要设置标准为 C99 ,可以使用编译参数 -std=c99
。如果需要设置标准为带有 GNU 扩展的 C99 ,可以使用编译参数 -std=gnu9x
。
gcc 编译器默认使用带有 GNU 扩展的 C89 作为标准,即以 -std=gnu89
作为默认值。
实际上,即便你指定了所用的标准,一些不属于所用标准的语法事实上会被编译通过,这是编译器在背地里干的好事。为了保证兼容性,可以使用编译参数 -pedantic-errors
要求编译器对所有不遵循标准 C 的代码严格地报告编译错误,拒绝兼容任何当前标准不支持的语法以及任何扩展语法。
C99 引入的新特性仅仅是一条条罗列出来都能占据两页纸,因此我不打算在面向初学者的文章中逐一介绍,这里只会给出一些初学者需要注意的变化。并不是说这里没有列出的新特性就不重要,只是它们可能相对来说不是特别常用,对于初学者而言不需要去关注而已。
注意,许多 C99 特性已经被编译器模糊地支持了(即便是在 C89 标准下),因此如果你的编译器能编译通过也不要感到奇怪。你可以使用编译参数 -pedantic-errors
,那么编译器就会关闭所有的扩展支持,并严格地遵循你所设置的 C 语言标准。
在大多数情况下 C99 都是向后兼容 C89 的,但在这一方面变得更为严格了。在 C99 中,缺少类型说明符的声明不再隐式地假定为 int 类型,包括函数返回值和参数的声明。
也就是说,在 C89 中下列代码可以正确编译,而在 C99 中则不行:
在 gcc 编译器中使用编译参数 -std=c99 -pedantic-errors
会得到如下报错:
C 标准委员会认为,对于编译器而言,相比于默默地处理那些依赖于隐式 int 声明的历史遗留代码,诊断出类型说明符的无意遗漏更为重要。
在 C99 之前, C 语言标准要求函数体内的声明语句全部放在其他语句之前,显然这是一个不太人性化的规定,只要不是太老的编程语言几乎都没有这样的语法规定。
从 C99 开始, C 语言允许程序中混合声明和代码,换句话说,在 C99 中下列代码可以正确编译,而在 C89 中则不行:
在 gcc 编译器中使用编译参数 -std=c89 -pedantic-errors
会得到如下报错:
在计算机编程中, 变长数组 (variable length array, or runtime-sized array) 指的是长度在运行时确定(而不是在编译时确定)的数组。变长数组的意思并不是说数组的长度可以随意变化,不要被它的名字误导了。
在 C99 之前,数组的长度必须在编译时确定。通俗地讲,你不能使用一个变量的值作为数组的长度,哪怕这个变量是由 const
修饰的。如果希望将多个数组的长度统一用一个量来表示,我们只能使用宏定义来达成目的:
从 C99 开始, C 语言支持使用变长数组,换句话说,在 C99 中下列代码可以正确编译,而在 C89 中则不行:
在 gcc 编译器中使用编译参数 -std=c89 -pedantic-errors
会得到如下报错:
注:在 C 语言的类型系统中,变长数组被归类为 variably modified type 。目前我没有找到该类型的一个合适的中文翻译。
从 C99 开始, C 语言支持使用 //
来实现单行注释。你可能会对此感到惊讶,但这确实是 C99 才提出的,虽然绝大多数编译器都会允许你在 C89 设置下使用单行注释,但如果使用严格的编译参数,编译器就会毫不留情地给出一个编译错误。
在 C99 中下列代码可以正确编译,而在 C89 中则不行:
在 gcc 编译器中使用编译参数 -std=c89 -pedantic-errors
会得到如下报错:
在 C99 之前,我们无法在 for 循环的初始化语句中声明临时变量,从程序设计的角度讲这样并不是好事,因为除了代码的可读性和美观问题以外,从逻辑上讲,变量 应当仅供 for 循环内部使用,但是现在,循环外部的代码可以随意地访问 的值:
在上例中,这当然不会有什么问题,但是在有些时候就未必了。
从 C99 开始, C 语言支持 for 循环初始化声明,我们可以让 成为真正意义上的临时变量:
或许作为初学者的你还并不能理解,那么请你记住:在编译环境允许的情况下,尽可能将 这类循环体内使用的临时变量声明为真正的临时变量。
C99 新增了对 复合字面量 (compound literal) 的支持。
下面这段代码演示了数组的复合字面量的使用方法。程序的第 行声明并初始化了一个指针 ,它指向一个匿名的、长度为 的数组的首元素。
下面这段代码演示了结构体的复合字面量的使用方法。在 C89 中,只有第 行能通过编译,而第 行将报告错误,因为 C89 只允许在初始化时使用 {}
语法,从 C99 开始才提供了对复合字面量的支持。
C99 新增了对 指定初始值 (designated initializer) 的支持。
在 C89 中,如果你想初始化一个稀疏数组,可能只能这么做:
或者这么做:
而指定初始值的特性允许你以这样的语法对数组进行初始化,这无疑带来了一些便捷:
类似的特性也存在于结构体的初始化和赋值中,其语法如下所示:
上述代码的输出结果如下:
long long int 类型
C99 新增了 long long int 数据类型。在大部分 PC 机上 long long 类型是 位的,有符号数和无符号数的取值范围分别为 和 。
如果进行实际工程开发的话需要注意,这个取值范围在不同的机器上可能会不一样,可以用 sizeof()
运算符查看其实际位数。
但是这个 long long 类型并没有这么简单,当你想使用 scanf/printf 对 long long 类型的变量进行输入输出时,应当使用什么样的格式符号,是取决于代码运行环境的,可能是 %I64d
或 %lld
之一,也可能这两种格式符号都合法。
如果搞不清楚的话可以先试试 %lld
,现在在多数情况下 %lld
都是可用的。
_Bool 类型
C99 新增了 _Bool 数据类型,用于表示布尔值。
现代的编程语言基本上都提供了布尔类型, C 语言也做出了改进。布尔类型的值只能是 或 。
可能是为了方便或是与 C++ 语言相统一, C99 还增加了一些宏定义,包含于头文件 <stdbool.h>
中。实际上这个头文件里的内容非常少,排除掉为了和 C++ 语言兼容而写下的条件宏,就只有这么点内容:
也就是说,你可以像这样直观地使用布尔类型,用 true/false 代替 0/1 :
当然,如果不使用这个头文件,你也可以像这样等效地使用布尔类型,只是这不太方便,代码看起来也没有 true/false 那么直观:
_Complex 和 _Imaginary 类型
C99 新增了 _Complex 和 _Imaginary 数据类型,用于表示复数和虚数,包含于头文件 <complex.h>
中。
关于 C99 复数的内容我不打算展开介绍,毕竟这对初学者来说不是必要的知识,有兴趣的读者可自行查阅相关资料。
C99 新增了 种保留字:
_Bool
, _Complex
, _Imaginary
:上一节已经介绍过了。
inline
:用于定义内联函数。 C 语言在 C99 首次引入内联函数,相比于 C++ 语言, C 语言中的内联函数有诸多复杂的规则,初学者无需深入了解。
restrict
:用于修饰指针,初学者无需深入了解。
有一些关于 main 函数的谣言广为流传,因此我特别用一节来解释这个问题。
一些说法认为,在 C89 中允许 main 函数的返回值为 void 类型,而从 C99 开始要求 main 函数的返回值必须为 int 类型:
事实上,虽然有些编译器允许这种形式,但是还没有任何标准考虑接受它。 C++ 之父 Bjarne Stroustrup 在他的主页上的 FAQ 中明确地表示 void main()
的定义从来就不存在于 C 或 C++ 中。我专门查阅了 C89 和 C99 标准,也没有见到任何允许 void main()
的说明。所以,编译器不必接受这种形式,并且很多编译器也不允许这么写。
事实上,当我在gcc编译器中使用编译参数 -std=c89 -pedantic-errors
时,上述代码确实不能通过编译,这也证明了 C89 也并不支持 void main()
的写法:
因此,为了保证你的程序不会在严格的编译环境下发生错误,请你务必将 main 函数的返回值声明为 int 类型:
其中 return 0
有时可以被省略,在省略的情况下,绝大部分编译器会自动将 设为 main 函数的返回值。但我个人建议不要省略。
Wikipedia的 C 语言条目,其中描述了 C 语言标准的变迁概要。