[隐藏]

本篇主要介绍几个基本概念。

一、基本术语

1、由实现定义(implementation-defined)

C标准中并不明确指定具体行为或值,而由编译器自己定义也必须明确定义。

例:当对signed类型的值进行右移时,对左边空出的部分进行算术移位(补符号位)还是逻辑移位(补0)。

char、int等所能表示的大小范围。但标准规定了它们的最小范围。

2、地区指定(locale-specific)

具体的国家、地区、文化和语言所决定的行为或值,也是需要编译器明确定义的。

例:用除了26个小写字母的其他字符调用islower的返回值。

3、未定义的(undefined)

对使用不可移植或错误的结构或数据所做出的反应。这种情况编译器可能会报错,也可能不会,总之,这样的代码尽量不要出现在你的程序中。

例:整数溢出。

4、未指明的(unspecified)

这种情况标准通常提供了几种处理方式,编译器可自由选择,同时编译器也可不明确指定。

例:我们通常在程序结束时,给系统返回一个0,而如果返回值不能转化为int型,那么返回给系统的终止状态就是unspecified。

一个函数的实参表达式列表的执行顺序也是unspecified。

二、字符集(Character set)

C99标准中规定了两种字符集——源码字符集(source character set )和 执行字符集(execution character set )。

源码字符集就是源码文件是使用的编码(encoding),执行字符集也就是程序执行时内存中的字符编码,标准没有规定它们使用哪种编码,这两种字符集所用编码也可以不同。

接下来主要介绍执行字符集。标准规定了字符集必须包括英语26个大写字母、小写字母,数字0到9,以及以下字符

! # % & ( ) * + , - . / :
; < = > ? [ \ ] ^ _ { | } ~

还包括空格、水平制表符、垂直制表符、格式反馈字符和换行符。因为它们都是不可见的,所以也被称作空白字符。

标准规定0到9的值应该是依次递增的,所以你可以使用if (c>=’0′ && c<=’9′) 来判断一个字符是否是数字。

同时标准也定义了一些标点符号(punctuator):

