C 基础(1) - 初识C语言
在开始之前,先写一下这个系列的内容的规划, 首先还是从C 最基本的语法知识点开始,为什么还要从基础开始呢,从我自身的角度来看,一个扎实的基础知识对自己以后的开发或者知识体系的建构是非常重要的,扎实的基础可以为自己以后的开发起到事半功倍的效果,一个小的基础知识的不懂或者不理解都有可能为自己以后的开发工作造成很多困扰,所以,本系列还是从简到难,一步步深入理解并结合其他系列,例如计算机组成原理,数据结构,操作系统,计算机网络等同步更新和学习。
1. C语言的起源
从硬件控制到通用编程语言的诞生。
C 语言是计算机编程领域的 “基石级” 语言,其诞生并非偶然,而是源于 20 世纪 60 年代末至 70 年代初计算机硬件技术发展、操作系统开发需求以及编程语言演进的共同推动。它的起源紧密围绕贝尔实验室(Bell Labs) 的技术项目,核心设计者为丹尼斯・里奇(Dennis Ritchie) ,。然而,C语言不完全是里奇突发奇想而来,他是在B 语言(汤普逊发明)的基础 上进行设计。至于B 语言的起源,那是另一个故事。C语言设计的初衷是将其作为程序员使用的一种编程 工具,因此,其主要目标是成为有用的语言。
2. 选择C语言的理由
C 语言的特性使其能适配从 “无操作系统的嵌入式芯片” 到 “高性能服务器” 的所有场景,具体包括:
- 系统级开发:操作系统内核(Linux、Windows、FreeBSD)、编译器(GCC、Clang)、虚拟机(JVM 的部分核心模块);
- 嵌入式开发:单片机(51、STM32)、物联网设备(ESP32)、汽车电子(ECU 控制)、工业控制(PLC 程序);
- 高性能应用:网络服务器(Nginx)、数据库(MySQL 的存储引擎)、实时信号处理(音频 / 视频编解码);
- 硬件驱动开发:显卡驱动、网卡驱动、打印机驱动(直接与硬件寄存器交互);
- 底层工具开发:调试器(GDB)、版本控制工具(Git)、命令行工具(Linux 的
ls
/cd
等命令)。
C 语言的核心优势并非 “语法简洁” 或 “开发效率高”(反而它需要手动管理内存,开发周期可能更长),而是其不可替代的底层控制能力、极致的执行效率、跨平台的兼容性—— 这些特性使其成为 “底层开发的刚需语言”,同时也是理解计算机原理的 “最佳学习工具”。
如果你的开发需求涉及 “硬件控制”“高性能”“跨平台底层程序”,或你想深入计算机底层逻辑(而非仅做上层应用开发),那么 C 语言是无可替代的选择。
3. 计算机能做什么
在学习如何用C语言编程之前,最好先了解一下计算机的工作原理。这些知识有助于你理解用C语言编写程序和运行C程序时所发生的事情之间有什么联系。 现代的计算机由多种部件构成。中央处理单元(CPU)承担绝大部分的运算工作。随机存取内存(RAM) 是存储程序和文件的工作区;而永久内存存储设备(过去一般指机械硬盘,现在还包括固态硬盘)即使在 关闭计算机后,也不会丢失之前储存的程序和文件。另外,还有各种外围设备(如,键盘、鼠标、触摸屏、 监视器)提供人与计算机之间的交互。CPU 负责处理程序,接下来我们重点讨论它的工作原理。
CPU 的工作非常简单,至少从以下简短的描述中看是这样。它从内存中获取并执行一条指令,然后再 从内存中获取并执行下一条指令,诸如此类(一个吉赫兹的CPU 一秒钟能重复这样的操作大约十亿次,因 此,CPU 能以惊人的速度从事枯燥的工作)。CPU 有自己的小工作区————由若干个寄存器组成,每个寄存器都可以储存一个数字。一个寄存器储存下一条指令的内存地址,CPU 使用该地址来获取和更新下一条指 令。在获取指令后,CPU 在另一个寄存器中储存该指令,并更新第1个寄存器储存下一条指令的地址。CPU能理解的指令有限(这些指令的集合叫作指令集)。而且,这些指令相当具体,其中的许多指令都是用于请 求计算机把一个数字从一个位置移动到另一个位置。例如,从内存移动到寄存器。
下面介绍两个有趣的知识。其一,储存在计算机中的所有内容都是数字。计算机以数字形式储存数字和字 符(如,在文本文档中使用的字母)。每个字符都有一个数字码。计算机载入寄存器的指令也以数字形式储存, 指令集中的每条指令都有一个数字码。其二,计算机程序最终必须以数字指令码(即,机器语言)来表示。
简而言之,计算机的工作原理是:如果希望计算机做某些事,就必须为其提供特殊的指令列表(程序), 确切地告诉计算机要做的事以及如何做。你必须用计算机能直接明白的语言(机器语言)创建程序。这是一项繁琐、乏味、费力的任务。计算机要完成诸如两数相加这样简单的事,就得分成类似以下几个步骤。
1. 从内存位置 2000 上把一个数字拷贝到寄存器1。
2. 从内存位置 2004 上把另一个数字拷贝到寄存器2。
3. 把寄存器2中的内容与寄存器1中的内容相加,把结果储存在寄存器1中。
4. 把寄存器1中的内容拷贝到内存位置 2008
而你要做的是,必须用数字码来表示以上的每个步骤!
如果以这种方式编写程序很合你的意,那不得不说抱歉,因为用机器语言编程的黄金时代已一去不复 返。但是,如果你对有趣的事情比较感兴趣,不妨试试高级编程语言。
4. 高级计算机语言和编译器
高级编程语言(如,C)以多种方式简化了编程工作。首先,不必用数字码表示指令;其次,使用的指 令更贴近你如何想这个问题,而不是类似计算机那样繁琐的步骤。使用高级编程语言,可以在更抽象的层 面表达你的想法,不用考虑 CPU 在完成任务时具体需要哪些步骤。例如,对于两数相加,可以这样写:
total = mine + yours;
对我们而言,光看这行代码就知道要计算机做什么;而看用机器语言写成的等价指令(多条以数字码 形式表现的指令)则费劲得多。但是,对计算机而言却恰恰相反。在计算机看来,高级指令就是一堆无法理解的无用数据。编译器在这里派上了用场。编译器是把高级语言程序翻译成计算机能理解的机器语言指 令集的程序。程序员进行高级思维活动,而编译器则负责处理冗长乏味的细节工作。
编译器还有一个优势。一般而言,不同 CPU 制造商使用的指令系统和编码格式不同。例如,用 Intel Core i7 (英特尔酷睿i7) CPU 编写的机器语言程序对于 ARM Cortex-A57 CPU 而言什么都不是。但是,可以找到 与特定类型 CPU 匹配的编译器。因此,使用合适的编译器或编译器集,便可把一种高级语言程序转换成供各种不同类型 CPU 使用的机器语言程序。一旦解决了一个编程问题,便可让编译器集翻译成不同 CPU 使 用的机器语言。
简而言之,高级语言(如C、Java、Pascal)以更抽象的方式描述行为,不受限于特定 CPU 或指令集。 而且,高级语言简单易学,用高级语言编程比用机器语言编程容易得多。
5. 语言标准
目前,有许多C实现可用。在理想情况下,编写C程序时,假设该程序中未使用机器特定的编程技术, 那么它的运行情况在任何实现中都应该相同。要在实践中做到这一点,不同的实现要遵循同一个标准。
C语言发展之初,并没有所谓的C标准。1987年,布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇 (Dennis Ritchie)合著的 The C Programming Language(《C语言程序设计》)第1版是公认的C标准,通常 称之为 K&RC 或经典 C。特别是,该书中的附录中的“C语言参考手册”已成为实现C的指导标准。例如, 编译器都声称提供完整的 K&R 实现。虽然这本书中的附录定义了C语言,但却没有定义C库。与大多数 语言不同的是,C语言比其他语言更依赖库,因此需要一个标准库。实际上,由于缺乏官方标准,UNIX 实现提供的库已成为了标准库。
5.1 第1个ANSI/ISOC 标准
随着C的不断发展,越来越广泛地应用于更多系统中,C 社区意识到需要一个更全面、更新颖、更严格的标准。鉴于此,美国国家标准协会(ANSI)于1983 年组建了一个委员会(X3J11),开发了一套新标 准,并于1989年正式公布。该标准(ANSI C)定义了C语言和C标准库。国际标准化组织于1990年采用了这套C标准 (ISOC)。ISOC 和ANSIC是完全相同的标准。ANSI/ISO 标准的最终版本通常叫作C89(因 为ANSI于1989年批准该标准)或C90(因为 ISO 于1990年批准该标准)。另外,由于ANSI先公布C标准,因此业界人士通常使用ANSI C
在该委员会制定的指导原则中,最有趣的可能是:保持C的精神。委员会在表述这一精神时列出了以 下几点:
- 信任程序员;
- 不要妨碍程序员做需要做的事:
- 保持语言精练简单;
- 只提供一种方法执行一项操作;
- 让程序运行更快,即使不能保证其可移植性。
在最后一点上,标准委员会的用意是:作为实现,应该针对目标计算机来定义最合适的某特定操作, 而不是强加一个抽象、统一的定义。在学习C语言过程中,许多方面都反映了这一哲学思想。
5.2 C99 标准
1994年,ANSI/ISO 联合委员会(C9X委员会)开始修订C标准,最终发布了C99 标准。该委员会遵 循了最初C90 标准的原则,包括保持语言的精练简单。委员会的用意不是在C语言中添加新特性,而是为 了达到新的目标。第1个目标是,支持国际化编程。例如,提供多种方法处理国际字符集。第2个目标是, “调整现有实践致力于解决明显的缺陷”。因此,在遇到需要将C移至64位处理器时,委员会根据现实生 活中处理问题的经验来添加标准。第3个目标是,为适应科学和工程项目中的关键数值计算,提高C的适 应性,让C 比 FORTRAN 更有竞争力。 这3点(国际化、弥补缺陷和提高计算的实用性)是主要的修订目标。在其他方面的改变则更为保守, 例如,尽量与 C90、C++兼容,让语言在概念上保持简单。用委员会的话说:“……委员会很满意让 C++成 为大型、功能强大的语言”。
C99 的修订保留了C语言的精髓,C仍是一门简洁高效的语言。虽然该标准已发布了很长时间,但并非所有的编译器都完全实现 C99的所有改动。因此,你可能发现C99的 一些改动在自己的系统中不可用,或者只有改变编译器的设置才可用。
5.3 C11标准
维护标准任重道远。标准委员会在2007年承诺C标准的下一个版本是CIX,2011年终于发布了 C11 标准。此次,委员会提出了一些新的指导原则。出于对当前编程安全的担忧,不那么强调“信任程序员” 目标了。而且,供应商并未像对C90 那样很好地接受和支持C99。这使得C99的一些特性成为C11 的可选项。因为委员会认为,不应要求服务小型机市场的供应商支持其目标环境中用不到的特性。另外需要强调 的是,修订标准的原因不是因为原标准不能用,而是需要跟进新的技术。例如,新标准添加了可选项支持 当前使用多处理器的计算机。
6. 使用C语言的7 个标准
C是编译型语言。如果之前使用过编译型语言(如,Pascal 或 FORTRAN),就会很熟悉组建C程序的几个 基本步骤。但是,如果以前使用的是解释型语言(如,BASIC)或面向图形界面语言(如,Visual Basic),或者 甚至没接触过任何编程语言,就有必要学习如何编译。别担心,这并不复杂。首先,为了让读者对编程有大概 的了解,我们把编写C程序的过程分解成7个步骤(见图 1.3)。注意,这是理想状态。在实际的使用过程中, 尤其是在较大型的项目中,可能要做一些重复的工作,根据下一个步骤的情况来调整或改进上一个步骤。
7. 编程机制
生成程序的具体过程因计算机环境而异。C是可移植性语言,因此可以在许多环境中使用,包括 UNIX、 Linux、MS-DOS (一些人仍在使用)、Windows 和 Macintosh OS。有些产品会随着时间的推移发生演变或被取代。
首先,来看看许多C环境(包括上面提到的5种环境)共有的一些方面。虽然不必详细了解计算机内 部如何运行C程序,但是,了解一下编程机制不仅能丰富编程相关的背景知识,还有助于理解为何要经过 一些特殊的步骤才能得到C程序。
用C语言编写程序时,编写的内容被储存在文本文件中,该文件被称为源代码文件(source code file)。 大部分C系统,包括之前提到的,都要求文件名以.c 结尾(如,wordcount.c和budget.c)。在文件名中点号(.)前面的部分称为基本名(basename),点号后面的部分称为扩展名(extension)。因此,budget 是基本名,c是扩展名。基本名与扩展名的组合(budget.c)就是文件名。文件名应该满足特定计算机操 作系统的特殊要求。例如,MS-DOS 是 IBM PC及其兼容机的操作系统,比较老旧,它要求基本名不能超 过8个字符。因此,刚才提到的文件名 wordcount.c 就是无效的 DOS 文件名。有些 UNIX 系统限制整个 文件名(包括扩展名)不超过14个字符,而有些 UNIX 系统则允许使用更长的文件名,最多255个字符。 Linux、Windows 和 Macintosh OS 都允许使用长文件名。
接下来,我们来看一下具体的应用,假设有一个名为concrete.c 的源文件,其中的C源代码如程序 清单 所示。
#include <stdio.h>int main(void)
{printf ("Concrete contains gravel and cement.\n");return 0;
}
如果看不懂程序清单的代码,不用担心,我们将在第2章学习相关知识。
7.1 目标代码文件、可执行文件和库
C编程的基本策略是,用程序把源代码文件转换为可执行文件(其中包含可直接运行的机器语言代码)。 典型的C实现通过编译和链接两个步骤来完成这一过程。编译器把源代码转换成中间代码,链接器把中间代码和其他代码合并,生成可执行文件。C使用这种分而治之的方法方便对程序进行模块化,可以独立编译单独的模块,稍后再用链接器合并已编译的模块。通过这种方式,如果只更改某个模块,不必因此重新 编译其他模块。另外,链接器还将你编写的程序和预编译的库代码合并。
中间文件有多种形式。我们在这里描述的是最普遍的一种形式,即把源代码转换为机器语言代码,并把结果放在目标代码文件(或简称目标文件)中(这里假设源代码只有一个文件)。虽然目标文件中包含机器语言代码,但是并不能直接运行该文件。因为目标文件中储存的是编译器翻译的源代码,这还不是一个完整的程序。
目标代码文件缺失启动代码(startup code)。启动代码充当着程序和操作系统之间的接口。例如,可以 在MS Windows 或 Linux 系统下运行 IBM PC 兼容机。这两种情况所使用的硬件相同,所以目标代码相同,但是 Windows 和 Linux 所需的启动代码不同,因为这些系统处理程序的方式不同。
目标代码还缺少库函数。几乎所有的C程序都要使用C标准库中的函数。例如,concrete.c 中就使 用了 printf() 函数。目标代码文件并不包含该函数的代码,它只包含了使用 printf()函数的指令。 printf()函数真正的代码储存在另一个被称为库的文件中。库文件中有许多函数的目标代码。 链接器的作用是,把你编写的目标代码、系统的标准启动代码和库代码这3部分合并成一个文件,即 可执行文件。对于库代码,链接器只会把程序中要用到的库函数代码提取出来
7.2 在 UNIX 系统上编译
虽然在我们看来,程序完美无缺,但是对计算机而言,这是一堆乱码。计算机不明白#include 和 printf 是什么(也许你现在也不明白,但是学到后面就会明白,而计算机却不会)。如前所述,我们需要 编译器将我们编写的代码(源代码)翻译成计算机能看懂的代码(机器代码)。最后生成的可执行文件中包 含计算机要完成任务所需的所有机器代码。
以前,UNIX C 编译器要调用语言定义的cc 命令。但是,它没有跟上标准发展的脚步,已经退出了历 史舞台。但是, UNIX 系统提供的C编译器通常来自一些其他源,然后以cc 命令作为编译器的别名。因此, 虽然在不同的系统中会调用不同的编译器,但用户仍可以继续使用相同的命令。
编译 inform.c,要输入以下命令:
cc inform.c
几秒钟后,会返回 UNIX 的提示,告诉用户任务已完成。如果程序编写错误,你可能会看到警告或错 误消息,但我们先假设编写的程序完全正确(如果编译器报告 void 的错误,说明你的系统未更新成 ANSI C编译器,只需删除 void 即可)。如果使用 1s 命令列出文件,会发现有一个a.out 文件(见图 1.5)。该 文件是包含已翻译(或已编译)程序的可执行文件。要运行该文件,只需输入:
a.out
输出内容如下:
A .c is used to end a C program filename.
如果要储存可执行文件(a.out),应该把它重命名。否则,该文件会被下一次编译程序时生成的新 a.out 文件替换。
7.3 GNU 编译器集合和LLVM 项目
GNU 项目始于1987年,是一个开发大量免费 UNIX 软件的集合(GNU的意思是“GNU's Not UNIX", 即 GNU 不是 UNIX)。GNU 编译器集合(也被称为GCC,其中包含 GCC C编译器)是该项目的产品之一。 GCC 在一个指导委员会的带领下,持续不断地开发,它的C编译器紧跟C标准的改动。GCC 有各种版本 以适应不同的硬件平台和操作系统,包括 UNIX、Linux 和 Windows。用gcc 命令便可调用GCCC 编译器。 许多使用 gcc 的系统都用 cc 作为gcc 的别名。
LLVM 项目成为cc 的另一个替代品。该项目是与编译器相关的开源软件集合,始于伊利诺伊大学 的2000份研究项目。它的 Clang 编译器处理 C代码,可以通过 clang 调用。有多种版本供不同的平 台使用,包括 Linux。2012年,Clang 成为 FreeBSD 的默认 C 编译器。Clang 也对最新的C标准支持得 很好。
GNU 和 LLVM 都可以使用-v 选项来显示版本信息,因此各系统都使用 cc 别名来代替 gcc 或 clang 命令。以下组合:
CC -v
显示你所使用的编译器及其版本。
gcc 和 clang 命令都可以根据不同的版本选择运行时选项来调用不同C标准。
gcc-std=c99 inform.c'
gcc -std=c1x inform.c
gcc -std=c11 inform.c
1行调用C99 标准,第2行调用 GCC 接受C11 之前的草案标准,第3行调用 GCC 接受的C11 标准 版本。Clang 编译器在这一点上用法与 GCC 相同。