第二篇 基础语法篇
第4章
C语言中的基本元素
本章将正式进入C语言编程话题。我们在第1章已经大致介绍了C语言的编译、连接和加载运行流程,参见图1-2。我们首先介绍C语言单个源文件的基本构成以及基本元素。
我们在图4-1中能看到,一个可用来编译执行的基本C源文件主要包含4个部分。
图4-1 C源文件的基本构成
第1部分是注释。注释主要用于给源代码做批注,方便阅读和维护。编译器会忽略所有注释部分,而且注释部分在预编译处理结束后就不存在了。我们将在10.9节讨论程序注释。
第2部分是预处理器(Preprocessor)。图4-1中的第9行代码就是一条#include预处理器,它将标准库头文件“stdio.h”中的所有内容都直接放到当前源文件中,这样我们就可以将它看作在第9行这个位置插入此头文件的所有内容(这里我们可以先无视上面的注释部分的处理)。“stdio.h”文件包含了第16行所用到的puts标准库函数的原型。我们将在第10章详细讨论预处理器。
第3部分是主函数入口main。它是C程序的入口函数。也就是说,当操作系统加载完我们构建生成的C程序后,率先执行的就是main函数。关于main函数,我们将在9.9节中介绍。
第4部分是用{ }包围着的函数具体实现代码(第13~17行)。这里的实现就是第16行打印输出两行文字。
C语言的头文件一般用.h后缀表示,源文件一般用.c后缀表示。C源文件是一个文本文件,所以它是由一系列字符构成的。下一节将介绍C源文件中可用的字符集以及执行C程序时可用的字符集。
4.1 C语言中的字符集
一般来说,编程语言的字符集都可分为两组:一组叫源字符集,另一组叫执行字符集。所谓“源字符集”是指在写C源代码时用的字符集,也就是呈现在C源文件中的字符集。而“执行字符集”是指编译构建完源文件后的目标二进制文件中所表示的字符集,它将用于运行在当前的执行环境中。比如,我们在控制台或者GUI窗口视图上所看到的文字信息就属于执行字符集。
C语言标准允许C语言实现采用多字符扩展字符集,但是必须要满足一组基本字符集。基本字符集都包含在ASCII码可显字符集中,包括半角的大写字母A~Z、小写字母a~z、半角的阿拉伯数字0到9以及下列符号:
! " # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^ _ { | } ~
为了叙述方便,上述这排符号后续将统称为“标点符号”;而大小写半角英文字母统称为“字母”;半角阿拉伯数字0到9统称为“数字”。
由于在C语言的上述基本字符集中有9个字符超出了ISO 646不变字符集的范围,分别是:# \ ^ [ ] | { } ~。所以,在C90标准中就引入了三字符连拼(Trigraph)的方式来表达这9个字符:
??= 对应于 #
??) 对应于 ]
??! 对应于 |
??( 对应于 [
??' 对应于 ^
??> 对应于 }
??/ 对应于 \
??< 对应于 {
??- 对应于 ~
例如:
?? =define arraycheck(a, b) a? ? (b? ? ) ? ? ! ? ? ! b? ? (a? ? )
printf(“Eh? ? ? /n”);
// 上述代码等价于:
#define arraycheck(a, b) a[b] || b[a]
printf(“Eh? \n”);
这里我们还能再呈现一下源字符集与执行字符集的差异。上述代码中,“? ? ? /n”表示源字符集,它在C源文件中就是如此写的;而最后翻译成的“\n”就相当于执行字符集,显示在命令行程序中就是一个换行。
由于C++17标准打算废弃三字符连拼,笔者估计下一个C语言标准也将废弃三字符连拼机制,因此不建议各位使用,大家只要了解一下这个历史即可。
C99标准中引入了对其中5个字符的双字符连拼(Digraph)表示。
<: 对应于 [
:> 对应于 ]
<% 对应于 {
%> 对应于 }
%: 对应于 #
双字符连拼在下一个标准中还能正常使用。尽管Trigraph与Digraph基本用不上,不过在看一些较早之前欧洲一些国家的人所写的代码时能知道那是什么。由于笔者在日本做过一些项目,所以知道在Windows系统下的日语环境中,“\”这个符号会被显示成“¥”。因此当我们看到“¥”符号时能反应出是“\”就行。
4.2 C语言中的token
在编程语言中经常会涉及“token”这个词,token这里不是指网络通信中所谓的“令牌”,而是用于词法解析的,通过指定一个词位(词的单位)的类别来结构化表示该词位。如以下代码:
int a = 3 << 2;
这里就有7个token,分别是:int、a、=、3、<<、2以及最后的分号;。这一行代码中就已经列出了C语言中的常用几种token,分别是关键字(int)、标识符(a)、字面量(3和2)、操作符(=和<<)、其他标点符号(;)。每个token之间用空白符或标点符号进行分隔。空白符主要包括空格(white space)、制表符(tab)以及换行回车。像上述代码也能写成以下形式,两者是等价的。
int a=3<<2;
但是,这里int与a之间必须用空白符分割。
C语言标准中定义了token和预处理token,分别用于在编译时和预编译时的符号解析。token包括关键字、标识符、常量、字符串字面量以及标点符号。预处理token主要包括头文件名、标识符、预处理数、字符常量、字符串字面量、标点符号以及不属于上述符号的每个非空白字符。
下面我们将分别描述标识符、关键字、常量与字符串字面量、标点符号这几种token。预处理token将放在第10章做详细描述。
4.2.1 C语言中的标识符
在C11标准中提到,C语言中的标识符可以表示一个对象(object),一个函数(function),一个结构体(structure)、联合体(union)或枚举(enumeration)的一个名字(C11标准中将结构体、联合体以及枚举类型的名字称为tag)或其中一个成员、一个typedef名、一个跳转标签(label)名、一个宏(macro)名或一个宏的形参(parameter)。当我们提到“标识符”时,要意识到标识符不仅仅是上述所描述实体的名称,而且也是对它们的引用(reference)。
一般C语言的实现约定,一个标识符由基本字符集中的所有大小写英文字母、阿拉伯数字0到9以及下划线_构成,并且标识符不能以数字开头。比如:aBc、_ab、C11、_3_都是有效的标识符;5ab、a(2、886都是无效的标识符。有些C语言实现允许将$作为构成标识符的有效字符,但有些是将含有$的标识符作为一种内部使用的特殊符号来用,所以我们在命名标识符的时候应该避免使用$符号。此外,C11标准允许使用多字节扩展字符集(通用字符名)来命名标识符,但不能违背上述基本约定。比如,在Apple LLVM编译器中,允许使用中文、拉丁字母、希腊字母等作为标识符:αντιo、bonné、小鳥遊·六花、ラーメン等都是有效标识符,但是像3百九、十*二,这些就是无效的标识符。此外,C语言标准中还规定,如果一个标识符含有通用字符名,那么每一个通用字符名必须落在ISO/IEC 10646编码方式的以下范围内(用十六进制表示):
1)00A8,00AA,00AD,00AF,00B2~00B5,00B7~00BA,00BC ~00BE, 00C0~00D6,00D8~00F6,00F8~00FF;
2)0100~167F,1681~180D,180F~1FFF;
3)200B~200D,202A~202E,203F~2040,2054,2060~206F;
4)2070~218F,2460~24FF,2776~2793,2C00~2DFF,2E80~2FFF;
5)3004~3007,3021~302F,3031~303F;
6)3040~D7FF;
7)F900~FD3D, FD40~FDCF, FDF0~FE44, FE47~FFFD;
8)10000~1FFFD,20000~2FFFD,30000~3FFFD,40000~4FFFD,50000~5FFFD,60000~6FFFD,70000~7FFFD,80000~8FFFD,90000~9FFFD, A0000~AFFFD, B0000~BFFFD, C0000~CFFFD, D0000~DFFFD, E0000~EFFFD。
此外,标识符的第一个通用字符名不能落在以下范围内:0300~036F,1DC0~1DFF,20D0~20FF, FE20~FE2F。
在C语言标准中没有特别设定一个标识符的最大长度。不过具体的C语言实现可以根据自己的情况设定标识符最大长度。
在同一作用域(scope)内,一个标识符应该指定一个确切的实体。如果编译器在当前上下文中无法判定某个标识符用于引用哪个实体,那么就会发生编译错误。关于作用域的详细介绍请参见11.1节。
4.2.2 C语言中的关键字
在编程语言中所谓的“关键字”(keyword)是指被编程语言编译器保留用作特定语义的token,它们不能被程序员当作其他标识符来使用。C11标准中的关键字见表4-1。
表4-1 C11标准中的关键字
在上述关键字中有些是由大写、小写以及下划线混合组成的,各位在编写代码的时候需要注意大小写。这些关键字会从第5章开始分别进行介绍。
看到以上这些关键字读者可能会感到奇怪,为何有些关键字是以下划线打头的呢?以下划线打头的关键字均是从C99标准开始引入的。由于在C99之前,有不少C语言编译器已经对C99标准新引入的特性给予支持,为了防止C99标准的关键字与一些编译器已有的扩展关键字冲突,从而通过以下划线作为前缀,然后首字母大写来定义这些关键字。而通过C语言新标准引入新的标准库可使得这些关键字能被统一。
所以,大家在使用以下划线打头的关键字时,请尽量先引入相应的标准库头文件,然后使用非下划线形式的相应关键字。比如,<stdbool.h>头文件中将_Bool类型定义为了bool类型;<complex.h>头文件中将_Complex定义为了complex,等等。我们最好使用bool、complex来代替_Bool和_Complex,这样一来书写更为简洁,二来又有更好的向前兼容以及跨平台等特性。当然,还有一些关键字是没有相应标准库定义形式的,比如_Generic,我们在使用的时候直接用_Generic即可。
4.2.3 C语言中的常量与字符串字面量
C语言中,常量(contant)有4种,分别是整数常量、浮点数常量、枚举常量以及字符常量。每个常量都具有一个特定的类型以及该常量所指定的值,常量值必须在其类型所能表示的范围内。整数常量和字符常量将在5.1节中描述;浮点数常量将在5.2节中描述;枚举常量将在6.1节描述。
字符串字面量我们之前已经见过了,图4-1中的u8“Hello, world\n你好,世界!”就是一个字符串字面量。在C11中,一个字符串字面量由一对双引号包裹的一系列的字符构成。如果字符串中含有诸如回车、双引号等字符的话,需要对它们进行转义,转义字符将在5.1.6节中描述。此外,字符串的第一个双引号前可以加u8、u和U这三种前缀。u8指定了该字符串字面量是一个UTF-8字符串;u表示该字符串字面量是一个UTF-16字符串;U表示该字符串字面量是一个UTF-32字符串。如果不加前缀,则默认为当前系统实现的字符编码格式。字符串字面量将在7.10节做进一步描述。
4.2.4 C语言中的标点符号
C语言的标点符号如下:
[ ] ( ) { } . ->
++ -- & * + - ~ !
/ % << >> < > <= >=
== ! = ^ | && || ? :
; ... = *= /= %= += -=
<<= >>= &= ^= |= , # ##
<: :> <% %> %: %:%:
标点符号是具有独立语法和语义意义的符号。它作为一个要执行的操作时,又称为操作符(operator)。操作符所作用的实体称为操作数(operand)。比如,3+2这个表达式中,+是一个操作符,表示整数加法操作。而3和2则是+的操作数,3作为+的左操作数;2作为+的右操作数。
上述列出的标点符号中,有些无法单独成为一个操作符,比如[、], (、)等,而是需要将它们组合起来[ ]、( )才行。而在( )操作符里边的表达式则作为该操作符的操作数。比如:(3+2)的操作数是表达式3+2。此外,有些标点符号可进行组合形成一个操作符,比如<<、+=、>>=等。这些组合标点符号之间不允许带有空白符,比如<<表示左移操作,而< <仅仅表示两个小于号。
C语言中,操作符按照可作用的操作数个数来分可分为单目操作符(unary operator)、双目操作符(binary operator)和三目操作符(ternary operator)。
1)单目操作符有!(表示逻辑非)、&(用作地址操作符时)、*(作为间接操作符时)、+(表示正数符号时)、-(表示负数符号时)、~(表示按位取反)。
2)双目操作符有++(表示自增操作)、--(表示自减操作)。
3)三目操作符只有一组,即?与:的组合,作为条件表达式的操作符,这将在8.2节中详细描述。
其余的,除了#和##作为预处理操作符之外,上述列出的操作数中都是双目操作符。
不同的操作符可能会有不同的计算优先次序。在计算一个表达式时,如果该表达式含有多个操作符,那么这些操作符按照优先级高的先开始计算,然后再计算低优先级的操作。如果几个操作符具有相同优先级,那么按照从左到右的顺序依次计算。在C11标准中定义了如下表达式的计算优先次序,排列从高到低。
1)基本表达式:标识符、常量、字符串字面量、圆括号表达式(比如(3+2))、泛型表达式。
2)后缀操作符:数组下标(比如a[0])、函数调用、结构体与联合体成员访问操作符(.和->)、后缀自增及自减操作符(比如a++; a--)、复合字面量(比如(int[]){1, 2, 3})。
3)单目操作符:前缀自增与自减操作符、地址操作符与间接操作符(比如++a;--a)、单目算术操作符(+、-、!、~,其中这里的+和-表示正负号)、sizeof操作符与_Alignof操作符。
4)类型投射操作符(详见5.6节)。
5)乘法操作符(包括乘、除、求余数*、/、%)。
6)加法操作符(+、-)。
7)移位操作符(左移、右移)。
8)关系操作符(大于、小于、大于等于、小于等于)。
9)相等性操作符(等于和不等于,==、! =)。
10)按位与操作符。
11)按位异或操作符。
12)按位或操作符。
13)逻辑与操作符。
14)逻辑或操作符。
15)条件操作符(即三目表达式)。
16)赋值操作符。
17)逗号操作符。
下面举一个简单的例子:
int a = 3 + 2 * 10 / 4- -(3-2);
上述代码中,(3-2)最先被计算得到结果1,然后再计算-(1)的结果是-1,然后计算2*10的结果等于20,再计算20/4的结果等5,再是3+5的结果等于8,然后是8-(-1)的结果是9,最后是将结果9赋值给变量a。
4.3 关于C语言中的“对象”
C11标准将“对象”定义为执行环境中的数据存储区域,对象中的内容用于表达它的值。当引用了某一对象时,该对象就可称为具有一个特定类型。言下之意,C语言标准中的“对象”是指数据实体,而不是一个函数。此外,它具有一个特定的存储区域,无论是在寄存器中还是在存储器中。另外,它具有一个特定的类型。
C语言不是一门面向对象的编程语言,所以这里的“对象”与面向对象编程语言所涉及的对象概念有些差别,不过从范围上来讲,这里的“对象”比面向对象中的对象范围更广。从总体上将对象进行划分可分为两大类——变量和常量。
□变量是指在程序运行时,允许该对象所存放的值被修改。
□常量是指在程序运行时,该对象所存放的值不允许被修改。
在C语言实现中,常量可以被写入ROM,尤其对于嵌入式设备而言,更有可能如此。这样,一旦对某个常量对象进行修改,那么系统会直接发出异常。而在通用桌面操作系统中,常量也被分配在RAM中,所以我们仍然可以通过类型转换或是其他奇技淫巧对常量对象进行修改,不过后果是无法预估的。
在计算机编程语言中还有一个比较常见的概念就是字面量。在传统编程语言中,字面量就是指在源代码中用于表示一个固定值的文字记号。
比如,像3、-10、3.14、"hello"等都属于字面量。
其中:
□3、-10表示整数字面量。
□3.14表示浮点数字面量。
□"hello"表示一个字符串字面量。
这些字面量往往都是常量,而像一般的整数字面量在概念上我们也无需关心它到底是不是一个对象,即不需要关心它有没有自己的存储空间。由于字面量以及像(3+2)等常量表达式是在编译时就能计算出结果的,所以对于这些字面量的算术逻辑计算也无需在程序运行时体现出来。
另外,C11还包括了结构体、联合体以及数组的复合字面量。这些复合字面量无需是常量,而且它们自己所包含的元素也完全可以是变量,并且在运行时也完全可被修改。
4.4 C语言中的“副作用”
在很多编程语言中都会提到“副作用”(side effects)这个概念。在C11标准中对副作用是这么描述的:对一个易变对象的访问、对一个对象的修改、对一个文件的修改,或调用一个函数,所有这些操作都具有副作用。副作用对执行环境中的状态做了改变。对一个表达式的计算通常包含了对值的计算以及对副作用的初始化。对一个左值表达式的值计算包含了判定该表达式所表示对象的标识。
通常来讲,所谓副作用就是在C源代码中的某一条表达式在目标程序中执行时,对当前程序的执行状态产生了或潜在产生改变,那么我们称该表达式产生了副作用。所谓程序执行状态包含了许多元素,比如对目标程序指令、寄存器的值、存储器中的数据等。
4.5 C语言标准库中的printf函数
我们这里先简单介绍一下本书后续会大量使用的控制台字符串输出函数printf。这是一个C语言标准库函数。printf函数的原型为:
int printf(const char * restrict format, ...);
此函数第一个参数format是一个字符串格式符,后面的省略号表示不定个数的参数,这些参数的数据类型需要分别与format所指向的字符串中的格式匹配。函数最后返回的是一个int类型整数,表示被传递到控制台的字符的个数。如果输出或者字符串编码发生错误,那么该函数将返回一个负值。但当前大部分编译器的实现并非返回传递到控制台的字符个数,而是字节个数,这对输出UTF-8编码的字符串时尤为如此。
下面简单介绍一下本书中常用的format字符串中的格式符。
1)%c:对应参数是一个int类型,但实际运行时会将该int类型对象转换为unsigned char类型。
2)%d:对应参数是一个int类型。
3)%f:对应参数是一个double类型。
4)%ld:对应参数是一个long int类型。
5)%s:对应参数是一个const char*类型,表示输出一个字符串。
6)%u:对应参数是一个unsigned int类型。
7)%zu:对应参数是一个size_t类型。
8)%td:对应参数是一个ptrdiff_t类型。
9)%x(或%X):对应参数是一个int类型,不过会以十六进制形式输出,其中大于9的数字根据字母x大小写进行转换,如果是%x,则大于9的数用a~f表示;如果是%X,则用A~F表示。
10)%%:输出一个%符号。
各位可以在自己的计算机上尝试编写下列代码,熟悉一下pritnf函数的使用方式:
#include <stdio.h> #include <math.h> int main(int argc, const char * argv[]) { int len = printf("你好\n"); printf("长度为:%d\n", len); printf("输出字符是:%c,输出浮点数是:%f\n", 'A', M_PI); printf("100的十六进制数为:0x%X\n", 100); const char *s = "hello, world! "; printf("几乎100%会出现在编程语言教科书上的字符串是:%s\n", s); }
各位可以编译运行上述代码。如果各位在某些Unix/Linux上实践,没有中文输入法也没有关系,可以用相应的英文来代替上述中文。另外,上述字符串中所出现的\n是一个转义字符,关于转义字符,我们将在5.1.6节中加以描述。
4.6 本章小结
本章我们大概描述了C语言构成的基本元素。一开始,我们列出了一个完整的C语言源文件应该包含的几个部分。然后我们提到了C语言中的可用字符集以及各类符号与它们的定义。关于C语言执行环境限制的更多详细信息可参考此博文:http://www.cnblogs.com/zenny-chen/p/4251813.html。
通过本章学习,各位应该已经能体会到C语言书写的大致格式,并且通过本章列出的一些代码片段,自己能试试身手写一些简单短小的代码出来,然后利用printf函数可以打印出一些计算结果。