[ ] { ( ) . ->
++ & * + - ~ !
/ % << >> < > <= >= == != ^ | && ||
? : ;
= *= += -= /= %= <<= >>= &= ^= |=
, # ##

想必大家对这些符号再熟悉不过了,但需要注意的是,你不能在这个符号中嵌入空白(空格、制表符等),比如你不能把==写成= =(中间有一个空格),后者是两个符号。

另外还包括六个双并词(digraph)

<: :> <% %> %: %:%:

它们分别和以下符号含义相同,除了出现在字符串中,它们是可以互换的(有些编译器并未实现)。

[ ] { } # ##

标准还定义了一些三字母词,用于在缺少必需字符的字符集上表示这些字符。下面是这些三字母词及它们所代表的字符。

三字母词 所代表的字符
??- ~
??= #
??) ]
??! |
??( [
??’ ^
??> }
??/ \
??< {

与上述双并词不同的是,在字符串中双并词不会被替换为所对应的符号,而三字母词会被替换成各自所代表的字符,而且这种替换是发生在所有编译过程之前的。

例如,在实现了这两种符号的编译器中

%:include <stdio.h>

??=include <stdio.h>

将被替换为

#include <stdio.h>

printf(“How are you???/n”);

将被替换为

printf(“How are you?\n”);

但是

printf(“How are you?<:”);

会打印出

How are you?<:

那怎么才能打印出 How are you???/n 呢?

为了解决这个问题,标准定义了转义序列(escape sequences),它由一个反斜杠(backslash)\  后接一个或多个字符。

\’ 用来表示一个字符常量’
\” 用在字符串常量中表示一个双引号
\? 防止?被解释成三字母词
\\ 用于表示一个反斜杠
\a 奏响终端铃声或产生其他一些可听见或可看见的信号
\b 退格键
\f 进纸字符
\n 换行符
\r 回车符
\t 水平制表符
\v 垂直制表符
\ddd ddd表示1~3个八进制数字,这个转义符表示的就是给定的八进制数值多代表的字符
\xddd 与上例类似,只是这个数值是用十六进制表示的

转义符\后面也可以跟一个换行符(new-line character,这里指回车键),编译器会把\和换行符去掉,这样它的下一行字符将被连接到 \ 所在的位置,但是如果你在\后面多打了一个空格,那么将会出错,\ newline\newline并不相同。

例如,

prin\
tf(“How are you?\n”);

是合法的,只要\后面没有任何东西,

另一个值得一提的是水平制表符(\t)。在编写程序时我们通常要用缩进来体现代码的结构,有些人提倡禁用制表符缩进,因为每个编辑器的TAB键定义不同,如果使用不当,可能导致代码在别的编辑器打开时变的一团糟,所以坚持使用空格来缩进,通常是4个空格(2个太少,8个太多,其他个数不是2的整数幂)。另外,在使用printf打印制表符时,我们也无法知道它的准确距离是多少,这是依赖于操作系统的,通常是8个字符宽度,但这不归C标准管。

三、注释(Comment)

C99中的注释和很多语言一样,有两种,/**/和//。

在程序中,/*会被当做一段注释的开始,直到遇到*/。所以,*a/*b 会出现问题。

//也标示着注释的开始,不同的是,//会把换行符(new-line character)当作注释结束的标志。

所以,我们可以用/* */来注释一段内容,用//来注释一行。

但是注释只能出现在空格可以出现的地方,在字符串常量(String literal)中的/*和//不会被当作注释的起始符。

例如:

“a//b” 四个字符的字符串常量
i = a/**//b; 等价于i = a /b;
//\

f();

函数f()不会被执行,因为上一行的换行符被转义了,注释没有结束
/\

/f();

同上
/*//*/ f(); f()会被执行
n = a//**//b

+c;

等价于 n = a +c;
int/**/i; 等价于int i;
in/**/t i; 错误

实际上,程序中所有的注释都会被一个空格代替,其中的换行符会被保留。

再看下面的例子

#define LINECOMMENT //
#define BSECTIONCOMMENT /*
#define ESECTIONCOMMENT */

这样的写法不会如你愿,因为注释是在预处理指令被处理之前被替换的。

我们不仅用注释来给代码写说明,也会经常把暂时不用的代码注释掉,在需要的时候再取消注释。但需要注意是,注释是不允许嵌套的。

你通常不会在//和newline的组合中嵌套注释,但嵌套却很可能出现在/**/中。对于 /* 开始的注释,一旦遇到 */ 注释即结束,所以你必须保证你要注释掉的内容中不存在 */。

比如你在注释一段代码之后需要注释的更多代码,这时你就要把之前用到的 */ 全部删除。

当然也有一种更简单有效的方法,那就是使用条件编译(conditional compilation)

#if  0
0000statements
#endif

在#if和#endif中间的内容都会被安全的去除。

四、标识符(identifier)

标识符,就是变量、函数、宏等的名字。在C语言中,它们由字母、数字和下划线组成,但开头不能是数字(这很容易理解,如果允许标识符以数字开头,那么编译器在词法分析阶段就无法识别一个全部为数字的字串是标识符还是整常数)。在C99中,标识符还可以使用某些通用字符名(Universal character name)。

C语言是对大小写敏感的,也就是说,标识符a和A是不同的。但我们通常不应该依赖此规则命名标识符。

C语言对标识符的长度是没有限制的,但是在C89中,只要求编译器区分前31个字符,对外部链接(external identifier)的标识符区分前6个字符并且不必区分大小写。如果你写的两个函数分别是print_int和print_float,编译器可能无法区别。但是大多数编译器能区分的长度都大于最小限制,所以,取一个有意义的名字比担心编译器无法识别更有意义。

C99规定至少区分内部链接标识符的前63个字符,外部链接标识符的前31个字符并且区分大小写。在内部链接标识符中,每个通用字符名被当作一个字符;而在外部链接标识符中,每个UCS码点小于等于0000FFFF的通用字符名被视为6个字符,大于0000FFFF的被当作10个字符。

C标准中也保留了一些关键字(Keyword),它们对编译器而言有特定的意义,所以不能用作标识符:

auto for struct
break goto switch
case if typedef
char inline union
const int unsigned
continue long void
default register volatile
do restrict while
double return _Bool
else short _Complex
enum signed _Imaginary
extern sizeof
float static

另外,用户标识符也尽量不要与标准库中标识符相同,C标准中把它们作为保留字而不允许用户定义为自己的标识符(以一个下划线开头后继一个大写字母或再一个下划线,即像_A或__a,也是保留字,你不应该使用。),但这并不是强制的,所以你即使重新定义了保留的标识符,编译器也不一定报错,但这可能会使程序不可移植或运行出错。

C99允许我们在标识符、字符常量和字符串字面值中出现使用ISO/IEC 10646编码的字符,它类似于转义序列。
可以使用两种方式书写通用字符名(\udddd和\Udddddddd),每个d都是一个16进制的数字。\udddd可以表示码点小于等于FFFF的字符,\Udddddddd表示码点更大的字符(\u00A0、\u00a0、\U000000A0、\U000000a0是一样的),但它们的取值范围是有限制的,通用字符名不能表示小于00A0(除了0024($),0040(@),0060(`))或在D800和DFFF之间(包括D800和DFFF)的字符,这些不允许使用的字符大多是基本字符集(basic character set)或控制字符。想了解更多信息可以登录http://www.unicode.org/

五、编译过程

一段C程序必须编译成可执行文件后,才能在机器上运行。一次编译包括以下阶段:

  1. 如果必要,源文件中的多字节字符(multibyte characters)将依照具体实现的方式映射到源码字符集。三字母词将被与之相对应的单字符替换
  2. 反斜杠(\)后接一个换行符的组合将直接被删除
  3. 源程序被分解为预处理符号和空白字符序列。每个注释被一个空格代替,换行符被保留。连续的空白字符(除了换行符)被保留还是被一个空格代替是由实现定义的
  4. 执行预处理指令和宏展开,_Pragma 一元操作符表达式也被执行。如果一个字符序列经过符号串接(##操作符)后生成了一个通用字符名,那么将产生未定义行为。通过 #include 指令引入的文件内容将被放在这条 #include 语句出现的地方,并递归地进行1-4阶段。最后所有预处理指令将被删除
  5. 每个源码字符集和转义字符将被执行字符集中与之对应的字符替换,如果执行字符集中没有与之对应的字符,那么替换将由实现定义
  6. 相邻的字符串字面值被拼接
  7. 用来分割符号的空白字符将不再有任何意义。每个预处理符号将被转换成一个符号,这些符号就是依照语法规则翻译得到的编译单元
  8. 编译器在这一阶段解决对外部变量和函数的引用,生成一个包含所有必要信息的可执行文件。