C语言规范

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;
}