C语言编程魔法书.pdf
http://www.100md.com
2020年4月15日
![]() |
| 第1页 |
![]() |
| 第4页 |
![]() |
| 第12页 |
![]() |
| 第40页 |
![]() |
| 第618页 |
参见附件(10834KB,738页)。
C语言编程魔法书是关于C语言编程的书籍,主要讲述了学习C语言的预备知识,C语言编程的环境搭建,C语言中的基本元素,基本数据类型,C语言的控制流语句等等内容。

C语言编程魔法书内容
主要讲解C11标准的语法内容,并且从整个编译、连接到加载过程都会涉及。同时在后会分别介绍GCC编译器与Clang编译器的C语言语法扩展。通过阅读本书,读者能够完全掌握新标准的C语言编程。并且对C语言设计思想、实现方式也能有个大概的了解。
书籍作者介绍
陈轶,网名zenny_chen,C语言与汇编语言重度用户与拥趸者,安沃传媒移动客户端及HTML5技术研发总监、首席科学家。多年高性能计算、嵌八式系统与移动互联网实践经验,深谙实时操作系统内核、设备驱动研发,对多媒体高性能计算编程、移动端开发以及各种处理器架构如数家珍,同时精通计算机底层基础技术与各种编程语言,尤其精通C/C++、Objective—C以及Swift。现任OpenGPU社区“并行计算讨论区”以及“高级计算机体系结构讨论区”两大版块的版主,以及CocoaChina社区的Swift编程语言讨论区与代码例子区的版主。《OpenCL异构并行计算原理、机制与优化实践》作者之一,App Store以及MacApp Store中CPU Dasher的作者,曾任理光上海图像技术有限公司高级体系结构工程师。
在线
对于当前主流桌面操作系统而言,可使用VisualC++、GCC以及LLVMClang这三大编译器。其中,VisualC++(简称MSVC)只能用于Windows操作系统;其余两个,除了可用于Windows操作系统之外,主要用于Unix/Linux操作系统。像现在很多版本的Linux都默认使用GCC作为C语言编译器。而像FreeBSD、macOS等系统默认使用LLVMClang编译器。由于当前LLVM项目主要在Apple的主推下发展的,所以在macOS中,Clang编译器又被称为AppleLLVM编译器。MSVC编译器主要用于Windows操作系统平台下的应用程序开发,它不开源。用户可以使用VisualStudioCommunity版本来使用它,但是如果要把通过VisualStudioCommunity工具生成出来的应用进行商用,那么就得好好阅读一下微软的许可证和说明书了。而使用GCC与Clang编译器构建出来的应用一般没有任何限制,程序员可以将应用程序随意发布和进行商用。不过由于MSVC编译器对C99标准的支持就十分有限,加之它压根不支持任何C11标准,所以本书的代码例子不会针对MSVC进行描述。所幸的是,VisualStudioCommunity2017加入了对Clang编译器的支持,官方称之为——ClangwithMicrosoftCodeGen,当前版本基于的是Clang3.8。也就是说,应用于VisualStudio集成开发环境中的Clang编译器前端可支持Clang编译器的所有语法特性,而后端生成的代码则与MSVC效果一样,包括像long整数类型在64位编译模式下长度仍然为4个字节,所以各位使用的时候也需要注意。为了方便描述,本书后面涉及VisualStudio集成开发环境下的Clang编译器简称为VS-Clang编译器。
而在嵌入式系统方面,可用的C语言编译器就非常丰富了。比如用于Keil公司51系列单片机的KeilC51编译器;当前大红大紫的Arduino板搭载的开发套件,可Clang编译器的所有语法特性,而后端生成的代码则与MSVC效果一样,包括像long整数类型在64位编译模式下长度仍然为4个字节,所以各位使用的时候也需要注意。为了方便描述,本书后面涉及VisualStudio集成开发环境下的Clang编译器简称为VS-Clang编译器。
目录
第一篇 预备知识篇
第1章C魔法概览 /2
1.1 例说编程语言 2
1.2 用C语言编程的基本注意事项 6
1.3 主流C语言编译器介绍 8
1.4 关于GNU规范的语法扩展 10
1.5 用C语言构建一个可执行程序的流程 11
1.6 本章小结 12
第2章学习C语言的预备知识 /14
2.1 计算机体系结构简介 14
2.1.1 贮存器 15
2.1.2 存储器 15
2.1.3 寄存器 16
2.1.4 计算单元 16
2.1.5 程序执行流程 16
2.2 整数在计算机中的表示 17
2.2.1 原码表示法 18
2.2.2 补码表示法 19
2.2.3 八进制数与十六进制数 20
2.3 浮点数在计算机中的表示 22
2.4 地址与字节对齐 24
2.5 字符编码 25
2.6 大端与小端 28
2.7 按位逻辑运算 28
2.8 移位操作 30
2.8.1 算术左移与逻辑左移 30
2.8.2 逻辑右移 30
2.8.3 算术右移 31
2.8.4 循环右移 32
2.9 本章小结 32
第3章C语言编程的环境搭建 /33
3.1 Windows操作系统下搭建C语言编程环境 33
3.1.1 安装Visual Studio Community 2017 34
3.1.2 安装MinGW编译器 40
3.1.3 安装LLVM Clang编译器 43
3.2 macOS系统下搭建C语言编程环境 45
3.3 本章小结 51
第二篇 基础语法篇
第4章C语言中的基本元素 /54
4.1 C语言中的字符集 55
4.2 C语言中的token 56
4.2.1 C语言中的标识符 57
4.2.2 C语言中的关键字 58
4.2.3 C语言中的常量与字符串字面量 60
4.2.4 C语言中的标点符号 60
4.3 关于C语言中的“对象” 62
4.4 C语言中的“副作用” 63
4.5 C语言标准库中的printf函数 63
4.6 本章小结 65
第5章基本数据类型 /66
5.1 整数类型 66
5.1.1 int类型 67
5.1.2 short类型 68
5.1.3 long类型 69
5.1.4 long long类型 71
5.1.5 布尔类型 72
5.1.6 字符类型 73
5.1.7 宽字符以及Unicode字符类型 77
5.1.8 size_t与ptrdiff_t类型 81
5.1.9 C语言中的标准整数类型 82
5.2 浮点类型 84
5.3 数据精度与类型转换 86
5.3.1 整数晋升 87
5.3.2 带符号与无符号整数之间的转换 87
5.3.3 浮点数与浮点数的转换以及浮点数与整数之间的转换 90
5.4 C语言基本运算操作符 92
5.4.1 加、减、乘、除与求模运算操作符 92
5.4.2 按位逻辑操作符 93
5.4.3 自增、自减操作符 94
5.4.4 关系操作符、相等性操作符与逻辑操作符 95
5.4.5 移位操作符 96
5.4.6 圆括号操作符 97
5.5 sizeof操作符 98
5.6 投射操作符 99
5.7 本章小结 101
第6章用户自定义类型 /102
6.1 枚举类型 102
6.2 结构体类型 105
6.2.1 结构体概述 105
6.2.2 用结构体创建对象并访问其成员 107
6.2.3 结构体复合字面量 110
6.3 联合体类型 113
6.4 位域 117
6.4.1 位域的一般特性 118
6.4.2 位域成员的存放与布局 120
6.4.3 匿名位域 122
6.4.4 位域使用示例 124
6.5 字节对齐与字节填充 125
6.5.1 _Alignof操作符 125
6.5.2 _Alignas对齐说明符 126
6.5.3 结构体成员的字节对齐与字节填充 129
6.6 复数类型 133
6.7 本章小结 135
第7章C语言的数组与指针 /136
7.1 一维数组 136
7.2 多维数组 141
7.3 变长数组 145
7.4 一级指针与对象地址 147
7.4.1 地址与指针的基本概念 148
7.4.2 访问指针对象所指对象的内容 149
7.4.3 指针对象的其他操作 150
7.5 多级指针 153
7.6 指向用户自定义类型的指针 156
7.7 指针与数组的关系 159
7.8 指向数组的指针 162
7.9 void类型、指向void类型的指针与空指针 165
7.10 字符数组与字符串字面量 167
7.11 完整与不完整类型 170
7.12 灵活的数组成员 171
7.13 本章小结 173
第8章C语言的控制流语句 /174
8.1 逗号表达式 174
8.2 条件表达式 176
8.3 if-else语句 176
8.4 switch-case语句 179
8.5 while与do-while迭代语句 182
8.6 for迭代语句 187
8.7 goto语句 189
8.8 本章小结 192
第9章C语言的函数 /193
9.1 函数的声明与定义 194
9.2 函数调用与实现 196
9.2.1 函数调用的顺序点 197
9.2.2 函数的栈空间 200
9.2.3 函数的参数传递与返回 201
9.2.4 通过形参修改实参的值 204
9.3 数组类型作为函数形参 205
9.4 带有不定参数类型及个数的函数声明与调用 209
9.5 函数的递归调用 212
9.6 内联函数 218
9.7 函数的返回类型与无返回函数 221
9.8 指向函数的指针 223
9.9 C语言中的主函数main 226
9.10 函数与函数调用作为sizeof操作符 228
9.11 本章小结 229
第10章C语言预处理器 /230
10.1 宏定义 231
10.1.1 宏的基本使用 232
10.1.2 宏定义中的#操作符 234
10.1.3 宏定义中的##操作符 236
10.1.4 宏替换 238
10.1.5 可变参数的宏定义 240
10.2 C语言中预定义的宏 243
10.2.1 C语言强制要求的预定义宏 243
10.2.2 环境宏 244
10.2.3 条件特征宏
C语言编程魔法书截图


