C语言编程魔法书:基于C11标准(txt+pdf+epub+mobi电子书下载)


发布时间:2021-05-16 10:09:13

点击下载

作者:陈轶

出版社:机械工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

C语言编程魔法书:基于C11标准

C语言编程魔法书:基于C11标准试读:

前言

F O R E W O R D

为什么要写这本书

本人在2001年上了大学本科,读计算机科学与技术专业。在第一年的上半学期,对计算机编程还没什么感觉。但是就在考“C语言程序设计”这门专业课的前一个月,感觉这门课学了那么久几乎什么都不会,可把我急坏了。然后就在这短短一个月的时间里又是看书,又是上机实验,终于考了70多分,算是过关了……不过奇怪的是在考试结束后,就发现自己对编程有了感情。到了大二,我们上“数据结构”所使用的教材是基于C++编程语言的,因为之前没学过C++语言,所以只能自学。而在这个过程中,我发现自己对编程更加热爱。在上完大三之后,我在暑假里又把之前的C语言重新巩固一番。有了计算机组成、操作系统、汇编语言、数据结构等知识积淀之后再去看C语言编程就感觉容易多了。我也是由此喜欢上了C编程语言。

10年之后,发现国内市面上很多C语言参考书仍然显得非常陈旧。不仅基于古老的C89/90标准,而且还在用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语言的最新标准,即ISO/IEC 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.net/zenny_chen进行专题讨论,我会尽量在线上为你提供最满意的解答。同时,你也可以通过微博http://weibo.com/zenny1chen与我联系,或发送电子邮件到zenny_chen@163.com。期待能够得到你们的真挚反馈,在技术之路上互勉共进。另外,本书最后两章的代码可以在作者的GitHub上获取:https://github.com/zenny-chen。

致谢

首先感谢我的父母和妻子对我写作此书的大力支持,尤其是我妻子在我忙于工作、编写此书时帮忙照顾孩子和做饭。然后感谢我公司老板对我写作此书的鼓舞与期待。

这里还要感谢机械工业出版社华章公司的编辑高婧雅,在一年多的时间里给予我的大力支持和帮助。

最后感谢支持我的技术爱好者,感谢你们对我的支持以及对我的信任。

我想和作者聊聊

为了能更好地与读者进行联系,笔者这里留了一个QQ讨论群。各位如果在阅读此书中有任何疑问可以来本群询问,大家可以一起探讨。各位可以扫一扫下方的二维码,进此群的提示语为:“C语言编程魔法书”,或者查询群号86540289申请入群。

陈轶|第一篇|预备知识篇 第1章C魔法概览

本章内容主要对C编程语言(以下简称C语言)进行大体介绍,包括它的历史以及C语言标准的演化进程。然后介绍一下C语言编程思想,当前主流C语言编译器以及GNU语法扩展。最后简单介绍一下从用C语言编写程序到编译、构建一个可执行程序的大致过程。

计算机编程语言从对计算机硬件底层的抽象程度进行分类,可分为:机器语言、汇编语言以及高级语言。下面由底层到高层分别介绍这几种类别的编程语言。1.1 例说编程语言

1)机器语言是直接通过十六进制数表示当前处理器架构的机器指令码。指令码包含了当前指令的功能(比如算术逻辑运算、移位、分支、中断、I/O等)、寄存器、立即数等多种元素。每种处理器架构所对应的机器码的字节长度也各不相同,有些是固定长度的(比如ARM、MIPS等架构),有些是可变长度的(比如x86架构)。

2)汇编语言(Assembly Language)通过简单的指令助记符(memonics)来表示对应机器指令的功能、寄存器编号、立即数(immediates)等元素。汇编语言是对机器指令的简单抽象,通过汇编器(assembler)可以将汇编语句翻译成对应的机器指令码。

3)高级语言的表达形式更为抽象且贴近我们日常的语言表述。而且,高级语言比起汇编语言往往更具有表达力,且拥有更加丰富的语法特性,以便将程序进行结构化和模块化。比如,高级语言具有自定义变量标识符、自定义数据结构、分支与循环、更形象自然的表达式等。高级语言一般通过编译器(compiler)可直接将表达式翻译为对应的机器指令码;也可以将高级语言先翻译为中间语言(类似于汇编,但可能比汇编适用范围更广、更利于跨平台的字节码),最后将中间语言翻译为最终的机器指令码。

当然,有些书中还介绍了第四代语言,它基于高级语言,比高级语言更抽象,只需要一些简单的描述语句就能让计算机做比较复杂的工作。比如SQL(结构化查询语言,用于数据库查询)算是一种第四代语言。

下面,为了能让大家对这三种层次的编程语言有一个感性的认识,这里将列举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

