前言
C 语言是一门抽象的
、面向过程
的语言,C 语言广泛应用于底层开发
,C 语言在计算机体系中占据着不可替代的作用,可以说 C 语言是编程的基础,也就是说,不管你学习任何语言,都应该把 C 语言放在首先要学
的位置上。下面这张图更好的说明 C 语言的重要性
可以看到,C 语言是一种底层语言,是一种系统层级的语言,操作系统就是使用 C 语言来编写的,比如 Windows、Linux、UNIX 。如果说其他语言是光鲜亮丽的外表,那么 C 语言就是灵魂,永远那么朴实无华。
C 语言特性
那么,既然 C 语言这么重要,它有什么值得我们去学的地方呢?我们不应该只因为它重要而去学,我们更在意的是学完我们能学会什么,能让我们获得什么。
C 语言的设计
C 语言是 1972 年,由贝尔实验室的丹尼斯·里奇(Dennis Ritch)
和肯·汤普逊(Ken Thompson)
在开发 UNIX 操作系统时设计了C语言。C 语言是一门流行的语言,它把计算机科学理论和工程实践理论完美的融合在一起,使用户能够完成模块化的编程和设计。
计算机科学理论:简称 CS、是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。
C 语言具有高效性
C 语言是一门高效性语言,它被设计用来充分发挥计算机的优势,因此 C 语言程序运行速度很快,C 语言能够合理了使用内存来获得最大的运行速度
C 语言具有可移植性
C 语言是一门具有可移植性的语言,这就意味着,对于在一台计算机上编写的 C 语言程序可以在另一台计算机上轻松地运行,从而极大的减少了程序移植的工作量。
C 语言特点
- C 语言是一门简洁的语言,因为 C 语言设计更加靠近底层,因此不需要众多 Java 、C# 等高级语言才有的特性,程序的编写要求不是很严格。
- C 语言具有结构化控制语句,C 语言是一门结构化的语言,它提供的控制语句具有结构化特征,如 for 循环、if⋯ else 判断语句和 switch 语句等。
- C 语言具有丰富的数据类型,不仅包含有传统的字符型、整型、浮点型、数组类型等数据类型,还具有其他编程语言所不具备的数据类型,比如指针。
- C 语言能够直接对内存地址进行读写,因此可以实现汇编语言的主要功能,并可直接操作硬件。
- C 语言速度快,生成的目标代码执行效率高。
下面让我们通过一个简单的示例来说明一下 C 语言
入门级 C 语言程序
下面我们来看一个很简单的 C 语言程序,我觉得工具无所谓大家用着顺手就行。
第一个 C 语言程序
#include <stdio.h> int main(int argc, const char * argv[]) { printf("Hello, World!\n"); printf("my Name is cxuan \n") printf("number = %d \n", number); return 0; }
你可能不知道这段代码是什么意思,不过别着急,我们先运行一下看看结果。
这段程序输出了 Hello,World!
和 My Name is cxuan
,下面我们解释一下各行代码的含义。
首先,第一行的 #include <stdio.h>
, 这行代码包含另一个文件,这一行告诉编译器把 stdio.h
的内容包含在当前程序中。stdio.h
是 C 编译器软件包的标准部分,它能够提供键盘输入和显示器输出。
什么是 C 标准软件包?C 是由 Dennis M 在1972年开发的通用,过程性,命令式计算机编程语言。C标准库是一组 C 语言内置函数,常量和头文件,例如<stdio.h>,<stdlib.h>,<math.h>等。此库将用作 C 程序员的参考手册。
我们后面会介绍 stdio.h ,现在你知道它是什么就好。
在 stdio.h 下面一行代码就是 main
函数。
C 程序能够包含一个或多个函数,函数是 C 语言的根本,就和方法是 Java 的基本构成一样。main()
表示一个函数名,int
表示的是 main 函数返回一个整数。void 表明 main() 不带任何参数。这些我们后面也会详细说明,只需要记住 int 和 void 是标准 ANSI C
定义 main() 的一部分(如果使用 ANSI C 之前的编译器,请忽略 void)。
然后是 /*一个简单的 C 语言程序*/
表示的是注释,注释使用 /**/
来表示,注释的内容在两个符号之间。这些符号能够提高程序的可读性。
注意:注释只是为了帮助程序员理解代码的含义,编译器会忽略注释
下面就是 {
,这是左花括号,它表示的是函数体的开始,而最后的右花括号 }
表示函数体的结束。{ }
中间是书写代码的地方,也叫做代码块。
int number
表示的是将会使用一个名为 number 的变量,而且 number 是 int
整数类型。
number = 11
表示的是把值 11 赋值给 number 的变量。
printf(Hello,world!\n);
表示调用一个函数,这个语句使用 printf()
函数,在屏幕上显示 Hello,world
, printf() 函数是 C 标准库函数中的一种,它能够把程序运行的结果输出到显示器上。而代码 \n
表示的是 换行
,也就是另起一行,把光标移到下一行。
然后接下来的一行 printf() 和上面一行是一样的,我们就不多说了。最后一行 printf() 有点意思,你会发现有一个 %d
的语法,它的意思表示的是使用整形输出字符串。
代码块的最后一行是 return 0
,它可以看成是 main 函数的结束,最后一行是代码块 }
,它表示的是程序的结束。
好了,我们现在写完了第一个 C 语言程序,有没有对 C 有了更深的认识呢?肯定没有。。。这才哪到哪,继续学习吧。
现在,我们可以归纳为 C 语言程序的几个组成要素,如下图所示
C 语言执行流程
C 语言程序成为高级语言的原因是它能够读取并理解人们的思想。然而,为了能够在系统中运行 hello.c
程序,则各个 C 语句必须由其他程序转换为一系列低级机器语言指令。这些指令被打包作为可执行对象程序
,存储在二进制磁盘文件中。目标程序也称为可执行目标文件。
在 UNIX 系统中,从源文件到对象文件的转换是由编译器
执行完成的。
gcc -o hello hello.c
gcc 编译器驱动从源文件读取 hello.c
,并把它翻译成一个可执行文件 hello
。这个翻译过程可用如下图来表示
这就是一个完整的 hello world 程序执行过程,会涉及几个核心组件:预处理器、编译器、汇编器、连接器,下面我们逐个击破。
预处理阶段(Preprocessing phase)
,预处理器会根据开始的#
字符,修改源 C 程序。#include <stdio.h> 命令就会告诉预处理器去读系统头文件stdio.h
中的内容,并把它插入到程序作为文本。然后就得到了另外一个 C 程序hello.i
,这个程序通常是以.i
为结尾。- 然后是
编译阶段(Compilation phase)
,编译器会把文本文件hello.i
翻译成文本hello.s
,它包括一段汇编语言程序(assembly-language program)
。 - 编译完成之后是
汇编阶段(Assembly phase)
,这一步,汇编器 as
会把 hello.s 翻译成机器指令,把这些指令打包成可重定位的二进制程序(relocatable object program)
放在 hello.c 文件中。它包含的 17 个字节是函数 main 的指令编码,如果我们在文本编辑器中打开 hello.o 将会看到一堆乱码。 - 最后一个是
链接阶段(Linking phase)
,我们的 hello 程序会调用printf
函数,它是 C 编译器提供的 C 标准库中的一部分。printf 函数位于一个叫做printf.o
文件中,它是一个单独的预编译好的目标文件,而这个文件必须要和我们的 hello.o 进行链接,连接器(ld)
会处理这个合并操作。结果是,hello 文件,它是一个可执行的目标文件(或称为可执行文件),已准备好加载到内存中并由系统执行。
你需要理解编译系统做了什么
对于上面这种简单的 hello 程序来说,我们可以依赖编译系统(compilation system)
来提供一个正确和有效的机器代码。然而,对于我们上面讲的程序员来说,编译器有几大特征你需要知道
优化程序性能(Optimizing program performance)
,现代编译器是一种高效的用来生成良好代码的工具。对于程序员来说,你无需为了编写高质量的代码而去理解编译器内部做了什么工作。然而,为了编写出高效的 C 语言程序,我们需要了解一些基本的机器码以及编译器将不同的 C 语句转化为机器代码的过程。理解链接时出现的错误(Understanding link-time errors)
,在我们的经验中,一些非常复杂的错误大多是由链接阶段引起的,特别是当你想要构建大型软件项目时。避免安全漏洞(Avoiding security holes)
,近些年来,缓冲区溢出(buffer overflow vulnerabilities)
是造成网络和 Internet 服务的罪魁祸首,所以我们有必要去规避这种问题。
系统硬件组成
为了理解 hello 程序在运行时发生了什么,我们需要首先对系统的硬件有一个认识。下面这是一张 Intel 系统产品的模型,我们来对其进行解释
总线(Buses)
:在整个系统中运行的是称为总线的电气管道的集合,这些总线在组件之间来回传输字节信息。通常总线被设计成传送定长的字节块,也就是字(word)
。字中的字节数(字长)是一个基本的系统参数,各个系统中都不尽相同。现在大部分的字都是 4 个字节(32 位)或者 8 个字节(64 位)。
I/O 设备(I/O Devices)
:Input/Output 设备是系统和外部世界的连接。上图中有四类 I/O 设备:用于用户输入的键盘和鼠标,用于用户输出的显示器,一个磁盘驱动用来长时间的保存数据和程序。刚开始的时候,可执行程序就保存在磁盘上。
每个I/O 设备连接 I/O 总线都被称为控制器(controller)
或者是适配器(Adapter)
。控制器和适配器之间的主要区别在于封装方式。控制器是 I/O 设备本身或者系统的主印制板电路(通常称作主板)上的芯片组。而适配器则是一块插在主板插槽上的卡。无论组织形式如何,它们的最终目的都是彼此交换信息。主存(Main Memory)
,主存是一个临时存储设备
,而不是永久性存储,磁盘是永久性存储
的设备。主存既保存程序,又保存处理器执行流程所处理的数据。从物理组成上说,主存是由一系列DRAM(dynamic random access memory)
动态随机存储构成的集合。逻辑上说,内存就是一个线性的字节数组,有它唯一的地址编号,从 0 开始。一般来说,组成程序的每条机器指令都由不同数量的字节构成,C 程序变量相对应的数据项的大小根据类型进行变化。比如,在 Linux 的 x86-64 机器上,short 类型的数据需要 2 个字节,int 和 float 需要 4 个字节,而 long 和 double 需要 8 个字节。处理器(Processor)
,CPU(central processing unit)
或者简单的处理器,是解释(并执行)存储在主存储器中的指令的引擎。处理器的核心大小为一个字的存储设备(或寄存器),称为程序计数器(PC)
。在任何时刻,PC 都指向主存中的某条机器语言指令(即含有该条指令的地址)。
从系统通电开始,直到系统断电,处理器一直在不断地执行程序计数器指向的指令,再更新程序计数器,使其指向下一条指令。处理器根据其指令集体系结构定义的指令模型进行操作。在这个模型中,指令按照严格的顺序执行,执行一条指令涉及执行一系列的步骤。处理器从程序计数器指向的内存中读取指令,解释指令中的位,执行该指令指示的一些简单操作,然后更新程序计数器以指向下一条指令。指令与指令之间可能连续,可能不连续(比如 jmp 指令就不会顺序读取)
下面是 CPU 可能执行简单操作的几个步骤加载(Load)
:从主存中拷贝一个字节或者一个字到内存中,覆盖寄存器先前的内容存储(Store)
:将寄存器中的字节或字复制到主存储器中的某个位置,从而覆盖该位置的先前内容操作(Operate)
:把两个寄存器的内容复制到ALU(Arithmetic logic unit)
。把两个字进行算术运算,并把结果存储在寄存器中,重写寄存器先前的内容。
算术逻辑单元(ALU)是对数字二进制数执行算术和按位运算的组合数字电子电路。
跳转(jump)
:从指令中抽取一个字,把这个字复制到程序计数器(PC)
中,覆盖原来的值