C语言编程魔法书:基于C11标准
陈轶 著
ISBN:978-7-111-56521-5
本书纸版由机械工业出版社于2017年出版,电子版由华章分社(北京华章图文
信息有限公司,北京奥维博世图书发行有限公司)全球范围内制作与发行。
版权所有,侵权必究
客服热线:+ 86-10-68995265
客服信箱:service@bbbvip.com
官方网址:www.hzmedia.com.cn
新浪微博 @华章数媒
微信公众号 华章电子书(微信号:hzebook)目录
前言
第一篇 预备知识篇
第1章 C魔法概览
1.1 例说编程语言
1.2 用C语言编程的基本注意事项
1.3 主流C语言编译器介绍
1.4 关于GNU规范的语法扩展
1.5 用C语言构建一个可执行程序的流程
1.6 本章小结
第2章 学习C语言的预备知识
2.1 计算机体系结构简介
2.2 整数在计算机中的表示
2.3 浮点数在计算机中的表示2.4 地址与字节对齐
2.5 字符编码
2.6 大端与小端
2.7 按位逻辑运算
2.8 移位操作
2.9 本章小结
第3章 C语言编程的环境搭建
3.1 Windows操作系统下搭建C语言编程环境
3.2 macOS系统下搭建C语言编程环境
3.3 本章小结
第二篇 基础语法篇
第4章 C语言中的基本元素
4.1 C语言中的字符集
4.2 C语言中的token
4.3 关于C语言中的“对象”
4.4 C语言中的“副作用”4.5 C语言标准库中的printf函数
4.6 本章小结
第5章 基本数据类型
5.1 整数类型
5.2 浮点类型
5.3 数据精度与类型转换
5.4 C语言基本运算操作符
5.5 sizeof操作符
5.6 投射操作符
5.7 本章小结
第6章 用户自定义类型
6.1 枚举类型
6.2 结构体类型
6.3 联合体类型
6.4 位域
6.5 字节对齐与字节填充6.6 复数类型
6.7 本章小结
第7章 C语言的数组与指针
7.1 一维数组
7.2 多维数组
7.3 变长数组
7.4 一级指针与对象地址
7.5 多级指针
7.6 指向用户自定义类型的指针
7.7 指针与数组的关系
7.8 指向数组的指针
7.9 void类型、指向void类型的指针与空指针
7.10 字符数组与字符串字面量
7.11 完整与不完整类型
7.12 灵活的数组成员
7.13 本章小结第8章 C语言的控制流语句
8.1 逗号表达式
8.2 条件表达式
8.3 if-else语句
8.4 switch-case语句
8.5 while与do-while迭代语句
8.6 for迭代语句
8.7 goto语句
8.8 本章小结
第9章 C语言的函数
9.1 函数的声明与定义
9.2 函数调用与实现
9.3 数组类型作为函数形参
9.4 带有不定参数类型及个数的函数声明与调用
9.5 函数的递归调用
9.6 内联函数9.7 函数的返回类型与无返回函数
9.8 指向函数的指针
9.9 C语言中的主函数main
9.10 函数与函数调用作为sizeof操作符
9.11 本章小结
第10章 C语言预处理器
10.1 宏定义
10.2 C语言中预定义的宏
10.3 条件预编译
10.4 源文件包含预处理指示符
10.5 error预处理指示符
10.6 line预处理指示符
10.7 undef预处理指示符
10.8 pragma预编译指示符与操作符
10.9 空指示符与C语言中的程序注释
10.10 本章小结第11章 C语言程序的编译上下文
11.1 C语言程序中的作用域和名字空间
11.2 全局对象与函数
11.3 静态对象与函数
11.4 局部对象
11.5 对象的存储与生命周期
11.6 _Thread_local对象
11.7 本章小结
第三篇 高级语法篇
第12章 C语言中的类型限定符
12.1 const限定符
12.2 volatile限定符
12.3 restrict限定符
12.4 _Atomic限定符
12.5 本章小结
第13章 C语言的类型系统13.1 对象类型与函数类型
13.2 对声明符的进一步说明
13.3 更复杂的声明
13.4 typedef类型定义
13.5 本章小结
第14章 C11标准中的表达式、左值与求值顺序
14.1 常量表达式
14.2 泛型选择表达式
14.3 静态断言
14.4 C语言中的左值
14.5 C语言中表达式的求值顺序
14.6 C语言中的语句
14.7 本章小结
第15章 函数调用约定与ABI
15.1 Windows操作系统环境下x86处理器的函数调用约定
15.2 UnixLinux操作系统环境下x86处理器的函数调用约定15.3 ARM处理器环境下的函数调用约定
15.4 本章小结
第16章 创建静态库与动态库
16.1 Windows系统下创建静态库与动态库
16.2 macOS系统下创建静态库与动态库
16.3 Linux系统下创建并使用静态库与动态库
16.4 本章小结
第四篇 语法扩展篇
第17章 GCC对C11标准的语法扩展
17.1 在表达式中使用复合语句与声明
17.2 声明语句块作用域的跳转标签
17.3 跳转标签作为值
17.4 嵌套函数
17.5 使用typeof来获取对象类型
17.6 使用__auto_type做类型自动推导
17.7 对复数操作的扩展17.8 半精度浮点类型
17.9 长度为零的数组
17.10 对可变参数个数的宏的语法扩展
17.11 case语句中使用范围表达式
17.12 投射到一个联合体类型
17.13 使用二进制整数字面量
17.14 使用__attribute__指定函数、对象与类型的属性
17.15 本章小结
第18章 Clang编译器对C11标准的扩展
18.1 特征检查宏
18.2 _Nullable与_Nonnull
18.3 函数重载
18.4 Blocks语法
18.5 本章小结
第19章 对C语言的未来展望
19.1 C语言中的属性19.2 fallthrough属性
19.3 数组片段
19.4 其他语法特性
19.5 本章小结
第五篇 项目实践篇
第20章 制作UTF-8与UTF-16编码字符串的转码器
20.1 UTF-8字符编码格式
20.2 UTF-16字符编码格式
20.3 代码示例
20.4 本章小结
第21章 制作控制台计算器
21.1 对数字的解析
21.2 对操作符的优先级处理
21.3 代码示例
21.4 本章小结前言
为什么要写这本书
本人在2001年上了大学本科,读计算机科学与技术专业。在第一年的上半学
期,对计算机编程还没什么感觉。但是就在考“C语言程序设计”这门专业课的前一
个月,感觉这门课学了那么久几乎什么都不会,可把我急坏了。然后就在这短短一
个月的时间里又是看书,又是上机实验,终于考了70多分,算是过关了……不过奇怪
的是在考试结束后,就发现自己对编程有了感情。到了大二,我们上“数据结
构”所使用的教材是基于C++编程语言的,因为之前没学过C++语言,所以只能自
学。而在这个过程中,我发现自己对编程更加热爱。在上完大三之后,我在暑假里
又把之前的C语言重新巩固一番。有了计算机组成、操作系统、汇编语言、数据结构
等知识积淀之后再去看C语言编程就感觉容易多了。我也是由此喜欢上了C编程语
言。
10年之后,发现国内市面上很多C语言参考书仍然显得非常陈旧。不仅基于古老
的C8990标准,而且还在用Visual C++6.0这种既收费又老旧的开发环境教学生。
对于比较新的C99标准的讲解屈指可数,更鲜有针对最新的C11标准的书籍。出于对
C语言的热爱,在此热切希望能把最新标准的C语言奉献给各位读者,也想把C语言的
方方面面讲透并且能讲得通俗易懂,方便读者去思考实践,所以这也是我写这本书
的主要原因。当各位阅读完本书之后,会发现C语言竟然如此强大!而且在大部分时
候,尤其是我们想集中注意力解决某个特定问题的时候,使用C语言要比用其他一些
基于面向对象的类C编程语言(比如C++、Java等)要直观得多!本书之所以叫“C语言编程魔法书”,是因为像“宝典”、“圣经”之类的词已
经被用滥了。再者,C语言本身就拥有极其强大的魔力,你能用它做几乎所有的事
情。而且几乎每一个C语言编译器都能内联汇编语言,或者与C++、Objective-C直
接兼容,而对于像Java、C、Python等许多编程语言也有相应的接口。所以,我认
为C语言在计算机编程语言领域中就好比数学在自然科学中的地位和作用,它是很多
编程语言的基础,而且很多编程语言的编译器或解释器也都是基于C语言来写的。
就在2015年2月,Khronos标准组织发布了最具现代化的图形API——Vulkan,其主机端接口用的API是纯C语言。此外,像OpenGL、OpenCL、OpenAL、OpenVG等
开放标准都基于纯C语言。此外,最近10年来TIOBE每月的编程语言排名,C语言排
名始终能进前两名,也能说明它的使用范围之广,而且许多开源项目也多多少少会
使用C语言来编写。况且学了C语言之后,再学习C++、Java等面向对象编程语言也
会轻松很多。尤其像C++和Objective-C,没有C语言基础是完全不行的。所以个人
十分推荐计算机系的大学生将C语言作为自己的计算机入门编程语言!
本书特色
从技术层面上讲,本书介绍了C语言的最新标准,即ISOIEC 9899:2011。同
时,也介绍了主流开源C语言编译器GCC与Clang对标准C语言语法的扩充。而且所基
于的编译器和开发环境也是比较新的Visual Studio Community 2017、GCC 5,以及Clang 3.8(Apple LLVM 8.0,基于Xcode 8)。
从适合读者阅读和掌握知识的结构安排上讲,本书分为“预备知识篇”、“基
础语法篇”、“高级语法篇”,以及“语法扩展篇”,还有最后的“项目实践
篇”。从基础到高级,循序渐进地为读者描述C语言编程方法。本书尤其着重C语言标准语法上的精确描述,通过许多代码片段给读者介绍各种C语言语法知识,并且能
反映出C语言的灵活性以及在使用上的约束。
本书推崇读者使用合法免费的C语言编译器以及集成开发环境,希望读者能有正
确的软件版权意识,这样才能更好地为我国软件事业增添光彩,为打造良好的应用
市场以及生态环境作出贡献。因此,本书主要选择使用GCC、Clang这两个主流开源
免费的C语言编译器,而集成开发环境(IDE)则采用Visual Studio
Community、Eclipse、Xcode这三个常用的免费开发工具,其中,Visual Studio
Community不是开源的,而Xcode则是部分开源的。
本书虽然会讲解整个C编程语言,涉及了几乎所有的语法点,但是考虑到本书读
者可能是初学C语言,且没有多少计算机专业知识,所以本书措辞会尽量通俗,而不
过于追求学术化。某些描述可能会不太严谨,但对于本书所用到的GCC、Clang这两
大主流编译器而言将完全适用。另外,考虑到不少读者从事嵌入式系统开发工作,所以对于C语言标准中出现的所谓“由实现定义的”场合会尽量区分情况分别阐明。
本书的最终的目的就是让读者至少能熟练掌握C语言编程,能将它灵活地运用于实际
工程中。
读者对象
·嵌入式系统开发者
·移动或桌面客户端应用程序开发者
·服务器端应用程序开发者·系统架构师
·计算机、电子工程、通信专业的大学生
·其他对C语言编程感兴趣的人员
如何阅读本书
本书一共分为四大篇。
预备知识篇(第1~3章),简单描述C语言的概况、学习C语言的预备知识,以
及在Windows、macOS和Linux三大桌面环境下搭建编写C环境的方法。
第1章 C魔法概览。主要介绍C语言的来历和演化,用它编写代码的编程模式以
及我们可以用于实践的主流C语言编译器。
第2章 学习C语言的预备知识。这一章主要为不太熟悉计算机系统的读者提供
一些基础的计算机理论知识和相关概念,比如整数与浮点数在计算机中的表示方
法、字符编码格式、按位逻辑计算、移位操作等。
第3章 C语言编程的环境搭建。这一章主要介绍了Windows、macOS以及Linux
系统下如何安装并使用主流编译器与集成开发环境。
基础语法篇(第4~11章)讲解C语言的基本语法。这是C语言程序员必须掌握
的。
第4章 C语言中的基本元素。这一章描述了C语言中常用字符集以及合法token
的构成。此外还介绍了标识符、关键字以及标点符号的使用说明。第5章 基本数据类型。这一章介绍了整数类型、字符类型、浮点类型数据的表
示,以及它们之间的类型转换。此外还描述了对于这些基本数据类型的算术逻辑操
作、投射操作以及通过sizeof操作符获取数据类型与对象相应的字节数。
第6章 用户自定义类型。这一章描述了枚举、结构体以及联合体这三种用户自
定义类型,并介绍了它们的特性以及各种使用方式。
第7章 C语言的数组和指针。这一章十分关键,也是C语言的语法难点。这里详
细介绍了C语言中一维数组与多维数组的表示以及如何对它们进行操作,然后介绍了
C语言中的指针类型,详细阐述了指针类型的使用技巧以及需要注意的事项。
第8章 C语言的控制流语句。这一章介绍了C语言的条件语句、选择语句以及循
环等控制流语句。
第9章 C语言的函数。这一章介绍了C语言中的函数概念,包括C语言函数的声
明及定义,还有C函数的调用。此外还介绍了C语言函数标识符作为表达式时的类
型。
第10章 C语言的预处理器。这章包含了目前C11标准中所支持的所有预处理器
特性,包括宏定义、预处理条件、预编译指示符与操作符以及C代码的注释。
第11章 C语言的编译上下文。这一章介绍了C语言对象与函数的作用域和名字
空间。详细介绍了C语言中的四大作用域以及在不同作用域中的对象的生命周期。此
外还介绍了对象与函数的连接属性,包括外部连接和内部连接。
高级语法篇(第12~16章)讲述C语言的一些高级特性。这一部分内容不需要C
语言程序员必须掌握,但需要对此有个大概了解。第12章 C语言中的类型限定符。该章介绍了C11标准中支持的const、volatile、restrict与_Atomic这四种限定符。详细说明了限定符用于修饰含有指
针的对象时,在号的不同位置所起到的不同作用。然后分别介绍这四种限定符的具
体含义。
第13章 C语言中的类型系统。这一章把C语言语法体系中的整个类型系统再梳
理了一遍。这一章介绍了对于一些复杂类型的对象如何去剖析、理解,然后自己如
何去声明自己想要的复杂类型的对象和函数。这一章所描述的其实是整个C语言语法
体系的核心,如果大家能掌握的话,那么基本就算是真正掌握C语言了。其实,对于
任一强类型的编程语言而言,其系统类型总是扮演着十分重要的角色,我们学习此
类语言都需要透彻理解其整个类型系统。
第14章 C11标准中的表达式、左值与求值顺序。该章先介绍了C11标准中各类
表达式以及它们的计算优先级。然后介绍了“左值”这个概念,并讲解了表达式之
间的求值顺序。
第15章 函数调用约定与ABI。该章与C语言标准并无太大关系,但却与实际项
目开发有关。这一章介绍了主流C语言编译器所采用的函数调用约定,然后详细描述
了函数调用的过程,包括参数传递和返回值的具体处理。该章对嵌入式系统开发者
以及需要将C语言与汇编语言进行交互使用的高性能计算开发者而言,将大为有用。
第16章 创建动态库与静态库。这一章介绍了用主流C语言编译工具构建静态库
以及动态库的方法,并介绍如何使用这些库文件。
语法扩展篇(第17~19章)讲述了GCC与Clang编译器对C语言的扩展。
第17章 GCC对C11标准的扩展。该章先简单介绍GNU语法扩展,然后介绍GCC编译器中常用的扩展语法。
第18章 Clang编译器对C11标准的扩展。该章介绍了Clang编译器对C11标准
的语法扩展。最后还介绍了Apple开源的Grand Central Dispatch库的简单使
用。
第19章 对C语言的未来展望。该章主要介绍了C语言的设计理念以及当前C语言
标准委员会的工作组正在为C语言新增的内容,还谈到了哪些特性不会被添加到C语
言中去。
项目实践篇(第20~21章),这里通过两个实际的C语言项目来介绍我们如何利
用C语言来创作出自己的程序。
第20章 描述了UTF-8编码格式的字符串与UTF-16编码格式的字符串进行相互
转换的例子。
第21章 介绍一个看似简单而功能很丰富的基于控制台的计算器程序。
建议零基础的读者要了解第一篇的预备知识,这对于后面深入学习C语言编程很
有帮助。另外,这部分读者可以先不用强行看第三篇,尤其是第15章。因为第三篇
涉及的知识比较深,而第15章又会直接引入汇编语言,这对于没有一定计算机专业
知识的读者会比较难以理解。如果是有一定计算机专业知识的读者可以略过第一
篇,直接阅读第二篇。另外,如果是从事嵌入式系统开发的、或从事系统底层开发
的资深程序员,建议仔细阅读第三、第四篇,相信这部分内容会对你的工作很有帮
助。
勘误和支持由于笔者的水平有限,编写时间仓促,书中难免会出现一些错误或者不准确的
地方,恳请读者批评指正。如果你有更多的宝贵意见,欢迎你访问我的个人博客网
站http:blog.csdn.netzenny_chen进行专题讨论,我会尽量在线上为你提供
最满意的解答。同时,你也可以通过微博http:weibo.comzenny1chen与我联
系,或发送电子邮件到zenny_chen@163.com。期待能够得到你们的真挚反馈,在
技术之路上互勉共进。另外,本书最后两章的代码可以在作者的GitHub上获
取:https:github.comzenny-chen。
致谢
首先感谢我的父母和妻子对我写作此书的大力支持,尤其是我妻子在我忙于工
作、编写此书时帮忙照顾孩子和做饭。然后感谢我公司老板对我写作此书的鼓舞与
期待。
这里还要感谢机械工业出版社华章公司的编辑高婧雅,在一年多的时间里给予
我的大力支持和帮助。
最后感谢支持我的技术爱好者,感谢你们对我的支持以及对我的信任。
我想和作者聊聊
为了能更好地与读者进行联系,笔者这里留了一个QQ讨论群。各位如果在阅读
此书中有任何疑问可以来本群询问,大家可以一起探讨。各位可以扫一扫下方的二
维码,进此群的提示语为:“C语言编程魔法书”,或者查询群号86540289申请入第一篇 预备知识篇
言、汇编语言以及高级语言。下面由底层到高层分别介绍这几种类别的编程语言。
计算机编程语言从对计算机硬件底层的抽象程度进行分类,可分为:机器语
的大致过程。
GNU语法扩展。最后简单介绍一下从用C语言编写程序到编译、构建一个可执行程序
及C语言标准的演化进程。然后介绍一下C语言编程思想,当前主流C语言编译器以及
本章内容主要对C编程语言(以下简称C语言)进行大体介绍,包括它的历史以
第1章 C魔法概览
语言,用于数据库查询)算是一种第四代语言。
只需要一些简单的描述语句就能让计算机做比较复杂的工作。比如SQL(结构化查询
当然,有些书中还介绍了第四代语言,它基于高级语言,比高级语言更抽象,为最终的机器指令码。
编,但可能比汇编适用范围更广、更利于跨平台的字节码),最后将中间语言翻译
达式翻译为对应的机器指令码;也可以将高级语言先翻译为中间语言(类似于汇
循环、更形象自然的表达式等。高级语言一般通过编译器(compiler)可直接将表
构化和模块化。比如,高级语言具有自定义变量标识符、自定义数据结构、分支与
比起汇编语言往往更具有表达力,且拥有更加丰富的语法特性,以便将程序进行结
3)高级语言的表达形式更为抽象且贴近我们日常的语言表述。而且,高级语言
的机器指令码。
是对机器指令的简单抽象,通过汇编器(assembler)可以将汇编语句翻译成对应
表示对应机器指令的功能、寄存器编号、立即数(immediates)等元素。汇编语言
2)汇编语言(Assembly Language)通过简单的指令助记符(memonics)来
构)。
有些是固定长度的(比如ARM、MIPS等架构),有些是可变长度的(比如x86架
器、立即数等多种元素。每种处理器架构所对应的机器码的字节长度也各不相同,包含了当前指令的功能(比如算术逻辑运算、移位、分支、中断、IO等)、寄存
1)机器语言是直接通过十六进制数表示当前处理器架构的机器指令码。指令码
1.1 例说编程语言下面,为了能让大家对这三种层次的编程语言有一个感性的认识,这里将列举
ARMv8架构处理器下的机器语言、汇编语言,加上它们相应的C语言。读者如果手头
有Xcode,并且有包含Apple A7或更高版本处理器的iOS设备的话,可以直接编译运
行,并能看到最终效果。
下面首先列出一个文件名为my_sub.s的汇编源文件,其中包含了机器语言和汇
编语言。见代码清单1-1:
代码清单1-1 机器语言与汇编语言
.text
.align 4
ifdef __arm64__
.globl _my_sub_machine
.globl _my_sub_assembly
用机器语言实现减法操作
_my_sub_machine:
.long 0x4b010000
.long 0xd65f03c0
用汇编语言实现减法操作
_my_sub_assembly:
sub w0, w0, w1
ret
endif
示将寄存器w0与寄存器w1的值进行相减,然后将结果写回w0寄存器中。
说,“0x4b010000”这串32位的十六进制代码意思就是“sub w0,w0,w1”,表
令。这两条机器指令正好与_my_sub_assembly中的两条汇编指令相对应。也就是
在代码清单1-1中,_my_sub_machine程序片段中的两条.long语句即为机器指而“0xd65f03c0”指令码对应于“ret”(更确切地说是ret x30),表示返回当
前过程(procedure)。在汇编语言中,一般会使用过程或者例程(routine)来表
示一个可执行的程序片段。在C语言中一般都用函数(function)表示。我们在这
里能够明显看到,汇编语言采用指令助记符的方式比写机器指令码要直观得多,而
且也不容易出错。“sub”指令的功能从助记符上就能知道是“减法”功能;而w0、w1也明确指明了使用的寄存器是w0和w1。这些在“0x4b010000”这种机器指令码上
都无法直观地表现出来。
代码清单1-2列出C语言是如何表达一个减法操作的。
代码清单1-2 减法操作对应的C语言
static int my_sub_c(int a, int b)
{
return a - b;
}
代码清单1-2所列出的C语言代码与代码清单1-1中的机器指令码和汇编语言完
全对应,意思一目了然——将参数变量a的值与参数变量b的值进行相减,然后将结
果返回。从这里我们就能看到机器语言、汇编语言以及以C语言为代表的高级语言之
间在表达力上的差距了。高级语言的目的就是为了给程序员提供更良好的编程工
具,更简洁、更富有表达力的语言,使得我们程序员能提升生产力,并且能构思出
更多精彩炫酷的应用,而不是把太多的精力都投入在如何让计算机执行的细节上。
代码清单1-3能让我们在主函数或其他函数中测试上述已经编写好的函数。
代码清单1-3 展示减法操作的结果
ifdef __arm64__extern int my_sub_machine(int a, int b);
extern int my_sub_assembly(int a, int b);
int result_machine = my_sub_machine(10, 2);
int result_assembly = my_sub_assembly(5, 3);
int result_c = my_sub_c(6, 2);
printf(Three results: %d, %d, %d\n, result_machine, result_assembly, result_c);
endif
执行了上述代码之后,我们最后能在控制台看到输出结果:“Three
results:8,2,4”。可见,上述三种不同的编程语言,计算功能是完全一致的,都是对两个输入参数做减法操作,然后返回差值。然而就可读性、可理解性以及编
程便利性而言,显然C语言比起其他两者要强得多。而可读性最差的无疑就是机器指
令码了。
1.C语言的类别与产生
对于高级语言来说,从表达上又可分为命令式编程语言(imperative
programming language)和陈述型编程语言(declarative programming
language)。命令式语言主要包括过程式(procedural)、结构化
(structured)以及面向对象(object-oriented)的编程语言;陈述型编程语言
主要包括函数式(functional)以及逻辑型(logical)编程语言。而C语言则属
于结构化的命令式编程语言。不过现在很多命令式编程语言也包含了一些函数式编
程语言的特征。在本书中,后面第18章中谈到的Blocks语法就是一个很典型的函数
式编程语言的语法。
C语言最初由Dennis Ritchie于1969年到1973年在ATT贝尔实验室里开发出
来,主要用于重新实现Unix操作系统。此时,C语言又被称为KR C。其中,K表示
Kernighan的首字母,而R则是Ritchie的首字母。KR C语言与后来标准化的C语言有很大差异。比如,如果函数返回类型为int,则int可省:int my_function
{},也可以写成my_function{}。编译器不会有任何警告,更不会报错。另
外,还有现在看来比较奇葩的函数定义,像我们现在定义这么一个函数——void
my_function(int a,charp){},如果是用KR C语法定义的话要写成:void
my_function(a,p)int a;charp;{}。KR的C语法中,定义一个函数时,其
形参列表先列出形参的标识符,然后在函数声明的后面紧跟着对形参标识符的完整
声明,最后是函数体。这在现行标准中已经被逐步废弃使用了。另外,当时的第一
本C语言专业书《The C Programming Language》也并非一个正式的编程语言规
范,但被用了许多年。
2.C90标准
由于C语言被各大公司所使用(包括当时处于鼎盛时期的IBM PC),因此到了
1989年,C语言由美国国家标准协会(ANSI)进行了标准化,此时C语言又被称为
ANSI C。而仅过一年,ANSI C就被国际标准化组织ISO给采纳了。此时,C语言在
ISO中有了一个官方名称——ISOIEC 9899:1990。其中,9899是C语言在ISO标
准中的代号,像C++在ISO标准中的代号是14882。而冒号后面的1990表示当前修订
好的版本是在1990年发布的。对于ISOIEC 9899:1990的俗称或简称,有些地方
称为C89,有些地方称为C90,或者C8990。不管怎么称呼,它们都指代这个最初的
C语言国际标准。这个版本的C语言标准作为KR C的一个超集(即KR C是此标准C
的一个子集),把后来引入的许多非官方特性也一起整合了进去。其中包括了从
C++借鉴的函数原型(Function Prototypes),指向void的指针,对国际字符集
以及本地语言环境的支持。在此标准中,尽管已经将函数定义的方式改为现在我们
常用的那种方式,不过KR的语法形式仍然兼容。3.C99标准
在随后的几年里,C语言的标准化委员会又不断地对C语言进行改进,到了1999
年,正式发布了ISOIEC 9899:1999,简称为C99标准。C99标准引入了许多特
性,包括内联函数(inline functions)、可变长度的数组、灵活的数组成员(用
于结构体)、复合字面量、指定成员的初始化器、对IEEE754浮点数的改进、支持
不定参数个数的宏定义,在数据类型上还增加了long long int以及复数类型。毫
不夸张地说,即便到目前为止,很少有C语言编译器是完整支持C99的。像主流的
GCC以及Clang编译器都能支持高达90%以上,而微软的Visual Studio 2015中的C
编译器只能支持到70%左右。
4.C11标准
2007年,C语言标准委员会又重新开始修订C语言,到了2011年正式发布了
ISOIEC 9899:2011,简称为C11标准。C11标准新引入的特征尽管没C99相对C90
引入的那么多,但是这些也都十分有用,比如:字节对齐说明符、泛型机制
(generic selection)、对多线程的支持、静态断言、原子操作以及对Unicode
的支持。本书将主要针对C11标准为大家详细讲解C编程语言。关于C语言历史与演化
进程的详细介绍可参考维基百
科:https:en.wikipedia.orgwikiC_%28programming_language%29。
笔者近两年也是在不断地了解C语言标准委员会的最新动态(可参
见:http:www.open-std.orgjtc1sc22wg14),其中看到有人提出想为C
语言添加面向对象的特性,包括增加类、继承、多态等已被C++语言所广泛使用的语
法特性,但是最终被委员会驳回了。因为这些复杂的语法特性并不符合C语言的设计
理念以及设计哲学,况且C++已经有了这些特性,C语言无需再对它们进行支持。笔者将在第19章给大家谈谈C语言设计理念与发展方向。1.2 用C语言编程的基本注意事项
C语言的发明其实基于Unix操作系统。当时在C语言未面世之前,Dennis
Ritchie所在的ATT贝尔实验室用的Unix系统是完全用汇编语言写的。汇编语言的
优势是直接面向处理器本身,能直接对底层硬件进行控制,充分发挥处理器的硬件
能力。然而,它的缺陷也是显而易见的。
1.汇编语言的不足
首先,不可移植性。每种处理器,其指令集都大相径庭,比如ARM有ARM的指令
集架构(ISA),Intel x86有x86的ISA,还有MIPS、Power(原来为PowerPC),Motorola 68000等;再加上各类微控制器单元(Micro-Controller Unit,MCU)、各类数字信号处理器(Digital Signal Processor,DSP),每种ISA都
有其相应的汇编语言。那么多处理器如果对每一种都使用不同的汇编语言来实现同
一个操作系统,那操作系统的开发人员真要崩溃了……而且即便实现出来,可能各个
处理器上的实现也会有所不同,标准也很难被统一起来。
其次,汇编语言本身要比高级语言精密。因为汇编语言面对的都是寄存器、存
储器以及各类底层硬件,而不是一种抽象的数据模型,所以代码编写时需要非常谨
慎,而且调试程序也十分麻烦,且非常容易出错。所以,如果有一种既能面向底层
硬件,又能对数据以及程序进行抽象的高级语言出现,那势必既能不太影响程序执
行效率,又能大大提升程序的可执行性、可读性以及编写的效率,这将是非常伟大
的贡献。C语言也就是在这种背景下诞生的。
如果说,汇编语言面向的是底层硬件、一种过程化的编程风格的话,那么C语言就是面向数据流和算法、一种结构化的编程风格。C语言是一种结构化的、静态类型
的编译型编程语言。也就是说,用C语言编写了源代码之后,需要通过C语言编译器
进行编译,构建为相应的处理器能直接执行的机器码,然后处理器可以对生成出来
的机器码进行执行。所以在各个处理器上,处理器厂商或第三方只需要为当前处理
器写一个对应的C语言编译器即可。然后任何符合C语言标准的程序都能在上面编译
后执行,除了需要支持某些机器特定的功能和特性外(后面会介绍)。
2.C语言编写程序要注意什么
那么我们在用C语言写程序的时候应该注意哪些方面呢?
1)可移植性:C语言被设计出来的一大初衷就是为了能将同一个源代码放到各
个不同的平台上编译运行。因此,如果我们的代码要在多种不同架构的处理器上运
行的话,我们就得注意C语言标准规定了哪些特性是编译器必须遵守的,哪些特性是
平台或编译器自己实现的。我们要尽量使用标准中已明文规定的编程规范,尽可能
避免在不同平台可能会产生不同行为的语法特性。当然,由于上面提到的处理器种
类太过多样,尤其在嵌入式开发领域,很多MCU用的还都是8位处理器,这种情况下C
源代码就很难被移植到32位或64位系统下了。本书后面将会指出大部分主流平台对C
语言标准中所提到的“实现定义”行为的区别。另外,也会提到一些技巧来应对不
同的平台特性。
2)可维护性:可维护性在实际工程项目的研发中非常重要。它体现在最初工程
架构的设计、对各个功能模块的划分、相应的开发人员安排,还有后期的测试。一
般来说,现在一个工程如果是从无到有进行开发的话会采用螺旋式开发模型。也就
是说,一个项目启动后,可以先做一个功能简单但能正常工作的产品原型。然后在
此基础上不断地为它增加更多功能,或对之前的功能进行修改。在此期间,我们如何对整个工程进行模块化划分,从而能安排不同开发人员针对不同功能模块进行开
发就变得尤为重要。另外,在工程开发过程中,如果有人员流动,那么如何将即将
离职的开发人员手中的工作交付给新人也关系到整个项目的进展。因此,一个良好
的C语言代码应该具有可读性、良好的文档化注释风格,以及较详细的设计文档。对
于一个较大的工程项目来说,开发人员不仅仅需要把自己的代码写好,而且要写得
能让别人看懂,并且要做好详细的设计文档,这样才能把项目风险降低。
3)可延展性:大家或许已经知道,像微软的Windows操作系统由数千名工程师
合作研发;Linux操作系统对外开源,参与其中的研发人员也有数百上千人。如果我
们在一个开发团队中负责一个需要由多人合作开发的工程项目,那么我们写的功能
模块需要与其他人写的功能模块进行对接。所以,我们在开发一个较大工程项目
时,需要协调好各自对外的模块接口(Application Program Interface,API)。由于C语言没有全局名字空间(namespace)这个概念,所以命名一个对外
接口也是非常重要的,否则可能会与其他功能模块的接口名发生冲突。本书后面会
对C语言函数命名以及符号连接做进一步介绍。
4)性能:性能是提升程序使用者效率和生产力的体现。一个应用程序的性能越
高,那么计算一个任务所花费的时间越短,也越节省计算机的耗电。而对于如何提
升性能,一方面需要程序员对处理器架构、硬件特性有一定了解;另一方面需要程
序员拥有比较丰富的算法知识,能针对实际需求灵活采用高效的算法。而像C语言这
种十分接近硬件底层的高级编程语言,能极大限度地发挥处理器的特长,从而达到
高效的运行性能。1.3 主流C语言编译器介绍
对于当前主流桌面操作系统而言,可使用Visual C++、GCC以及LLVM Clang这
三大编译器。其中,Visual C++(简称MSVC)只能用于Windows操作系统;其余两
个,除了可用于Windows操作系统之外,主要用于UnixLinux操作系统。像现在很
多版本的Linux都默认使用GCC作为C语言编译器。而像FreeBSD、macOS等系统默认
使用LLVM Clang编译器。由于当前LLVM项目主要在Apple的主推下发展的,所以在
macOS中,Clang编译器又被称为Apple LLVM编译器。MSVC编译器主要用于
Windows操作系统平台下的应用程序开发,它不开源。用户可以使用Visual
Studio Community版本来免费使用它,但是如果要把通过Visual Studio
Community工具生成出来的应用进行商用,那么就得好好阅读一下微软的许可证和
说明书了。而使用GCC与Clang编译器构建出来的应用一般没有任何限制,程序员可
以将应用程序随意发布和进行商用。不过由于MSVC编译器对C99标准的支持就十分
有限,加之它压根不支持任何C11标准,所以本书的代码例子不会针对MSVC进行描
述。所幸的是,Visual Studio Community 2017加入了对Clang编译器的支持,官方称之为——Clang with Microsoft CodeGen,当前版本基于的是Clang
3.8。也就是说,应用于Visual Studio集成开发环境中的Clang编译器前端可支持
Clang编译器的所有语法特性,而后端生成的代码则与MSVC效果一样,包括像long
整数类型在64位编译模式下长度仍然为4个字节,所以各位使用的时候也需要注意。
为了方便描述,本书后面涉及Visual Studio集成开发环境下的Clang编译器简称
为VS-Clang编译器。
而在嵌入式系统方面,可用的C语言编译器就非常丰富了。比如用于Keil公司
51系列单片机的Keil C51编译器;当前大红大紫的Arduino板搭载的开发套件,可用针对AVR微控制器的AVR GCC编译器;ARM自己出的ADS(ARM Development
Suite)、RVDS(RealView Development Suite)和当前最新的DS-5 Studio;
DSP设计商TI(Texas Instruments)的CCS(Code Composer Studio);DSP设
计商ADI(Analog Devices,Inc.)的Visual DSP++编译器,等等。通常,用于
嵌入式系统开发的编译工具链都没有免费版本,而且一般需要通过国内代理进行购
买。所以,这对于个人开发者或者嵌入式系统爱好者而言是一道不低的门槛。不过
Arduino的开发套件是可使用的,并且用它做开发板连接调试也十分简
单。Arduino所采用的C编译器是基于GCC的。还有像树莓派(Raspberry Pi)这种
迷你电脑可以直接使用GCC和Clang编译器。此外,还有像nVidia公司推出的
Jetson TK系列开发板也可直接使用GCC和Clang编译器。树莓派与Jetson TK都默
认安装了Linux操作系统。在嵌入式领域,一般比较低端的单片机,比如8位的MCU
所对应的C编译器可能只支持C90标准,有些甚至连C90标准的很多特性都不支持。
因为它们一方面内存小,ROM的容量也小;另一方面,本身处理器机能就十分有限,有些甚至无法支持函数指针,因为处理器本身不包含通过寄存器做间接过程调用的
指令。而像32位处理器或DSP,一般都至少能支持C99标准,它们本身的性能也十分
强大。而像ARM出的RVDS编译器甚至可用GNU语法扩展。
图1-1展示了上述C语言编译器的分类。图1-1 C语言编译器的分类1.4 关于GNU规范的语法扩展
GNU是一款能用于构建类Unix操作系统的计算机软件合集,由自由软件之父
Richard Stallman开创,于1983年9月27日对外发布。GNU完全由自由软件(free
software)构成。GNU语法扩展源自于GCC编译器,在1987年发布1.0版本,称为
GNU C Compiler。随后,GCC编译器前端[1]支持了C++、Objective-CC++、Fortran、Ada、Java以及最近跃升的Go等编程语言,因此现在GCC被称为GNU
Compiler Collection。由于在20世纪90年代,GNU C编译器就对C90标准做了相
当多的语法扩展,包括复合字面量、匿名结构体和数组、可指定的初始化器等,这
些语法扩展被广泛使用,尤其是大量用于Linux内核代码中,因此C99标准将这些语
法特性全都列入标准之中。
正因为GCC本身是开源自由软件,因此很多商用编译器也基于GCC进行扩展。像
ARM的RVCT(RealView Compiler Toolkit)本身就支持GNU扩展。还有不少开发
平台本身就直接使用GCC编译工具。由于有不少大公司顶级开发人员的参与,因此
GCC编译器的目标代码优化能力相当高,而且还支持许多不同的处理器。所以,GCC
当前被广泛使用并博得开发者的好评。像Linux操作系统基本默认使用GCC作为默认
编译器,包括Android的NDK开发工具一开始也是如此。
然而,由于GCC基于比较严格的GPL许可证,许多大型商业开发商对它望而却
步。该许可证允许使用者免费使用软件,但是要求不能随意对它进行篡改并重新发
布。如果开发者对它进行篡改,然后发布自己修改之后的软件,那么必须要把自己
修改的那部分也开源出来。因此,在2003年诞生了一个LLVM开源项目,基于更为宽
松的BSD许可证,其编译器称为Clang。BSD许可证允许开发者随意对软件进行修改并重新发布,甚至可以将修改过的版本作为自主版权,因而这个许可证深受大公司
的欢迎。现在Apple对LLVM项目的投入非常大。macOS上的开发工具Xocde从4.0版
本起就开始使用Clang编译工具链,随后Apple将自己改写的Clang编译器称为
Apple LLVM。当前最新的Xcode 8所使用的Apple LLVM版本为8.x。而当前
Android NDK也支持了Clang编译器工具链。Clang编译器并非基于GCC,它是从头
开始写的。但是它的目标是尽量与GCC编译器兼容,所以Clang编译器包含大部分
GNU语法扩展,除此之外还含有它自己特有的C语言扩展。当然也有一些特性是GCC
含有而Clang不具备的,不过这些特性一般很少使用。
我们现在可以看到GNU语法扩展适用性十分广泛。如果读者当前在做
LinuxUnix或Windows上的C语言编程开发,或者是在开发macOSiOS应用,又或
者是在开发Android应用,那么完全可以毫无顾忌地使用GNU语法扩展。本书最后几
个章节会分别介绍GCC编译器特定的语法扩展以及Clang编译器特定的语法扩展。由
于Clang编译器已经包含了大部分GNU语法扩展,因此在介绍GCC语法扩展的时候,如果当前特性Clang不支持,则会指明。
[1] 源代码编译流程请见1.5节图1-2。1.5 用C语言构建一个可执行程序的流程
从用C语言写源代码,然后经过编译器、连接器到最终可执行程序的流程图大致
如图1-2所示。
从图1-2中我们可以清晰地看到C语言编译器的大致流程。首先,我们先用C语言
把源代码写好,然后交给C语言编译器。C语言编译器内部分为前端和后端。前端负
责将C语言代码进行词法和语法上的解析,然后可以生成中间代码。中间代码这部分
不是必须的,但是它能够为程序的跨平台移植带来诸多好处。比如,同样的一份C语
言源代码在一台计算机上编译完之后,生成一套中间代码。然后针对不同的目标平
台(比如要将这一套代码分别编译成ARM处理器的二进制机器码、MIPS处理器的二
进制机器码以及x86处理器的二进制机器码),只需要编写相应目标平台的编译器后
端即可。所以,这么做就可以把编译器的前端与后端剥离开来(这在软件工程上又
可称为解耦合),不同处理器厂商可以针对自家的处理器特性,对中间代码生成到
目标二进制代码的过程再度进行优化。接下来,由C语言编译器后端生成源文件相应
的目标文件。目标文件在Windows系统上往往是.obj文件;而在UnixLinux系统上
往往是.o文件。C语言的源文件在所有平台上都统一用.c文件表示。最后,对于各个
独立的目标文件,通过连接器将它们合并成一个最终可执行文件。连接器与C语言编
译器是完全独立的。所以,只要最终目标代码的ABI(应用程序二进制接口)一致,我们可以把各个编译器生成的目标代码都放在一起,最后连接生成一个可执行文
件。比如,有些源代码可用GCC编译,有些使用Clang编译,还有些汇编语言源文件
可直接通过汇编器生成目标代码,最后将所有这些生成出来的目标代码连接为可执
行文件。最终用户可以在当前的操作系统上加载可执行文件进行执行。操作系统利
用加载器将可执行文件中相关的机器码存放到内存中来执行应用程序。图1-2 C语言源代码编译流程图1.6 本章小结
本章简要地介绍了计算编程语言的分类,描述了C语言的历史及演化,以及C语
言的编程思想。此外还介绍了GNU的来龙去脉以及C语言编译器将C语言代码翻译成最
终机器码的大致流程。
C语言作为一门更接近硬件底层的高级编程语言具有良好的抽象力、表达力和灵
活性。此外,它具有非常高效的运行时性能。当前的C语言编译器最终翻译成的机器
指令码与我们手工写汇编语言所得到的性能在大部分情况下相差无几。C语言基本能
达成我们对性能的要求,而在某些对性能要求十分严苛的热点(hotspot)上,我
们可以对这些功能模块手工编写汇编代码。C语言与汇编语言的ABI是完全兼容的,而且大部分C语言编译器还支持直接内联汇编语言。因此,C语言从1970年直到现在
都是系统级编程的首要编程语言。第2章 学习C语言的预备知识
我们在第1章已经大致介绍了C语言的概念以及编译、连接流程。我们知道C语言
是高级语言中比较偏硬件底层的编程语言,因此对于用C语言的编程人员而言,了解
一些关于处理器架构方面的知识是很有必要的,对于嵌入式系统开发的程序员而言
更是如此了。
另外,C语言中有很多按位计算以及逻辑计算,所以对于初学者来说,如果对整
数编码方式等计算机基础知识不熟悉,那么对这些操作的理解也会变得十分困难。
因此,本章将主要给C语言初学者、同时也是计算机编程初学者,提供计算机编程中
会涉及的基本知识,这样,在本书后面讲解到一系列相关概念时,初学者也不会感
到陌生。2.1 计算机体系结构简介
图2-1为一个简单的计算机体系结构图。
一个简单的计算机系统包含了中央处理器(CPU)以及存储器和其他外部设备。
而在CPU内部则由计算单元、通用目的寄存器、程序序列器、数据地址生成器等部件
构成。下面我们将从外到内分别简单地介绍这些组件。
2.1.1 贮存器
贮存器(Storage)尽管在图2-1中没有表示出来,但我们对它一定不会陌生,比如我们在PC上使用的硬盘(Hard Disk)就是一种贮存器。贮存器是一种存储
器,不过它可用于持久保存数据而不丢失。因此我们通常把具有可持久保存的存储
器统称为贮存器。现在PC上用得比较现代化的贮存器就是SSD(Solid-State
Disk)了,俗称固态硬盘。当然,贮存器就其存储介质来说属于ROM(Read-Only
Memory),即只读存储器。这类存储器的特点是数据能持久保留,比如我们PC上的
文件,即便在关闭计算机之后也一直会保存在你的硬盘上,而且PC上的软件往往也
是以可执行文件的形式保存在硬盘上的。但是它的读写速度非常缓慢,尤其是老式
的SATA磁盘,写操作则更慢。因为通常对ROM的数据修改都要通过先读取某段数据
所在的扇区,然后对该数据进行修改,再擦除所涉及的扇区,最后把修改好的数据
所包含的扇区再写回去。而对于ROM来说,其扇区是有写入次数限制的,所以写入次
数越多,损耗就越大。当我们发现一个硬盘访问很慢的时候,通常就是其扇区(或
磁道)已经破损严重了,这是在不断纠错并交换良好的扇区所引发的延迟。在嵌入式系统中,我们用的ROM一般是EPROM、EEPROM、Flash ROM等。这些硬件的详细资
料各位可以从网上轻易获得,这里不再赘述。
图2-1 简单的计算机体系结构图
2.1.2 存储器存储器(Memory)一般是指我们通常所说的内存或主存(Main Memory)。其
存储介质属于RAM(Random Access Memory),即随机访问存储器。它的特点是访
问速度快,可对单个字节进行读写,这与ROM需要擦除整个扇区再对整个扇区写入的
方式有所不同,因此更高效、灵活。但是RAM的数据无法持久化,掉电之后就会消
失。此外,RAM的成本也比ROM高昂得多,我们对比一下16GB的内存条与256GB SSD
的价格就能知道。然而正因为RAM的访问速度快,并且离CPU更近,所以在许多系统
中都是将程序代码与数据先读取到RAM中之后再让CPU去执行处理的。当然,在一些
嵌入式系统中也有让CPU直接执行ROM中的代码并访问读ROM中常量数据的情况,因
为这类系统中总线频率以及CPU频率都相对较低,并且ROM也是与CPU以
SoC(System-On-Chip,系统级芯片)的方式整合在一块芯片上的,所以访问成本
要低很多。而有些环境对ROM的读取速度甚至比读取RAM还更快些。
注意:在本书中所出现的“存储器”均表示内存,即RAM。而将可持久保存
数据的存储器都一律称为“贮存器”。了解了这些概念后,我们在国外网站购买Mac
或PC时,看到相关的术语就不会手足无措了。这里提供Apple美国官网的Mac配置信
息网页,各位可以参考:www.apple.commacbook-prospecs。
2.1.3 寄存器
寄存器是在CPU核心中的、用于暂存数据的存储单元。一般处理器内部对数据的
算术逻辑计算往往都需要通过寄存器(Register),而不是直接对外部存储器进行
操作。因此,如果我们要计算一个加法或乘法计算,需要先把相关数据从外部存储
器读到处理器自己的通用目的寄存器中,然后对寄存器做计算操作,再将计算结果
也放入寄存器,最后将结果寄存器中的数据再写入外部存储器。寄存器的访问速度非常快,它是这三种存储介质中速度最快的,但是数量也是最少的。像在传统的32
位x86处理器体系结构下,程序员一般能直接用的通用目的寄存器只有EAX、EBX、ECX、EDX、ESI、EDI、EBP这7个。还有一个ESP用于操作堆栈,往往无法用来处理
通用计算。
2.1.4 计算单元
计算单元一般由算术逻辑单元(ALU)、乘法器、移位器构成。当然,像一般高
级点的处理器还包含除法器,以及用于做浮点数计算的浮点处理单元(FPU)。它们
一般都直接对寄存器进行操作。而涉及数据读写的指令会由专门的加载、存储处理
单元进行操作。
2.1.5 程序执行流程
处理器在执行一段程序时,通常先从外部存储器取得指令,然后对指令进行译
码处理,转换为相关的一系列操作。这些操作可能是对寄存器的算术逻辑运算,也
可能是对存储器的读写操作,然后执行相关计算。最后把计算结果写回寄存器或写
回到存储器。不过处理器在执行一系列指令的时候并不是每条指令都必须先经过上
面所描述的整个过程才能执行下一条,而是采用流水线的方式执行,如图2-2所示。
图2-2体现了一个简单的处理器执行完一条指令的完整过程。我们这里假设从第
一个取指令阶段到最后的写回阶段,这5个阶段均花费1个周期,倘若不是采用流水
线的方式,而是每完成一条指令的执行再执行下一条指令,那么每条指令的处理都
需要5个周期。而一旦采用流水线方式处理,那么我们可以看到,在第一条指令执行到译码阶段时,处理器可以对第二条指令做取指令操作;当第一条指令执行到执行
阶段时,第二条指令执行到了译码阶段,此时第三条指令开始做取指令阶段,然后
以此类推。这样,当整条流水线填充满之后,即执行到了第5条指令,那么对于后续
指令而言,处理每一条指令的时间均只需要一个周期。
图2-2 处理器执行流水线
这里需要注意的是,并不是每条指令都需要访存操作,只有当需要对外部存储
器做读写操作时才会动用访存执行单元。然而大部分指令都需要写回寄存器操作,即便像一条用于比较大小的指令,或一条系统中断指令,它们也会影响状态寄存
器。当然,很多处理器会有空操作(NOP)指令,它仅仅占用一个时钟周期,而不会
对除了指令指针寄存器以外的任何寄存器产生影响。2.2 整数在计算机中的表示
我们日常用的整数都是十进制数(Decimal),也就是我们通常所说的逢十进
一。因为我们人类有十根手指,所以自然而然地会想到采用十进制的计数和计算方
式。然而,现在几乎所有计算机都采用二进制数(Binary)编码方式,所以我们日
常所用到的整数如果要用计算机来表示的话,需要表示成二进制的方式。
二进制数则是逢二进一,所以在整串数中只有0和1两种数字。比如,十进制数
0,对应二进制为0;十进制数1,对应二进制数1;十进制数2,对应二进制数10;
十进制数3,对应二进制数11。因此,对于非负整数而言,二进制数第n位(n从0开
始计)如果是1,那么就对应十进制数的2n,然后每个位计算得到的十进制数再依次
相加得到最终十进制数的值。比如,一个5位二进制数10010,最低位为最右边的
位,记为0号位,数值为0;最高位为最左边的位,记为4号位,数值为1。那么它所
对应的十进制数为:24+21=18。因为该二进制数除了4号位和1号位为1之外,其余位
都是0,因此0乘以2n肯定为0。图2-3为二进制数10010换算成十进制数的方法图。
图2-3 5位二进制数对应十进制的计算
在计算机术语中,把二进制数中的某一位数又称为一个比特(bit)。比特这个单位对于计算机而言,在度量上是最小的单位。除了比特之外,还有字节(byte)
这个术语。一个字节由8个比特构成。在某些单片机架构下还引入了半字节
(nybble或nibble)这个概念,表示4个比特。然后,还有字(word)这个术语。
字在不同计算机架构下表示的含义不同。在x86架构下,一个字为2个字节;而在
ARM等众多32位RISC体系结构下,一个字表示为4个字节。随着计算机带宽的提升,能被处理器一次处理的数据宽度也不断提升,因此出现了双字(double word)、四字(quad word)、八字(octa word)等概念。双字的宽度为2个字,四字宽度
为4个字,所以它们在不同处理器体系结构下所占用的字节个数也会不同。
我们上面介绍了非负整数的二进制表达方法,那么对于负数,二进制又该如何
表达呢?在计算机中有原码和补码两种表示方法,而最为常用的是补码的表示方
法。下面我们分别对原码和补码进行介绍。
2.2.1 原码表示法
对于无正负符号的原码,其二进制表达如上节所述。而对于含有正负符号的原
码,其二进制表示含有一位符号位,用于表示正负号。一般都是以二进制数的最高
有效位(即最左边的比特)作为符号位,其余各位比特表示该数的绝对值大小。比
如,十进制数6用一个8位的原码表示为00000110;如果是-6,则表示为
10000110。二进制的原码表示示例如图2-4所示。图2-4 二进制数的原码表示
原码的表示非常直观,但是对于计算机算术运算而言就带来了许多麻烦。比
如,我们用上述的6与-6相加,即00000110+10000110,结果为10001100,也就是
十进制数-12,显然不是我们想要的结果。所以,如果某个处理器用原码表示二进制
数,那么它参与加减法的时候必须对两个操作数的正负符号加以判断,然后再判定
使用加法操作还是减法操作,最后还要判定结果的正负符号,可谓相当麻烦。所
以,当前计算机的处理器往往采用补码的方式来表达带符号的二进制数。
2.2.2 补码表示法
正由于原码含有上述缺点,所以人们开发出了另一种带符号的二进制码表示法
——补码。补码与原码一样,用最高位比特表示符号位,其余各位比特则表示数值
大小。如果符号位为0,说明整个二进制数为正数或零;如果为1,那么表示整个二
进制数为负数。当符号位为0时,二进制补码表示法与原码一模一样,但是当符号位
为负数时,情况就完全不同了。此时,对二进制数的补码表示需要按以下步骤进行:
1)先将该二进制数以绝对值的原码形式写好;
2)对整个二进制数(包括符号位),每一个比特都取反。所谓取反就是说,原
来一个比特的数值为0时,则要变1;为1时,则要变0。
变换好之后,将二进制数做加1计算,最终结果就是该负数的补码值了。
下面我们还是用6来举例,+6的二进制补码跟原码一样,还是00000110。而-6
的计算过程,按照上述流程如下:
1)先将-6用绝对值+6的形式表示:00000110;
2)对每个比特位取反,包括符号位在内,得到:11111001;
3)将变换好的数做加1计算,最终得到:11111010。
由于二进制补码的表示与通常我们可直接读懂的二进制数的表示有很大不同,所以给定一个二进制补码,我们往往需要先获得其绝对值大小才能知道它的具体数
值。获得其绝对值的过程为:先判定符号位,如果符号位为0,那么就以通常的二进
制数表示法来读即可。如果符号位为1,那么就以上述同样的过程得到其对应的绝对
值。比如,如果给定11111010这个二进制数,我们看到最高位符号位为1,说明是
负数,我们就以上述过程来求解:
1)先将该二进制数每个比特做取反计算,得到:00000101;
2)然后将变换得到的值做加1计算,最终获得:00000110。所以11111010的绝对值为00000110,即6。
对于补码表示,我们已经知道最高位比特表示符号位,其余的表示具体数值。
但是这里有一个特殊情况,即符号位为1,其余位比特为都为0的情况。比如一个8位
二进制补码:10000000,此时它的值是多少?因为我们通过上述流程,求得其绝对
值的大小也是10000000,所以当前大部分计算机处理器的实现将它作为-128,但估
计仍然有一些处理器会把它作为-0。因为C语言标准中对于数值范围的表示已经明确
表示出8位带符号的整数范围可以是-128到+127,也可以是-127到+127,但最小值
不得大于-127,最大值不得小于+127。第5章会有更详细的描述。
补码的这种表示法的优点就是可以无视符号位,随意进行算术运算操作。比
如,像我们上面所举的例子:6+(-6),计算结果:
00000110+11111010=00000000
最后,上述计算结果的最高位符号位所产生的进位被丢弃(在处理器中可能会
设置相应的进位标志位)。我们自己计算的话也非常方便,在计算过程中,无需关
心两个二进制补码的正负数的情况,也无需关心符号位所产生的影响。我们只需要
像计算普通二进制数一样去计算即可。把最终的计算结果拿出来判断,是正数还是
负数。当然,二进制补码会产生溢出情况,比如两个8位二进制补码加法:
120+50=01111000+00110010=10101010
然而,这个数并不是170,而是-86。首先,170已经超出了带符号8位二进制数
可表示的最大范围了;其次,最高位变为1,用补码表示来讲就是负数表示形式。所
以,这两个正数的加法计算就产生了负数结果,这种现象称为上溢。如果我们要避
免在计算过程中出现上溢情况,需要用更高位宽的二进制数来表示,以提升精度。比如,如果我们将上述加法用16位二进制数表示,那么就不会有上溢问题了。
另外,在C语言标准中没有明确规定C语言编译器的实现以及运行时环境必须采
用哪种二进制编码方式,而是对整数类型标明最大可表示的数值范围。目前大部分C
语言实现都是对带符号整数采用补码的表示方式。这些会在第5章做进一步讲解。
2.2.3 八进制数与十六进制数
上面我们对二进制数编码形式做了比较详细的介绍。我们在编写程序或者查看
一些计算机相关的技术文档时常常还会碰到八进制数与十六进制数的表示,尤其是
十六进制数用得非常多。下面我们就简单介绍一下这两种基数(radix)的表示方
法。
这里跟各位再分享一个术语——基数。基数也就是我们通常所说的,某一个数
用多少进制表达。对于像“01001000是几进制数”这种话,如果用更专业的表达方
式来说的话就是,“01001000的基数是几”。基数为2就是二进制;基数为10则是
十进制。
八进制数是逢八进一,因此每位数的范围是从0~7。八进制数转十进制数也很
简单,我们可以用二进制数转十进制数类似的方法来炮制八进制数转十进制数——
以一个八进制数每位数值作为系数,然后乘以8n,然后计算得到的结果全都相加,最后得到相应的十进制数。其中,n表示当前该位所对应的位置索引(同样以0开始
计)。比如,八进制数5271对应的十进制数的计算过程如图2-5所示。图2-5 八进制数转十进制数
八进制数对应于二进制数的话正好占用3个比特(范围从000~111),一般在
通信领域以及信息加密等领域会用到八进制编码方式。而十六进制数比八进制数用
得更多,因为十六进制数正好占用4个比特,即4位二进制数(范围从0000~
1111)。4个比特相当于半个字节。所以,无论是开发工具还是程序调试工具,一般
都会用十六进制数来表示计算机内部的二进制数据,这样更易读,而且也更省显示
空间(因为一个字节原本需要8位二进制数,而十六进制数只要两位即可表示)。下
面就介绍一下十六机制数的表示方法。
十六进制数逢十六进一,因此每一位数的范围是从0到15。由于我们通常在数学
上所用的十进制数无法用一位来表示10~15这6个数,因而在计算机领域中,我们通
常用英文字母A(或小写a)来表示10;B(或小写b)来表示11;C(或小写c)来表
示12;D(或小写d)来表示13;E(或小写e)来表示14;F(或小写f)来表示15。
十六机制数转十进制数的方式与八进制数转十进制数类似——以一个十六进制数每
位数值作为系数,然后乘以16n,然后计算得到的结果全都相加,最后得到相应的十
进制数。其中,n表示当前位所对应的位置索引(同样以0开始计)。比如,一个4位
十六进制数C0DE的计算过程如图2-6所示:图2-6 十六进制数转十进制数
上述4位十六进制数C0DE,倘若用二进制数表示,则为:1100000011011110。
可见,用十六进制数表示要简洁得多,而且换算成十进制数也相对比较容易,尤其
对于一个字节长度的整数来说。为了能更快速地换算二进制数、十进制数与十六进
制数,请各位读者务必熟记下表:
表2-1 二进制数、十进制数与十六进制数的换算表
习惯上,用0或0o打头的数表示八进制数,0x打头的数表示十六进制数。比如,0123、0777表示八进制数;0x123,0xABCD表示十六进制数。2.3 浮点数在计算机中的表示
当前主流处理器一般都能支持32位的单精度浮点数与64位的双精度浮点数的表
示和计算,并且能遵循IEEE754-1985工业标准。现在此标准最新的版本是2008,其
中增加了对16位半精度浮点数以及128位四精度浮点数的描述。C语言标准引入了一
个浮点模型,可用来表达任意精度的浮点数,尽管当前主流C语言编译器尚未很好地
支持半精度浮点数与四精度浮点数的表示和计算。关于C语言标准对浮点数的描述,我们稍后将在5.2节做更详细的介绍。
为了更好地理解IEEE754-1985中规格化(normalized)浮点数的表示法,我
们先来介绍一下浮点数用一般二进制数的表示方法。一个浮点数包含了整数部分和
尾数(即小数)部分。整数部分的表示与我们之前所讨论过的一样,第n位就表示
2n,n从0开始计。而尾数部分则是第m位表示2-m,m从1开始计。对于一个
0101.1010的二进制浮点数对应十进制数的计算如图2-7所示:
图2-7 二进制浮点数转十进制数
图2-7中,整i位即表示第i位整数;尾i位即表示第i位尾数。其中,第3位整数
为最高位整数;第4位尾数表示最低位尾数。对二进制浮点数的表示有了概念之后,我们就可以看IEEE754-1985标准中对规格化浮点数的描述了。IEEE754-1985对32位单精度与64位双精度两种精度的浮点数进行描述。32位单精度浮点可表示的数值
范围在±1.18×10-38到±3.4×1038,大约含有7位十进制有效数;64位双精度浮点可
表示的数值范围在±2.23×10-308到±1.80×10308,大约含有15位十进制有效数。我
们看到IEEE定义的浮点数的绝对值范围可以是一个远大于1的数,也可以是一个大于
零但远小于1的数,即它的小数精度是可浮动的,所以称之为浮点数。如果说是定点
数的话,它也可表示一个小数,但是其整数位数与小数位数的精度都是固定的。比
如一个16.16的定点数表示整数部分采用16个比特,尾数部分也采用16个比特。而
对于一个32位浮点数来说,既能使用16.16的格式,也能使用30.2的格式(即30个
比特表示整数,2个比特表示尾数)或其他各种形式。而IEEE754-1985对规格化单
精度浮点数的格式如下定义:
1)1位符号位,一般是最高位(31位),表示正负号。0表示正数,1表示负
数。
2)8位指数位,又称阶码,位于23到30位。(阶码的计算后面会详细介绍。)
3)23位尾数,位于0到22位。
我们下面举一个实际的例子来详细说明一个十进制小数5.625如何表示成
IEEE754标准的规格化32位单精度浮点数。
1)5.625是一个正数,所以符号位为0,即第31位为0。
2)我们将5.625依照图2-7那样写成一般小数的表示法——0101.101。
3)我们将此二进制浮点数用科学计数法来表示,使得二进制整数位为最高位的
1。这里最高位为1的比特是从左往右数是第二个比特,所以将小数点就放到该比特的后面,得到1.01101×22。二进制数的科学记数法,底数的值显然就是2。
4)此时,我们能看到尾数部分是小数点后面的那串二进制数,即01101,而指
数为2。现在我们来求阶码。阶码用的是中经指数偏差(exponent bias)处理后的
指数,即用上述得到的指数加上偏差值所求得的和。IEEE754在单精度浮点中规
定,偏差值为127。所以本例中,阶码部分为2+127=129,用二进制数表示就是
10000001。
5)尾数部分从大到小照抄,低位的用0填充即可,所以这里的尾数部分二进制
数为:01101000000000000000000。
6)将整个处理完的二进制数串起来获得:0(符号位)10000001(阶码)
01101000000000000000000(尾数),用十六进制数表达就是:40B40000。
十进制小数转64位双精度浮点数的方法与上述雷同,只不过阶码用11位比特来
表示,尾数则用52位比特表示,而偏差值则规定为1023。2.4 地址与字节对齐
由于C语言是一门接近底层硬件的编程语言,它能直接对存储器地址进行访问
(当前大部分处理器在操作系统的应用层所访问到的逻辑地址,而部分嵌入式系统
由于不含带存储器管理单元,因此可直接访问物理地址)。在计算机中,所谓“地
址”就是用来标识存储单元的一个编号,就好比我们住房的门牌号。没有门牌号,快递就没法发货;如果门牌号记错了,那么快递就会把货物送错地方。计算机中的
地址也是一样,我们为了要访问存储器中特定单元的一个数据,那么我们首先要获
悉该数据所在的地址,然后我们通过这个地址来访问它。访问存储器,我们也简称
为“访存”(Memory Access)。访问地址,我们也简称为“寻
址”(Addressing)。我们在图2-1中也看到,一般计算机架构中都会有地址总线
和数据总线。CPU先通过地址总线发送寻址信号,以指定所要访问存储器单元的地
址。然后再通过数据总线向该地址读写数据,这样就完成了一次访存操作。这好比
于快递送货,我们先打电话告诉快递通信地址,然后快递员把货送到该地址(写数
据),或者去该地址拿货(读数据)送到别家。
一般对于32位系统来说,处理器一次可访问1个(8比特)字节、2个字节或4个
字节。当访问单个字节时,对CPU不做对齐限制;而当访问多个字节时,比如要访问
N个字节,由于计算机总线设计等诸多因素,要求CPU所访问的起始地址满足N个字节
的倍数来访问存储器。如果在访问存储器时没有按照特定要求做字节对齐,那么可
能会引发访存性能问题,甚至直接导致寻址错误而引发异常(引发异常后通常会导
致当前应用意外退出,在嵌入式系统中可能就直接死机或复位)。
下面我们给出一张图2-8来描述,看看一般对32位系统而言如何正确地做到访存字节对齐。
图2-8展示了如何正确对齐访问1个字节、2个字节和4个字节的情况。图中画出
了6个存储单元内容,地址低16位从0x1000到0x1005,每个存储单元为1个字节。对
于仅访问1个字节的情况,图2-8所有地址都能直接访问并满足字节对齐的情况。对
于一次访问2个字节的情况,要满足对齐要求,只能访问0x1000、0x1002、0x1004
等必须要能被2整除的地址。对于一次访问4字节的情况,要满足对齐要求,则只能
访问0x1000、0x1004等必须要能被4整除的地址。
图2-8 字节对齐
然而,并不是说要访问多少字节,就必须要保证访问能被多少整除的地址才能
满足对齐要求。如果一次访问8字节,对于32位系统而言,通过32位通用目的寄存器
来读写存储器的话,某些CPU会自动将8字节的访存分为两次进行操作,每次为4字
节,因此只要保证4字节对齐就能满足对齐要求。这些都根据特定的处理器来做具体
处理。就笔者用过的一些处理器而言,像x86、ARM等处理器,当访存不满足对齐要求
时并不会引发总线异常,但是访问性能会降低很多。因为原本可一次通信的数据传
输可能需要拆分为多次,并且前后还要保证数据的一致性,所以还可能会有锁步之
类的操作。而像Blackfin DSP则会直接引发总线异常,导致整个系统的崩溃(如果
不对此异常做处理的话)。另外,像ARMv5或更低版本的处理器,在对非对齐的存储
器地址进行访问时,CPU会先自动向下定位到对齐地址,然后通过向右循环移位的方
式处理数据,这就使得传输数据并不是原本想一次传输的数据内容,也就是说写入
的或读出的数据是失真的。比如,根据图2-8所示内容,如果我们要对一款ARM7EJ-
S处理器(ARMv5TEJ架构)从地址0x1002读4字节内容,那么实际获取到的数据为
0x02010403;而在x86架构或ARMv7架构的处理器下,则能获得0x06050403。2.5 字符编码
我们从2.2节到2.4节讲述的都是数值信息(整数与浮点数),本小节我们将讨
论字符信息。在计算机中我们所处理的字符信息,即文本信息(包括数字、字母、文字、标点符号等)是以一种特定编码格式来定义的。为了使世界各国的文本信息
能够通用,就需要对字符编码做标准化。我们现在最常用也最基本的字符编码系统
是ASCII码(American Standard Code for Information Interchange,美国
信息交换标准码)。ASCII码定义每个字符仅占一个字节,可表示阿拉伯数字0~
9、26个大小写英文字母,以及我们现在在标准键盘上能看到的所有标点符号、一些
控制字符(比如换行、回车、换页、振铃等)。ASCII码最高位是奇偶校验位,用于
通信校验,所以真正有编码意义的是低7个比特,因此只能用于表示128个字符(值
从0~127)。由于ASCII是美国国家标准,所以后来国际化标准组织将它进行国际
标准化,定义为了ISOIEC 646标准。两者所定义的内容是等价的。
ISOIEC 646对于英文系国家而言是基本够用了,但是对于拉丁语系、希腊等
国家来说就不够用了。所以后来ISO组织就把原先ISOIEC 646所定义字符的最高位
也用上了,这样就又能增加128个不同的字符,发布了ISOIEC 8859标准。然而,欧洲大陆虽小,但国家却有数百个,128种扩展字符仍然不够用。因此后来就在
8859的基础上,引入了8859-n,n从1~16,每一种都支持了一定数量的不同的字
母,这样基本能满足欧美国家的文字表示需求。当然,有些国家之间仍然需要切换
编码格式,比如ISOIEC8859-1的语言环境看8859-2的就可能显示乱码,所以,还
得切换到8859-2的字符编码格式下才能正常显示。
而在中国大陆,我们自己也定义了一套用于显示简体中文的字符集——GB2312。它在1981年5月1日开始实施,是中国国家标准的简体中文字符集,全称为
《信息交换用汉字编码字符集·基本集》。它收录了6763个汉字,包括拉丁字母、希
腊字母、日语假名、俄语和蒙古语用的西里尔字母在内的682个全角字符。然后又出
现了GBK字符集,GBK1.0收录了21886个符号,其中汉字就包含了21003个。GBK字
符集主要扩展了繁体中文字。由于像GB2312与GBK能表示成千上万种字符,因此这
已经远超1个字节所能表示的范围。它们所采用的是动态变长字节编码,并且与
ASCII码兼容。如果表示ASCII码部分,那么仅1个字节即可,并且该字节最高位为
0。如果要表示汉字等扩展字符,那么头1个字节的最高位为1,然后再增加一个字节
(即用两个字节)进行表示。所以,理论上,除了第1个字节的最高位不能动之外,其余比特都能表示具体的字符信息,因而最多可表示27+215=32896种字符。
当然,正由于GB2312与GBK主要用于亚洲国家,所以当欧美国家的人看到这些
字符信息时显示的是乱码,他们必须切换到相应的汉字编码环境下看才能看到正确
的文本信息。为了能真正将全球各国语言进行互换通信,出现了
Unicode(Universal Character Set,UCS)标准。它对应于编码标准ISOIEC
10646。Unicode前后也出现了多个版本。早先的UCS-2采用固定的双字节编码方
式,理论上可表示216=65536种字符,因此极大地涵盖了各种语言的文字符号。
不过后来,标准委员会意识到,对于像希伯来字母、拉丁字母等压根就不需要
用两个字节表示,而且定长的双字节表示与原有的ASCII码又不兼容,因此后来出现
了现在用得更多的UTF-8编码标准。UTF-8属于变长的编码方式,它最少可用1个字
节表示1个字符,最多用4个字节表示1个字符,判别依据就是看第1个字节的最高位
有多少个1。如果第1个字节的最高位是0,那么该字符用1个字节表示;最高3位是
110,那么用2个字节表示;最高4位是1110,那么用3个字节表示;最高位是
11110,那么该字符由4个字节来表示。所以UTF-8现在大量用于网络通信的字符编码格式,包括大多数网页用的默认字符编码也都是UTF-8编码。尽管UTF-8更为灵
活,而且也与ASCII码完全兼容,但不利于程序解析。所以现在很多编程语言的编译
器以及运行时库用得更多的是UTF-16编码来处理源代码解析以及各类文本解析,它
与之前的UCS-2编码完全兼容,但也是变长编码方式,可用双字节或四字节来表示一
个字符。如果用双字节表示UTF-16编码的话,范围从0x0000到0xD7FF,以及从
0xE000到0xFFFF。这里留出0xD800到0xDFFF,不作为具体字符的编码表示,而是
用于四字节编码时的编码替换。当UTF-16表示0x10000到0x10FFFF之间的字符时,先将该范围内的值减去0x10000,使得结果落在0x00000到0xFFFFF范围内。然后将
结果划分为高10位与低10位两组。将低10位的值与0xDC00相加,获得低16位;高10
位与0xD800相加,获得高16位。比如,一个Unicode定义的码点(code point)为
0x10437的字符,用UTF-16编码表示的步骤如下。
1)先将它减去0x10000——0x10437-0x10000=0x0437。
2)将该结果分为低10位与高10位,0x0437用20位二进制表示为
00000000010000110111,因此高10位是0000000001=0x01;低10位则是
0000110111,即0x037。
3)将高10位与0xD800相加,得到0xD801;将低10位与0xDC00相加,获得
0xDC37。因此最终UTF-16编码为0xD801DC37。
我们看到,尽管UTF-16也是变长编码表示,但是仅低16位就能表示很多字符符
号,况且即便要表示更广范围的字符,也只是第二种四字节的表示方法,这远比
UTF-8四种不同的编码方式要简洁很多。因此,UTF-16用在很多编程语言运行时系
统字符编码的场合比较多。像现在的Java、Objective-C等编程语言环境内部系统
所表示的字符都是UTF-16编码方式。另外,现在还有UTF-32编码方式,这一开始也是Unicode标准搞出来的UCS-4
标准,它与UCS-2一样,是定长编码方式,但每个字符用固定的4字节来表示。不过
现在此格式用得很少,而且HTML5标准组织也公开声明开发者应当尽量避免在页面中
使用UTF-32编码格式,因为在HTML5规范中所描述的编码侦测算法,故意不对它与
UTF-16编码做区分。2.6 大端与小端
现代计算机系统中含有两种存放数据的字节序:大端(Big-endian)和小端
(Little-endian)。所谓大端字节序是指在读写一个大于1个字节的数据时,其数
据的最高字节存放在起始地址单元处,数据的最低字节存放在最高地址单元处。所
谓小端字节序是指在读写一个大于1个字节的数据时,其数据的最低字节存放在起始
地址单元处,而数据的最高字节存放在最高地址单元处。比如,我们要在地址
0x00001000处存放一个0x04030201的32位整数,其大端、小端存放情况如图2-9所
示。
图2-9 大端与小端
当前,通用桌面处理器以及智能移动设备的处理器一般都用小端字节序。通信
设备中用大端字节序比较普遍。
本书后续所要叙述的内容中,若无特殊说明,都是基于小端字节序进行描述。2.7 按位逻辑运算
按位逻辑运算在计算机编程中会经常涉及,这些运算都是针对二进制比特进行
操作的。所谓的“按位”计算就是指对一组数据的每个比特逐位进行计算,并且对
每个比特的计算结果不会影响其他位。常用的按位逻辑运算包括“按位与”、“按
位或”、“按位异或”以及“按位取反”四种。下面将分别介绍这4种运算方式。
1)按位与:它是一个双目操作,需要两个操作数,在C语言中用表示。两个比
特的按位与结果如下:
00=0; 01=0; 10=0; 11=1
也就是说,两个比特中如果有一个比特是0,那么按位与的结果就是0,只有当
两个比特都为1的时候,按位与的结果才为1。比如,对两个字节01001010和
11110011进行按位与的结果为01000010。按位与一般可用于判定某个标志位是否被
设置。比如,我们假定处理一个游戏手柄的按键事件,用一个字节来存放按键被按
下的标志,前4个比特分别表示“上”、“下”、“左”、“右”。比特4表示按下
了“A”键,比特5表示按下了“B”键,比特6表示按下了“X”键,比特7表示按下
了“Y”键。那么当我们接收到二进制数01010100时,说明用户同时按下
了“左”方向键、“A”键和“X”键。那么我们判定按键标志时可以通过按位与二
进制数1来判定是否按下了“上”键,按位与二进制数10做按位与操作来判定是否按
下了“下”键,跟二进制数100做与操作来判定是否按下了“左”键,以此类推。如
果按位与的结果是0,说明当前此按键没有被按下,如果结果不为零,说明此按键被
按下。2)按位或:它是一个双目操作符,需要两个操作数,在C语言中用“|”表示。
两个比特的按位或结果如下:
0|0=0; 0|1=1; 1|0=1; 1|1=1
也就是说,只要有一个比特的值是1,那么按位或的结果就是1,只有当两个比
特的值都为0的时候,按位或的结果才是0。比如,对于两个字节01001010和
11110011进行按位或的结果为11111011。按位或一般可用于设置标志位。就如同上
述例子,如果用户按下了“上”键,那么系统底层会将最低位设置为1;如果用户按
下了“Y”键,那么系统底层会将最高位设置为1。随后系统会将这串信息发送到应
用UI层。
3)按位异或:它是一个双目操作,需要两个操作数,在C语言中用^表示。两个
比特的按位异或结果如下
0^0=0; 0^1=1; 1^0=1; 1^1=0
也就是说,如果两个比特的值相同,那么按位异或的结果为0,不同为1。比
如,对于两个字节01001010和11110011进行按位或的结果为10111001。按位异或
适用于多种场景,比如我们用一个输入比特与1进行异或就可以反转该输入比特的
值,输入为0,那么结果为1;输入为1,那么结果为0。任一比特与0异或,那么结果
还是原比特的值。按位异或跟按位与和按位或不同,它可以对数据信息进行叠加组
合。因为给定任一比特,对于另外一个比特的输入,不同的输入值对应不同的输
出,所以我们通过异或能还原信息。比如,我们有两个整数a和b,我们设c=a^b。
对于c,我们可以通过c^a重新得到b,也可以通过c^b来重新得到a。所以异或在信
息编码、数据加密等技术上应用得非常多。4)按位取反:它是一个单目操作,只需要一个操作数,在C语言中用~表示。一
个比特的按位取反结果如下:~0=1;~1=0。比如,对一个字节01001010进行按位取
反的结果为10110101。2.8 移位操作
现代处理器的计算单元中一般都会包含移位器。移位器往往能执行算术左移
(Arithmetic Shift Left)、算术右移(Arithmetic Shift Right)、逻辑左
移(Logical Shift Left)、逻辑右移(Logical Shift Right)、循环右移
(Rotational Shift Right)这些操作。
下面我们将分别介绍这些移位操作,这里需要提醒各位的是,移位操作一般总
是对整数数据进行操作,并且移入移出的都是二进制比特。然而,不同的处理器架
构对移位操作的实现可能会有一些不同。比如,如果对一个32位寄存器做移位操
作,倘若指定要移动的比特数超过了31,那么在x86处理器中是将指定的比特移动位
数做模32处理(也就是求除以32的余数,比如左移32位相当于左移0位、右移33位
相当于右移1位);而在ARM、AVR等处理器中,对一个32位的整数做左移和逻辑右
移超出31位的结果都将是零。
2.8.1 算术左移与逻辑左移
由于算术左移与逻辑左移操作基本是相同的,仅仅对标志位的影响有些区别,所以合并在一起讲。左移的操作步骤十分简单,假设我们要左移N位,那么先将整数
的每个比特向左移动N位,然后空出的低N位填零。图2-10展示了对一个8位整数分
别做左移1位与左移2位的过程。图2-10 算术左移与逻辑左移
图2-10中间由小写字母a~h构成的方格图即表示一个8位二进制整数,每个小
写字母表示一比特,并且字母a作为最高位比特,字母h作为最低位比特。左移1位
后,原来的8位二进制数就变成了bcdefgh0;左移2位后,原来的8位二进制数就变
成了cdefgh00。
2.8.2 逻辑右移
逻辑右移的操作步序是:先将整数的每一个比特向右移动N位,然后高N位用零
来填补。图2-11展示了一个8位二进制整数分别逻辑右移1位和2位的过程。图2-11 逻辑右移
图2-11中间由小写字母a~h构成的方格图即表示一个8位二进制整数,每个小
写字母表示一位比特,并且字母a作为最高位比特,字母h作为最低位比特。将原始
二进制8位数据逻辑右移1位后,二进制数据变为0abcdefg;逻辑右移2位后,二进
制数据变为00abcdef。
2.8.3 算术右移
算术右移与逻辑右移类似,只不过移出N位之后,高N位不是用零来填充,而是
根据原始整数的最高位,如果原始整数的最高位为1,那么移位后的高N位用1来填
充;如果是0,则用0来填充。图2-12展示了一个8位二进制整数分别算术右移1位和
2位的过程。图2-12 算术右移
图2-12中间由小写字母a~h构成的方格图,即表示一个8位二进制整数,每个
小写字母表示一位比特,并且字母a作为最高位比特,字母h作为最低位比特。将原
始8位二进制整数算术右移1位之后,该二进制数变为aabcdefg;将它算术右移2位
之后,变为aaabcdef。
2.8.4 循环右移
循环右移的步序是:先将原始二进制整数右移N位,移出的N位依次放入到高N
位。图2-13展示了将一个8位二进制整数分别循环右移1位和2位的过程。图2-13 循环右移
图2-13中间由小写字母a~h构成的方格图,即表示一个8位二进制整数,每个
小写字母表示一位比特,并且字母a作为最高位比特,字母h作为最低位比特。将原
始8位二进制整数循环右移1位之后,该二进制整数变为habcdefg;将它循环右移2
位之后,该二进制整数变为ghabcdef。2.9 本章小结
本章大致介绍了计算机体系结构以及程序执行的大致流程,然后描述了整数以
及浮点数在计算机中的存储方式,之后还介绍了地址与字节对齐、字符编码、处理
器大端与小端字节序,以及按位逻辑运算和移位操作。由于这些知识都是学习C语言
必备的,C语言中有相关语法与这些概念对应,所以各位最好能先理解、掌握这些基
本知识,这样对后续学习C语言将有很大帮助。第3章 C语言编程的环境搭建
我们在第2章讲述了学习C语言所必需的一些预备知识。本章将给大家介绍常用
桌面操作系统下的C语言环境搭建。这里所讲述的C语言编译器以及集成开发环境
(IDE)都是可合法的,本书不鼓励各位使用盗版或破解软件,所以下面会
列出下载这些合法免费软件的官方链接,大家把编程环境搭建完之后即可上机实践
编程。3.1 Windows操作系统下搭建C语言编程环境
Windows操作系统下默认不自带任何C语言编译器,大家必须从网上下载自己所
需要的C语言编译器。如果各位想通过C语言开发Windows系统平台相关的应用,或
者主要想在Windows平台对C语言程序进行调试,那么往往首选Visual Studio
Community。这款开发环境是免费的,里面自带了微软自家的C语言编译器——简称
为MSVC。不过当前MSVC无法支持最新的C11标准新特性,而且即便是C99标准也是支
持得比较有限,所以它并不适合学习C11最新标准。但对于C语言初学者而言,这款
集成开发环境还是非常适合的。幸运的是,2017年3月微软最新推出的Visual
Studio Community 2017包含了Clang编译器前端工具,如果我们勾选安装的话即
可使用Clang来作为C语言编译器。尽管Visual Studio下的Clang编译器尚处于试
验阶段,但大部分功能都可用了。目前笔者测试下来,它对原子操作还没支持好,另外像UTF-8、UTF-16等字符编码问题还与Windows操作系统本身相关,所以要涉
及这些问题的话,我们只能使用系统特定的接口去解决或者使用下面提到的MinGW以
及Clang官方提供的编译工具链去解决。
所以,如果大家想在Windows操作系统下学习更为完整的C11标准最新特性,那
么建议下载MinGW,如果是64位的Windows系统的话则最好下载Mingw-w64。如果还
想学习Clang编译器语法扩展的话,也可以再下载单独的Clang编译器。
3.1.1 安装Visual Studio Community 2017
Visual Studio Community最新版本可在微软的Visual Studio官方网站下载:
https:www.visualstudio.comthank-you-downloading-visual-
studio?sku=Communityrel=15。
当我们下载好Visual Studio Community的安装程序之后,将它打开运行。随
后会看到一个选择安装组件的对话框。我们在该对话框的右侧能看到已经勾选上的
组件以及一些没有勾选上的组件。这里我们必须勾选上“ClangC2(实验)”这一
项,如图3-1所示。因为不安装Clang,后面就无法用它编译C源代码。
图3-1 Visual Studio Community安装界面
安装完成之后,我们打开Visual Studio Community 2017,首先出现欢迎界
面。Visual Studio在首次启动时就会很明显地提示我们注册账号或用账号登录。
我们先用Hotmail或MSN账号登录注册,如果不注册仅有30天左右的试用时间,但一旦注册完之后就能永久使用了。我们登录完自己的账号之后就可以开始新建一个C语
言的项目工程了。
我们找到菜单栏最左边的“文件”,然后选择“新建”,再点击“项目”,如
图3-2所示。
图3-2 欢迎界面中新建项目
随后我们会看到新建项目的对话框。在左侧边栏中找到“Visual C++”,然后
选中“Win32”,随后在中间栏选择“Win32 Console Application”,最后,在
底下输入此工程创建后存放的目录路径以及工程名,如图3-3所示。图3-3 Visual Sutdio新建项目
点击“OK”按钮后进入应用设置向导界面,如图3-4所示。
我们看到图3-4这个界面时,先别着急点击“下一步”按钮,应先点击左边边栏
中的“应用程序设置”,对此进行初步配置。然后进入图3-5所示的界面。图3-4 Visual Studio应用设置向导
图3-5 Visual Studio项目创建时的应用设置
图3-5所示的界面中,在“附加选项”中,先取消勾选“预编译头”,然后勾
选“空项目”。最后,点击“完成”按钮进入到我们所创建的cdemo项目工程的主界
面。此时,整个工程是空的,只有文件夹而没有任何文件,需要手工新建C源文件。用鼠标右键单击“源文件”,选择“添加”,然后再点击“新建项”,如图3-6所
示。
图3-6 Visual Studio添加C源文件
在随后弹出的如图3-7所示的对话框中,选中中间栏中的“C++文件
(.cpp)”那一项,然后在底下“名称”一栏输入源文件名。图3-7 Visual Studio命名C源文件名
注意:这里需要注意,默认文件后缀名是.cpp,即C++源文件,因为Visual C++默认采用C++编程语言,因此我们这里要手工填写.c文件后缀名,使得
后续我们用C编译器进行编译构建整个控制台应用。
完成之后,我们点击“添加”按钮,然后再次进入工程主界面,此时即可看到C
源文件的编辑界面了。
我们在进入源文件编辑界面后,先对Visual Studio的文本编辑选项做些处
理,以便于我们后续可以流畅地编写代码。如图3-8所示,我们在上面的菜单栏找
到“工具”,然后选择“选项”。图3-8 Visual Studio准备设置编辑选项
点击进入后能看到如图3-9所示的对话框。在左边栏找到“文本编辑器”这个选
项,然后将它展开,选中“所有语言”,随后我们勾选上“行号”,这样,在编辑
每个文本文件时都能看到行号,便于我们查找代码中的语法错误以及调试代码用。图3-9 设置编辑选项
最后,再选中“制表符”选项,对制表符进行设置,如图3-10所示。习惯上,我们一般将Tab Size设置为4个半角字符,缩进大小也是4个半角字符,然后每个制
表符用4个空格代替,这样用其他编辑器浏览Visual Studio编辑过的源文件也不会
导致格式错乱。
图3-10 Visual Studio设置制表符
接下来我们设置当前的项目工程的属性选项。我们找到菜单栏的“项目”,然
后点击“cdemo属性”,如图3-11所示。图3-11 Visual Studio设置项目属性
在配置界面的常规页面中(见图3-12),先找到左上角的“配置”选项,选
择“所有配置”。这样,我们后续做的所有配置都对Debug模式与Release模式同时
有效。然后,在右侧找到“平台工具集”,这里需要选择使用“Visual Studio
2017-Clang with Microsoft CodeGen”,这个选项使得我们对当前的项目工程
使用Clang编译工具链进行编译构建。图3-12 Visual Studio对cdemo项目工程的常规设置
随后我们展开CC++这一项,此时仍然需要先将左上角的“配置”设置为“所有
配置”。然后找到“语言”,将“C语言标准”设置为GNU11标准。这样我们就能在
Visual Studio Community集成开发环境下编写调试大部分基于GNU11标准的C语
言代码了。设置如图3-13所示。
图3-13 Visual Studio设置C语言标准全都设置完成之后,我们就可以编写第一个C语言程序了。同一般C语言教程一
样,我们这里也通过输出一个“Hello,world!”字样,作为第一个C语言代码的
演示程序。我们输入图3-14中所示的代码,然后点击工具栏中的绿色三角箭头(图
3-14中用矩形框圈出)即可编译运行了。在程序最后的getchar作用在于:弹
出的控制台应用不会在程序终止时马上自动关闭,而是等用户输入一个回车时再关
闭。
图3-14 在Windows控制台输出字符串
在图3-14所示的界面中,椭圆圈出来的部分用于设置当前程序以调试模式构建
还是以发布模式构建。如果以调试模式构建,我们可以利用Visual Studio内建的
调试器做断点跟踪,查看局部对象与全局对象状态以及寄存器状态等,便于调试程
序。如果以发布模式构建,那么当前程序会被大幅优化,使得程序运行性能大幅提升,但难以调试。图3-14中,中间用矩形框圈出的部分是设置当前目标程序的执行
模式,默认为x86,即32位执行模式。这里我们将它设置成了64位执行模式。
3.1.2 安装MinGW编译器
MinGW编译器是著名开源C语言编译器GCC对Windows操作系统的一个移植版本。
通过MinGW,我们就可以在Windows下享用大部分GCC编译器所带来的强大功能了。
这对跨平台的C语言开发而言十分有用。下面我们就来介绍如何下载安装MinGW编译
器。
首先,我们直接进入这个网址下载安装文件:
http:sourceforge.netprojectsmingwfileslatestdownload?
source=files。这个文件非常小,因为MinGW采用的是在线安装模式,萃取线上各
个最新release版本的组件进行下载。
然后,我们双击安装包,初步安装完毕后弹出对话框如图3-15所示。绿色进度
条表示已经安装好了。图3-15 MinGW初步安装成功
我们点击“Continue”按钮后,出现选择安装更多组件的对话框。我们在左侧
栏点击“Basic”,即采用基本安装。然后,在右侧栏安装上全部列出的组件。要选
中某个安装组件,鼠标右键该包名,然后在快捷菜单中选择“Mark for
Installation”命令,如图3-16所示。图3-16 MinGW安装,选中安装包
全都选择好之后,我们最后更新刚选好的安装包。我们在菜单栏选
中“Installation”,然后点击“Update Catalogue”,如图3-17所示。
图3-17 MinGW更新安装包
之后会弹出如图3-18所示的界面,点击最左边的“Review Changes”按钮,会弹出如图3-19所示的对话框。
图3-18 MinGW安装要求确认
点击“Apply”按钮之后,就会下载安装设置更新后的安装包。等待全都安装完
毕后,点击“Close”按钮,退出整个安装程序。图3-19 MinGW安装更新
安装结束后,不要着急使用,而是先将MinGW的bin文件夹注册到环境变量中。
先打开“文件资源管理器”,在左侧栏中找到“此电脑”或“我的电脑”,鼠标右
键单击它,选择“属性”,进入后点击左侧的“高级系统设置”,如图3-20所示。图3-20 进入环境变量的设置界面
进入图3-20的对话框之后,点击“环境变量”按钮,进入到“环境变量”对话
框。我们在“系统变量”区域选中“Path”变量,然后点击“编辑”按钮,弹
出“编辑系统变量”对话框。在“变量值”中往后添加刚才安装后的MinGW中的bin
文件夹所在目录。在环境变量中的每个值之间用半角分号“;”进行分隔,如图3-
21所示。图3-21 进入环境变量设置Path完成之后,我们就可以打开控制台程序(方法是右键桌面上左下角“开始”按
钮,然后选择命令提示符),然后进入要编译的C源文件所在的目录。然后用gcc命
令对指定C源文件进行编译构建,如图3-22所示。
这里,我们借用之前在Visual Studio Community下编辑好的源文件
test.c。我们先用cd命令定位到test.c所在的目录。然后用gcc--version命令查
看当前GCC编译器的版本。最后,用gcc-std=gnu11 test.c进行编译,最终在当前
目录生成a.exe可执行文件。我们直接键入a,回车,即可看到程序输出结果。
要注意的是,MinGW是32位的C语言编译器,所以它构建出来的程序也是32位
的。如果各位用的Windows操作系统是64位的,那么可以使用Mingw-w64编译器。
下载地址如下:https:sourceforge.netprojectsmingw-
w64fileslatestdownload?source=files。
Mingw-w64的安装、设置过程与32位的MinGW类似,这里不再赘述。
3.1.3 安装LLVM Clang编译器
LLVM(Low Level Virtual Machine)起源于一个大学项目,它是一个编译
器基础架构项目,用于设计一组具有良好定义的、可重用的库。LLVM起先用于替代
GCC(这里的GCC是指GNU Compiler Collection)栈中的代码生成器,然后对GCC
中已有的许多编译器进行修改以适配LLVM。后来LLVM发起了开发一个全新的适用于
不少编程语言的编译器前端,称为Clang。Clang主要支持C、C++、Objective-C等
编程语言,并且主要由Apple公司大力支持和维护。LLVM与Clang都基于BSD许可
证,比GPL更宽松。正因如此,现在许多硬件商都逐渐开始投入对LLVM的支持,像Khronos开放标准组织也基于LLVM IR(Intermediate Representation)开发出
了自己的一套SPIR-V。Clang编译器在语法上力争支持各大主流编译器的语法扩
展,包括GCC和MSVC,所以微软也已经把Clang纳入Visual Studio集成开发环境的
工具集中。
图3-22 用GCC构建C程序
我们首先在LLVM Clang官网下载最新稳定发布版本的Clang安装
包:http:llvm.orgreleasesdownload.html。然后,要注意的是选择32位
版本,如图3-23所示。图3-23 下载Clang for Windows(32-bit)
由于Clang主要是一个编译器前端,因此它需要依赖其他编译器的连接器以及某
些运行时库。所以,我们光安装Clang是无法直接成功构建应用程序的,因而我们要
使用Clang的话,必须在此之前先把MinGW安装好。MinGW是32位的,因此为了二进
制兼容,我们所选取的Clang也必须是32位的。当然,如果之前安装的是64位的
MingW-W64,那么这里需要下载安装64位的Clang。安装Clang的过程非常简单,可根据安装向导简单地做些选择即可完成安装。安
装完成后,可以去“系统”里的环境变量中看,把LLVM目录下的bin文件夹的路径
添加到Path环境变量中,如图3-24所示。然后就可以再次使用命令行工具直接编译
运行程序了。
图3-24 用Clang编译器构建应用程序3.2 macOS系统下搭建C语言编程环境
macOS系统也不默认自带C语言编译器。然而,用户可以自己去Mac App Store
macOS下的强大开发工具——Xcode:
https:itunes.apple.comcnappxcodeid497799835?mt=12。该集成开发
工具采用Apple定制版本的Clang编译器,称为Apple LLVM编译器。它自带C、C++、Objective-C以及Apple自己新推出的Swift编程语言编译器,还有一系列功
能强大的代码静态分析以及性能剖析工具。
下载完Xcode之后,把它打开。如果是第一次启动,Xcode会自动更新一些资
源,完了之后弹出主界面,如图3-25所示。
我们选择第二个选项,点击它即可创建应用程序工程。第一个选项仅用于操练
把玩Swift编程语言,而第二个选项用于创建真正的应用或库。当然,有些应用可直
接提交到App Store审核,有些则不行。
点击“Create a new Xcode project”之后,出现图3-26所示的对话框。在
图3-26中,我们看到在上面一栏中所选的项目工程为macOS的应用。然后在下边,我们选择“Command Line Tool”,即命令行工具。最左边的Cocoa Application
用于创建macOS系统上基于GUI以及沙盒机制的应用,它可以上传到Mac App
Store。中间的“Game”专门用于游戏应用,也可上传到Mac App Store。而最右
边的“Command Line Tool”构建出来的应用则无法上传到Mac App Store,但是
它能访问macOS的整个文件系统,并且没有采用沙盒机制。另外,开发者用Command
Line Tool开发出来的应用也可以直接放到网上供其他人下载使用。图3-25 Xcode欢迎界面图3-26 选择MacOS命令行工具应用项目
我们点击“Next”按钮之后出现如图3-27所示的对话框。在第1行用英文输入
自己的产品名称,这个后面将用于自动生成的工程名称。然后第2行填写组织名。第
3行填写组织标识,格式为com.<公司名>.<产品名>。当然,第2、第3行对于我们的
demo而言可以随意填写。第5行我们要选择C,表示使用C语言。
图3-27 输入macOS命令行应用的属性
点击“Next”按钮可看到图3-28所示的目录选择对话框。图3-28 macOS命令行应用生成目录选择对话框
这里选择将新创建的项目工程放到哪个目录下。另外,这里要注意的是,我们
不要勾选“Create Git repository”这一选项。因为它会在工程本地做git版本
管理,对于我们一般应用而言没有任何必要,而且这会随着工程构建的次数增多而
增大,很占磁盘空间。而且如果要将本地工程拷贝到其他环境,也会带来许多不
便。我们最后点击“Create”按钮之后,工程就会被创建好。
工程被创建完之后,Xcode默认会打开,包括会自动创建一个main.c的C语言源
文件。此时,我们不用着急编辑、运行,可以先设置一下编译选项。我们首先点击蓝色的“CDemo”项目工程图标,然后点击中间一
栏“TARGETS”下的“CDemo”控制台图标,最后在右边栏的最上方选中“Build
Settings”,然后在下面选中“All”和“Combined”。随后,我们找到“Apple
LLVM x.x-Language”这一栏,将“C Language Dialect”选为gnu11,这个选
项将贯穿本书内容。到此,我们的C语言编译选项就设定好了,如图3-29所示。
图3-29 macOS项目设置工程配置选项
如果我们想对最终生成的代码再做一些优化,可以设置图3-30中的一些选项。图3-30 macOS项目设置其他编译选项
我们将C++的异常以及运行时类型(RTTI)全都关闭,另外也将Objective-C
的异常关闭。这样,最终的应用程序中将不会包含异常栈,同时,编译器后端优化
也能更省力不少。大家可以观察到,将这几个选项关闭后,最终生成的可执行文件
会比开启时要小一些。
最后,我们可以设置一下Xcode自身的偏好设置,将行号显示出来,如图3-31
所示。图3-31 打开Xcode偏好设置
我们在菜单栏上,选择“Xcode”,然后点击“Pre-ferences...”,弹出图
3-32所示的对话框。我们把“Line numbers”勾选上即可在文本编辑框中看到行
号。另外,Xcode默认字符编码已经是UTF-8了,因此不需要我们做额外的设置。图3-32 Xcode设置文本编辑属性
由于Xcode默认字体可能会显得比较小,因此如果想设置字体以及背景颜色的话
可以选择“FontsColors”选项。
在进入到此对话框后,我们点击左侧栏下边的“+”号,添加一个新的字体,并
且选择“Duplicate‘Default’”,如图3-33所示。这使得我们所新增的字体以
默认字体和颜色作为基准,然后对它做大小修改。图3-33 Xcode字体设置,添加新字体
如图3-34所示,我们这里新增了一个叫“Defualt_Big”的字体,然后在中间
这栏,我们先选中“Plain Text”,然后将滚动条滚动到最下方,按住Shift键再
选中最后一条“Other Preprocessor Macros”,这样可以将所有种类的文字格式
全都选中,随后我们点击“T”字样的按钮来调整这些文字格式的字体大小。这里,原先的字体大小为“Menlo Regular-11.0”,设置之后这里变为“Menlo
Regular-14.0”。图3-34 Xcode设置新字体格式与大小
现在,我们就可以直接运行Xcode自动帮我们生成好的main.c中的C源代码了。
我们直接点击右上角的三角箭头按钮即可编译并运行这段代码,如图3-35所示。图3-35 编译运行macOS控制台应用
我们在下面的调试控制台中能看到图3-35这两行文字。其中,最后一句是应用
退出后系统自动打印的。我们可以看到,macOS下能非常轻松地直接输出中文,而不
需要各种复杂的编码转换。
图3-36 macOS控制台程序运行结果3.3 本章小结
本章主要讲述了Windows操作系统下如何使用Visual Studio Community、MinGW和LLVM Clang进行C语言程序开发,同时也讲解了如何在macOS下使用Xcode
做C语言程序开发。因为Windows操作系统与macOS系统用得比较广泛,而且它们都
主要基于GUI的集成开发环境进行编程,所以我们做重点讲解。而在各个版本的
Linux下基本都默认安装了GCC编译器,各位可以直接在Linux系统下的命令行终端
使用gcc命令对C语言源文件做编译构建。而当前FreeBSD最新发布版本默认使用了
LLVM Clang编译器,各位也可以直接在命令行终端使用clang命令对C语言源文件做
编译构建。
另外,Linux、FreeBSD系统下,笔者推荐使用的集成开发环境是Eclipse。它
拥有比较基本的代码智能感知,设置断点进行调试的功能,而且它也是一款开源免
费的软件。当然,要启动Eclipse必须先下载JRE(Java Runtime
Environment),这个可以从Oracle官网下载。
截至本章,第一部分的讲解结束,各位读者应该对C语言的由来、用途以及各种
准备工作都了解得差不多了吧?下面我们将进入第二部分,正式开启C语言魔法的大
门!第二篇 基础语法篇第4章 C语言中的基本元素
本章将正式进入C语言编程话题。我们在第1章已经大致介绍了C语言的编译、连
接和加载运行流程,参见图1-2。我们首先介绍C语言单个源文件的基本构成以及基
本元素。
图4-1 C源文件的基本构成
我们在图4-1中能看到,一个可用来编译执行的基本C源文件主要包含4个部分。
第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个字符:??= 对应于 ??) 对应于 ]??! 对应于 |??( 对应于 [??' 对应于 ^??> 对应于 }?? 对应于 \??< 对应于 {??- 对应于 ~
例如:
=def?ine 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编译器中,允许使用中文、拉丁字母、希腊字母等作为
标识符:αντιο、bonné、小鳥遊·六花、ラーメン等都是有效标识符,但是像3百
九、十二,这些就是无效的标识符。此外,C语言标准中还规定,如果一个标识符
含有通用字符名,那么每一个通用字符名必须落在ISOIEC 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语言新标准引入新的标准库可使得这些关键字能被统一。
所以,大家在使用以下划线打头的关键字时,请尽量先引入相应的标准库头文
件,然后使用非下划线形式的相应关键字。比如,头文件中将_Bool类
型定义为了bool类型;头文件中将_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,然后计算210的结果等于20,再计算204的结果等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
include
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);
}
各位可以编译运行上述代码。如果各位在某些UnixLinux上实践,没有中文输
入法也没有关系,可以用相应的英文来代替上述中文。另外,上述字符串中所出现
的\n是一个转义字符,关于转义字符,我们将在5.1.6节中加以描述。4.6 本章小结
本章我们大概描述了C语言构成的基本元素。一开始,我们列出了一个完整的C
语言源文件应该包含的几个部分。然后我们提到了C语言中的可用字符集以及各类符
号与它们的定义。关于C语言执行环境限制的更多详细信息可参考此博
文:http:www.cnblogs.comzenny-chenp4251813.html。
通过本章学习,各位应该已经能体会到C语言书写的大致格式,并且通过本章列
出的一些代码片段,自己能试试身手写一些简单短小的代码出来,然后利用printf
函数可以打印出一些计算结果。第5章 基本数据类型
本章将介绍C语言中的基本数据类型以及相关的算术逻辑运算。C语言中的基本
数据分为两大类,一类是整数类型,另一类是浮点类型。整数类型还包括字符类型
以及布尔类型。浮点数类型包括单精度浮点数、双精度浮点数以及扩展双精度浮点
类型。
对任一整数对象和浮点数对象,我们都能用+、-、、对它们做加、减、乘、除算术运算操作,当然做除法时除数不能为零,否则会导致程序运行时异常。另
外,对于整数之间的操作还能使用%(求模操作)进行求余数,比如5%2的结果为1,但浮点数之间不能进行求模操作。我们还能对整数做按位操作以及移位操作,同样
这些操作不支持浮点数。5.1 整数类型
C语言中整数类型包括int、short、long、long long、布尔、字符等,除了
字符与布尔类型以外,其他所有整数类型都支持带符号与无符号的表示方式。关于
带符号与无符号整数类型的表示方式可以参考第2章的内容。此外,C语言标准中没
有明确规定每一种整数类型所占用的字节数,这些全都是由C语言的实现来定义的,但是C语言标准给了若干约束,所以C语言实现应该至少能满足这些约束。为了方便
叙述,我们这里仍然根据主流桌面端编译器(GCC、Clang)以及主流32位与64位处
理器环境的实现进行讲解。
5.1.1 int类型
用关键字int声明的一个整数对象具有int类型。在具体的C语言执行环境中,int数据的最小值与最大值分别定义为头文件中的INT_MIN和
INT_MAX。在我们常用的32位与64位环境中,int默认为是带符号的(相当于
signed int),占用4个字节(即32位),其最小值为-231(即0x80000000),最
大值为231-1(即0x7FFFFFFF)。int所对应的无符号类型是unsigned int,通常
在32位与64位环境下也占用4个字节,最小值为0,最大值为232-1(即
0xFFFFFFFF)。在具体C语言执行环境中的最大值定义为头文件中的
UINT_MAX。
int类型对应的整数字面量可直接按照自然方式书写,比如0、-128、127、+2233等都默认表示为int类型。此外,整数字面量可以分别使用八进制、十进制以及十六进制的方式进行表达。八进制的整数字面量表达方式为以0打头,比
如:01、023、-0477这些都是属于八进制整数字面量。而十六进制整数字面量则是
以0x或0X打头,比如:0x123、-0x0045、0xabcdef这些都是有效的十六进制整数
字面量。而其他没有任何前缀的整数字面量都表示为十进制整数。如果想要表达一
个unsigned int类型的整数字面量,可在一般整数字面量后直接添加字母u或U。本
书习惯上使用大写的U。比如0U、01U、-128U、2048U、+2233U等都属于unsigned
int类型。当然,即便字面量后面不加U后缀,这些数也能赋值给unsigned int类型
的对象,因为它们会被编译器进行默认转换。此外,当我们要声明一个unsigned
int类型的对象时,int可以省略。比如,unsigned a=0;,其中对象a的类型即为
unsigned int类型,=是一个赋值操作符(assignment operators),将其右操
作数0赋值给左操作数a。下面举一些例子,各位也可以在自己的计算机上试试,如
代码清单5-1所示。
代码清单5-1 int类型介绍
include
include
int main(int argc, const char argv[])
{
int a = 10; 声明了int类型对象a,并且将整数10赋值给它
以下语句声明了unsigned int类型对象b,并且将整数100赋值给它
unsigned int b = +100U;
unsigned c = -1U; 声明了unsigned int类型对象c
printf(a + b = %d\n, a + b); 这里a+b的结果为110
printf(c = 0x%X\n, c); 这里,c为0xFFFFFFFF
printf(a + c = %d\n, a + c); 这里加法结果溢出,但可将它看作为10-1的结果
a = 0x7FFFFFFF;
a += 1; a += 1相当于a = a + 1
printf(a = %d\n, a); 加法结果溢出,结果为-2147483648,相当于0x80000000
printf(INT_MIN = %d\n, INT_MIN); 查看当前C语言实现下int类型的最小值
printf(INT_MAX = %d\n, INT_MAX); 查看当前C语言实现下int类型的最大值
查看当前C语言实现下unsigned int类型的最大值
由于unsigned int的最小值已被标准定义为0
printf(UINT_MAX = %u\n, UINT_MAX);
}
这里顺便再提一下,根据C语言标准,我们在一个C语言源文件的末尾处最好添
加一个换行符,并且不再添加任何其他的空白符。在某些老版本的GCC上(比如3.x
版本),如果源文件末不是以换行符结尾,则GCC编译器在编译后会有警告,要求在
源文件末尾处添加一个换行符。
上述代码已经涉及了很多额外的知识,比如加减法运算结果溢出的行为,还有
+=操作符的意义等。由于这些知识比较简单易懂,所以我们就不在正文中加以赘
述,而直接以代码注释的方式给出。各位在自己的计算机上编译运行后自然就能知
晓其用途。当然,各位在敲上述代码的时候,注释部分(以及其后面的文字)不
需要打出来,这些仅仅是对代码的注解,对程序本身没有其他作用。
5.1.2 short类型
short类型(标准表达为signed short int类型,其中signed与int均可省
略)我们一般称之为短整型。在我们通常的32位及64位系统下占用2个字节(即16
位),其最小值为-215(即0x8000),最大值为215-1(即0x7FFF)。在C语言执行
环境下,其最大、最小值分别定义为头文件中的SHRT_MAX和
SHRT_MIN。short类型所对应的无符号类型为unsigned short(标准表达为
unsigned short int,其中int可省)。它通常在32位及64位系统下占2个字节,最小值为0,最大值为216-1(即0xFFFF)。在C语言执行环境中,其最大值定义为
头文件中的USHRT_MAX。short类型与unsigned short类型没有特别对应的整数字面量,它们可直接用
int与unsigned int相应的整数字面量进行赋值,如代码清单5-2所示的用法。
代码清单5-2 short类型介绍
include
include
int main(int argc, const char argv[])
{
这里同时声明了short类型对象a和b
并且将a赋值为100,b赋值为200
short a = 100, b = 200;
printf(a - b = %d\n, a - b);
这里声明了unsigned short类型的对象c
unsigned short c = 100;
c -= 200; 相当于c = c - 200;
printf(c = %hu\n, c); 结果为65436(即65536 - 100)
printf(SHRT_MIN = %d\n, SHRT_MIN); 查看当前C语言实现下short类型的最小值
printf(SHRT_MAX = %d\n, SHRT_MAX); 查看当前C语言实现下short类型的最大值
查看当前C语言实现下unsigned short类型的最大值
由于unsigned short的最小值已被标准定义为0
printf(USHRT_MAX = %u\n, USHRT_MAX);
}
代码清单5-2中,字符串格式符%hu对应一个unsigned short类型的参数。
65536表示为216。这里表述了第2章介绍的概念,即如何将一个带符号整数(补码形
式)转为无符号整数的表示形式。
5.1.3 long类型
long类型(标准表达为signed long int类型,其中signed与int均可省略)
我们一般称之为长整型。在我们通常的32位环境下long类型占用4个字节(即32位),而在64位系统下,当前几个主流桌面编译器就有所区别了。MSVC与VS-Clang
仍然为4个字节,而GCC与Clang则是8个字节(即64位)。long类型所对应的无符号
类型为unsigned long(标准表达为unsigned long int,int可省),我们一般
称之为无符号长整型,它的字节长度与long类型一致。在C语言执行环境中,long
类型的最小值与最大值分别定义为头文件中的LONG_MIN与LONG_MAX。
unsigned long类型的最大值定义为头文件中的ULONG_MAX,其最小值 ......
陈轶 著
ISBN:978-7-111-56521-5
本书纸版由机械工业出版社于2017年出版,电子版由华章分社(北京华章图文
信息有限公司,北京奥维博世图书发行有限公司)全球范围内制作与发行。
版权所有,侵权必究
客服热线:+ 86-10-68995265
客服信箱:service@bbbvip.com
官方网址:www.hzmedia.com.cn
新浪微博 @华章数媒
微信公众号 华章电子书(微信号:hzebook)目录
前言
第一篇 预备知识篇
第1章 C魔法概览
1.1 例说编程语言
1.2 用C语言编程的基本注意事项
1.3 主流C语言编译器介绍
1.4 关于GNU规范的语法扩展
1.5 用C语言构建一个可执行程序的流程
1.6 本章小结
第2章 学习C语言的预备知识
2.1 计算机体系结构简介
2.2 整数在计算机中的表示
2.3 浮点数在计算机中的表示2.4 地址与字节对齐
2.5 字符编码
2.6 大端与小端
2.7 按位逻辑运算
2.8 移位操作
2.9 本章小结
第3章 C语言编程的环境搭建
3.1 Windows操作系统下搭建C语言编程环境
3.2 macOS系统下搭建C语言编程环境
3.3 本章小结
第二篇 基础语法篇
第4章 C语言中的基本元素
4.1 C语言中的字符集
4.2 C语言中的token
4.3 关于C语言中的“对象”
4.4 C语言中的“副作用”4.5 C语言标准库中的printf函数
4.6 本章小结
第5章 基本数据类型
5.1 整数类型
5.2 浮点类型
5.3 数据精度与类型转换
5.4 C语言基本运算操作符
5.5 sizeof操作符
5.6 投射操作符
5.7 本章小结
第6章 用户自定义类型
6.1 枚举类型
6.2 结构体类型
6.3 联合体类型
6.4 位域
6.5 字节对齐与字节填充6.6 复数类型
6.7 本章小结
第7章 C语言的数组与指针
7.1 一维数组
7.2 多维数组
7.3 变长数组
7.4 一级指针与对象地址
7.5 多级指针
7.6 指向用户自定义类型的指针
7.7 指针与数组的关系
7.8 指向数组的指针
7.9 void类型、指向void类型的指针与空指针
7.10 字符数组与字符串字面量
7.11 完整与不完整类型
7.12 灵活的数组成员
7.13 本章小结第8章 C语言的控制流语句
8.1 逗号表达式
8.2 条件表达式
8.3 if-else语句
8.4 switch-case语句
8.5 while与do-while迭代语句
8.6 for迭代语句
8.7 goto语句
8.8 本章小结
第9章 C语言的函数
9.1 函数的声明与定义
9.2 函数调用与实现
9.3 数组类型作为函数形参
9.4 带有不定参数类型及个数的函数声明与调用
9.5 函数的递归调用
9.6 内联函数9.7 函数的返回类型与无返回函数
9.8 指向函数的指针
9.9 C语言中的主函数main
9.10 函数与函数调用作为sizeof操作符
9.11 本章小结
第10章 C语言预处理器
10.1 宏定义
10.2 C语言中预定义的宏
10.3 条件预编译
10.4 源文件包含预处理指示符
10.5 error预处理指示符
10.6 line预处理指示符
10.7 undef预处理指示符
10.8 pragma预编译指示符与操作符
10.9 空指示符与C语言中的程序注释
10.10 本章小结第11章 C语言程序的编译上下文
11.1 C语言程序中的作用域和名字空间
11.2 全局对象与函数
11.3 静态对象与函数
11.4 局部对象
11.5 对象的存储与生命周期
11.6 _Thread_local对象
11.7 本章小结
第三篇 高级语法篇
第12章 C语言中的类型限定符
12.1 const限定符
12.2 volatile限定符
12.3 restrict限定符
12.4 _Atomic限定符
12.5 本章小结
第13章 C语言的类型系统13.1 对象类型与函数类型
13.2 对声明符的进一步说明
13.3 更复杂的声明
13.4 typedef类型定义
13.5 本章小结
第14章 C11标准中的表达式、左值与求值顺序
14.1 常量表达式
14.2 泛型选择表达式
14.3 静态断言
14.4 C语言中的左值
14.5 C语言中表达式的求值顺序
14.6 C语言中的语句
14.7 本章小结
第15章 函数调用约定与ABI
15.1 Windows操作系统环境下x86处理器的函数调用约定
15.2 UnixLinux操作系统环境下x86处理器的函数调用约定15.3 ARM处理器环境下的函数调用约定
15.4 本章小结
第16章 创建静态库与动态库
16.1 Windows系统下创建静态库与动态库
16.2 macOS系统下创建静态库与动态库
16.3 Linux系统下创建并使用静态库与动态库
16.4 本章小结
第四篇 语法扩展篇
第17章 GCC对C11标准的语法扩展
17.1 在表达式中使用复合语句与声明
17.2 声明语句块作用域的跳转标签
17.3 跳转标签作为值
17.4 嵌套函数
17.5 使用typeof来获取对象类型
17.6 使用__auto_type做类型自动推导
17.7 对复数操作的扩展17.8 半精度浮点类型
17.9 长度为零的数组
17.10 对可变参数个数的宏的语法扩展
17.11 case语句中使用范围表达式
17.12 投射到一个联合体类型
17.13 使用二进制整数字面量
17.14 使用__attribute__指定函数、对象与类型的属性
17.15 本章小结
第18章 Clang编译器对C11标准的扩展
18.1 特征检查宏
18.2 _Nullable与_Nonnull
18.3 函数重载
18.4 Blocks语法
18.5 本章小结
第19章 对C语言的未来展望
19.1 C语言中的属性19.2 fallthrough属性
19.3 数组片段
19.4 其他语法特性
19.5 本章小结
第五篇 项目实践篇
第20章 制作UTF-8与UTF-16编码字符串的转码器
20.1 UTF-8字符编码格式
20.2 UTF-16字符编码格式
20.3 代码示例
20.4 本章小结
第21章 制作控制台计算器
21.1 对数字的解析
21.2 对操作符的优先级处理
21.3 代码示例
21.4 本章小结前言
为什么要写这本书
本人在2001年上了大学本科,读计算机科学与技术专业。在第一年的上半学
期,对计算机编程还没什么感觉。但是就在考“C语言程序设计”这门专业课的前一
个月,感觉这门课学了那么久几乎什么都不会,可把我急坏了。然后就在这短短一
个月的时间里又是看书,又是上机实验,终于考了70多分,算是过关了……不过奇怪
的是在考试结束后,就发现自己对编程有了感情。到了大二,我们上“数据结
构”所使用的教材是基于C++编程语言的,因为之前没学过C++语言,所以只能自
学。而在这个过程中,我发现自己对编程更加热爱。在上完大三之后,我在暑假里
又把之前的C语言重新巩固一番。有了计算机组成、操作系统、汇编语言、数据结构
等知识积淀之后再去看C语言编程就感觉容易多了。我也是由此喜欢上了C编程语
言。
10年之后,发现国内市面上很多C语言参考书仍然显得非常陈旧。不仅基于古老
的C8990标准,而且还在用Visual C++6.0这种既收费又老旧的开发环境教学生。
对于比较新的C99标准的讲解屈指可数,更鲜有针对最新的C11标准的书籍。出于对
C语言的热爱,在此热切希望能把最新标准的C语言奉献给各位读者,也想把C语言的
方方面面讲透并且能讲得通俗易懂,方便读者去思考实践,所以这也是我写这本书
的主要原因。当各位阅读完本书之后,会发现C语言竟然如此强大!而且在大部分时
候,尤其是我们想集中注意力解决某个特定问题的时候,使用C语言要比用其他一些
基于面向对象的类C编程语言(比如C++、Java等)要直观得多!本书之所以叫“C语言编程魔法书”,是因为像“宝典”、“圣经”之类的词已
经被用滥了。再者,C语言本身就拥有极其强大的魔力,你能用它做几乎所有的事
情。而且几乎每一个C语言编译器都能内联汇编语言,或者与C++、Objective-C直
接兼容,而对于像Java、C、Python等许多编程语言也有相应的接口。所以,我认
为C语言在计算机编程语言领域中就好比数学在自然科学中的地位和作用,它是很多
编程语言的基础,而且很多编程语言的编译器或解释器也都是基于C语言来写的。
就在2015年2月,Khronos标准组织发布了最具现代化的图形API——Vulkan,其主机端接口用的API是纯C语言。此外,像OpenGL、OpenCL、OpenAL、OpenVG等
开放标准都基于纯C语言。此外,最近10年来TIOBE每月的编程语言排名,C语言排
名始终能进前两名,也能说明它的使用范围之广,而且许多开源项目也多多少少会
使用C语言来编写。况且学了C语言之后,再学习C++、Java等面向对象编程语言也
会轻松很多。尤其像C++和Objective-C,没有C语言基础是完全不行的。所以个人
十分推荐计算机系的大学生将C语言作为自己的计算机入门编程语言!
本书特色
从技术层面上讲,本书介绍了C语言的最新标准,即ISOIEC 9899:2011。同
时,也介绍了主流开源C语言编译器GCC与Clang对标准C语言语法的扩充。而且所基
于的编译器和开发环境也是比较新的Visual Studio Community 2017、GCC 5,以及Clang 3.8(Apple LLVM 8.0,基于Xcode 8)。
从适合读者阅读和掌握知识的结构安排上讲,本书分为“预备知识篇”、“基
础语法篇”、“高级语法篇”,以及“语法扩展篇”,还有最后的“项目实践
篇”。从基础到高级,循序渐进地为读者描述C语言编程方法。本书尤其着重C语言标准语法上的精确描述,通过许多代码片段给读者介绍各种C语言语法知识,并且能
反映出C语言的灵活性以及在使用上的约束。
本书推崇读者使用合法免费的C语言编译器以及集成开发环境,希望读者能有正
确的软件版权意识,这样才能更好地为我国软件事业增添光彩,为打造良好的应用
市场以及生态环境作出贡献。因此,本书主要选择使用GCC、Clang这两个主流开源
免费的C语言编译器,而集成开发环境(IDE)则采用Visual Studio
Community、Eclipse、Xcode这三个常用的免费开发工具,其中,Visual Studio
Community不是开源的,而Xcode则是部分开源的。
本书虽然会讲解整个C编程语言,涉及了几乎所有的语法点,但是考虑到本书读
者可能是初学C语言,且没有多少计算机专业知识,所以本书措辞会尽量通俗,而不
过于追求学术化。某些描述可能会不太严谨,但对于本书所用到的GCC、Clang这两
大主流编译器而言将完全适用。另外,考虑到不少读者从事嵌入式系统开发工作,所以对于C语言标准中出现的所谓“由实现定义的”场合会尽量区分情况分别阐明。
本书的最终的目的就是让读者至少能熟练掌握C语言编程,能将它灵活地运用于实际
工程中。
读者对象
·嵌入式系统开发者
·移动或桌面客户端应用程序开发者
·服务器端应用程序开发者·系统架构师
·计算机、电子工程、通信专业的大学生
·其他对C语言编程感兴趣的人员
如何阅读本书
本书一共分为四大篇。
预备知识篇(第1~3章),简单描述C语言的概况、学习C语言的预备知识,以
及在Windows、macOS和Linux三大桌面环境下搭建编写C环境的方法。
第1章 C魔法概览。主要介绍C语言的来历和演化,用它编写代码的编程模式以
及我们可以用于实践的主流C语言编译器。
第2章 学习C语言的预备知识。这一章主要为不太熟悉计算机系统的读者提供
一些基础的计算机理论知识和相关概念,比如整数与浮点数在计算机中的表示方
法、字符编码格式、按位逻辑计算、移位操作等。
第3章 C语言编程的环境搭建。这一章主要介绍了Windows、macOS以及Linux
系统下如何安装并使用主流编译器与集成开发环境。
基础语法篇(第4~11章)讲解C语言的基本语法。这是C语言程序员必须掌握
的。
第4章 C语言中的基本元素。这一章描述了C语言中常用字符集以及合法token
的构成。此外还介绍了标识符、关键字以及标点符号的使用说明。第5章 基本数据类型。这一章介绍了整数类型、字符类型、浮点类型数据的表
示,以及它们之间的类型转换。此外还描述了对于这些基本数据类型的算术逻辑操
作、投射操作以及通过sizeof操作符获取数据类型与对象相应的字节数。
第6章 用户自定义类型。这一章描述了枚举、结构体以及联合体这三种用户自
定义类型,并介绍了它们的特性以及各种使用方式。
第7章 C语言的数组和指针。这一章十分关键,也是C语言的语法难点。这里详
细介绍了C语言中一维数组与多维数组的表示以及如何对它们进行操作,然后介绍了
C语言中的指针类型,详细阐述了指针类型的使用技巧以及需要注意的事项。
第8章 C语言的控制流语句。这一章介绍了C语言的条件语句、选择语句以及循
环等控制流语句。
第9章 C语言的函数。这一章介绍了C语言中的函数概念,包括C语言函数的声
明及定义,还有C函数的调用。此外还介绍了C语言函数标识符作为表达式时的类
型。
第10章 C语言的预处理器。这章包含了目前C11标准中所支持的所有预处理器
特性,包括宏定义、预处理条件、预编译指示符与操作符以及C代码的注释。
第11章 C语言的编译上下文。这一章介绍了C语言对象与函数的作用域和名字
空间。详细介绍了C语言中的四大作用域以及在不同作用域中的对象的生命周期。此
外还介绍了对象与函数的连接属性,包括外部连接和内部连接。
高级语法篇(第12~16章)讲述C语言的一些高级特性。这一部分内容不需要C
语言程序员必须掌握,但需要对此有个大概了解。第12章 C语言中的类型限定符。该章介绍了C11标准中支持的const、volatile、restrict与_Atomic这四种限定符。详细说明了限定符用于修饰含有指
针的对象时,在号的不同位置所起到的不同作用。然后分别介绍这四种限定符的具
体含义。
第13章 C语言中的类型系统。这一章把C语言语法体系中的整个类型系统再梳
理了一遍。这一章介绍了对于一些复杂类型的对象如何去剖析、理解,然后自己如
何去声明自己想要的复杂类型的对象和函数。这一章所描述的其实是整个C语言语法
体系的核心,如果大家能掌握的话,那么基本就算是真正掌握C语言了。其实,对于
任一强类型的编程语言而言,其系统类型总是扮演着十分重要的角色,我们学习此
类语言都需要透彻理解其整个类型系统。
第14章 C11标准中的表达式、左值与求值顺序。该章先介绍了C11标准中各类
表达式以及它们的计算优先级。然后介绍了“左值”这个概念,并讲解了表达式之
间的求值顺序。
第15章 函数调用约定与ABI。该章与C语言标准并无太大关系,但却与实际项
目开发有关。这一章介绍了主流C语言编译器所采用的函数调用约定,然后详细描述
了函数调用的过程,包括参数传递和返回值的具体处理。该章对嵌入式系统开发者
以及需要将C语言与汇编语言进行交互使用的高性能计算开发者而言,将大为有用。
第16章 创建动态库与静态库。这一章介绍了用主流C语言编译工具构建静态库
以及动态库的方法,并介绍如何使用这些库文件。
语法扩展篇(第17~19章)讲述了GCC与Clang编译器对C语言的扩展。
第17章 GCC对C11标准的扩展。该章先简单介绍GNU语法扩展,然后介绍GCC编译器中常用的扩展语法。
第18章 Clang编译器对C11标准的扩展。该章介绍了Clang编译器对C11标准
的语法扩展。最后还介绍了Apple开源的Grand Central Dispatch库的简单使
用。
第19章 对C语言的未来展望。该章主要介绍了C语言的设计理念以及当前C语言
标准委员会的工作组正在为C语言新增的内容,还谈到了哪些特性不会被添加到C语
言中去。
项目实践篇(第20~21章),这里通过两个实际的C语言项目来介绍我们如何利
用C语言来创作出自己的程序。
第20章 描述了UTF-8编码格式的字符串与UTF-16编码格式的字符串进行相互
转换的例子。
第21章 介绍一个看似简单而功能很丰富的基于控制台的计算器程序。
建议零基础的读者要了解第一篇的预备知识,这对于后面深入学习C语言编程很
有帮助。另外,这部分读者可以先不用强行看第三篇,尤其是第15章。因为第三篇
涉及的知识比较深,而第15章又会直接引入汇编语言,这对于没有一定计算机专业
知识的读者会比较难以理解。如果是有一定计算机专业知识的读者可以略过第一
篇,直接阅读第二篇。另外,如果是从事嵌入式系统开发的、或从事系统底层开发
的资深程序员,建议仔细阅读第三、第四篇,相信这部分内容会对你的工作很有帮
助。
勘误和支持由于笔者的水平有限,编写时间仓促,书中难免会出现一些错误或者不准确的
地方,恳请读者批评指正。如果你有更多的宝贵意见,欢迎你访问我的个人博客网
站http:blog.csdn.netzenny_chen进行专题讨论,我会尽量在线上为你提供
最满意的解答。同时,你也可以通过微博http:weibo.comzenny1chen与我联
系,或发送电子邮件到zenny_chen@163.com。期待能够得到你们的真挚反馈,在
技术之路上互勉共进。另外,本书最后两章的代码可以在作者的GitHub上获
取:https:github.comzenny-chen。
致谢
首先感谢我的父母和妻子对我写作此书的大力支持,尤其是我妻子在我忙于工
作、编写此书时帮忙照顾孩子和做饭。然后感谢我公司老板对我写作此书的鼓舞与
期待。
这里还要感谢机械工业出版社华章公司的编辑高婧雅,在一年多的时间里给予
我的大力支持和帮助。
最后感谢支持我的技术爱好者,感谢你们对我的支持以及对我的信任。
我想和作者聊聊
为了能更好地与读者进行联系,笔者这里留了一个QQ讨论群。各位如果在阅读
此书中有任何疑问可以来本群询问,大家可以一起探讨。各位可以扫一扫下方的二
维码,进此群的提示语为:“C语言编程魔法书”,或者查询群号86540289申请入第一篇 预备知识篇
言、汇编语言以及高级语言。下面由底层到高层分别介绍这几种类别的编程语言。
计算机编程语言从对计算机硬件底层的抽象程度进行分类,可分为:机器语
的大致过程。
GNU语法扩展。最后简单介绍一下从用C语言编写程序到编译、构建一个可执行程序
及C语言标准的演化进程。然后介绍一下C语言编程思想,当前主流C语言编译器以及
本章内容主要对C编程语言(以下简称C语言)进行大体介绍,包括它的历史以
第1章 C魔法概览
语言,用于数据库查询)算是一种第四代语言。
只需要一些简单的描述语句就能让计算机做比较复杂的工作。比如SQL(结构化查询
当然,有些书中还介绍了第四代语言,它基于高级语言,比高级语言更抽象,为最终的机器指令码。
编,但可能比汇编适用范围更广、更利于跨平台的字节码),最后将中间语言翻译
达式翻译为对应的机器指令码;也可以将高级语言先翻译为中间语言(类似于汇
循环、更形象自然的表达式等。高级语言一般通过编译器(compiler)可直接将表
构化和模块化。比如,高级语言具有自定义变量标识符、自定义数据结构、分支与
比起汇编语言往往更具有表达力,且拥有更加丰富的语法特性,以便将程序进行结
3)高级语言的表达形式更为抽象且贴近我们日常的语言表述。而且,高级语言
的机器指令码。
是对机器指令的简单抽象,通过汇编器(assembler)可以将汇编语句翻译成对应
表示对应机器指令的功能、寄存器编号、立即数(immediates)等元素。汇编语言
2)汇编语言(Assembly Language)通过简单的指令助记符(memonics)来
构)。
有些是固定长度的(比如ARM、MIPS等架构),有些是可变长度的(比如x86架
器、立即数等多种元素。每种处理器架构所对应的机器码的字节长度也各不相同,包含了当前指令的功能(比如算术逻辑运算、移位、分支、中断、IO等)、寄存
1)机器语言是直接通过十六进制数表示当前处理器架构的机器指令码。指令码
1.1 例说编程语言下面,为了能让大家对这三种层次的编程语言有一个感性的认识,这里将列举
ARMv8架构处理器下的机器语言、汇编语言,加上它们相应的C语言。读者如果手头
有Xcode,并且有包含Apple A7或更高版本处理器的iOS设备的话,可以直接编译运
行,并能看到最终效果。
下面首先列出一个文件名为my_sub.s的汇编源文件,其中包含了机器语言和汇
编语言。见代码清单1-1:
代码清单1-1 机器语言与汇编语言
.text
.align 4
ifdef __arm64__
.globl _my_sub_machine
.globl _my_sub_assembly
用机器语言实现减法操作
_my_sub_machine:
.long 0x4b010000
.long 0xd65f03c0
用汇编语言实现减法操作
_my_sub_assembly:
sub w0, w0, w1
ret
endif
示将寄存器w0与寄存器w1的值进行相减,然后将结果写回w0寄存器中。
说,“0x4b010000”这串32位的十六进制代码意思就是“sub w0,w0,w1”,表
令。这两条机器指令正好与_my_sub_assembly中的两条汇编指令相对应。也就是
在代码清单1-1中,_my_sub_machine程序片段中的两条.long语句即为机器指而“0xd65f03c0”指令码对应于“ret”(更确切地说是ret x30),表示返回当
前过程(procedure)。在汇编语言中,一般会使用过程或者例程(routine)来表
示一个可执行的程序片段。在C语言中一般都用函数(function)表示。我们在这
里能够明显看到,汇编语言采用指令助记符的方式比写机器指令码要直观得多,而
且也不容易出错。“sub”指令的功能从助记符上就能知道是“减法”功能;而w0、w1也明确指明了使用的寄存器是w0和w1。这些在“0x4b010000”这种机器指令码上
都无法直观地表现出来。
代码清单1-2列出C语言是如何表达一个减法操作的。
代码清单1-2 减法操作对应的C语言
static int my_sub_c(int a, int b)
{
return a - b;
}
代码清单1-2所列出的C语言代码与代码清单1-1中的机器指令码和汇编语言完
全对应,意思一目了然——将参数变量a的值与参数变量b的值进行相减,然后将结
果返回。从这里我们就能看到机器语言、汇编语言以及以C语言为代表的高级语言之
间在表达力上的差距了。高级语言的目的就是为了给程序员提供更良好的编程工
具,更简洁、更富有表达力的语言,使得我们程序员能提升生产力,并且能构思出
更多精彩炫酷的应用,而不是把太多的精力都投入在如何让计算机执行的细节上。
代码清单1-3能让我们在主函数或其他函数中测试上述已经编写好的函数。
代码清单1-3 展示减法操作的结果
ifdef __arm64__extern int my_sub_machine(int a, int b);
extern int my_sub_assembly(int a, int b);
int result_machine = my_sub_machine(10, 2);
int result_assembly = my_sub_assembly(5, 3);
int result_c = my_sub_c(6, 2);
printf(Three results: %d, %d, %d\n, result_machine, result_assembly, result_c);
endif
执行了上述代码之后,我们最后能在控制台看到输出结果:“Three
results:8,2,4”。可见,上述三种不同的编程语言,计算功能是完全一致的,都是对两个输入参数做减法操作,然后返回差值。然而就可读性、可理解性以及编
程便利性而言,显然C语言比起其他两者要强得多。而可读性最差的无疑就是机器指
令码了。
1.C语言的类别与产生
对于高级语言来说,从表达上又可分为命令式编程语言(imperative
programming language)和陈述型编程语言(declarative programming
language)。命令式语言主要包括过程式(procedural)、结构化
(structured)以及面向对象(object-oriented)的编程语言;陈述型编程语言
主要包括函数式(functional)以及逻辑型(logical)编程语言。而C语言则属
于结构化的命令式编程语言。不过现在很多命令式编程语言也包含了一些函数式编
程语言的特征。在本书中,后面第18章中谈到的Blocks语法就是一个很典型的函数
式编程语言的语法。
C语言最初由Dennis Ritchie于1969年到1973年在ATT贝尔实验室里开发出
来,主要用于重新实现Unix操作系统。此时,C语言又被称为KR C。其中,K表示
Kernighan的首字母,而R则是Ritchie的首字母。KR C语言与后来标准化的C语言有很大差异。比如,如果函数返回类型为int,则int可省:int my_function
{},也可以写成my_function{}。编译器不会有任何警告,更不会报错。另
外,还有现在看来比较奇葩的函数定义,像我们现在定义这么一个函数——void
my_function(int a,charp){},如果是用KR C语法定义的话要写成:void
my_function(a,p)int a;charp;{}。KR的C语法中,定义一个函数时,其
形参列表先列出形参的标识符,然后在函数声明的后面紧跟着对形参标识符的完整
声明,最后是函数体。这在现行标准中已经被逐步废弃使用了。另外,当时的第一
本C语言专业书《The C Programming Language》也并非一个正式的编程语言规
范,但被用了许多年。
2.C90标准
由于C语言被各大公司所使用(包括当时处于鼎盛时期的IBM PC),因此到了
1989年,C语言由美国国家标准协会(ANSI)进行了标准化,此时C语言又被称为
ANSI C。而仅过一年,ANSI C就被国际标准化组织ISO给采纳了。此时,C语言在
ISO中有了一个官方名称——ISOIEC 9899:1990。其中,9899是C语言在ISO标
准中的代号,像C++在ISO标准中的代号是14882。而冒号后面的1990表示当前修订
好的版本是在1990年发布的。对于ISOIEC 9899:1990的俗称或简称,有些地方
称为C89,有些地方称为C90,或者C8990。不管怎么称呼,它们都指代这个最初的
C语言国际标准。这个版本的C语言标准作为KR C的一个超集(即KR C是此标准C
的一个子集),把后来引入的许多非官方特性也一起整合了进去。其中包括了从
C++借鉴的函数原型(Function Prototypes),指向void的指针,对国际字符集
以及本地语言环境的支持。在此标准中,尽管已经将函数定义的方式改为现在我们
常用的那种方式,不过KR的语法形式仍然兼容。3.C99标准
在随后的几年里,C语言的标准化委员会又不断地对C语言进行改进,到了1999
年,正式发布了ISOIEC 9899:1999,简称为C99标准。C99标准引入了许多特
性,包括内联函数(inline functions)、可变长度的数组、灵活的数组成员(用
于结构体)、复合字面量、指定成员的初始化器、对IEEE754浮点数的改进、支持
不定参数个数的宏定义,在数据类型上还增加了long long int以及复数类型。毫
不夸张地说,即便到目前为止,很少有C语言编译器是完整支持C99的。像主流的
GCC以及Clang编译器都能支持高达90%以上,而微软的Visual Studio 2015中的C
编译器只能支持到70%左右。
4.C11标准
2007年,C语言标准委员会又重新开始修订C语言,到了2011年正式发布了
ISOIEC 9899:2011,简称为C11标准。C11标准新引入的特征尽管没C99相对C90
引入的那么多,但是这些也都十分有用,比如:字节对齐说明符、泛型机制
(generic selection)、对多线程的支持、静态断言、原子操作以及对Unicode
的支持。本书将主要针对C11标准为大家详细讲解C编程语言。关于C语言历史与演化
进程的详细介绍可参考维基百
科:https:en.wikipedia.orgwikiC_%28programming_language%29。
笔者近两年也是在不断地了解C语言标准委员会的最新动态(可参
见:http:www.open-std.orgjtc1sc22wg14),其中看到有人提出想为C
语言添加面向对象的特性,包括增加类、继承、多态等已被C++语言所广泛使用的语
法特性,但是最终被委员会驳回了。因为这些复杂的语法特性并不符合C语言的设计
理念以及设计哲学,况且C++已经有了这些特性,C语言无需再对它们进行支持。笔者将在第19章给大家谈谈C语言设计理念与发展方向。1.2 用C语言编程的基本注意事项
C语言的发明其实基于Unix操作系统。当时在C语言未面世之前,Dennis
Ritchie所在的ATT贝尔实验室用的Unix系统是完全用汇编语言写的。汇编语言的
优势是直接面向处理器本身,能直接对底层硬件进行控制,充分发挥处理器的硬件
能力。然而,它的缺陷也是显而易见的。
1.汇编语言的不足
首先,不可移植性。每种处理器,其指令集都大相径庭,比如ARM有ARM的指令
集架构(ISA),Intel x86有x86的ISA,还有MIPS、Power(原来为PowerPC),Motorola 68000等;再加上各类微控制器单元(Micro-Controller Unit,MCU)、各类数字信号处理器(Digital Signal Processor,DSP),每种ISA都
有其相应的汇编语言。那么多处理器如果对每一种都使用不同的汇编语言来实现同
一个操作系统,那操作系统的开发人员真要崩溃了……而且即便实现出来,可能各个
处理器上的实现也会有所不同,标准也很难被统一起来。
其次,汇编语言本身要比高级语言精密。因为汇编语言面对的都是寄存器、存
储器以及各类底层硬件,而不是一种抽象的数据模型,所以代码编写时需要非常谨
慎,而且调试程序也十分麻烦,且非常容易出错。所以,如果有一种既能面向底层
硬件,又能对数据以及程序进行抽象的高级语言出现,那势必既能不太影响程序执
行效率,又能大大提升程序的可执行性、可读性以及编写的效率,这将是非常伟大
的贡献。C语言也就是在这种背景下诞生的。
如果说,汇编语言面向的是底层硬件、一种过程化的编程风格的话,那么C语言就是面向数据流和算法、一种结构化的编程风格。C语言是一种结构化的、静态类型
的编译型编程语言。也就是说,用C语言编写了源代码之后,需要通过C语言编译器
进行编译,构建为相应的处理器能直接执行的机器码,然后处理器可以对生成出来
的机器码进行执行。所以在各个处理器上,处理器厂商或第三方只需要为当前处理
器写一个对应的C语言编译器即可。然后任何符合C语言标准的程序都能在上面编译
后执行,除了需要支持某些机器特定的功能和特性外(后面会介绍)。
2.C语言编写程序要注意什么
那么我们在用C语言写程序的时候应该注意哪些方面呢?
1)可移植性:C语言被设计出来的一大初衷就是为了能将同一个源代码放到各
个不同的平台上编译运行。因此,如果我们的代码要在多种不同架构的处理器上运
行的话,我们就得注意C语言标准规定了哪些特性是编译器必须遵守的,哪些特性是
平台或编译器自己实现的。我们要尽量使用标准中已明文规定的编程规范,尽可能
避免在不同平台可能会产生不同行为的语法特性。当然,由于上面提到的处理器种
类太过多样,尤其在嵌入式开发领域,很多MCU用的还都是8位处理器,这种情况下C
源代码就很难被移植到32位或64位系统下了。本书后面将会指出大部分主流平台对C
语言标准中所提到的“实现定义”行为的区别。另外,也会提到一些技巧来应对不
同的平台特性。
2)可维护性:可维护性在实际工程项目的研发中非常重要。它体现在最初工程
架构的设计、对各个功能模块的划分、相应的开发人员安排,还有后期的测试。一
般来说,现在一个工程如果是从无到有进行开发的话会采用螺旋式开发模型。也就
是说,一个项目启动后,可以先做一个功能简单但能正常工作的产品原型。然后在
此基础上不断地为它增加更多功能,或对之前的功能进行修改。在此期间,我们如何对整个工程进行模块化划分,从而能安排不同开发人员针对不同功能模块进行开
发就变得尤为重要。另外,在工程开发过程中,如果有人员流动,那么如何将即将
离职的开发人员手中的工作交付给新人也关系到整个项目的进展。因此,一个良好
的C语言代码应该具有可读性、良好的文档化注释风格,以及较详细的设计文档。对
于一个较大的工程项目来说,开发人员不仅仅需要把自己的代码写好,而且要写得
能让别人看懂,并且要做好详细的设计文档,这样才能把项目风险降低。
3)可延展性:大家或许已经知道,像微软的Windows操作系统由数千名工程师
合作研发;Linux操作系统对外开源,参与其中的研发人员也有数百上千人。如果我
们在一个开发团队中负责一个需要由多人合作开发的工程项目,那么我们写的功能
模块需要与其他人写的功能模块进行对接。所以,我们在开发一个较大工程项目
时,需要协调好各自对外的模块接口(Application Program Interface,API)。由于C语言没有全局名字空间(namespace)这个概念,所以命名一个对外
接口也是非常重要的,否则可能会与其他功能模块的接口名发生冲突。本书后面会
对C语言函数命名以及符号连接做进一步介绍。
4)性能:性能是提升程序使用者效率和生产力的体现。一个应用程序的性能越
高,那么计算一个任务所花费的时间越短,也越节省计算机的耗电。而对于如何提
升性能,一方面需要程序员对处理器架构、硬件特性有一定了解;另一方面需要程
序员拥有比较丰富的算法知识,能针对实际需求灵活采用高效的算法。而像C语言这
种十分接近硬件底层的高级编程语言,能极大限度地发挥处理器的特长,从而达到
高效的运行性能。1.3 主流C语言编译器介绍
对于当前主流桌面操作系统而言,可使用Visual C++、GCC以及LLVM Clang这
三大编译器。其中,Visual C++(简称MSVC)只能用于Windows操作系统;其余两
个,除了可用于Windows操作系统之外,主要用于UnixLinux操作系统。像现在很
多版本的Linux都默认使用GCC作为C语言编译器。而像FreeBSD、macOS等系统默认
使用LLVM Clang编译器。由于当前LLVM项目主要在Apple的主推下发展的,所以在
macOS中,Clang编译器又被称为Apple LLVM编译器。MSVC编译器主要用于
Windows操作系统平台下的应用程序开发,它不开源。用户可以使用Visual
Studio Community版本来免费使用它,但是如果要把通过Visual Studio
Community工具生成出来的应用进行商用,那么就得好好阅读一下微软的许可证和
说明书了。而使用GCC与Clang编译器构建出来的应用一般没有任何限制,程序员可
以将应用程序随意发布和进行商用。不过由于MSVC编译器对C99标准的支持就十分
有限,加之它压根不支持任何C11标准,所以本书的代码例子不会针对MSVC进行描
述。所幸的是,Visual Studio Community 2017加入了对Clang编译器的支持,官方称之为——Clang with Microsoft CodeGen,当前版本基于的是Clang
3.8。也就是说,应用于Visual Studio集成开发环境中的Clang编译器前端可支持
Clang编译器的所有语法特性,而后端生成的代码则与MSVC效果一样,包括像long
整数类型在64位编译模式下长度仍然为4个字节,所以各位使用的时候也需要注意。
为了方便描述,本书后面涉及Visual Studio集成开发环境下的Clang编译器简称
为VS-Clang编译器。
而在嵌入式系统方面,可用的C语言编译器就非常丰富了。比如用于Keil公司
51系列单片机的Keil C51编译器;当前大红大紫的Arduino板搭载的开发套件,可用针对AVR微控制器的AVR GCC编译器;ARM自己出的ADS(ARM Development
Suite)、RVDS(RealView Development Suite)和当前最新的DS-5 Studio;
DSP设计商TI(Texas Instruments)的CCS(Code Composer Studio);DSP设
计商ADI(Analog Devices,Inc.)的Visual DSP++编译器,等等。通常,用于
嵌入式系统开发的编译工具链都没有免费版本,而且一般需要通过国内代理进行购
买。所以,这对于个人开发者或者嵌入式系统爱好者而言是一道不低的门槛。不过
Arduino的开发套件是可使用的,并且用它做开发板连接调试也十分简
单。Arduino所采用的C编译器是基于GCC的。还有像树莓派(Raspberry Pi)这种
迷你电脑可以直接使用GCC和Clang编译器。此外,还有像nVidia公司推出的
Jetson TK系列开发板也可直接使用GCC和Clang编译器。树莓派与Jetson TK都默
认安装了Linux操作系统。在嵌入式领域,一般比较低端的单片机,比如8位的MCU
所对应的C编译器可能只支持C90标准,有些甚至连C90标准的很多特性都不支持。
因为它们一方面内存小,ROM的容量也小;另一方面,本身处理器机能就十分有限,有些甚至无法支持函数指针,因为处理器本身不包含通过寄存器做间接过程调用的
指令。而像32位处理器或DSP,一般都至少能支持C99标准,它们本身的性能也十分
强大。而像ARM出的RVDS编译器甚至可用GNU语法扩展。
图1-1展示了上述C语言编译器的分类。图1-1 C语言编译器的分类1.4 关于GNU规范的语法扩展
GNU是一款能用于构建类Unix操作系统的计算机软件合集,由自由软件之父
Richard Stallman开创,于1983年9月27日对外发布。GNU完全由自由软件(free
software)构成。GNU语法扩展源自于GCC编译器,在1987年发布1.0版本,称为
GNU C Compiler。随后,GCC编译器前端[1]支持了C++、Objective-CC++、Fortran、Ada、Java以及最近跃升的Go等编程语言,因此现在GCC被称为GNU
Compiler Collection。由于在20世纪90年代,GNU C编译器就对C90标准做了相
当多的语法扩展,包括复合字面量、匿名结构体和数组、可指定的初始化器等,这
些语法扩展被广泛使用,尤其是大量用于Linux内核代码中,因此C99标准将这些语
法特性全都列入标准之中。
正因为GCC本身是开源自由软件,因此很多商用编译器也基于GCC进行扩展。像
ARM的RVCT(RealView Compiler Toolkit)本身就支持GNU扩展。还有不少开发
平台本身就直接使用GCC编译工具。由于有不少大公司顶级开发人员的参与,因此
GCC编译器的目标代码优化能力相当高,而且还支持许多不同的处理器。所以,GCC
当前被广泛使用并博得开发者的好评。像Linux操作系统基本默认使用GCC作为默认
编译器,包括Android的NDK开发工具一开始也是如此。
然而,由于GCC基于比较严格的GPL许可证,许多大型商业开发商对它望而却
步。该许可证允许使用者免费使用软件,但是要求不能随意对它进行篡改并重新发
布。如果开发者对它进行篡改,然后发布自己修改之后的软件,那么必须要把自己
修改的那部分也开源出来。因此,在2003年诞生了一个LLVM开源项目,基于更为宽
松的BSD许可证,其编译器称为Clang。BSD许可证允许开发者随意对软件进行修改并重新发布,甚至可以将修改过的版本作为自主版权,因而这个许可证深受大公司
的欢迎。现在Apple对LLVM项目的投入非常大。macOS上的开发工具Xocde从4.0版
本起就开始使用Clang编译工具链,随后Apple将自己改写的Clang编译器称为
Apple LLVM。当前最新的Xcode 8所使用的Apple LLVM版本为8.x。而当前
Android NDK也支持了Clang编译器工具链。Clang编译器并非基于GCC,它是从头
开始写的。但是它的目标是尽量与GCC编译器兼容,所以Clang编译器包含大部分
GNU语法扩展,除此之外还含有它自己特有的C语言扩展。当然也有一些特性是GCC
含有而Clang不具备的,不过这些特性一般很少使用。
我们现在可以看到GNU语法扩展适用性十分广泛。如果读者当前在做
LinuxUnix或Windows上的C语言编程开发,或者是在开发macOSiOS应用,又或
者是在开发Android应用,那么完全可以毫无顾忌地使用GNU语法扩展。本书最后几
个章节会分别介绍GCC编译器特定的语法扩展以及Clang编译器特定的语法扩展。由
于Clang编译器已经包含了大部分GNU语法扩展,因此在介绍GCC语法扩展的时候,如果当前特性Clang不支持,则会指明。
[1] 源代码编译流程请见1.5节图1-2。1.5 用C语言构建一个可执行程序的流程
从用C语言写源代码,然后经过编译器、连接器到最终可执行程序的流程图大致
如图1-2所示。
从图1-2中我们可以清晰地看到C语言编译器的大致流程。首先,我们先用C语言
把源代码写好,然后交给C语言编译器。C语言编译器内部分为前端和后端。前端负
责将C语言代码进行词法和语法上的解析,然后可以生成中间代码。中间代码这部分
不是必须的,但是它能够为程序的跨平台移植带来诸多好处。比如,同样的一份C语
言源代码在一台计算机上编译完之后,生成一套中间代码。然后针对不同的目标平
台(比如要将这一套代码分别编译成ARM处理器的二进制机器码、MIPS处理器的二
进制机器码以及x86处理器的二进制机器码),只需要编写相应目标平台的编译器后
端即可。所以,这么做就可以把编译器的前端与后端剥离开来(这在软件工程上又
可称为解耦合),不同处理器厂商可以针对自家的处理器特性,对中间代码生成到
目标二进制代码的过程再度进行优化。接下来,由C语言编译器后端生成源文件相应
的目标文件。目标文件在Windows系统上往往是.obj文件;而在UnixLinux系统上
往往是.o文件。C语言的源文件在所有平台上都统一用.c文件表示。最后,对于各个
独立的目标文件,通过连接器将它们合并成一个最终可执行文件。连接器与C语言编
译器是完全独立的。所以,只要最终目标代码的ABI(应用程序二进制接口)一致,我们可以把各个编译器生成的目标代码都放在一起,最后连接生成一个可执行文
件。比如,有些源代码可用GCC编译,有些使用Clang编译,还有些汇编语言源文件
可直接通过汇编器生成目标代码,最后将所有这些生成出来的目标代码连接为可执
行文件。最终用户可以在当前的操作系统上加载可执行文件进行执行。操作系统利
用加载器将可执行文件中相关的机器码存放到内存中来执行应用程序。图1-2 C语言源代码编译流程图1.6 本章小结
本章简要地介绍了计算编程语言的分类,描述了C语言的历史及演化,以及C语
言的编程思想。此外还介绍了GNU的来龙去脉以及C语言编译器将C语言代码翻译成最
终机器码的大致流程。
C语言作为一门更接近硬件底层的高级编程语言具有良好的抽象力、表达力和灵
活性。此外,它具有非常高效的运行时性能。当前的C语言编译器最终翻译成的机器
指令码与我们手工写汇编语言所得到的性能在大部分情况下相差无几。C语言基本能
达成我们对性能的要求,而在某些对性能要求十分严苛的热点(hotspot)上,我
们可以对这些功能模块手工编写汇编代码。C语言与汇编语言的ABI是完全兼容的,而且大部分C语言编译器还支持直接内联汇编语言。因此,C语言从1970年直到现在
都是系统级编程的首要编程语言。第2章 学习C语言的预备知识
我们在第1章已经大致介绍了C语言的概念以及编译、连接流程。我们知道C语言
是高级语言中比较偏硬件底层的编程语言,因此对于用C语言的编程人员而言,了解
一些关于处理器架构方面的知识是很有必要的,对于嵌入式系统开发的程序员而言
更是如此了。
另外,C语言中有很多按位计算以及逻辑计算,所以对于初学者来说,如果对整
数编码方式等计算机基础知识不熟悉,那么对这些操作的理解也会变得十分困难。
因此,本章将主要给C语言初学者、同时也是计算机编程初学者,提供计算机编程中
会涉及的基本知识,这样,在本书后面讲解到一系列相关概念时,初学者也不会感
到陌生。2.1 计算机体系结构简介
图2-1为一个简单的计算机体系结构图。
一个简单的计算机系统包含了中央处理器(CPU)以及存储器和其他外部设备。
而在CPU内部则由计算单元、通用目的寄存器、程序序列器、数据地址生成器等部件
构成。下面我们将从外到内分别简单地介绍这些组件。
2.1.1 贮存器
贮存器(Storage)尽管在图2-1中没有表示出来,但我们对它一定不会陌生,比如我们在PC上使用的硬盘(Hard Disk)就是一种贮存器。贮存器是一种存储
器,不过它可用于持久保存数据而不丢失。因此我们通常把具有可持久保存的存储
器统称为贮存器。现在PC上用得比较现代化的贮存器就是SSD(Solid-State
Disk)了,俗称固态硬盘。当然,贮存器就其存储介质来说属于ROM(Read-Only
Memory),即只读存储器。这类存储器的特点是数据能持久保留,比如我们PC上的
文件,即便在关闭计算机之后也一直会保存在你的硬盘上,而且PC上的软件往往也
是以可执行文件的形式保存在硬盘上的。但是它的读写速度非常缓慢,尤其是老式
的SATA磁盘,写操作则更慢。因为通常对ROM的数据修改都要通过先读取某段数据
所在的扇区,然后对该数据进行修改,再擦除所涉及的扇区,最后把修改好的数据
所包含的扇区再写回去。而对于ROM来说,其扇区是有写入次数限制的,所以写入次
数越多,损耗就越大。当我们发现一个硬盘访问很慢的时候,通常就是其扇区(或
磁道)已经破损严重了,这是在不断纠错并交换良好的扇区所引发的延迟。在嵌入式系统中,我们用的ROM一般是EPROM、EEPROM、Flash ROM等。这些硬件的详细资
料各位可以从网上轻易获得,这里不再赘述。
图2-1 简单的计算机体系结构图
2.1.2 存储器存储器(Memory)一般是指我们通常所说的内存或主存(Main Memory)。其
存储介质属于RAM(Random Access Memory),即随机访问存储器。它的特点是访
问速度快,可对单个字节进行读写,这与ROM需要擦除整个扇区再对整个扇区写入的
方式有所不同,因此更高效、灵活。但是RAM的数据无法持久化,掉电之后就会消
失。此外,RAM的成本也比ROM高昂得多,我们对比一下16GB的内存条与256GB SSD
的价格就能知道。然而正因为RAM的访问速度快,并且离CPU更近,所以在许多系统
中都是将程序代码与数据先读取到RAM中之后再让CPU去执行处理的。当然,在一些
嵌入式系统中也有让CPU直接执行ROM中的代码并访问读ROM中常量数据的情况,因
为这类系统中总线频率以及CPU频率都相对较低,并且ROM也是与CPU以
SoC(System-On-Chip,系统级芯片)的方式整合在一块芯片上的,所以访问成本
要低很多。而有些环境对ROM的读取速度甚至比读取RAM还更快些。
注意:在本书中所出现的“存储器”均表示内存,即RAM。而将可持久保存
数据的存储器都一律称为“贮存器”。了解了这些概念后,我们在国外网站购买Mac
或PC时,看到相关的术语就不会手足无措了。这里提供Apple美国官网的Mac配置信
息网页,各位可以参考:www.apple.commacbook-prospecs。
2.1.3 寄存器
寄存器是在CPU核心中的、用于暂存数据的存储单元。一般处理器内部对数据的
算术逻辑计算往往都需要通过寄存器(Register),而不是直接对外部存储器进行
操作。因此,如果我们要计算一个加法或乘法计算,需要先把相关数据从外部存储
器读到处理器自己的通用目的寄存器中,然后对寄存器做计算操作,再将计算结果
也放入寄存器,最后将结果寄存器中的数据再写入外部存储器。寄存器的访问速度非常快,它是这三种存储介质中速度最快的,但是数量也是最少的。像在传统的32
位x86处理器体系结构下,程序员一般能直接用的通用目的寄存器只有EAX、EBX、ECX、EDX、ESI、EDI、EBP这7个。还有一个ESP用于操作堆栈,往往无法用来处理
通用计算。
2.1.4 计算单元
计算单元一般由算术逻辑单元(ALU)、乘法器、移位器构成。当然,像一般高
级点的处理器还包含除法器,以及用于做浮点数计算的浮点处理单元(FPU)。它们
一般都直接对寄存器进行操作。而涉及数据读写的指令会由专门的加载、存储处理
单元进行操作。
2.1.5 程序执行流程
处理器在执行一段程序时,通常先从外部存储器取得指令,然后对指令进行译
码处理,转换为相关的一系列操作。这些操作可能是对寄存器的算术逻辑运算,也
可能是对存储器的读写操作,然后执行相关计算。最后把计算结果写回寄存器或写
回到存储器。不过处理器在执行一系列指令的时候并不是每条指令都必须先经过上
面所描述的整个过程才能执行下一条,而是采用流水线的方式执行,如图2-2所示。
图2-2体现了一个简单的处理器执行完一条指令的完整过程。我们这里假设从第
一个取指令阶段到最后的写回阶段,这5个阶段均花费1个周期,倘若不是采用流水
线的方式,而是每完成一条指令的执行再执行下一条指令,那么每条指令的处理都
需要5个周期。而一旦采用流水线方式处理,那么我们可以看到,在第一条指令执行到译码阶段时,处理器可以对第二条指令做取指令操作;当第一条指令执行到执行
阶段时,第二条指令执行到了译码阶段,此时第三条指令开始做取指令阶段,然后
以此类推。这样,当整条流水线填充满之后,即执行到了第5条指令,那么对于后续
指令而言,处理每一条指令的时间均只需要一个周期。
图2-2 处理器执行流水线
这里需要注意的是,并不是每条指令都需要访存操作,只有当需要对外部存储
器做读写操作时才会动用访存执行单元。然而大部分指令都需要写回寄存器操作,即便像一条用于比较大小的指令,或一条系统中断指令,它们也会影响状态寄存
器。当然,很多处理器会有空操作(NOP)指令,它仅仅占用一个时钟周期,而不会
对除了指令指针寄存器以外的任何寄存器产生影响。2.2 整数在计算机中的表示
我们日常用的整数都是十进制数(Decimal),也就是我们通常所说的逢十进
一。因为我们人类有十根手指,所以自然而然地会想到采用十进制的计数和计算方
式。然而,现在几乎所有计算机都采用二进制数(Binary)编码方式,所以我们日
常所用到的整数如果要用计算机来表示的话,需要表示成二进制的方式。
二进制数则是逢二进一,所以在整串数中只有0和1两种数字。比如,十进制数
0,对应二进制为0;十进制数1,对应二进制数1;十进制数2,对应二进制数10;
十进制数3,对应二进制数11。因此,对于非负整数而言,二进制数第n位(n从0开
始计)如果是1,那么就对应十进制数的2n,然后每个位计算得到的十进制数再依次
相加得到最终十进制数的值。比如,一个5位二进制数10010,最低位为最右边的
位,记为0号位,数值为0;最高位为最左边的位,记为4号位,数值为1。那么它所
对应的十进制数为:24+21=18。因为该二进制数除了4号位和1号位为1之外,其余位
都是0,因此0乘以2n肯定为0。图2-3为二进制数10010换算成十进制数的方法图。
图2-3 5位二进制数对应十进制的计算
在计算机术语中,把二进制数中的某一位数又称为一个比特(bit)。比特这个单位对于计算机而言,在度量上是最小的单位。除了比特之外,还有字节(byte)
这个术语。一个字节由8个比特构成。在某些单片机架构下还引入了半字节
(nybble或nibble)这个概念,表示4个比特。然后,还有字(word)这个术语。
字在不同计算机架构下表示的含义不同。在x86架构下,一个字为2个字节;而在
ARM等众多32位RISC体系结构下,一个字表示为4个字节。随着计算机带宽的提升,能被处理器一次处理的数据宽度也不断提升,因此出现了双字(double word)、四字(quad word)、八字(octa word)等概念。双字的宽度为2个字,四字宽度
为4个字,所以它们在不同处理器体系结构下所占用的字节个数也会不同。
我们上面介绍了非负整数的二进制表达方法,那么对于负数,二进制又该如何
表达呢?在计算机中有原码和补码两种表示方法,而最为常用的是补码的表示方
法。下面我们分别对原码和补码进行介绍。
2.2.1 原码表示法
对于无正负符号的原码,其二进制表达如上节所述。而对于含有正负符号的原
码,其二进制表示含有一位符号位,用于表示正负号。一般都是以二进制数的最高
有效位(即最左边的比特)作为符号位,其余各位比特表示该数的绝对值大小。比
如,十进制数6用一个8位的原码表示为00000110;如果是-6,则表示为
10000110。二进制的原码表示示例如图2-4所示。图2-4 二进制数的原码表示
原码的表示非常直观,但是对于计算机算术运算而言就带来了许多麻烦。比
如,我们用上述的6与-6相加,即00000110+10000110,结果为10001100,也就是
十进制数-12,显然不是我们想要的结果。所以,如果某个处理器用原码表示二进制
数,那么它参与加减法的时候必须对两个操作数的正负符号加以判断,然后再判定
使用加法操作还是减法操作,最后还要判定结果的正负符号,可谓相当麻烦。所
以,当前计算机的处理器往往采用补码的方式来表达带符号的二进制数。
2.2.2 补码表示法
正由于原码含有上述缺点,所以人们开发出了另一种带符号的二进制码表示法
——补码。补码与原码一样,用最高位比特表示符号位,其余各位比特则表示数值
大小。如果符号位为0,说明整个二进制数为正数或零;如果为1,那么表示整个二
进制数为负数。当符号位为0时,二进制补码表示法与原码一模一样,但是当符号位
为负数时,情况就完全不同了。此时,对二进制数的补码表示需要按以下步骤进行:
1)先将该二进制数以绝对值的原码形式写好;
2)对整个二进制数(包括符号位),每一个比特都取反。所谓取反就是说,原
来一个比特的数值为0时,则要变1;为1时,则要变0。
变换好之后,将二进制数做加1计算,最终结果就是该负数的补码值了。
下面我们还是用6来举例,+6的二进制补码跟原码一样,还是00000110。而-6
的计算过程,按照上述流程如下:
1)先将-6用绝对值+6的形式表示:00000110;
2)对每个比特位取反,包括符号位在内,得到:11111001;
3)将变换好的数做加1计算,最终得到:11111010。
由于二进制补码的表示与通常我们可直接读懂的二进制数的表示有很大不同,所以给定一个二进制补码,我们往往需要先获得其绝对值大小才能知道它的具体数
值。获得其绝对值的过程为:先判定符号位,如果符号位为0,那么就以通常的二进
制数表示法来读即可。如果符号位为1,那么就以上述同样的过程得到其对应的绝对
值。比如,如果给定11111010这个二进制数,我们看到最高位符号位为1,说明是
负数,我们就以上述过程来求解:
1)先将该二进制数每个比特做取反计算,得到:00000101;
2)然后将变换得到的值做加1计算,最终获得:00000110。所以11111010的绝对值为00000110,即6。
对于补码表示,我们已经知道最高位比特表示符号位,其余的表示具体数值。
但是这里有一个特殊情况,即符号位为1,其余位比特为都为0的情况。比如一个8位
二进制补码:10000000,此时它的值是多少?因为我们通过上述流程,求得其绝对
值的大小也是10000000,所以当前大部分计算机处理器的实现将它作为-128,但估
计仍然有一些处理器会把它作为-0。因为C语言标准中对于数值范围的表示已经明确
表示出8位带符号的整数范围可以是-128到+127,也可以是-127到+127,但最小值
不得大于-127,最大值不得小于+127。第5章会有更详细的描述。
补码的这种表示法的优点就是可以无视符号位,随意进行算术运算操作。比
如,像我们上面所举的例子:6+(-6),计算结果:
00000110+11111010=00000000
最后,上述计算结果的最高位符号位所产生的进位被丢弃(在处理器中可能会
设置相应的进位标志位)。我们自己计算的话也非常方便,在计算过程中,无需关
心两个二进制补码的正负数的情况,也无需关心符号位所产生的影响。我们只需要
像计算普通二进制数一样去计算即可。把最终的计算结果拿出来判断,是正数还是
负数。当然,二进制补码会产生溢出情况,比如两个8位二进制补码加法:
120+50=01111000+00110010=10101010
然而,这个数并不是170,而是-86。首先,170已经超出了带符号8位二进制数
可表示的最大范围了;其次,最高位变为1,用补码表示来讲就是负数表示形式。所
以,这两个正数的加法计算就产生了负数结果,这种现象称为上溢。如果我们要避
免在计算过程中出现上溢情况,需要用更高位宽的二进制数来表示,以提升精度。比如,如果我们将上述加法用16位二进制数表示,那么就不会有上溢问题了。
另外,在C语言标准中没有明确规定C语言编译器的实现以及运行时环境必须采
用哪种二进制编码方式,而是对整数类型标明最大可表示的数值范围。目前大部分C
语言实现都是对带符号整数采用补码的表示方式。这些会在第5章做进一步讲解。
2.2.3 八进制数与十六进制数
上面我们对二进制数编码形式做了比较详细的介绍。我们在编写程序或者查看
一些计算机相关的技术文档时常常还会碰到八进制数与十六进制数的表示,尤其是
十六进制数用得非常多。下面我们就简单介绍一下这两种基数(radix)的表示方
法。
这里跟各位再分享一个术语——基数。基数也就是我们通常所说的,某一个数
用多少进制表达。对于像“01001000是几进制数”这种话,如果用更专业的表达方
式来说的话就是,“01001000的基数是几”。基数为2就是二进制;基数为10则是
十进制。
八进制数是逢八进一,因此每位数的范围是从0~7。八进制数转十进制数也很
简单,我们可以用二进制数转十进制数类似的方法来炮制八进制数转十进制数——
以一个八进制数每位数值作为系数,然后乘以8n,然后计算得到的结果全都相加,最后得到相应的十进制数。其中,n表示当前该位所对应的位置索引(同样以0开始
计)。比如,八进制数5271对应的十进制数的计算过程如图2-5所示。图2-5 八进制数转十进制数
八进制数对应于二进制数的话正好占用3个比特(范围从000~111),一般在
通信领域以及信息加密等领域会用到八进制编码方式。而十六进制数比八进制数用
得更多,因为十六进制数正好占用4个比特,即4位二进制数(范围从0000~
1111)。4个比特相当于半个字节。所以,无论是开发工具还是程序调试工具,一般
都会用十六进制数来表示计算机内部的二进制数据,这样更易读,而且也更省显示
空间(因为一个字节原本需要8位二进制数,而十六进制数只要两位即可表示)。下
面就介绍一下十六机制数的表示方法。
十六进制数逢十六进一,因此每一位数的范围是从0到15。由于我们通常在数学
上所用的十进制数无法用一位来表示10~15这6个数,因而在计算机领域中,我们通
常用英文字母A(或小写a)来表示10;B(或小写b)来表示11;C(或小写c)来表
示12;D(或小写d)来表示13;E(或小写e)来表示14;F(或小写f)来表示15。
十六机制数转十进制数的方式与八进制数转十进制数类似——以一个十六进制数每
位数值作为系数,然后乘以16n,然后计算得到的结果全都相加,最后得到相应的十
进制数。其中,n表示当前位所对应的位置索引(同样以0开始计)。比如,一个4位
十六进制数C0DE的计算过程如图2-6所示:图2-6 十六进制数转十进制数
上述4位十六进制数C0DE,倘若用二进制数表示,则为:1100000011011110。
可见,用十六进制数表示要简洁得多,而且换算成十进制数也相对比较容易,尤其
对于一个字节长度的整数来说。为了能更快速地换算二进制数、十进制数与十六进
制数,请各位读者务必熟记下表:
表2-1 二进制数、十进制数与十六进制数的换算表
习惯上,用0或0o打头的数表示八进制数,0x打头的数表示十六进制数。比如,0123、0777表示八进制数;0x123,0xABCD表示十六进制数。2.3 浮点数在计算机中的表示
当前主流处理器一般都能支持32位的单精度浮点数与64位的双精度浮点数的表
示和计算,并且能遵循IEEE754-1985工业标准。现在此标准最新的版本是2008,其
中增加了对16位半精度浮点数以及128位四精度浮点数的描述。C语言标准引入了一
个浮点模型,可用来表达任意精度的浮点数,尽管当前主流C语言编译器尚未很好地
支持半精度浮点数与四精度浮点数的表示和计算。关于C语言标准对浮点数的描述,我们稍后将在5.2节做更详细的介绍。
为了更好地理解IEEE754-1985中规格化(normalized)浮点数的表示法,我
们先来介绍一下浮点数用一般二进制数的表示方法。一个浮点数包含了整数部分和
尾数(即小数)部分。整数部分的表示与我们之前所讨论过的一样,第n位就表示
2n,n从0开始计。而尾数部分则是第m位表示2-m,m从1开始计。对于一个
0101.1010的二进制浮点数对应十进制数的计算如图2-7所示:
图2-7 二进制浮点数转十进制数
图2-7中,整i位即表示第i位整数;尾i位即表示第i位尾数。其中,第3位整数
为最高位整数;第4位尾数表示最低位尾数。对二进制浮点数的表示有了概念之后,我们就可以看IEEE754-1985标准中对规格化浮点数的描述了。IEEE754-1985对32位单精度与64位双精度两种精度的浮点数进行描述。32位单精度浮点可表示的数值
范围在±1.18×10-38到±3.4×1038,大约含有7位十进制有效数;64位双精度浮点可
表示的数值范围在±2.23×10-308到±1.80×10308,大约含有15位十进制有效数。我
们看到IEEE定义的浮点数的绝对值范围可以是一个远大于1的数,也可以是一个大于
零但远小于1的数,即它的小数精度是可浮动的,所以称之为浮点数。如果说是定点
数的话,它也可表示一个小数,但是其整数位数与小数位数的精度都是固定的。比
如一个16.16的定点数表示整数部分采用16个比特,尾数部分也采用16个比特。而
对于一个32位浮点数来说,既能使用16.16的格式,也能使用30.2的格式(即30个
比特表示整数,2个比特表示尾数)或其他各种形式。而IEEE754-1985对规格化单
精度浮点数的格式如下定义:
1)1位符号位,一般是最高位(31位),表示正负号。0表示正数,1表示负
数。
2)8位指数位,又称阶码,位于23到30位。(阶码的计算后面会详细介绍。)
3)23位尾数,位于0到22位。
我们下面举一个实际的例子来详细说明一个十进制小数5.625如何表示成
IEEE754标准的规格化32位单精度浮点数。
1)5.625是一个正数,所以符号位为0,即第31位为0。
2)我们将5.625依照图2-7那样写成一般小数的表示法——0101.101。
3)我们将此二进制浮点数用科学计数法来表示,使得二进制整数位为最高位的
1。这里最高位为1的比特是从左往右数是第二个比特,所以将小数点就放到该比特的后面,得到1.01101×22。二进制数的科学记数法,底数的值显然就是2。
4)此时,我们能看到尾数部分是小数点后面的那串二进制数,即01101,而指
数为2。现在我们来求阶码。阶码用的是中经指数偏差(exponent bias)处理后的
指数,即用上述得到的指数加上偏差值所求得的和。IEEE754在单精度浮点中规
定,偏差值为127。所以本例中,阶码部分为2+127=129,用二进制数表示就是
10000001。
5)尾数部分从大到小照抄,低位的用0填充即可,所以这里的尾数部分二进制
数为:01101000000000000000000。
6)将整个处理完的二进制数串起来获得:0(符号位)10000001(阶码)
01101000000000000000000(尾数),用十六进制数表达就是:40B40000。
十进制小数转64位双精度浮点数的方法与上述雷同,只不过阶码用11位比特来
表示,尾数则用52位比特表示,而偏差值则规定为1023。2.4 地址与字节对齐
由于C语言是一门接近底层硬件的编程语言,它能直接对存储器地址进行访问
(当前大部分处理器在操作系统的应用层所访问到的逻辑地址,而部分嵌入式系统
由于不含带存储器管理单元,因此可直接访问物理地址)。在计算机中,所谓“地
址”就是用来标识存储单元的一个编号,就好比我们住房的门牌号。没有门牌号,快递就没法发货;如果门牌号记错了,那么快递就会把货物送错地方。计算机中的
地址也是一样,我们为了要访问存储器中特定单元的一个数据,那么我们首先要获
悉该数据所在的地址,然后我们通过这个地址来访问它。访问存储器,我们也简称
为“访存”(Memory Access)。访问地址,我们也简称为“寻
址”(Addressing)。我们在图2-1中也看到,一般计算机架构中都会有地址总线
和数据总线。CPU先通过地址总线发送寻址信号,以指定所要访问存储器单元的地
址。然后再通过数据总线向该地址读写数据,这样就完成了一次访存操作。这好比
于快递送货,我们先打电话告诉快递通信地址,然后快递员把货送到该地址(写数
据),或者去该地址拿货(读数据)送到别家。
一般对于32位系统来说,处理器一次可访问1个(8比特)字节、2个字节或4个
字节。当访问单个字节时,对CPU不做对齐限制;而当访问多个字节时,比如要访问
N个字节,由于计算机总线设计等诸多因素,要求CPU所访问的起始地址满足N个字节
的倍数来访问存储器。如果在访问存储器时没有按照特定要求做字节对齐,那么可
能会引发访存性能问题,甚至直接导致寻址错误而引发异常(引发异常后通常会导
致当前应用意外退出,在嵌入式系统中可能就直接死机或复位)。
下面我们给出一张图2-8来描述,看看一般对32位系统而言如何正确地做到访存字节对齐。
图2-8展示了如何正确对齐访问1个字节、2个字节和4个字节的情况。图中画出
了6个存储单元内容,地址低16位从0x1000到0x1005,每个存储单元为1个字节。对
于仅访问1个字节的情况,图2-8所有地址都能直接访问并满足字节对齐的情况。对
于一次访问2个字节的情况,要满足对齐要求,只能访问0x1000、0x1002、0x1004
等必须要能被2整除的地址。对于一次访问4字节的情况,要满足对齐要求,则只能
访问0x1000、0x1004等必须要能被4整除的地址。
图2-8 字节对齐
然而,并不是说要访问多少字节,就必须要保证访问能被多少整除的地址才能
满足对齐要求。如果一次访问8字节,对于32位系统而言,通过32位通用目的寄存器
来读写存储器的话,某些CPU会自动将8字节的访存分为两次进行操作,每次为4字
节,因此只要保证4字节对齐就能满足对齐要求。这些都根据特定的处理器来做具体
处理。就笔者用过的一些处理器而言,像x86、ARM等处理器,当访存不满足对齐要求
时并不会引发总线异常,但是访问性能会降低很多。因为原本可一次通信的数据传
输可能需要拆分为多次,并且前后还要保证数据的一致性,所以还可能会有锁步之
类的操作。而像Blackfin DSP则会直接引发总线异常,导致整个系统的崩溃(如果
不对此异常做处理的话)。另外,像ARMv5或更低版本的处理器,在对非对齐的存储
器地址进行访问时,CPU会先自动向下定位到对齐地址,然后通过向右循环移位的方
式处理数据,这就使得传输数据并不是原本想一次传输的数据内容,也就是说写入
的或读出的数据是失真的。比如,根据图2-8所示内容,如果我们要对一款ARM7EJ-
S处理器(ARMv5TEJ架构)从地址0x1002读4字节内容,那么实际获取到的数据为
0x02010403;而在x86架构或ARMv7架构的处理器下,则能获得0x06050403。2.5 字符编码
我们从2.2节到2.4节讲述的都是数值信息(整数与浮点数),本小节我们将讨
论字符信息。在计算机中我们所处理的字符信息,即文本信息(包括数字、字母、文字、标点符号等)是以一种特定编码格式来定义的。为了使世界各国的文本信息
能够通用,就需要对字符编码做标准化。我们现在最常用也最基本的字符编码系统
是ASCII码(American Standard Code for Information Interchange,美国
信息交换标准码)。ASCII码定义每个字符仅占一个字节,可表示阿拉伯数字0~
9、26个大小写英文字母,以及我们现在在标准键盘上能看到的所有标点符号、一些
控制字符(比如换行、回车、换页、振铃等)。ASCII码最高位是奇偶校验位,用于
通信校验,所以真正有编码意义的是低7个比特,因此只能用于表示128个字符(值
从0~127)。由于ASCII是美国国家标准,所以后来国际化标准组织将它进行国际
标准化,定义为了ISOIEC 646标准。两者所定义的内容是等价的。
ISOIEC 646对于英文系国家而言是基本够用了,但是对于拉丁语系、希腊等
国家来说就不够用了。所以后来ISO组织就把原先ISOIEC 646所定义字符的最高位
也用上了,这样就又能增加128个不同的字符,发布了ISOIEC 8859标准。然而,欧洲大陆虽小,但国家却有数百个,128种扩展字符仍然不够用。因此后来就在
8859的基础上,引入了8859-n,n从1~16,每一种都支持了一定数量的不同的字
母,这样基本能满足欧美国家的文字表示需求。当然,有些国家之间仍然需要切换
编码格式,比如ISOIEC8859-1的语言环境看8859-2的就可能显示乱码,所以,还
得切换到8859-2的字符编码格式下才能正常显示。
而在中国大陆,我们自己也定义了一套用于显示简体中文的字符集——GB2312。它在1981年5月1日开始实施,是中国国家标准的简体中文字符集,全称为
《信息交换用汉字编码字符集·基本集》。它收录了6763个汉字,包括拉丁字母、希
腊字母、日语假名、俄语和蒙古语用的西里尔字母在内的682个全角字符。然后又出
现了GBK字符集,GBK1.0收录了21886个符号,其中汉字就包含了21003个。GBK字
符集主要扩展了繁体中文字。由于像GB2312与GBK能表示成千上万种字符,因此这
已经远超1个字节所能表示的范围。它们所采用的是动态变长字节编码,并且与
ASCII码兼容。如果表示ASCII码部分,那么仅1个字节即可,并且该字节最高位为
0。如果要表示汉字等扩展字符,那么头1个字节的最高位为1,然后再增加一个字节
(即用两个字节)进行表示。所以,理论上,除了第1个字节的最高位不能动之外,其余比特都能表示具体的字符信息,因而最多可表示27+215=32896种字符。
当然,正由于GB2312与GBK主要用于亚洲国家,所以当欧美国家的人看到这些
字符信息时显示的是乱码,他们必须切换到相应的汉字编码环境下看才能看到正确
的文本信息。为了能真正将全球各国语言进行互换通信,出现了
Unicode(Universal Character Set,UCS)标准。它对应于编码标准ISOIEC
10646。Unicode前后也出现了多个版本。早先的UCS-2采用固定的双字节编码方
式,理论上可表示216=65536种字符,因此极大地涵盖了各种语言的文字符号。
不过后来,标准委员会意识到,对于像希伯来字母、拉丁字母等压根就不需要
用两个字节表示,而且定长的双字节表示与原有的ASCII码又不兼容,因此后来出现
了现在用得更多的UTF-8编码标准。UTF-8属于变长的编码方式,它最少可用1个字
节表示1个字符,最多用4个字节表示1个字符,判别依据就是看第1个字节的最高位
有多少个1。如果第1个字节的最高位是0,那么该字符用1个字节表示;最高3位是
110,那么用2个字节表示;最高4位是1110,那么用3个字节表示;最高位是
11110,那么该字符由4个字节来表示。所以UTF-8现在大量用于网络通信的字符编码格式,包括大多数网页用的默认字符编码也都是UTF-8编码。尽管UTF-8更为灵
活,而且也与ASCII码完全兼容,但不利于程序解析。所以现在很多编程语言的编译
器以及运行时库用得更多的是UTF-16编码来处理源代码解析以及各类文本解析,它
与之前的UCS-2编码完全兼容,但也是变长编码方式,可用双字节或四字节来表示一
个字符。如果用双字节表示UTF-16编码的话,范围从0x0000到0xD7FF,以及从
0xE000到0xFFFF。这里留出0xD800到0xDFFF,不作为具体字符的编码表示,而是
用于四字节编码时的编码替换。当UTF-16表示0x10000到0x10FFFF之间的字符时,先将该范围内的值减去0x10000,使得结果落在0x00000到0xFFFFF范围内。然后将
结果划分为高10位与低10位两组。将低10位的值与0xDC00相加,获得低16位;高10
位与0xD800相加,获得高16位。比如,一个Unicode定义的码点(code point)为
0x10437的字符,用UTF-16编码表示的步骤如下。
1)先将它减去0x10000——0x10437-0x10000=0x0437。
2)将该结果分为低10位与高10位,0x0437用20位二进制表示为
00000000010000110111,因此高10位是0000000001=0x01;低10位则是
0000110111,即0x037。
3)将高10位与0xD800相加,得到0xD801;将低10位与0xDC00相加,获得
0xDC37。因此最终UTF-16编码为0xD801DC37。
我们看到,尽管UTF-16也是变长编码表示,但是仅低16位就能表示很多字符符
号,况且即便要表示更广范围的字符,也只是第二种四字节的表示方法,这远比
UTF-8四种不同的编码方式要简洁很多。因此,UTF-16用在很多编程语言运行时系
统字符编码的场合比较多。像现在的Java、Objective-C等编程语言环境内部系统
所表示的字符都是UTF-16编码方式。另外,现在还有UTF-32编码方式,这一开始也是Unicode标准搞出来的UCS-4
标准,它与UCS-2一样,是定长编码方式,但每个字符用固定的4字节来表示。不过
现在此格式用得很少,而且HTML5标准组织也公开声明开发者应当尽量避免在页面中
使用UTF-32编码格式,因为在HTML5规范中所描述的编码侦测算法,故意不对它与
UTF-16编码做区分。2.6 大端与小端
现代计算机系统中含有两种存放数据的字节序:大端(Big-endian)和小端
(Little-endian)。所谓大端字节序是指在读写一个大于1个字节的数据时,其数
据的最高字节存放在起始地址单元处,数据的最低字节存放在最高地址单元处。所
谓小端字节序是指在读写一个大于1个字节的数据时,其数据的最低字节存放在起始
地址单元处,而数据的最高字节存放在最高地址单元处。比如,我们要在地址
0x00001000处存放一个0x04030201的32位整数,其大端、小端存放情况如图2-9所
示。
图2-9 大端与小端
当前,通用桌面处理器以及智能移动设备的处理器一般都用小端字节序。通信
设备中用大端字节序比较普遍。
本书后续所要叙述的内容中,若无特殊说明,都是基于小端字节序进行描述。2.7 按位逻辑运算
按位逻辑运算在计算机编程中会经常涉及,这些运算都是针对二进制比特进行
操作的。所谓的“按位”计算就是指对一组数据的每个比特逐位进行计算,并且对
每个比特的计算结果不会影响其他位。常用的按位逻辑运算包括“按位与”、“按
位或”、“按位异或”以及“按位取反”四种。下面将分别介绍这4种运算方式。
1)按位与:它是一个双目操作,需要两个操作数,在C语言中用表示。两个比
特的按位与结果如下:
00=0; 01=0; 10=0; 11=1
也就是说,两个比特中如果有一个比特是0,那么按位与的结果就是0,只有当
两个比特都为1的时候,按位与的结果才为1。比如,对两个字节01001010和
11110011进行按位与的结果为01000010。按位与一般可用于判定某个标志位是否被
设置。比如,我们假定处理一个游戏手柄的按键事件,用一个字节来存放按键被按
下的标志,前4个比特分别表示“上”、“下”、“左”、“右”。比特4表示按下
了“A”键,比特5表示按下了“B”键,比特6表示按下了“X”键,比特7表示按下
了“Y”键。那么当我们接收到二进制数01010100时,说明用户同时按下
了“左”方向键、“A”键和“X”键。那么我们判定按键标志时可以通过按位与二
进制数1来判定是否按下了“上”键,按位与二进制数10做按位与操作来判定是否按
下了“下”键,跟二进制数100做与操作来判定是否按下了“左”键,以此类推。如
果按位与的结果是0,说明当前此按键没有被按下,如果结果不为零,说明此按键被
按下。2)按位或:它是一个双目操作符,需要两个操作数,在C语言中用“|”表示。
两个比特的按位或结果如下:
0|0=0; 0|1=1; 1|0=1; 1|1=1
也就是说,只要有一个比特的值是1,那么按位或的结果就是1,只有当两个比
特的值都为0的时候,按位或的结果才是0。比如,对于两个字节01001010和
11110011进行按位或的结果为11111011。按位或一般可用于设置标志位。就如同上
述例子,如果用户按下了“上”键,那么系统底层会将最低位设置为1;如果用户按
下了“Y”键,那么系统底层会将最高位设置为1。随后系统会将这串信息发送到应
用UI层。
3)按位异或:它是一个双目操作,需要两个操作数,在C语言中用^表示。两个
比特的按位异或结果如下
0^0=0; 0^1=1; 1^0=1; 1^1=0
也就是说,如果两个比特的值相同,那么按位异或的结果为0,不同为1。比
如,对于两个字节01001010和11110011进行按位或的结果为10111001。按位异或
适用于多种场景,比如我们用一个输入比特与1进行异或就可以反转该输入比特的
值,输入为0,那么结果为1;输入为1,那么结果为0。任一比特与0异或,那么结果
还是原比特的值。按位异或跟按位与和按位或不同,它可以对数据信息进行叠加组
合。因为给定任一比特,对于另外一个比特的输入,不同的输入值对应不同的输
出,所以我们通过异或能还原信息。比如,我们有两个整数a和b,我们设c=a^b。
对于c,我们可以通过c^a重新得到b,也可以通过c^b来重新得到a。所以异或在信
息编码、数据加密等技术上应用得非常多。4)按位取反:它是一个单目操作,只需要一个操作数,在C语言中用~表示。一
个比特的按位取反结果如下:~0=1;~1=0。比如,对一个字节01001010进行按位取
反的结果为10110101。2.8 移位操作
现代处理器的计算单元中一般都会包含移位器。移位器往往能执行算术左移
(Arithmetic Shift Left)、算术右移(Arithmetic Shift Right)、逻辑左
移(Logical Shift Left)、逻辑右移(Logical Shift Right)、循环右移
(Rotational Shift Right)这些操作。
下面我们将分别介绍这些移位操作,这里需要提醒各位的是,移位操作一般总
是对整数数据进行操作,并且移入移出的都是二进制比特。然而,不同的处理器架
构对移位操作的实现可能会有一些不同。比如,如果对一个32位寄存器做移位操
作,倘若指定要移动的比特数超过了31,那么在x86处理器中是将指定的比特移动位
数做模32处理(也就是求除以32的余数,比如左移32位相当于左移0位、右移33位
相当于右移1位);而在ARM、AVR等处理器中,对一个32位的整数做左移和逻辑右
移超出31位的结果都将是零。
2.8.1 算术左移与逻辑左移
由于算术左移与逻辑左移操作基本是相同的,仅仅对标志位的影响有些区别,所以合并在一起讲。左移的操作步骤十分简单,假设我们要左移N位,那么先将整数
的每个比特向左移动N位,然后空出的低N位填零。图2-10展示了对一个8位整数分
别做左移1位与左移2位的过程。图2-10 算术左移与逻辑左移
图2-10中间由小写字母a~h构成的方格图即表示一个8位二进制整数,每个小
写字母表示一比特,并且字母a作为最高位比特,字母h作为最低位比特。左移1位
后,原来的8位二进制数就变成了bcdefgh0;左移2位后,原来的8位二进制数就变
成了cdefgh00。
2.8.2 逻辑右移
逻辑右移的操作步序是:先将整数的每一个比特向右移动N位,然后高N位用零
来填补。图2-11展示了一个8位二进制整数分别逻辑右移1位和2位的过程。图2-11 逻辑右移
图2-11中间由小写字母a~h构成的方格图即表示一个8位二进制整数,每个小
写字母表示一位比特,并且字母a作为最高位比特,字母h作为最低位比特。将原始
二进制8位数据逻辑右移1位后,二进制数据变为0abcdefg;逻辑右移2位后,二进
制数据变为00abcdef。
2.8.3 算术右移
算术右移与逻辑右移类似,只不过移出N位之后,高N位不是用零来填充,而是
根据原始整数的最高位,如果原始整数的最高位为1,那么移位后的高N位用1来填
充;如果是0,则用0来填充。图2-12展示了一个8位二进制整数分别算术右移1位和
2位的过程。图2-12 算术右移
图2-12中间由小写字母a~h构成的方格图,即表示一个8位二进制整数,每个
小写字母表示一位比特,并且字母a作为最高位比特,字母h作为最低位比特。将原
始8位二进制整数算术右移1位之后,该二进制数变为aabcdefg;将它算术右移2位
之后,变为aaabcdef。
2.8.4 循环右移
循环右移的步序是:先将原始二进制整数右移N位,移出的N位依次放入到高N
位。图2-13展示了将一个8位二进制整数分别循环右移1位和2位的过程。图2-13 循环右移
图2-13中间由小写字母a~h构成的方格图,即表示一个8位二进制整数,每个
小写字母表示一位比特,并且字母a作为最高位比特,字母h作为最低位比特。将原
始8位二进制整数循环右移1位之后,该二进制整数变为habcdefg;将它循环右移2
位之后,该二进制整数变为ghabcdef。2.9 本章小结
本章大致介绍了计算机体系结构以及程序执行的大致流程,然后描述了整数以
及浮点数在计算机中的存储方式,之后还介绍了地址与字节对齐、字符编码、处理
器大端与小端字节序,以及按位逻辑运算和移位操作。由于这些知识都是学习C语言
必备的,C语言中有相关语法与这些概念对应,所以各位最好能先理解、掌握这些基
本知识,这样对后续学习C语言将有很大帮助。第3章 C语言编程的环境搭建
我们在第2章讲述了学习C语言所必需的一些预备知识。本章将给大家介绍常用
桌面操作系统下的C语言环境搭建。这里所讲述的C语言编译器以及集成开发环境
(IDE)都是可合法的,本书不鼓励各位使用盗版或破解软件,所以下面会
列出下载这些合法免费软件的官方链接,大家把编程环境搭建完之后即可上机实践
编程。3.1 Windows操作系统下搭建C语言编程环境
Windows操作系统下默认不自带任何C语言编译器,大家必须从网上下载自己所
需要的C语言编译器。如果各位想通过C语言开发Windows系统平台相关的应用,或
者主要想在Windows平台对C语言程序进行调试,那么往往首选Visual Studio
Community。这款开发环境是免费的,里面自带了微软自家的C语言编译器——简称
为MSVC。不过当前MSVC无法支持最新的C11标准新特性,而且即便是C99标准也是支
持得比较有限,所以它并不适合学习C11最新标准。但对于C语言初学者而言,这款
集成开发环境还是非常适合的。幸运的是,2017年3月微软最新推出的Visual
Studio Community 2017包含了Clang编译器前端工具,如果我们勾选安装的话即
可使用Clang来作为C语言编译器。尽管Visual Studio下的Clang编译器尚处于试
验阶段,但大部分功能都可用了。目前笔者测试下来,它对原子操作还没支持好,另外像UTF-8、UTF-16等字符编码问题还与Windows操作系统本身相关,所以要涉
及这些问题的话,我们只能使用系统特定的接口去解决或者使用下面提到的MinGW以
及Clang官方提供的编译工具链去解决。
所以,如果大家想在Windows操作系统下学习更为完整的C11标准最新特性,那
么建议下载MinGW,如果是64位的Windows系统的话则最好下载Mingw-w64。如果还
想学习Clang编译器语法扩展的话,也可以再下载单独的Clang编译器。
3.1.1 安装Visual Studio Community 2017
Visual Studio Community最新版本可在微软的Visual Studio官方网站下载:
https:www.visualstudio.comthank-you-downloading-visual-
studio?sku=Communityrel=15。
当我们下载好Visual Studio Community的安装程序之后,将它打开运行。随
后会看到一个选择安装组件的对话框。我们在该对话框的右侧能看到已经勾选上的
组件以及一些没有勾选上的组件。这里我们必须勾选上“ClangC2(实验)”这一
项,如图3-1所示。因为不安装Clang,后面就无法用它编译C源代码。
图3-1 Visual Studio Community安装界面
安装完成之后,我们打开Visual Studio Community 2017,首先出现欢迎界
面。Visual Studio在首次启动时就会很明显地提示我们注册账号或用账号登录。
我们先用Hotmail或MSN账号登录注册,如果不注册仅有30天左右的试用时间,但一旦注册完之后就能永久使用了。我们登录完自己的账号之后就可以开始新建一个C语
言的项目工程了。
我们找到菜单栏最左边的“文件”,然后选择“新建”,再点击“项目”,如
图3-2所示。
图3-2 欢迎界面中新建项目
随后我们会看到新建项目的对话框。在左侧边栏中找到“Visual C++”,然后
选中“Win32”,随后在中间栏选择“Win32 Console Application”,最后,在
底下输入此工程创建后存放的目录路径以及工程名,如图3-3所示。图3-3 Visual Sutdio新建项目
点击“OK”按钮后进入应用设置向导界面,如图3-4所示。
我们看到图3-4这个界面时,先别着急点击“下一步”按钮,应先点击左边边栏
中的“应用程序设置”,对此进行初步配置。然后进入图3-5所示的界面。图3-4 Visual Studio应用设置向导
图3-5 Visual Studio项目创建时的应用设置
图3-5所示的界面中,在“附加选项”中,先取消勾选“预编译头”,然后勾
选“空项目”。最后,点击“完成”按钮进入到我们所创建的cdemo项目工程的主界
面。此时,整个工程是空的,只有文件夹而没有任何文件,需要手工新建C源文件。用鼠标右键单击“源文件”,选择“添加”,然后再点击“新建项”,如图3-6所
示。
图3-6 Visual Studio添加C源文件
在随后弹出的如图3-7所示的对话框中,选中中间栏中的“C++文件
(.cpp)”那一项,然后在底下“名称”一栏输入源文件名。图3-7 Visual Studio命名C源文件名
注意:这里需要注意,默认文件后缀名是.cpp,即C++源文件,因为Visual C++默认采用C++编程语言,因此我们这里要手工填写.c文件后缀名,使得
后续我们用C编译器进行编译构建整个控制台应用。
完成之后,我们点击“添加”按钮,然后再次进入工程主界面,此时即可看到C
源文件的编辑界面了。
我们在进入源文件编辑界面后,先对Visual Studio的文本编辑选项做些处
理,以便于我们后续可以流畅地编写代码。如图3-8所示,我们在上面的菜单栏找
到“工具”,然后选择“选项”。图3-8 Visual Studio准备设置编辑选项
点击进入后能看到如图3-9所示的对话框。在左边栏找到“文本编辑器”这个选
项,然后将它展开,选中“所有语言”,随后我们勾选上“行号”,这样,在编辑
每个文本文件时都能看到行号,便于我们查找代码中的语法错误以及调试代码用。图3-9 设置编辑选项
最后,再选中“制表符”选项,对制表符进行设置,如图3-10所示。习惯上,我们一般将Tab Size设置为4个半角字符,缩进大小也是4个半角字符,然后每个制
表符用4个空格代替,这样用其他编辑器浏览Visual Studio编辑过的源文件也不会
导致格式错乱。
图3-10 Visual Studio设置制表符
接下来我们设置当前的项目工程的属性选项。我们找到菜单栏的“项目”,然
后点击“cdemo属性”,如图3-11所示。图3-11 Visual Studio设置项目属性
在配置界面的常规页面中(见图3-12),先找到左上角的“配置”选项,选
择“所有配置”。这样,我们后续做的所有配置都对Debug模式与Release模式同时
有效。然后,在右侧找到“平台工具集”,这里需要选择使用“Visual Studio
2017-Clang with Microsoft CodeGen”,这个选项使得我们对当前的项目工程
使用Clang编译工具链进行编译构建。图3-12 Visual Studio对cdemo项目工程的常规设置
随后我们展开CC++这一项,此时仍然需要先将左上角的“配置”设置为“所有
配置”。然后找到“语言”,将“C语言标准”设置为GNU11标准。这样我们就能在
Visual Studio Community集成开发环境下编写调试大部分基于GNU11标准的C语
言代码了。设置如图3-13所示。
图3-13 Visual Studio设置C语言标准全都设置完成之后,我们就可以编写第一个C语言程序了。同一般C语言教程一
样,我们这里也通过输出一个“Hello,world!”字样,作为第一个C语言代码的
演示程序。我们输入图3-14中所示的代码,然后点击工具栏中的绿色三角箭头(图
3-14中用矩形框圈出)即可编译运行了。在程序最后的getchar作用在于:弹
出的控制台应用不会在程序终止时马上自动关闭,而是等用户输入一个回车时再关
闭。
图3-14 在Windows控制台输出字符串
在图3-14所示的界面中,椭圆圈出来的部分用于设置当前程序以调试模式构建
还是以发布模式构建。如果以调试模式构建,我们可以利用Visual Studio内建的
调试器做断点跟踪,查看局部对象与全局对象状态以及寄存器状态等,便于调试程
序。如果以发布模式构建,那么当前程序会被大幅优化,使得程序运行性能大幅提升,但难以调试。图3-14中,中间用矩形框圈出的部分是设置当前目标程序的执行
模式,默认为x86,即32位执行模式。这里我们将它设置成了64位执行模式。
3.1.2 安装MinGW编译器
MinGW编译器是著名开源C语言编译器GCC对Windows操作系统的一个移植版本。
通过MinGW,我们就可以在Windows下享用大部分GCC编译器所带来的强大功能了。
这对跨平台的C语言开发而言十分有用。下面我们就来介绍如何下载安装MinGW编译
器。
首先,我们直接进入这个网址下载安装文件:
http:sourceforge.netprojectsmingwfileslatestdownload?
source=files。这个文件非常小,因为MinGW采用的是在线安装模式,萃取线上各
个最新release版本的组件进行下载。
然后,我们双击安装包,初步安装完毕后弹出对话框如图3-15所示。绿色进度
条表示已经安装好了。图3-15 MinGW初步安装成功
我们点击“Continue”按钮后,出现选择安装更多组件的对话框。我们在左侧
栏点击“Basic”,即采用基本安装。然后,在右侧栏安装上全部列出的组件。要选
中某个安装组件,鼠标右键该包名,然后在快捷菜单中选择“Mark for
Installation”命令,如图3-16所示。图3-16 MinGW安装,选中安装包
全都选择好之后,我们最后更新刚选好的安装包。我们在菜单栏选
中“Installation”,然后点击“Update Catalogue”,如图3-17所示。
图3-17 MinGW更新安装包
之后会弹出如图3-18所示的界面,点击最左边的“Review Changes”按钮,会弹出如图3-19所示的对话框。
图3-18 MinGW安装要求确认
点击“Apply”按钮之后,就会下载安装设置更新后的安装包。等待全都安装完
毕后,点击“Close”按钮,退出整个安装程序。图3-19 MinGW安装更新
安装结束后,不要着急使用,而是先将MinGW的bin文件夹注册到环境变量中。
先打开“文件资源管理器”,在左侧栏中找到“此电脑”或“我的电脑”,鼠标右
键单击它,选择“属性”,进入后点击左侧的“高级系统设置”,如图3-20所示。图3-20 进入环境变量的设置界面
进入图3-20的对话框之后,点击“环境变量”按钮,进入到“环境变量”对话
框。我们在“系统变量”区域选中“Path”变量,然后点击“编辑”按钮,弹
出“编辑系统变量”对话框。在“变量值”中往后添加刚才安装后的MinGW中的bin
文件夹所在目录。在环境变量中的每个值之间用半角分号“;”进行分隔,如图3-
21所示。图3-21 进入环境变量设置Path完成之后,我们就可以打开控制台程序(方法是右键桌面上左下角“开始”按
钮,然后选择命令提示符),然后进入要编译的C源文件所在的目录。然后用gcc命
令对指定C源文件进行编译构建,如图3-22所示。
这里,我们借用之前在Visual Studio Community下编辑好的源文件
test.c。我们先用cd命令定位到test.c所在的目录。然后用gcc--version命令查
看当前GCC编译器的版本。最后,用gcc-std=gnu11 test.c进行编译,最终在当前
目录生成a.exe可执行文件。我们直接键入a,回车,即可看到程序输出结果。
要注意的是,MinGW是32位的C语言编译器,所以它构建出来的程序也是32位
的。如果各位用的Windows操作系统是64位的,那么可以使用Mingw-w64编译器。
下载地址如下:https:sourceforge.netprojectsmingw-
w64fileslatestdownload?source=files。
Mingw-w64的安装、设置过程与32位的MinGW类似,这里不再赘述。
3.1.3 安装LLVM Clang编译器
LLVM(Low Level Virtual Machine)起源于一个大学项目,它是一个编译
器基础架构项目,用于设计一组具有良好定义的、可重用的库。LLVM起先用于替代
GCC(这里的GCC是指GNU Compiler Collection)栈中的代码生成器,然后对GCC
中已有的许多编译器进行修改以适配LLVM。后来LLVM发起了开发一个全新的适用于
不少编程语言的编译器前端,称为Clang。Clang主要支持C、C++、Objective-C等
编程语言,并且主要由Apple公司大力支持和维护。LLVM与Clang都基于BSD许可
证,比GPL更宽松。正因如此,现在许多硬件商都逐渐开始投入对LLVM的支持,像Khronos开放标准组织也基于LLVM IR(Intermediate Representation)开发出
了自己的一套SPIR-V。Clang编译器在语法上力争支持各大主流编译器的语法扩
展,包括GCC和MSVC,所以微软也已经把Clang纳入Visual Studio集成开发环境的
工具集中。
图3-22 用GCC构建C程序
我们首先在LLVM Clang官网下载最新稳定发布版本的Clang安装
包:http:llvm.orgreleasesdownload.html。然后,要注意的是选择32位
版本,如图3-23所示。图3-23 下载Clang for Windows(32-bit)
由于Clang主要是一个编译器前端,因此它需要依赖其他编译器的连接器以及某
些运行时库。所以,我们光安装Clang是无法直接成功构建应用程序的,因而我们要
使用Clang的话,必须在此之前先把MinGW安装好。MinGW是32位的,因此为了二进
制兼容,我们所选取的Clang也必须是32位的。当然,如果之前安装的是64位的
MingW-W64,那么这里需要下载安装64位的Clang。安装Clang的过程非常简单,可根据安装向导简单地做些选择即可完成安装。安
装完成后,可以去“系统”里的环境变量中看,把LLVM目录下的bin文件夹的路径
添加到Path环境变量中,如图3-24所示。然后就可以再次使用命令行工具直接编译
运行程序了。
图3-24 用Clang编译器构建应用程序3.2 macOS系统下搭建C语言编程环境
macOS系统也不默认自带C语言编译器。然而,用户可以自己去Mac App Store
macOS下的强大开发工具——Xcode:
https:itunes.apple.comcnappxcodeid497799835?mt=12。该集成开发
工具采用Apple定制版本的Clang编译器,称为Apple LLVM编译器。它自带C、C++、Objective-C以及Apple自己新推出的Swift编程语言编译器,还有一系列功
能强大的代码静态分析以及性能剖析工具。
下载完Xcode之后,把它打开。如果是第一次启动,Xcode会自动更新一些资
源,完了之后弹出主界面,如图3-25所示。
我们选择第二个选项,点击它即可创建应用程序工程。第一个选项仅用于操练
把玩Swift编程语言,而第二个选项用于创建真正的应用或库。当然,有些应用可直
接提交到App Store审核,有些则不行。
点击“Create a new Xcode project”之后,出现图3-26所示的对话框。在
图3-26中,我们看到在上面一栏中所选的项目工程为macOS的应用。然后在下边,我们选择“Command Line Tool”,即命令行工具。最左边的Cocoa Application
用于创建macOS系统上基于GUI以及沙盒机制的应用,它可以上传到Mac App
Store。中间的“Game”专门用于游戏应用,也可上传到Mac App Store。而最右
边的“Command Line Tool”构建出来的应用则无法上传到Mac App Store,但是
它能访问macOS的整个文件系统,并且没有采用沙盒机制。另外,开发者用Command
Line Tool开发出来的应用也可以直接放到网上供其他人下载使用。图3-25 Xcode欢迎界面图3-26 选择MacOS命令行工具应用项目
我们点击“Next”按钮之后出现如图3-27所示的对话框。在第1行用英文输入
自己的产品名称,这个后面将用于自动生成的工程名称。然后第2行填写组织名。第
3行填写组织标识,格式为com.<公司名>.<产品名>。当然,第2、第3行对于我们的
demo而言可以随意填写。第5行我们要选择C,表示使用C语言。
图3-27 输入macOS命令行应用的属性
点击“Next”按钮可看到图3-28所示的目录选择对话框。图3-28 macOS命令行应用生成目录选择对话框
这里选择将新创建的项目工程放到哪个目录下。另外,这里要注意的是,我们
不要勾选“Create Git repository”这一选项。因为它会在工程本地做git版本
管理,对于我们一般应用而言没有任何必要,而且这会随着工程构建的次数增多而
增大,很占磁盘空间。而且如果要将本地工程拷贝到其他环境,也会带来许多不
便。我们最后点击“Create”按钮之后,工程就会被创建好。
工程被创建完之后,Xcode默认会打开,包括会自动创建一个main.c的C语言源
文件。此时,我们不用着急编辑、运行,可以先设置一下编译选项。我们首先点击蓝色的“CDemo”项目工程图标,然后点击中间一
栏“TARGETS”下的“CDemo”控制台图标,最后在右边栏的最上方选中“Build
Settings”,然后在下面选中“All”和“Combined”。随后,我们找到“Apple
LLVM x.x-Language”这一栏,将“C Language Dialect”选为gnu11,这个选
项将贯穿本书内容。到此,我们的C语言编译选项就设定好了,如图3-29所示。
图3-29 macOS项目设置工程配置选项
如果我们想对最终生成的代码再做一些优化,可以设置图3-30中的一些选项。图3-30 macOS项目设置其他编译选项
我们将C++的异常以及运行时类型(RTTI)全都关闭,另外也将Objective-C
的异常关闭。这样,最终的应用程序中将不会包含异常栈,同时,编译器后端优化
也能更省力不少。大家可以观察到,将这几个选项关闭后,最终生成的可执行文件
会比开启时要小一些。
最后,我们可以设置一下Xcode自身的偏好设置,将行号显示出来,如图3-31
所示。图3-31 打开Xcode偏好设置
我们在菜单栏上,选择“Xcode”,然后点击“Pre-ferences...”,弹出图
3-32所示的对话框。我们把“Line numbers”勾选上即可在文本编辑框中看到行
号。另外,Xcode默认字符编码已经是UTF-8了,因此不需要我们做额外的设置。图3-32 Xcode设置文本编辑属性
由于Xcode默认字体可能会显得比较小,因此如果想设置字体以及背景颜色的话
可以选择“FontsColors”选项。
在进入到此对话框后,我们点击左侧栏下边的“+”号,添加一个新的字体,并
且选择“Duplicate‘Default’”,如图3-33所示。这使得我们所新增的字体以
默认字体和颜色作为基准,然后对它做大小修改。图3-33 Xcode字体设置,添加新字体
如图3-34所示,我们这里新增了一个叫“Defualt_Big”的字体,然后在中间
这栏,我们先选中“Plain Text”,然后将滚动条滚动到最下方,按住Shift键再
选中最后一条“Other Preprocessor Macros”,这样可以将所有种类的文字格式
全都选中,随后我们点击“T”字样的按钮来调整这些文字格式的字体大小。这里,原先的字体大小为“Menlo Regular-11.0”,设置之后这里变为“Menlo
Regular-14.0”。图3-34 Xcode设置新字体格式与大小
现在,我们就可以直接运行Xcode自动帮我们生成好的main.c中的C源代码了。
我们直接点击右上角的三角箭头按钮即可编译并运行这段代码,如图3-35所示。图3-35 编译运行macOS控制台应用
我们在下面的调试控制台中能看到图3-35这两行文字。其中,最后一句是应用
退出后系统自动打印的。我们可以看到,macOS下能非常轻松地直接输出中文,而不
需要各种复杂的编码转换。
图3-36 macOS控制台程序运行结果3.3 本章小结
本章主要讲述了Windows操作系统下如何使用Visual Studio Community、MinGW和LLVM Clang进行C语言程序开发,同时也讲解了如何在macOS下使用Xcode
做C语言程序开发。因为Windows操作系统与macOS系统用得比较广泛,而且它们都
主要基于GUI的集成开发环境进行编程,所以我们做重点讲解。而在各个版本的
Linux下基本都默认安装了GCC编译器,各位可以直接在Linux系统下的命令行终端
使用gcc命令对C语言源文件做编译构建。而当前FreeBSD最新发布版本默认使用了
LLVM Clang编译器,各位也可以直接在命令行终端使用clang命令对C语言源文件做
编译构建。
另外,Linux、FreeBSD系统下,笔者推荐使用的集成开发环境是Eclipse。它
拥有比较基本的代码智能感知,设置断点进行调试的功能,而且它也是一款开源免
费的软件。当然,要启动Eclipse必须先下载JRE(Java Runtime
Environment),这个可以从Oracle官网下载。
截至本章,第一部分的讲解结束,各位读者应该对C语言的由来、用途以及各种
准备工作都了解得差不多了吧?下面我们将进入第二部分,正式开启C语言魔法的大
门!第二篇 基础语法篇第4章 C语言中的基本元素
本章将正式进入C语言编程话题。我们在第1章已经大致介绍了C语言的编译、连
接和加载运行流程,参见图1-2。我们首先介绍C语言单个源文件的基本构成以及基
本元素。
图4-1 C源文件的基本构成
我们在图4-1中能看到,一个可用来编译执行的基本C源文件主要包含4个部分。
第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个字符:??= 对应于 ??) 对应于 ]??! 对应于 |??( 对应于 [??' 对应于 ^??> 对应于 }?? 对应于 \??< 对应于 {??- 对应于 ~
例如:
=def?ine 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编译器中,允许使用中文、拉丁字母、希腊字母等作为
标识符:αντιο、bonné、小鳥遊·六花、ラーメン等都是有效标识符,但是像3百
九、十二,这些就是无效的标识符。此外,C语言标准中还规定,如果一个标识符
含有通用字符名,那么每一个通用字符名必须落在ISOIEC 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语言新标准引入新的标准库可使得这些关键字能被统一。
所以,大家在使用以下划线打头的关键字时,请尽量先引入相应的标准库头文
件,然后使用非下划线形式的相应关键字。比如,
型定义为了bool类型;
等。我们最好使用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,然后计算210的结果等于20,再计算204的结果等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
include
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);
}
各位可以编译运行上述代码。如果各位在某些UnixLinux上实践,没有中文输
入法也没有关系,可以用相应的英文来代替上述中文。另外,上述字符串中所出现
的\n是一个转义字符,关于转义字符,我们将在5.1.6节中加以描述。4.6 本章小结
本章我们大概描述了C语言构成的基本元素。一开始,我们列出了一个完整的C
语言源文件应该包含的几个部分。然后我们提到了C语言中的可用字符集以及各类符
号与它们的定义。关于C语言执行环境限制的更多详细信息可参考此博
文:http:www.cnblogs.comzenny-chenp4251813.html。
通过本章学习,各位应该已经能体会到C语言书写的大致格式,并且通过本章列
出的一些代码片段,自己能试试身手写一些简单短小的代码出来,然后利用printf
函数可以打印出一些计算结果。第5章 基本数据类型
本章将介绍C语言中的基本数据类型以及相关的算术逻辑运算。C语言中的基本
数据分为两大类,一类是整数类型,另一类是浮点类型。整数类型还包括字符类型
以及布尔类型。浮点数类型包括单精度浮点数、双精度浮点数以及扩展双精度浮点
类型。
对任一整数对象和浮点数对象,我们都能用+、-、、对它们做加、减、乘、除算术运算操作,当然做除法时除数不能为零,否则会导致程序运行时异常。另
外,对于整数之间的操作还能使用%(求模操作)进行求余数,比如5%2的结果为1,但浮点数之间不能进行求模操作。我们还能对整数做按位操作以及移位操作,同样
这些操作不支持浮点数。5.1 整数类型
C语言中整数类型包括int、short、long、long long、布尔、字符等,除了
字符与布尔类型以外,其他所有整数类型都支持带符号与无符号的表示方式。关于
带符号与无符号整数类型的表示方式可以参考第2章的内容。此外,C语言标准中没
有明确规定每一种整数类型所占用的字节数,这些全都是由C语言的实现来定义的,但是C语言标准给了若干约束,所以C语言实现应该至少能满足这些约束。为了方便
叙述,我们这里仍然根据主流桌面端编译器(GCC、Clang)以及主流32位与64位处
理器环境的实现进行讲解。
5.1.1 int类型
用关键字int声明的一个整数对象具有int类型。在具体的C语言执行环境中,int数据的最小值与最大值分别定义为
INT_MAX。在我们常用的32位与64位环境中,int默认为是带符号的(相当于
signed int),占用4个字节(即32位),其最小值为-231(即0x80000000),最
大值为231-1(即0x7FFFFFFF)。int所对应的无符号类型是unsigned int,通常
在32位与64位环境下也占用4个字节,最小值为0,最大值为232-1(即
0xFFFFFFFF)。在具体C语言执行环境中的最大值定义为
UINT_MAX。
int类型对应的整数字面量可直接按照自然方式书写,比如0、-128、127、+2233等都默认表示为int类型。此外,整数字面量可以分别使用八进制、十进制以及十六进制的方式进行表达。八进制的整数字面量表达方式为以0打头,比
如:01、023、-0477这些都是属于八进制整数字面量。而十六进制整数字面量则是
以0x或0X打头,比如:0x123、-0x0045、0xabcdef这些都是有效的十六进制整数
字面量。而其他没有任何前缀的整数字面量都表示为十进制整数。如果想要表达一
个unsigned int类型的整数字面量,可在一般整数字面量后直接添加字母u或U。本
书习惯上使用大写的U。比如0U、01U、-128U、2048U、+2233U等都属于unsigned
int类型。当然,即便字面量后面不加U后缀,这些数也能赋值给unsigned int类型
的对象,因为它们会被编译器进行默认转换。此外,当我们要声明一个unsigned
int类型的对象时,int可以省略。比如,unsigned a=0;,其中对象a的类型即为
unsigned int类型,=是一个赋值操作符(assignment operators),将其右操
作数0赋值给左操作数a。下面举一些例子,各位也可以在自己的计算机上试试,如
代码清单5-1所示。
代码清单5-1 int类型介绍
include
include
int main(int argc, const char argv[])
{
int a = 10; 声明了int类型对象a,并且将整数10赋值给它
以下语句声明了unsigned int类型对象b,并且将整数100赋值给它
unsigned int b = +100U;
unsigned c = -1U; 声明了unsigned int类型对象c
printf(a + b = %d\n, a + b); 这里a+b的结果为110
printf(c = 0x%X\n, c); 这里,c为0xFFFFFFFF
printf(a + c = %d\n, a + c); 这里加法结果溢出,但可将它看作为10-1的结果
a = 0x7FFFFFFF;
a += 1; a += 1相当于a = a + 1
printf(a = %d\n, a); 加法结果溢出,结果为-2147483648,相当于0x80000000
printf(INT_MIN = %d\n, INT_MIN); 查看当前C语言实现下int类型的最小值
printf(INT_MAX = %d\n, INT_MAX); 查看当前C语言实现下int类型的最大值
查看当前C语言实现下unsigned int类型的最大值
由于unsigned int的最小值已被标准定义为0
printf(UINT_MAX = %u\n, UINT_MAX);
}
这里顺便再提一下,根据C语言标准,我们在一个C语言源文件的末尾处最好添
加一个换行符,并且不再添加任何其他的空白符。在某些老版本的GCC上(比如3.x
版本),如果源文件末不是以换行符结尾,则GCC编译器在编译后会有警告,要求在
源文件末尾处添加一个换行符。
上述代码已经涉及了很多额外的知识,比如加减法运算结果溢出的行为,还有
+=操作符的意义等。由于这些知识比较简单易懂,所以我们就不在正文中加以赘
述,而直接以代码注释的方式给出。各位在自己的计算机上编译运行后自然就能知
晓其用途。当然,各位在敲上述代码的时候,注释部分(以及其后面的文字)不
需要打出来,这些仅仅是对代码的注解,对程序本身没有其他作用。
5.1.2 short类型
short类型(标准表达为signed short int类型,其中signed与int均可省
略)我们一般称之为短整型。在我们通常的32位及64位系统下占用2个字节(即16
位),其最小值为-215(即0x8000),最大值为215-1(即0x7FFF)。在C语言执行
环境下,其最大、最小值分别定义为
SHRT_MIN。short类型所对应的无符号类型为unsigned short(标准表达为
unsigned short int,其中int可省)。它通常在32位及64位系统下占2个字节,最小值为0,最大值为216-1(即0xFFFF)。在C语言执行环境中,其最大值定义为
int与unsigned int相应的整数字面量进行赋值,如代码清单5-2所示的用法。
代码清单5-2 short类型介绍
include
include
int main(int argc, const char argv[])
{
这里同时声明了short类型对象a和b
并且将a赋值为100,b赋值为200
short a = 100, b = 200;
printf(a - b = %d\n, a - b);
这里声明了unsigned short类型的对象c
unsigned short c = 100;
c -= 200; 相当于c = c - 200;
printf(c = %hu\n, c); 结果为65436(即65536 - 100)
printf(SHRT_MIN = %d\n, SHRT_MIN); 查看当前C语言实现下short类型的最小值
printf(SHRT_MAX = %d\n, SHRT_MAX); 查看当前C语言实现下short类型的最大值
查看当前C语言实现下unsigned short类型的最大值
由于unsigned short的最小值已被标准定义为0
printf(USHRT_MAX = %u\n, USHRT_MAX);
}
代码清单5-2中,字符串格式符%hu对应一个unsigned short类型的参数。
65536表示为216。这里表述了第2章介绍的概念,即如何将一个带符号整数(补码形
式)转为无符号整数的表示形式。
5.1.3 long类型
long类型(标准表达为signed long int类型,其中signed与int均可省略)
我们一般称之为长整型。在我们通常的32位环境下long类型占用4个字节(即32位),而在64位系统下,当前几个主流桌面编译器就有所区别了。MSVC与VS-Clang
仍然为4个字节,而GCC与Clang则是8个字节(即64位)。long类型所对应的无符号
类型为unsigned long(标准表达为unsigned long int,int可省),我们一般
称之为无符号长整型,它的字节长度与long类型一致。在C语言执行环境中,long
类型的最小值与最大值分别定义为
unsigned long类型的最大值定义为
您现在查看是摘要介绍页, 详见PDF附件(10834KB,738页)。




