C语言可谓最基础、应用最广泛的计算机语言,许多语言都脱胎于C语言。本文简要回顾C语言规范的历史及其关键特性。
C语言规范
1967年,26岁的丹尼斯·里奇(Dennis MacAlistair Ritchie)进入贝尔实验室开发 Unix,并于 1969 年圣诞节前推出第一个试运行版本。为了提高Unix的通用性和开发效率,丹尼斯·里奇借鉴B语言,发明了一种新的编程语言——C语言,他的同事汤姆森则使用C语言重写了 Unix,使它成为一种通用性强、移植简单的操作系统,从此开创了计算机编程史上的新篇章,C语言也成为了操作系统专用语言。 而丹尼斯·里奇也被称为C语言和 Unix 之父,他于2011年10月12日(北京时间为10月13日)去世,享年70岁。
80年代的 C 语言还没有标准化,来自“C Programming Language, First Edition, by Brian W. Kernighan, Dennis M. Ritchie. Prentice Hall PTR 1978”的 C 描述可算作“正式”的标准,所以此时的 C 也称为“K&R” C。
C89
伴随C语言应用的日益广泛,为统一C语言版本,1983 年美国国家标准局(American National Standards Institute,简称 ANSI)成立了一个委员会,专门来制定C语言标准,并于1989 被批准,因此通常被称为 ANSI C。由于这个版本是 89 年完成制定的,因此也被称为 C89。该标准1990年被ISO采纳,所以有时ISO C也被简称为C90。1995年, ISO对C90做了修订,添加了对多字节字符的支持,增加了3个新标准头文件iso646.h、wctype.h和wchar.h,被称为C95,但因为和C89基本一致,因此一般关注和介绍不多。
C89要求变量定义必须在代码段的开始位置,也不支持单行注释(//)。在嵌入式系统中常用的volatile关键字就是C89确定的,以避免编译器过度优化:
/* 寄存器一般定义为volatile,否则,多次赋值语句可能被优化删除 */
#define PINSEL0 (*((volatile unsigned long*)0xEFF2C001))
PINSEL0 = 0x01;
PINSEL0 = 0x02;
PINSEL0 = 0x04;
/* 不加volatile的空循环可能会被优化删除 */
volatile int i = 0;
for(; i<100000; i++);
/* 如果i不增加volatile,优化器可能认为i没有改变,while语句会被优化删除
而实际上i可能会被中断函数修改,因此优化会导致程序不可用 */
static int i = 0;
int main(void)
{
while(1)
{
if(i)
dosomething();
}
}
C99
C99 是1999年ISO发布的新规范,引入了许多新的特性,包括内联函数、restrict指针,可变长度的数组、灵活的数组成员(用于结构体)、复合字面量、指定成员的初始化器、对IEEE754浮点数的改进、支持不定参数个数的宏定义,在数据类型上还增加了 long long int 以及复数类型、bool类型和wchar类型,同时增加了对 // 注释的支持,并扩展支持源代码行长度到4095,变量名和函数名支持到63。常见的改进可参考以下示例代码:
//C99开始支持单行注释风格,并支持inline函数
inline int foo(int a, int b) { return a * a - b * b; }
struct vs
{
int n;
char p;
char s[]; //struct的最后一个成员支持变长数组
};
int main ()
{
int n;
scanf("%d", &n);
int s[n * 2];
//C99支持变长数组,并支持随时定义变量,也支持for内变量
for(int i = 0; i < n; ++i)
{
s[i] = i;
s[i + 1] = i * i;
}
//C99支持灵活的数组或结构体成员初始化
struct vs val[3] = {
[1].n = 3,
[1].p = 'c',
};
struct {char c; int v; int a; } v1 = {.c = 'a', .a = 0};
int arr[10] = {1, [5] = 4, [8] = 6, 7};
}
C11
2011年,发布了新的标准,被称为C11。新引入的特征尽管没 C99 相对 C89 引入的那么多,但是这些也都十分有用,比如:字节对齐说明符、泛型机制(generic selection)、无类型的范型宏、匿名结构和匿名联合、对多线程的支持、静态断言、原子操作以及对 Unicode 的支持等。
2018年,发布了C17或称为C18标准,C17 没有引入新的语言特性,只对 C11 进行了补充和修正。
各编译器对C11标准支持差异较大,比如以下代码gcc就不支持,但CPP编译是支持的:
int main()
{
int n = 3;
decltype(n) m = 4; // gcc 对decltype的支持不完整
return 0;
}
以下代码在gcc中也无法使用C11来编译,但g++编译通过:
#include<stdio.h>
#include<string.h>
#include<stddef.h>
int main ()
{
auto n = 16;
char s[n];
strcpy(s, "hello");
printf("%d: %s", n, s);
for(auto c:s)
{
printf("%c.", c);
}
char *p = nullptr;
return 0;
}
多线程的代码示例(在Linux编译通过,Mac编译失败):
#include<stdio.h>
#include<threads.h>
int foo(void* args)
{
printf("Hello from foo: %lu\n", thrd_current());
fflush(stdout);
return 0;
}
int main ()
{
thrd_t t1, t2;
thrd_create(&t1, foo, NULL);
thrd_create(&t2, foo, NULL);
int res;
thrd_join(t1, &res);
thrd_join(t2, &res);
return 0;
}
未来 C2X
对我们来说,C语言最大的三次标准变化是C89–>C99–>C11,每一个语言都在不断发展变化,不断自我演化的道路上。新的标准至今尚未发布,让我们拭目以待。
C语言冷知识
下面的代码是可以编译的,花括号和方括号分别用特殊的标记来替代,这是为了兼容而支持的特性,在C99规范中所定义。特殊字符如下:
<% %> <: :> %: %:%:
{ } [ ] # ##
另外,数组下标a[0],同样可以写成0[a]。
#include<stdio.h>
int main()
<%
int a<:3:> = <% 1, 2, 3 %>;
printf("%d, %d, %d\n", a<:0:>, a<:1:>, a<:2:>);
printf("%d, %d, %d\n", 0[a], 1[a], 2[a]);
return 0;
%>
下面的代码是可以编译通过的,你能发现其中的障眼法吗?
int main()
{
https://uio.cn
return 0;
}
C语言支持的趋向于操作符: <– –> ,其实也是障眼法:
int main()
{
int n = 3;
while (0<--n);
while (n-->0);
return 0;
}