在代码清单1-1中,_my_sub_machine程序片段中的两条.long语句即为机器指令。这两条机器指令正好与_my_sub_assembly中的两条汇编指令相对应。也就是说,“0x4b010000”这串32位的十六进制代码意思就是“sub w0,w0,w1”,表示将寄存器w0与寄存器w1的值进行相减,然后将结果写回w0寄存器中。而“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年在AT&T贝尔实验室里开发出来,主要用于重新实现Unix操作系统。此时,C语言又被称为K&R C。其中,K表示Kernighan的首字母,而R则是Ritchie的首字母。K&R C语言与后来标准化的C语言有很大差异。比如,如果函数返回类型为int,则int可省:int my_function(){},也可以写成my_function(){}。编译器不会有任何警告,更不会报错。另外,还有现在看来比较奇葩的函数定义,像我们现在定义这么一个函数——void my_function(int a,char*p){},如果是用K&R C语法定义的话要写成:void my_function(a,p)int a;char*p;{}。K&R的C语法中,定义一个函数时,其形参列表先列出形参的标识符,然后在函数声明的后面紧跟着对形参标识符的完整声明,最后是函数体。这在现行标准中已经被逐步废弃使用了。另外,当时的第一本C语言专业书《The C Programming Language》也并非一个正式的编程语言规范,但被用了许多年。

2.C90标准

由于C语言被各大公司所使用(包括当时处于鼎盛时期的IBM PC),因此到了1989年,C语言由美国国家标准协会(ANSI)进行了标准化,此时C语言又被称为ANSI C。而仅过一年,ANSI C就被国际标准化组织ISO给采纳了。此时,C语言在ISO中有了一个官方名称——ISO/IEC 9899:1990。其中,9899是C语言在ISO标准中的代号,像C++在ISO标准中的代号是14882。而冒号后面的1990表示当前修订好的版本是在1990年发布的。对于ISO/IEC 9899:1990的俗称或简称,有些地方称为C89,有些地方称为C90,或者C89/90。不管怎么称呼,它们都指代这个最初的C语言国际标准。这个版本的C语言标准作为K&R C的一个超集(即K&R C是此标准C的一个子集),把后来引入的许多非官方特性也一起整合了进去。其中包括了从C++借鉴的函数原型(Function Prototypes),指向void的指针,对国际字符集以及本地语言环境的支持。在此标准中,尽管已经将函数定义的方式改为现在我们常用的那种方式,不过K&R的语法形式仍然兼容。

3.C99标准

在随后的几年里,C语言的标准化委员会又不断地对C语言进行改进,到了1999年,正式发布了ISO/IEC 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年正式发布了ISO/IEC 9899:2011,简称为C11标准。C11标准新引入的特征尽管没C99相对C90引入的那么多,但是这些也都十分有用,比如:字节对齐说明符、泛型机制(generic selection)、对多线程的支持、静态断言、原子操作以及对Unicode的支持。本书将主要针对C11标准为大家详细讲解C编程语言。关于C语言历史与演化进程的详细介绍可参考维基百科:https://en.wikipedia.org/wiki/C_%28programming_language%29。

笔者近两年也是在不断地了解C语言标准委员会的最新动态(可参见:http://www.open-std.org/jtc1/sc22/wg14/),其中看到有人提出想为C语言添加面向对象的特性,包括增加类、继承、多态等已被C++语言所广泛使用的语法特性,但是最终被委员会驳回了。因为这些复杂的语法特性并不符合C语言的设计理念以及设计哲学,况且C++已经有了这些特性,C语言无需再对它们进行支持。笔者将在第19章给大家谈谈C语言设计理念与发展方向。1.2 用C语言编程的基本注意事项

C语言的发明其实基于Unix操作系统。当时在C语言未面世之前,Dennis Ritchie所在的AT&T贝尔实验室用的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操作系统之外,主要用于Unix/Linux操作系统。像现在很多版本的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-C/C++、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语法扩展适用性十分广泛。如果读者当前在做Linux/Unix或Windows上的C语言编程开发,或者是在开发macOS/iOS应用,又或者是在开发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文件;而在Unix/Linux系统上往往是.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.com/macbook-pro/specs/。

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,n那么就对应十进制数的2,然后每个位计算得到的十进制数再依次相加得到最终十进制数的值。比如,一个5位二进制数10010,最低位为最右边的位,记为0号位,数值为0;最高位为最左边的位,记为441号位,数值为1。那么它所对应的十进制数为:2+2=18。因为该二n进制数除了4号位和1号位为1之外,其余位都是0,因此0乘以2肯定为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,说明是负数,我们就以上述过程来求解:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载