基础语法
约 7843 字大约 26 分钟
2023-04-02
数据类型
基本数据类型
C 语言只提供了下列几种基本数据类型:
类型 | 说明 | |
---|---|---|
char | 字符型 | 占用一个字节 |
int | 整型 | 通常反映了所用机器中整数的最自然长度 |
float | 单精度浮点型 | |
double | 双精度浮点型 |
限定符
还可以在以上基本数据类型的前面加上一些限定符。
short 与 long
用于限定整型:
short int sh;
long int counter;
在上述这种类型的声明中,关键字int可以省略,通常大家都习惯这么写。
short 与 long 两个限定符的引入可以提供满足实际需要的不同长度的整型数。int 通常代表特定机器中整数的自然长度。short 类型通常为 16 位,long 类型通常为 32 位,int 类型可以为 16 位或 32 位。各编译器可以根据硬件特性自主选择合适的类型长度,但要遵循下列限制:short 与 int 类型至少为 16 位,而 long 类型至少为 32 位,并且 short 类型不得长于 int 类型,而int类型不得长于 long 类型。
signed 和 unsigned
类型限定符 signed 与 unsigned 可用于限定 char 类型或任何整型,unsigned 类型的数总是正值或 0 。如果 char 对象占用 8 位,那么 unsigned char 类型变量的取值范围为 0~255,而 signed char 类型变量的取值范围则为 -128~127(在采用对二的补码的机器上)。不带限定符的 char 类型数据是否带符号则取决于具体机器实现。
整数类型
下表列出了不同系统上关于标准整数类型的存储大小和值范围的细节:
数据类型 | 32位系统下大小(字节) | 64位系统下大小(字节) |
---|---|---|
int | 4 | 4 |
char | 1 | 1 |
short | 2 | 2 |
long | 4 | 8 |
long long | 8 | 8 |
double | 8 | 8 |
void * | 4 | 8 |
为了得到某个类型或某个变量在特定平台上的准确大小,可以使用 sizeof 运算符。表达式 sizeof(type) 得到对象或类型的存储字节大小。下面的实例演示了获取 int 类型的大小:
#include <stdio.h>
#include <limits.h>
int main()
{
printf("Storage size for int : %d \n", sizeof(int));
return 0;
}
void类型
void 类型指定没有可用的值,通常用于以下三种情况下:
序号 | 类型与描述 |
---|---|
1 | 函数返回为空 C 中有各种函数都不返回值,或者可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status); |
2 | 函数参数为空 C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void); |
3 | 指针指向 void 类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。 |
类型转换
当一个运算符的几个操作数类型不同时,就需要通过一些规则把它们转换为某种共同的类型。
一般来说,把较短的操作数转换为较长的操作数时,不会丢失信息。当把较长的操作数转换为较短的操作数时,超出的部分将被丢弃。
由短至长,会自动进行转换;由长至短,需要显示强制转换。
(类型名) 表达式
变量
变量名
C 语言变量名是由字母和数字组成的序列,但其第一个字符必须为字母。下划线“_”被看做是字母,通
常用于命名较长的变量名,以提高其可读性。
大写字母与小写字母是有区别的,所以,x 与 X 是两个不同的名字。
关键字不能作为变量名。
auto double int struct break else long switch
case enum register typedef char extern return union
const float short unsigned continue for signed void
default goto sizeof volatile do if static while
选择的变量名要能够尽量从字面上表达变量的用途,这样做不容易引起混淆。局部变量一般使用较短的变量名(尤其是循环控制变量),外部变量使用较长的名字。
变量声明
变量声明向编译器保证变量以给定的类型和名称存在,这样编译器在不需要知道变量完整细节的情况下也能继续进一步的编译。变量声明只在编译时有它的意义,在程序连接时编译器需要实际的变量声明。
当使用多个文件且只在其中一个文件中定义变量时(定义变量的文件在程序连接时是可用的),变量声明就显得非常有用。可以使用 extern 关键字在任何地方声明一个变量。虽然可以在程序中多次声明一个变量,但变量只能在某个文件、函数或代码块中被定义一次。
左值和右值
C 中有两种类型的表达式:
- 左值(lvalue):指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
- 右值(rvalue):术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。下面是一个有效的语句:
int g = 20;
但是下面这个就不是一个有效的语句,会生成编译时错误:
10 = 20;
常量
常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。
常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。
整数常量
整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
整数常量也可以带一个后缀,后缀是 U 和 L 的组合,U 表示无符号整数(unsigned),L 表示长整数(long)。后缀可以是大写,也可以是小写,U 和 L 的顺序任意。
下面列举几个整数常量的实例:
212 /* 合法的 */
215u /* 合法的 */
0xFeeL /* 合法的 */
078 /* 非法的:8 不是八进制的数字 */
032UU /* 非法的:不能重复后缀 */
以下是各种类型的整数常量的实例:
85 /* 十进制 */
0213 /* 八进制 */
0x4b /* 十六进制 */
30 /* 整数 */
30u /* 无符号整数 */
30l /* 长整数 */
30ul /* 无符号长整数 */
字符常量
字符常量是括在单引号中,例如,'x' 可以存储在 char 类型的简单变量中。
字符常量可以是一个普通的字符(例如 'x')、一个转义序列(例如 '\t'),或一个通用的字符(例如 '\u02C0')。
在 C 中,有一些特定的字符,当它们前面有反斜杠时,它们就具有特殊的含义,被用来表示如换行符(\n)或制表符(\t)等。下表列出了一些这样的转义序列码:
转义序列 | 含义 |
---|---|
\ | \ 字符 |
' | ' 字符 |
" | " 字符 |
? | ? 字符 |
\n | 换行符 |
\r | 回车 |
\t | 水平制表符 |
\ooo | 一到三位的八进制数 |
\xhh . . . | 一个或多个数字的十六进制数 |
字符串常量
字符串字面值或常量是括在双引号 "" 中的。一个字符串包含类似于字符常量的字符:普通的字符、转义序列和通用的字符。
可以使用空格做分隔符,把一个很长的字符串常量进行分行。
下面的实例显示了一些字符串常量。下面这三种形式所显示的字符串是相同的。
"hello, dear"
"hello, \
dear"
"hello, " "d" "ear"
定义常量
在 C 中,有两种简单的定义常量的方式:
- 使用 #define 预处理器。
- 使用 const 关键字。
#define 预处理器
下面是使用 #define 预处理器定义常量的形式:
#define identifier value
const 关键字
可以使用 const 前缀声明指定类型的常量,如下所示:
const type variable = value;
C 语言可以用const来定义常量,也可以用 #define来 定义常量。但是前者比后者有更多的优点:
- const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
- 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
运算符
算数运算符
下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数 | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
-- | 自减运算符,整数值减少 1 | A-- 将得到 9 |
取模运算符%不能应用于float 或double 类型。
++与--这两个运算符特殊的地方主要表现在:它们既可以用作前缀运算符(用在变量前面,如++n)。也可以用作后缀运算符(用在变量后面,如n++)。在这两种情况下,其效果都是将变量n的值加1。
但是,它们之间有一点不同。表达式++n先将n的值递增1,然后再使用变量n 的值,而表达式n++则是先使用变量n 的值,然后再将n 的值递增1。
自增与自减运算符只能作用于变量,类似于表达式(i+j)++是非法的。
关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 不为真。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 不为真。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 不为真。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
关系运算符的优先级比算术运算符低。因此,表达式i < lim - 1 等价于i < (lim - 1)。
逻辑运算符
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
根据定义,在关系表达式或逻辑表达式中,如果关系为真,则表达式的结果值为数值1;如果为假,则结果值为数值0。
位运算符
下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与操作,按二进制位进行“与”运算。运算规则: 0&0=0; 0&1=0; 1&0=0; 1&1=1; | (A & B) 将得到 12,即为 0000 1100 |
按位或运算符,按二进制位进行“或”运算。运算规则:`0 | ||
^ | 异或运算符,按二进制位进行“异或”运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0 | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 取反运算符,按二进制位进行“取反”运算。运算规则:~1=0; ~0=1; | (~A ) 将得到 -61,即为 1100 0011,2 的补码形式,带符号的二进制数。 |
<< | 二进制左移运算符。左操作数的值向左移动右操作数指定的位数(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 二进制右移运算符。左操作数的值向右移动右操作数指定的位数(正数左补0,负数左补1,右边丢弃)。 | A >> 2 将得到 15,即为 0000 1111 |
C语言提供了6个位操作运算符。这些运算符只能作用于整型操作数,即只能作用于带符号或无符号char、short、int、long类型:
赋值运算符
下表列出了 C 语言支持的赋值运算符:
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C = A 相当于 C = C A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
= | 按位或且赋值运算符 |
在赋值表达式中,如果表达式左边的变量重复出现在表达式的右边,如:i = i+2,则可以将这种表达式缩写为i += 2,其中的运算符+=称为赋值运算符。
大多数二元运算符(即有左、右两个操作数的运算符,比如+)都有一个相应的赋值运算符op=,其中,op可以是下面这些运算符之一:+、-、*、/、%、<<、>>、&、^、|
如果expr1和expr2是表达式,那么
expr1 op= expr2
等价于:
expr1 = (expr1) op (expr2)
它们的区别在于,前一种形式expr1只计算一次。注意,在第二种形式中,expr2两边的圆括号是必不可少的。
其他运算符
下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof()、&、* 和 ? :
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
运算符优先级和结合性
同一行中的各运算符具有相同的优先级,各行间从上往下优先级逐行降低。
运算符 | 结合性 |
---|---|
() [] -> . | 从左至右 |
! ~ ++ -- + - * (type) sizeof | 从右至左 |
* / % | 从左至右 |
+ - | 从左至右 |
<< >> | 从左至右 |
< <= > >= | 从左至右 |
== != | 从左至右 |
& | 从左至右 |
^ | 从左至右 |
&& | 从左至右 |
?: | 从左至右 |
= += -= *= /= %= &= ^= | = <<= >>= |
, | 从右至左 |
注意,位运算符&、^与|的优先级比运算符==与!=的低。这意味着,位测试表达式,如
if ((x & MASK) == 0) ...
必须用圆括号括起来才能得到正确结果。
控制流
选择
根据条件选择执行程序语句。C 语言把任何非零和非空的值假定为true,把零或NULL假定为false。
if :如果条件表达式成立,则执行大括号里的程序代码,否则不执行。
if(条件表达式)
{
// 程序代码
}
if...else :如果条件表达式成立,则执行程序代码1,否则执行程序代码2。
if(条件表达式)
{
// 程序代码1
}
else
{
// 程序代码2
}
if...else if :多分支时,那一个条件表达式成立就执行相应的程序代码。
if(条件表达式1)
{
// 程序代码1
}
else if(条件表达式2)
{
// 程序代码2
}
else if(条件表达式3)
{
// 程序代码3
}
else
{
// 程序代码4
}
switch...case :当变量的值等于哪一个值时,就执行相应的程序代码,如果变量的值不等于任何一个 case 的情况,就执行 default 里的程序代码。需要注意的是,根据变量的值的情况执行相应的程序代码的前提是每一个 case 里均有 break 语句用于终止程序,如果没有 break 语句,程序会顺序执行直到退出。
switch(变量)
{
case 值1:
{
// 程序代码1
break;
}
case 值2:
{
// 程序代码2
break;
}
case 值3:
{
// 程序代码3
break;
}
case 值4:
{
// 程序代码4
break;
}
default:
{
// 程序代码5
break;
}
}
循环
根据条件循环执行程序语句。
while :首先判断条件表达式是否成立,如果成立,则循环执行大括号里的程序代码,直至条件表达式不成立;如果不成立,则不执行。
while(条件表达式)
{
// 程序代码
}
do...while :首先执行大括号里的程序代码,然后判断条件表达式是否成立,如果成立,则继续执行大括号里的程序代码,直至条件表达式不成立;如果不成立,则不再继续执行。
do
{
// 程序代码
} while(条件表达式)
for :首先执行语句1(仅执行一次),然后判断条件表达式是否成立,如果成立则执行大括号里的程序代码,接着执行程序语句2,然后再次判断条件表达式是否成立,如此循环直至条件表达式不成立,不再执行。
for(程序语句1; 条件表达式; 程序语句2)
{
// 程序代码
}
跳转语句
break :强行退出循环,不在继续执行循环体了(switch 中除外)。
continue :退出当前轮次的循环,强制执行下一轮循环。
return :返回语句,返回到调用该方法的地方继续执行。
goto语句
从理论上讲,goto语句是没有必要的,实践中不使用goto语句也可以很容易地写出代码。
但是,在某些场合下goto语句还是用得着的。最常见的用法是终止程序在某些深度嵌套的结构中的处理过程,例如一次跳出两层或多层循环。
for ( ... )
{
for ( ... )
{
...
if (disaster)
goto error;
}
}
...
error:
/* clean up the mess */
在该例子中,如果错误处理代码很重要,并且错误可能出现在多个地方,使用goto语句将会比较方便。
函数与程序结构
函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数main() ,所有简单的程序都可以定义其他额外的函数。
函数定义
return_type function_name(parameter list)
{
body of the function
}
在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:
- 返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。
- 函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
- 参数:参数就像是占位符。当函数被调用时,向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
- 函数主体:函数主体包含一组定义函数执行任务的语句。
作用域规则
内部(局部)变量
内部变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。
函数的参数,形式参数,被当作该函数内的局部变量,它们会优先覆盖全局变量。
外部变量
外部变量可以在全局范围内访问,任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式进行声明。
这样做必须非常谨慎,因为这种方式可能对程序结构产生不良的影响,而且可能会导致程序中各个函数之间具有太多的数据联系。
静态变量
用static关键字声明的变量。
作用对象 | 说明 |
---|---|
外部变量 | 该函数名除了对该函数声明所在的文件可见外,其它文件都无法访问(同时这个名字不会和同一程序中的其它文件中的相同的名字相冲突) |
内部变量 | 只能在该函数中使用,不管其所在函数是否被调用,它一直存在,不会随着所在函数的被调用和退出而存在和消失 |
函数 | 该函数名除了对该函数声明所在的文件可见外,其它文件都无法访问 |
寄存器变量
用register声明的变量。
register声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想是,将register变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。但编译器可以忽略此选项。
实际使用时,底层硬件环境的实际情况对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。
另外,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。
变量初始化
当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:
数据类型 | 初始化默认值 |
---|---|
int | 0 |
char | '' |
pointer | NULL |
C预处理器
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
预定义宏
ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。
宏 | 描述 |
---|---|
DATE | 当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。 |
TIME | 当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。 |
FILE | 这会包含当前文件名,一个字符串常量。 |
LINE | 这会包含当前行号,一个十进制常量。 |
STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
预处理器运算符
C 预处理器提供了下列的运算符来帮助创建宏:
宏延续运算符(\)
一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
字符串常量化运算符(#)
在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:
#include <stdio.h>
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
int main(void)
{
message_for(Carole, Debra); // Carole and Debra: We love you!
return 0;
}
标记粘贴运算符(##)
宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:
#include <stdio.h>
#define tokenpaster(n) printf("token" #n " = %d", token##n)
int main(void)
{
int token34 = 40;
tokenpaster(34); // token34 = 40
return 0;
}
defined() 运算符
预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:
#include <stdio.h>
#if !defined(MESSAGE)
#define MESSAGE "You wish!"
#endif
int main(void)
{
printf("Here is the message: %s\n", MESSAGE); // Here is the message: You wish!
return 0;
}
参数化的宏
CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:
int square(int x)
{
return x * x;
}
我们可以使用宏重写上面的代码,如下:
#define square(x) ((x) * (x))
在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:
#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main(void)
{
printf("Max between 20 and 10 is %d\n", MAX(10, 20)); // Max between 20 and 10 is 20
return 0;
}
C 头文件
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。
在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。
引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。
A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
引用头文件的语法
使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:
#include <file>
这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,可以通过 -I 选项把目录前置在该列表前。
#include "file"
这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,可以通过 -I 选项把目录前置在该列表前。
只引用一次头文件
如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。
有条件引用
有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。可以通过一系列条件来实现这点,如下:
#if SYSTEM_1
# include "system_1.h"
#elif SYSTEM_2
# include "system_2.h"
#elif SYSTEM_3
...
#endif
但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,只需要使用宏名称代替即可:
#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H
SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被的 Makefile 定义。