||
1. 字元和标识符
1.1. 字元
下表列出了所有双字符序列(字元)以及它们对应的单个字符。
表格 1 字元
字元 |
含义 |
字元 |
含义 |
<: |
[ |
:> |
] |
<% |
{ |
%> |
} |
%: |
# |
%:%: |
## |
1.2. 标识符
C语言中的标识符由字符序列(大写字符或小写字符)、通用字符名、数字和下画线组成。标识符第一个字符必须是字母、下画线或者通用字符名。对于有外部连接的标识符,其前31个字符是有意义的,对于宏或者有内部连接的标识符,其前63个字符是有意义的。
1.2.1. 通用字符名
通用字符名可以用于标识符的名字、字符常量和字符串中。
1.2.2. 关键字
如下表列出了所有C语言的所有关键字
表格 2 关键字列表
_Bool |
_Complex |
_Imaginary |
auto |
break |
case |
char |
const |
continue |
default |
do |
double |
else |
enum |
extern |
float |
for |
goto |
if |
inline |
int |
long |
register |
restrict |
return |
short |
signed |
sizeof |
static |
struct |
switch |
typedef |
union |
unsigned |
void |
volatile |
while |
|
|
|
2. 注释
C语言使用两种注释://和/*、*/。主义/*类型的注释不允许嵌套使用。
3. 常量
3.1. 整数常量
十进制常量后接符号l或者L,该常量当做long int类型,如果超出范围,编译器当作long long int类型。八进制或十六进制后接符号l或者L,当超出long long int范围时,当作unsigned long long int类型。
十进制常量后接符号ll或者LL,该常量当作long long int类型。八进制或十六进制后接ll或者LL,当超出long long int范围时,当作unsigned long long int类型。
常量后接符号u或者U,则被当作一个无符号数。范围变动依次是:unsigned int –> unsigned long int -> unsigned long long int。
整数常量后可以同时加上无符号和长整型符号或长长整型符号。
如果十进制整型常量没有任何后缀,范围变动依次是:signed int -> long int -> long long int。
如果八进制或十六进制整型常量没有任何后缀,范围变动依次是:signed int -> unsigned int -> long int -> unsigned long int -> long long int -> unsigned long long int。
3.2. 浮点数常量
浮点数小数点前面的数字和小数点后面的数字都可以省略,但不能同时省略。如果浮点常数中有字母e或E,则该浮点数常量采用科学计数法。十六进制浮点常量前面以0x或0X开始,后面跟上一个或者多个十六进制数字,然后跟上表示指数的字符p或P,最后是表示以2为底数的指数部分。
3.3. 字符常量
用单引号括起来的单个字符组成一个字符常量。有多个字符,则由编译器决定。可以使用通用字符名表示那些不在标准字符集中的字符。
3.3.1. 转义序列
下表列出了所有的转义序列。
表格 3 特殊转义序列
字符 |
含义 |
字符 |
含义 |
\a |
声音警铃 |
\b |
退格 |
\f |
表单 |
\n |
换行 |
\r |
回车 |
\t |
水平制表 |
\v |
垂直制表 |
\\ |
反斜杠 |
\” |
双引号 |
\’ |
单引号 |
\? |
问号 |
\nnn |
八进制字符 |
\unnnn |
通用字符名 |
\Unnnnnnnn |
通用字符名 |
\xnn |
十六进制数字 |
|
|
3.3.2. 宽字符常量
宽字符常量可以写作L‘x’,这类常量的类型是wchar_t,该类型在标准头文件<stddef.h>中定义。使用宽字符可以表示那些无法用普通的char类型表示的字符。
3.4. 字符串常量
字符串常量由一对双引号括起来的零个或者多个字符序列组成,编译器自动在字符串后面加上结束的空字符(’\0’)。编译器将返回一个指向该字符序列第一个字符的指针。程序中不能修改字符常量。
3.4.1. 字符串连接
预处理器将自动把相邻的字符串连接起来,这些字符串中间可以用一个或多个空白字符分隔。
3.4.2. 多字节字符串
编译器可以自行定义多字节字符串以及相应的处理方法,以便能够在字符串中包含多字节字符。
3.4.3. 宽字节字符串
宽字符字符串可以在使用类似L‘...’的形式表示,这类常量的类型是wchar_t类型的指针,其中wchar_t类型在标准头文件<stddef.h>中定义。
3.5. 枚举常量
枚举类型中的标识符被当作该枚举类型的常量,编译器也可以将其当作int类型。
4. 数据类型与声明
4.1. 声明
当处理结构、联合、枚举数据类型和typedef语句的时候,编译器并不分配任何存储空间,这些语句只是告诉编译器某个特定类型的结构,并分配给该结构一个名字。
当声明某个数据类型之后,即可声明该数据类型的变量,。声明某个数据类型的变量将使得编译器为改变量分配内存空间,除非所声明的变量是一个外部变量。在声明外部变量时,编译器将根据具体情况决定是否分配内存空间。
4.2. 基本数据类型
如下列出了C语言的基本数据类型。
表格 4 基本数据类型总结
类型 |
意义 |
int |
整形数,最小保证16位 |
short int |
短整型数,精度小于int,某些计算机上占用的内存是int的一半,最小保证16位 |
long int |
长整型数,最小保证32位 |
long long int |
长长整型数,最小保证64位 |
unsigned int |
无符号整数,最小保证16位 |
float |
浮点数,可带小数点,最小保证6位有效数字 |
double |
双精度浮点数,最小保证10位有效数字 |
long double |
扩展双精度浮点数,最小保证10位有效数字 |
char |
字符类型,在某些计算机上,采用有符号扩展 |
unsigned char |
与char类型相同,但是在提升为更长的int类型时采用无符号扩展 |
signed char |
与char类型相同,但是采用有符号扩展 |
_Bool |
布尔类型,可以用来保存0和1 |
float _Complex |
复数 |
double _Complex |
扩展复数 |
long double _Complex |
扩展高精度复数 |
void |
无类型,用来表明某个函数没有返回值,或者用于丢弃某个表达式的结果,也可用于通用类型指针(void *)。 |
△ 使用_Complex和_Imaginary类型以及一组库函数可以处理复数及其运算,使用复数时应包含<complex.h>文件,该文件包含有处理复数所需的各类宏定义以及函数原型声明。头文件<stdbool.h>中定义了宏bool、true、false,可以让程序法方便使用布尔类型。
4.3. 导出数据类型
导出数据类型包括数组、结构、联合以及指针,返回某个特定数据类型的函数也被认为是一种导出数据类型。
4.3.1. 数组
数组中可以包含任何基本数据类型或者导出数据类型,还有函数指针,但是不能包含函数。数组声明采用如下的形式:
类型 name[n] = {初始化表达式,初始化表达式,...};
定义全局数组的时候,每一个初始化表达式都必须是常量表达式,初始化列表中的元素个数必须小于等于数组长度,字符数组例外。通过在初始化列表中指定需要初始化的元素编号,C语言允许我们以任意顺序初始化数组。
4.3.1.1. 变量长度数组
在函数内部或者语句快内部,C语言允许我们使用变量声明数组的长度。C语言运行时环境负责计算该数组的长度。变量长度数组不允许在声明的时候进行初始化。
4.3.1.2. 多维数组
多维数组的一般声明形式如下:
type name[d1][d2]...[dn] = initializationList;
4.3.2. 结构
结构的一般声明形式如下:
struct name
{
成员声明
成员声明
...
} 变量列表;
C语言允许使用一个同类型的结构变量初始化另外一个结构变量。
下面形式的结构成员变量成名:
type fieldName : n
说明该成员变量是一个位域,其中n是一个整数值,代表位域的宽度。如果在声明位域成员变量的时候没有给出其名字,那么编译器将为该成员保留内存空间,但是程序不能访问这些空间。如果没有指定位域的名字,而且n也等于0,那么分配给结构下一个成员变量的内存空间将从单元边界上开始,单元的大小有具体实现决定。取地址操作符不能作用于位域上,C语言不允许定义位域数组。
4.3.3. 联合
联合的一般声明形式如下:
union name
{
成员声明;
成员声明;
...
} 变量列表;
联合中所有成员共享一块内存空间,C语言编译器保证分配给联合的内存能够容纳其最大的成员变量。同类型的联合变量可以用于初始化另外一个联合变量。
4.3.4. 指针
声明指针变量的基本语法如下:
type *name;
C语言允许将指针类型变量与值为0的常量表达式进行比较,用以判断该指针是否是空指针。
整数类型与指针类型之间的相互转化,以及能够容纳一个指针所需的整数大小是与具体机器相关的。
void*是通用的指针类型,C语言保证任何类型的指针都可以保存到void*类型中,并且随后将其从void*类型的指针中取出而不改变原来的值。除此以外,C语言不允许不同类型指针之间的转换。
4.4. 枚举数据类型
定义枚举数据类型的一般格式如下:
enum name { enum_1, enum_2, ...} variableList;
每一个枚举值必须是一个合法的标识符,或者是对一个标识符的常量表达式赋值。variableList是可选的,它代表一组该类型的变量(也可以同时初始化)。声明一个枚举变量的值只能是定义时列出的枚举值之一。
4.5. typedef语句
typedef语句用于给基本数据类型和导出数据类型定义一个新的名字。书写的一般规则如下:首先写出使用该类型声明变量的语句,然后将变量名使用新的数据类型名替换,最后在整个语句前面加上关键字typedef。
4.6. 类型修饰符const、volatile和restrict
C语言允许在声明语句的类型前面放置关键字const,用于表示该变量的值不能被修改,但C语言标准并不要求编译器在程序试图修改const变量的值时发出警告。
volatile关键字告诉编译器某个变量的值在运行的时候有可能动态的改变。当volatile变量出现在表达式中时,C语言每次都从改变量的存储位置获取其值。
restrict关键字可以和指针变量一起使用,该关键字可以为编译器优化代码提供某些暗示(如register关键字对于变量的作用一样)。restrict关键字告诉编译器,它所修饰的指针是指向某个特定对象的唯一一个指针,即在同一个作用域内,没有其他指针指向这个特定对象。
5. 表达式
变量名、函数名、数组名、常量、函数调用、数组引用、结构以及联合引用都被C语言看作是表达式。除了void类型之外的任何表达式都被看做一种特殊的数据对象——左值。如果能够修改数据对象赋值的话,这个对象还被称为可修改的左值。
5.1. C语言的操作符总结
如下表列出了C语言的各种操作符。
表格 5 C语言的操作符总结
操作符 |
描述 |
关联性 |
() |
函数调用 |
|
[] |
数组元素引用 |
|
-> |
从指针引用成员 |
从左向右 |
. |
引用结构成员 |
|
- |
单边减法 |
|
+ |
单边加法 |
|
++ |
自增 |
|
-- |
自减 |
|
! |
逻辑非 |
|
~ |
一阶补数 |
从右向左 |
* |
指针引用 |
|
& |
取地址 |
|
Sizeof |
取对象大小 |
|
(type) |
类型转换 |
|
* |
乘法 |
|
/ |
除法 |
从左向右 |
% |
求余 |
|
+ |
加法 |
从左向右 |
- |
减法 |
|
<< |
左位移 |
从左向右 |
>> |
右位移 |
|
< |
小于 |
|
<= |
小于等于 |
从左向右 |
> |
大于 |
|
=> |
大于等于 |
|
== |
相等 |
从左向右 |
!= |
不等 |
|
& |
按位与 |
从左向右 |
^ |
按位异或 |
从左向右 |
| |
按位或 |
从左向右 |
&& |
逻辑与 |
从左向右 |
|| |
逻辑或 |
从左向右 |
?: |
条件 |
从右向左 |
= *= /= += -= &= ^= |= <<= >>= |
赋值 |
从右向左 |
, |
逗号 |
从右向左 |
△ C语言没有明确指定应该先对加号左面求值还是先对加号右面求值,以及函数的参数求值顺序,而这些求值顺序将影响如下表达式的意义:
x[i] + ++i;
x[i] = ++i;
f (i, ++i);
△ 另外对于操作符||和&&,都是按照从左到右的顺序,在判断语句中可能会出现短路现象。
5.2. 常量表达式
即每一部分都是常量的表达式,在下面的场合必须使用常量表达式:
a. switch语句中的每一个case子句后面;
b. 声明同时进行初始化的数组或者全局数组;
c. 枚举标识符的值;
d. 结构定义中位域的宽度;
e. 静态变量的初始化值;
f. 全局变量的初始化值;
g. #if预处理语句后面的值。
5.3. 算术操作符(void类型外的任何基本数据类型的表达式)
△ 除法运算中,如果两个操作数是整数,所得的结果将被取整。如果有一个操作数是负数,则取整的方向是未定义的。
5.4. 逻辑操作符(void类型外的任何基本数据类型,或者指针类型)
5.5. 关系操作符(void类型外的任何基本数据类型,或者指针类型)
5.6. 位操作符(整型表达式)
△ 对于左移操作和右移操作,仅仅执行整数提升操作。如果移动的位数为负或者大于等于被移动操作数的位数,那么移位操作的结果是未定义的。在某些计算机上,右移操作按照算术规则进行(用最高符号位填充空出来的位),而在另外一些计算机上则执行逻辑规则(用0填充)。移位操作结果与被提升的左操作数类型相同。
5.7. 自增和自减操作符(没有被const修饰过的可修改的左值)
5.8. 赋值操作符(没有被const修饰过的可修改的左值)
5.9. 条件操作符(表达式)
a ? b : c a不等于0的时候,表达式等于b,否则的话等于c。
△ 如果b和c是不同类型的算术数据类型,按照一般转换规则。如果一个是指针,一个是0,则后者将被当作前者类型的一个空指针。如果有一个指针是void*类型而另一个是某种特定类型指针,则后者被转换为void*类型的指针,表达式结果是void*类型。
5.10. 类型转换操作符
(基本数据类型、枚举数据类型、typedef语句定义类型或者某个导出数据类型)
5.11. sizeof操作符(同上,或者表达式)
sizeof(type) |
表达式的值等于容纳指定数据类型的值所需要的内存字节数 |
sizeof(a) |
表达式的值等于保存表达式a的结果所需的内存字节数 |
△ sizeof表达式结果的类型为size_t,该类型在标准头文件<stddef.h>中定义。
△ 如果a是一个变量长度数组,那么sizeof表达式将在运行时刻求值;否则的话,该表达式将在编译时刻求值,其结果被编译器当作常量表达式。
5.12. 逗号操作符(表达式)
a , b 编译器对a求值,然后再对b求值,整个表达式的结果和类型等于表达式b的结果和类型
5.13. 数组的基本操作
5.14. 结构的基本操作(包括赋值、函数调用和函数返回)
5.15. 指针的基本操作
x 是一个类型为t的左值表达式
pt 是一个可修改的左值表达式,类型为指向t的指针
v 是一个表达式
&x |
生成一个指向x的指针,表达式的类型为指向t的指针 |
pt = &x |
使得指针pt指向x,表达式的类型为指向t的指针 |
pt = 0 |
将指针pt设置为空指针 |
pt == 0 |
判断pt是否为空指针 |
*pt |
取得指针pt指向的值,表达式的类型为t |
*pt = v |
将表达式v的值保存在pt所指向的位置中,表达式的类型为t |
指向数组的指针:
打印P449和P450
5.16. 复合字面量
复合字面量是一个用小括号括起来的类型名后面加上初始化列表组成。该表达式产生一个指向类型的无名值。复合字面量可用于结构类型和数组类型,如果没有指定数组长度,则由初始化列表决定。
5.17. 基本数据类型的转换规则
C语言对于算术表达式中操作数的类型转换遵守一套预先定义的规则,该规则被称为普通算术转换规则。见P451页
6. 存储类型与作用域
术语“存储类型”用于描述编译器为变量分配内存的方式,也可以用于描述某个特定函数的使用范围。C语言一共有四种存储类型:auto、static、extern和register。声明的时候可以省略存储类型,使用默认值。
术语“作用域”用于描述某个特定的标识符在程序中的可见范围。
6.1. 函数
当为函数指定存储类型的时候,只能使用关键字static或者extern。声明为static的函数只能在定义该函数的文件内使用。声明为extern(默认值)的函数可以在其他文件中使用。
6.2. 变量
如下总结了可以用于变量声明的存储类型关键字,及各类变量的作用域和初始化方法。
表格 6 变量的存储类型、作用域以及初始化
存储类型 |
变量声明方式 |
饮用方法 |
初始化 |
注释 |
static |
语句块外部 语句块内部 |
任何位置 语句块内部 |
常量表达式 |
|
extern |
语句块外部 语句块内部 |
文件的任何位置 语句块内部 |
常量表达式 |
|
auto |
语句块内部 |
语句块内部 |
任何表达式 |
|
Register |
语句块内部 |
语句块内部 |
任何表达式 |
|
没有指定 |
语句块外部 |
本文件的任何位置,如果包含恰当的声明,也可以从其他文件中引用 |
常量表达式 |
|
没有指定 |
语句块内部 |
与auto相同 |
与auto相同 |
与auto相同 |
7. 函数
7.1. 函数定义
函数定义的一般格式如下:
returnType name ( type1 param1, type2 param2, ...)
{
variableDeclarations
programStatement
programStatement
...
return expression;
}
a. 如果函数的参数类型为一维数组,那么在参数列表中不需要说明该数组的长度。如果参数为多维数组,则第一维上的长度不需要指定。
b. 局部变量可以在函数的任何地方声明,只要这些声明语句出现在使用它们的那些语句之前即可。
c. 如果参数列表的最后是...(只能出现在参数列表最后),那么函数将接受不定数目的参数。
d. 可以在函数定义前面加上inline关键字,暗示编译器将函数的实际代码插入适当的位置,而不是插入调用函数的代码,这样可以获得更快的执行速度(编译器可以忽略这个请求)。
7.2. 函数调用
对于接受不定数目参数的函数,必须进行原型声明,否则编译器将根据在调用语句中看到的参数个数,推断函数所接收的参数类型。函数的参数传递按照值引用的方式进行,即在函数内部不能修改实际的参数。如果给函数传递一个指针参数的话,函数内部可以对该指针指向的位置进行修改,但是不能对实际的指针进行修改。
7.3. 函数指针
如果只给函数名而没有后面的小括号,那么该函数被编译器当作指向函数的指针,也可以将取地址操作符(&)应用于函数名,同样产生指向该函数的指针。如果fp是指向函数的指针,可以使用如下方式进行函数调用:
fp() 或者 (*fp)()
8. 语句
C语言语句由一个合法的表达式后面跟上一个分号组成,或者是下面所述的特殊语句。在任何语句前面都可以加上一个标号,该标号由一个合法的标识符后面跟上冒号组成。
8.1. 复合语句(包围在一对大花括号之间的一组语句被称为一条复合语句,或者是语句块)
8.2. break语句(break语句只能用于for、while、do或者switch语句内部,在遇到break语句之后,这些语句立即结束执行)
8.3. continue语句(continue语句只能用于循环语句内部。当遇到continue语句之后,循环体中continue语句后面的语句将被跳过,计算机将接着开始执行下一次循环)
8.4. do语句
do
programStatement
while ( expression );
如果表达式expression的值不为0,那么计算机将不断执行programStatement语句。
8.5. for语句
8.6. goto语句
8.7. if语句
8.8. 空语句
空语句只有一个分号,执行不产生任何效果。通常被用来作为for、do或者while语句的一个组成部分用以满足C语言的语法要求。
8.9. return语句
return; |
使程序的执行流程立刻回到调用者 |
return expressions; |
将expressions的值作为函数返回值返回给调用者,如果该值类型与函数声明的返回值类型不同,将被自动转换为所需的类型 |
8.10. switch语句
8.11. while语句
9. 预处理器
预处理器在编译器编译代码之前对其进行处理。预处理一般完成如下几类工作:1.将某些三元组替换为对应的字符;2.将所有以反斜杠\结尾的行与后面一行合并为一行;3.将程序分解为记号流;4.删除所有的注释,并用单个空格替换它们;5.处理预处理器指令并展开所有宏。
9.1. 三元组
为了处理程序中非ASCII字符,C语言定义了三元组,如下列出了所有的三元组。
表格 7 三元组序列
三元组 |
含义 |
三元组 |
含义 |
??= |
# |
??( |
[ |
??) |
] |
??< |
{ |
??> |
} |
??/ |
\ |
??’ |
^ |
??! |
| |
??- |
~ |
|
|
9.2. 预处理器指令
所有预处理器指令都必须以#开始,而且#必须是该行的第一个非空白字符。在#后面可以有一个或者多个空格和tab字符。
9.2.1. #define指令(#define name text)
定义了一个名为name的宏,并将该宏与其名字后的第一个空格后直到该行结束的字符串等价起来。C语言预处理器将用这个字符串替换随后程序中任何位置出现的符号name。
9.2.2. #error指令(#error text)
指令后面的text将被预处理器作为错误信息输出。
9.2.3. #if指令
#if constant_expression
...
#endif
预处理器将对常量表达式constant_expression求值,如果结果非0,那么#if和#endif之间的语句将被处理,否则预处理器河边一起都不会处理这些语句。可以加入#elif进行分支选择。
常量表达式中可以使用一个特殊的操作符defined,如defined(DEBUG),表示如果符号DEBUG已经定义过,#if和#endif之间的语句将被处理。其中符号周围的小括号不是必须的。
9.2.4. #ifdef指令
#ifdef identifier
...
#endif
如果identifier代表的宏已经被定义过了(可能是通过#define语句,也可能是通过命令行上的-D选项),#ifdef和#endif之间的语句将被处理,否则这些语句将被忽略。如#if一样,该指令后也可以有#elif指令和#else指令。
9.2.5. #ifndef指令
#ifndef identifier
...
#endif
如果identifier代表的宏还没有被定义过,那么#ifndef和#endif之间的指令将被处理,否则这些语句将被忽略。如#if一样,该指令后也可以有#elif指令和#else指令。
9.2.6. #include指令(#include “fileName”)
预处理器将在某些“实现特定”的目录中寻找指定的文件,一般优先查找当前源程序所在的目录。找到后,将文件内容插入到#include指令所在的位置。另一种常见的格式是#include <filename>,预处理器将在标准目录中寻找这些文件。
无论何种形式,都可以使用宏来代替具体被包含的文件名。如:
#define DATABASE_DEFS </usr/data/database.h>
...
#include DATABASE_DEFS
9.2.7. #line指令(#line constant “fileName”)
编译器将随后的语句当作是fileName中指定的文件中的语句,其行号从constant重新开始计算。如果没有指定fileName,编译器将默认是上一个#line指令中指定的文件名或者是当前源文件的名字(如果从来没有使用#line指令指定过文件)。
#line用于帮助编译器,在发出错误报告的时候给出正确的文件名和行号。
9.2.8. #pragma指令(#pragma text)
预处理器将执行某些与特定编译器实现相关的操作。如#pragma loop_opt(on)用来使编译器打开循环优化,不认识该指令的编译器将忽略掉。
在#pragma指令后面可以使用一个特殊的关键字STDC,目前的规范中规定STDC后面可以使用的开关有:FP_CONTRACT、FENV_ACCESS和CX_LIMITED_RANGE。
9.2.9. #undef语句(#undef indentifier)
该指令将取消前面定义的宏identifier。随后的#ifdef或者#ifndef指令将认为该宏没有被定义。
9.2.10. #指令
空指令,预处理器将忽略它。
9.3. 预定义符号
如下列出了C语言编译器的预定义符号。
表格 8 预定义符号
符号 |
含义 |
__LINE__ |
当前编译的行号 |
__FILE__ |
当前正在编译的文件 |
__DATE__ |
当前的日期,以“月月 日日 年年年年”的形式输出 |
__TIME__ |
当前的时间,以“时时:分分:秒秒”的形式输出 |
__STDC__ |
如果编译器符合ANSI C标准,该宏为1,否则为0 |
__STDC_HOSTED__ |
如果实现了所有C标准库,则该宏为1,否则为0 |
__STDC_VERSION__ |
被定义为199901L |