在前面的章节种,初步学习如何来开发一个最为简单的C语言程序,并且了解了C语言程序的运行机制。本节,我们来学习下C语言程序的基本组成元素。
token
在C语言程序中,编译器识别的基本元素是“token(符记)”。每个单独的单词和标点符号都被称为token。token是编译器不会分解为组件元素的最小程序文本。
比如,C语言语法中的关键字、标识符、常量、字符串文本和运算符都是token的示例。括号“[ ]”、大括号“{ }”、圆括号“()”和逗号“, ”等标点字符也是token。
空白字符
空格、制表符、换行符、回车符、换页符和垂直制表符被称为“空白字符”,因为它们的作用与打印页面上的单词和行之间的空格相同,都是为了阅读更容易。token由空白字符和其他token(例如运算符和标点符号)分隔。在解析代码时,C编译器会忽略空白字符,除非您将它们用作分隔符或用作字符常量或字符串文本的组成部分。
使用空白字符使程序更具可读性。这样要注意,编译器还会将注释也视为空格。
注释
“注释”是以正斜杠/星号组合“/*”开头的字符序列,编译器将其视为单个空白字符,否则将被忽略。注释可以包括可表示字符集中的任何字符组合,包括换行符,但不包括“结束注释”分隔符“*/”。注释可以占用多行,但不能嵌套。
注释可以出现在允许空格字符的任何地方。由于编译器将注释视为单个空白字符,因此不能在token中包括注释。编译器忽略注释中的字符。
使用注释是为了人更好的理解代码的含义。以下是一个编译器能接受的注释的示例:
/* Comments can contain keywords such as
for and while without generating errors. */
注释可以与代码语句显示在同一行上:
printf( "Hello\n" ); /* Comments can go here */
也可以选择在函数或程序模块之前使用描述性注释块:
/* MATHERR.C illustrates writing an error routine
* for math functions.
*/
由于注释不能包含嵌套注释,以下示例会导致错误:
/* Comment out this routine for testing
/* Open file */
fh = _open( "myfile.c", _O_RDONLY );
.
.
.
*/
发生错误的原因是编译器将“打开文件”一词之后的第一个“*/”识别为注释的结尾。它尝试处理剩余的文本,并在发现注释外的“*/”时生成错误。
此外还有两个正斜杠“//”的单行注释。以下是HelloWorld程序中的示例:
// 我的第一个C程序
printf("Hello World!");
以两个正斜杠“//”开头的注释将由下一个换行符终止,该换行符前面没有转义字符。在下一个示例中,换行符前面有反斜杠“\”,创建“转义序列”。此转义序列导致编译器将下一行视为上一行的一部分。比如以下示例:
// 我的第一个C程序 \
printf("Hello World!");
上述例子中的“printf("Hello World!");”语句将会被当做注释的一部分。因此,编写注释时,要小心反斜杠“\”,避免程序语句被不小心被当做注释而得不到执行。
提示:注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,按需注释。注释内容要简洁、明了、无歧义,信息全面且不冗余。需要注释的地方没有注释,代码则难以被读懂;而包含无用、重复信息的冗余注释不仅浪费维护成本,还会弱化真正有用的注释,最终让所有注释都不可信。注释跟代码一样重要。写注释时要换位思考,用注释去表达此时读者真正需要的信息。在代码的功能、意图层次上进行注释,即注释解释代码难以表达的意图,不要重复代码信息。修改代码时,也要保证其相关注释的一致性。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,让阅读者迷惑、费解,甚至误解。使用流利的中文或英文进行注释,为降低沟通成本,应使用团队内最擅长、沟通效率最高的语言写注释。
关键字
关键字是对C编译器具有特殊意义的单词。编写C语言程序时,使用的标识符不能具有与C关键字相同。
C语言使用以下关键字:
关键字 | 说明 |
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或函数返回值类型 |
const | 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变 |
continue | 结束当前循环,开始下一轮循环 |
default | 开关语句中的"其它"分支 |
do | 循环语句的循环体 |
double | 声明双精度浮点型变量或函数返回值类型 |
else | 条件语句否定分支(与 if 连用) |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
float | 声明浮点型变量或函数返回值类型 |
for | 一种循环语句 |
goto | 无条件跳转语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句(可以带参数,也可不带参数) |
short | 声明短整型变量或函数 |
signed | 声明有符号类型变量或函数 |
sizeof | 计算数据类型或变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
C99 新增关键字
_Bool | _Complex | _Imaginary | inline | restrict |
C11 新增关键字
_Alignas | _Alignof | _Atomic | _Generic | _Noreturn |
_Static_assert | _Thread_local |
标识符
“标识符”或“符号”是为程序中的变量、类型、函数和标签提供的名称。标识符名称的拼写和大小写必须与任何关键字不同。不能将关键字用作标识符;关键字保留供特殊使用可以通过在变量、类型或函数的声明中指定标识符来创建标识符。
观察下面的HelloWorld程序的代码:
#include <stdio.h> // 引入stdio库
int main() // 程序入口
{
// 我的第一个C程序
printf("Hello World!");
return 0; // 返回值
}
上面例子中,return是关键字,而main和printf是函数的标识符名称。
一种特殊类型的标识符,称为语句标签,可以在goto语句中使用。(声明在声明中描述,类型声明标签在goto和标记语句中描述。)
标识符可以使用非数字标识符和标识符数字。
- 非数字包括:_ a b c d e f g h i j k l mn o p q r s t u v w x y z A B C D E F G H I J K L MN O P Q R S T U V W X Y Z
- 数字包括:0 1 2 3 4 5 6 7 8 9
标识符名称的第一个字符必须是非数字(即第一个字符必须是下划线或大写字母或小写字母)。ANSI允许外部标识符的名称中使用6个有效字符,内部(函数内)标识符的名称中使用31个有效字符。外部标识符(在全局作用域声明或使用存储类extern声明的标识符)可能会受到额外的命名限制,因为这些标识符必须由链接器等其他软件处理。
多字节字符
多字节字符是由一个或多个字节的序列组成的字符。每个字节序列代表扩展字符集中的单个字符。多字节字符用于汉字等字符集。
宽字符是多语言字符代码,始终为16位宽。字符常量的类型为char;宽字符的类型为wchar_t。由于宽字符始终是固定大小的,因此使用宽字符简化了使用国际字符集的编程。
宽字符串文本L"hello"成为一个由六个wchar_t类型的整数组成的数组。
{L'h', L'e', L'l', L'l', L'o', 0}
Unicode规范是宽字符的规范。用于在多字节字符和宽字符之间转换的运行时库例程包括mbstocs、mbtowc、wcstombs和wctomb。
三字符组
C源程序的源字符集包含在7位ASCII字符集中,但是ISO 646-1983不变代码集的超集。三字符组(trigraph)序列允许C程序仅使用ISO(国际标准组织)不变代码集编写。三字符组是由三个字符组成的序列(由两个连续的问号引入),编译器将其替换为相应的标点符号。可以在C代码中使用三字符组,其字符集不包含某些标点符号的方便图形表示。
下表显示了9个三字符组序列。源文件中第一列标点符号的所有出现都将替换为第二列中的相应字符。
TRIGRAPH SEQUENCES | |
---|---|
Trigraph | Punctuation Character |
??= | # |
??( | [ |
??/ | \ |
??) | ] |
??' | ^ |
??< | { |
??! | | |
??> | } |
??- | ~ |
三字符组始终被视为单个源字符。三字符组的翻译发生在第一个翻译阶段,在识别字符串文本和字符常量中的转义字符之前。仅识别上表中显示的9个三字符组。所有其他字符序列均未翻译。
字符转义序列“\?”,防止对类似三字符组形的字符序列的误解。举例:
printf( "What??!\n" );
上述例子打印的字符串是“What|”,因为“??!”是一个被“|”字符替换的三字符组序列。编写以下语句以正确打印字符串:
printf( "What?\?!\n" );
在此printf语句中,第二个问号前面的反斜杠转义字符可以防止误解“??!”作为三字符组。
常量
常量是可以在程序中用作值的数字、字符或字符串。使用常量用来表示浮点、整数、枚举或字符等无法修改的值。
常量包括:
- 浮点常量
- 整数常量
- 枚举常量
- 字符常量
常量的特点是具有值和类型。
浮点常量
浮点常量(floating-point constant)是表示有符号实数的十进制数。有符号实数的表示包括整数部分、分数部分和指数。使用浮点常量表示无法更改的浮点值。
可以省略小数点之前的数字(值的整数部分)或小数点之后的数字(小数部分),但不能同时省略两者。只有使用科学记数法表示时,才能省略小数点。常量的数字或字符之间没有空格字符。
以下是说明了浮点常量用科学记数法表示的示例:
15.75
1.575E1 /* = 15.75 */
1575e-2 /* = 15.75 */
-2.5e-3 /* = -0.0025 */
25E-4 /* = 0.0025 */
浮点常量是正的,除非它们前面有减号“-”。在这种情况下,减号被视为一元算术否定运算符。浮点常量的类型有float(浮点)、double(双精度)或long double(长双精度)。
没有f、F、l或L后缀的浮点常量就是类型double。如果字母f或F是后缀,则常量类型为float。如果以字母l或L为后缀,则它的类型为long double。例如:
10.0L /* long double */
10.0 /* double */
10.0F /* float */
整数常量
整数常量(integer constant)是表示整数值的十进制(以10为基数)、八进制(以8为基数)或十六进制(以16为基数)数。使用整数常量表示无法更改的整数值。
整数常量为正,除非它们前面有减号“-”。减号被解释为一元算术否定运算符。
如果整数常量以0x或0X开头,则为十六进制。如果它以数字0开头,则为八进制。否则,假定为十进制。
以下整数常量是等效的:
28
0x1C /* = 28的十六进制表示 */
034 /* = 28的八进制*/
整数常量的数字之间没有空格字符。这些示例显示了一些有效的十进制、八进制和十六进制常数。
/* 十进制常量 */
int dec_int = 28;
unsigned dec_uint = 4000000024u;
long dec_long = 2000000022l;
unsigned long dec_ulong = 4000000000ul;
long long dec_llong = 9000000000LL;
unsigned long long dec_ullong = 900000000001ull;
__int64 dec_i64 = 9000000000002I64;
unsigned __int64 dec_ui64 = 90000000000004ui64;
/* 八进制常量 */
int oct_int = 024;
unsigned oct_uint = 04000000024u;
long oct_long = 02000000022l;
unsigned long oct_ulong = 04000000000UL;
long long oct_llong = 044000000000000ll;
unsigned long long oct_ullong = 044400000000000001Ull;
__int64 oct_i64 = 04444000000000000002i64;
unsigned __int64 oct_ui64 = 04444000000000000004uI64;
/* 十六进制常量 */
int hex_int = 0x2a;
unsigned hex_uint = 0XA0000024u;
long hex_long = 0x20000022l;
unsigned long hex_ulong = 0XA0000021uL;
long long hex_llong = 0x8a000000000000ll;
unsigned long long hex_ullong = 0x8A40000000000010uLL;
__int64 hex_i64 = 0x4a44000000000020I64;
unsigned __int64 hex_ui64 = 0x8a44000000000040Ui64;
每个整数常量都会根据其值和表达方式指定一个类型。可以通过在常量的末尾附加字母l或L来强制整数常量转为类型long ;可以通过在值中附加u或U来强制其转为类型unsigned 。
注意:小写字母l可能与数字1容易混淆,应避免使用。
长整数常量的某些形式如下:
/* 长十进制常量 */
10L
79L
/* 长八进制常量 */
012L
0115L
/* 长十六进制常量 */
0xaL or 0xAL
0X4fL or 0x4FL
/* 无符号长十进制常量 */
776745UL
778866LU
分配给常量的类型取决于常量表示的值。常量的值必须在其类型的可表示值范围内。常量的类型确定在表达式中使用常量或应用减号“-”时执行哪些转换。以下总结了整数常量的转换规则。
- 没有后缀的十进制常量的类型是int、long int或unsigned long int。可以表示常量值的这三种类型中的第一种是分配给常量的类型。
- 分配给没有后缀的八进制和十六进制常量的类型为int、unsigned int、long int或unsigned long int,具体取决于常量的大小。
- 分配给具有u或U后缀的常量的类型是unsigned int或unsigned long int,具体取决于它们的大小。
- 分配给具有l或L后缀的常量的类型是long int或unsigned long int,具体取决于它们的大小。
- 分配给具有u或U和l或L后缀的常量的类型unsigned long int。
枚举常量
字符常量
字符常量(character constant)是通过将可表示字符集中的单个字符括在单引号“''”中而形成的。字符常量用于表示执行字符集中的字符。
字符常量类型
前面没有字母L的整数字符常量类型为int。包含单个字符的整数字符常量的值是解释为整数的字符的数值。例如,字符a的数值十进制为97,十六进制为61。
从语法上讲,宽字符常量(wide-character constant)是以字母L为前缀的字符常量。宽字符常量的类型为wchar_t,这是STDDEF.H头文件中定义的整数类型。例如:
char schar = 'x'; /*字符常量 */
wchar_t wchar = L'x'; /*宽字符常量 */
宽字符常量为16位宽,并指定扩展执行字符集的成员。它们允许以字母表中的字符表示,这些字符太大,无法用char类型表示。
执行字符集
执行字符集(execution character set)不一定与编写C程序所用的源字符集相同。执行字符集包括源字符集中的所有字符,以及空字符、换行符、退格、水平制表符、垂直制表符、回车和转义序列。源字符集和执行字符集在其他实现中可能不同。
转义序列
由反斜杠(\)后跟字母或数字组合组成的字符组合称为转义序列(Escape Sequence)。要表示字符常量中的换行符、单引号或某些其他字符,必须使用转义序列。转义序列被视为单个字符,因此作为字符常量有效。
转义序列通常用于指定终端和打印机上的回车和制表符移动等操作。它们还用于提供非打印字符和通常具有特殊含义的字符的文字表示,如双引号“"”。下表列出了ANSI转义序列及其表示的内容。
Escape Sequence | Represents |
---|---|
\a | Bell (alert) |
\b | Backspace |
\f | Form feed |
\n | New line |
\r | Carriage return |
\t | Horizontal tab |
\v | Vertical tab |
\' | Single quotation mark |
\" | Double quotation mark |
\\ | Backslash |
\? | Literal question mark |
\ ooo | ASCII character in octal notation |
\x hh | ASCII character in hexadecimal notation |
\x hhhh | Unicode character in hexadecimal notation if this escape sequence is used in a wide-character constant or a Unicode string literal. For example, WCHAR f = L'\x4e00' or WCHAR b[] = L"The Chinese character for one is \x4e00". |
八进制和十六进制字符规范
序列\ooo意味着可以将ASCII字符集中的任何字符指定为三位八进制字符代码。八进制整数的数值指定所需字符或宽字符的值。
类似地,序列\xhhh允许将任何ASCII字符指定为十六进制字符代码。例如,可以将ASCII退格字符作为正常的C转义序列(\b),也可以将其编码为\010(八进制)或\x008(十六进制)。
在八进制转义序列中,只能使用数字0到7。八进制转义序列的长度永远不能超过三位数,并由第一个非八进制数字的字符终止。虽然不需要使用所有三位数,但必须至少使用一位。例如,ASCII退格字符的八进制表示为\10,字母A的八进制表示为\101,如ASCII图表中给出的那样。
同样,十六进制转义序列必须至少使用一个数字,但可以省略第二和第三位数字。因此,可以将退格字符的十六进制转义序列指定为\x8、\x08或\x008。
八进制或十六进制转义序列的值必须在字符常量的类型unsigned char和宽字符常量的类型wchar_t的可表示值范围内。
与八进制转义常量不同,转义序列中的十六进制数字的数量是无限的。十六进制转义序列终止于第一个不是十六进制数字的字符。由于十六进制数字包括字母a到f,因此必须小心确保转义序列终止于预期的数字。为避免混淆,可以将八进制或十六进制字符定义放置在宏定义中:
#define Bell '\x07'
对于十六进制值,可以隔开字符串以清楚地显示正确的值:
"\xabc" /* 一个字符 */
"\xab" "c" /* 两个字符 */
字符串文本
字符串文本是源字符集中的字符序列,用双引号“" "”括起来。字符串文本用于表示字符序列,这些字符加在一起形成以空终止的字符串。必须始终以字母L为宽字符串文本前缀。
下面的示例是一个简单的字符串文本:
char *amessage = "This is a string literal.";
转义序列表中列出的所有转义代码都在字符串文字中有效。要在字符串文字中表示双引号,请使用转义序列“\"”。单引号“'”可以在没有转义序列的情况下表示。当反斜杠“\”出现在字符串中时,它必须跟第二个反斜杠“\\”。当反斜杠出现在行尾时,它始终被解释为行继续符。
字符串文本的类型
字符串文本具有 char的类型数组(即 char[ ])。宽字符字符串具有 wchar_t的类型数组wchar_t[ ]。这意味着字符串是带有 char 类型的元素的数组。 数组中的元素数等于字符串中的字符数加上结尾的 null 字符。
字符串文本的存储
文本字符串的字符将按顺序存储在连续内存位置。 字符串文本中的转义序列(例如,“\\”或“\"”)将作为单个字符进行计数。 null 字符(由“\0”转义序列表示)自动追加到每个字符串并标记该字符串的末尾。请注意,编译器无法在两个不同的地址存储两个相同的字符串。
字符串文本的串联
若要形成占用多行的字符串文本,则可以将两个字符串串联起来。 为此,请键入反斜杠“\”,然后按回车键。反斜杠将使编译器忽略以下换行符。 例如,字符串文本
"Long strings can be bro\
ken into two or more pieces."
等同于字符串
"Long strings can be broken into two or more pieces."
在之前可能已使用过反斜杠后跟换行符的任何地方,都可以使用字符串串联,用来输入长于一行的字符串。
若要在字符串文本中强制换行,请在字符串中要换行的位置输入换行转义序列“\n”,如下所示 :
"Enter a number between 1 and 100\nOr press Return"
由于字符串可以在源代码的任何列中开始,而长字符串可以在后面的行的任何列中继续,因此可以放置字符串以增强源代码可读性。 在任一情况下,在输出时,字符串的屏幕表示形式都不受影响。 例如:
printf_s ( "This is the first half of the string, "
"this is the second half ") ;
只要将字符串中的每个部分都用双引号括起来,则各个部分都将作为单个字符串进行串联和输出。 此串联根据转换阶段指定的编译期间的事件序列发生。
"This is the first half of the string, this is the second half"
初始化为仅用空白分隔的两个不同的字符串文本的字符串指针将作为单个字符串存储。 当正确引用后(如以下示例所示),结果与上一示例的相同:
char *string = "This is the first half of the string, "
"this is the second half";
printf_s( "%s" , string ) ;
标点和特殊字符
C 字符集中的标点和特殊字符各有其用途,从组织程序文本到定义编译器或已编译程序所执行的任务。它们不指定要执行的操作。 某些标点符号也是运算符,编译器从上下文确定
运算符包括: ( ) [ ] { } * , : = ; ... #
这些字符在 C 中具有特殊含义。
参考
本系列【现代C语言编程实战】归档至:https://github.com/waylau/modern-c-programming