深入C语言和程序运行原理(1)— 如何更好地拥抱现代 C 语言?

拥抱c语言

 

相信来学习这门课的大部分同学,都或多或少接触过一些 C 语言的基础知识。但是,我认为掌握 C 语言的基本语法并不困难,更重要的是能够灵活、高效地使用这门语言,并通过观察语言背后机器的执行细节,来深入了解关于编译优化、程序执行,以及计算机体系结构等其他相互关联的知识

C 语言是工作中使用最多的语言之一。由于 C 语言语法简单、抽象层次较低,能够通过它在进行原型验证时精确地控制程序的运行状态。另一方面,在接触操作系统、Unix 系统编程、语言运行时,以及系统库等相关内容时,更深切地感受到了解 C 语言对于深入理解这些内容的重要性。

因此,这门课将不会介绍 C 语言的语法细节,而是结合 C 核心语法、汇编代码,以及计算机体系结构等相关知识,来讲述 C 语言、应用程序和操作系统三者之间的协作关系

C 语言过时了吗?

说到学习 C 语言,很多人都会有这样的问题:在新编程语言层出不穷的今天,C 语言已经诞生了这么久,会不会马上就要过时了?

对此,回答是:C 语言还远远没有过时,相反,学习 C 语言是非常有必要的。

从下图(图片来自 https://www.tiobe.com)中可以看到,自 2000 年以来,C 语言便一直处于 TIOBE 编程语言榜单的前两名。作为全球最知名的,反映编程语言热门程度的榜单,TIOBE 指数直接表明了哪些语言是应该被及时掌握的。它对世界范围内编程语言的整体走势有着重要意义。

编程语言榜单

不仅如此,C 语言本身也处在不断的“进化”过程中。下图展示了 C 语言诞生至今的几次重大版本的发布内容。可以看到,C 语言在发展的同时也在尽可能保持着自身的精简。而最近 C2x 标准草案的制定也证明了时至今日的 C 语言,仍然“老当益壮”。

C 语言演化进程

总之,C 语言远远没有过时。其实,C 语言问世几十年来,一直都是使用最广泛的编程语言之一

作为一种静态编译型语言,C 语言有着其自身所适合的应用场景。确实,我们无法使用它来编写 Web 应用,也无法使用它来高效快捷地构建深度学习应用。但要知道的是,用于支持这些应用正常运行的系统组件,乃至操作系统内核,都是使用 C 语言编写的。现如今,这个世界上几乎所有重要的软件都与 C 有着直接或间接的关系

C 语言被广泛应用于实现操作系统、嵌入式系统应用、编译器、数据库、驱动程序,以及服务器应用等较为底层和基础的系统级程序。除此之外,C 语言在诸如数值计算、工业控制、物联网,乃至科研领域也有着重要的应用。

C 语言重要应用领域

那么,为什么 C 语言使用如此广泛呢?原因主要有两个,一是它有着远优于大部分其他语言的程序精确控制能力,二是它高效的运行时性能。

精确控制程序

C 语言在设计上对平台独立的低层次汇编指令进行了适度的抽象,在大多数情况下,它的代码可以直接映射到硬件平台上的机器指令。因此,我们能够更加灵活地控制程序的具体表现行为。

我们来看个例子吧。使用 C 语言,我们甚至可以直接控制代码中某个变量值的存放位置,视情况来决定将它存放在栈内存中,还是寄存器中。如下图所示,这里,左右两个窗口中相同背景颜色的代码行,表示了 C 代码与其对应的汇编代码。可以看到,左侧 C 代码中第三行变量x 的值被存放到了 ebx 寄存器中。

C程序编源码及对应汇编码

在某些特殊场景下,我们甚至可以直接在 C 代码中嵌入汇编代码,以更细粒度的方式来控制程序的执行,或直接与更底层的硬件进行交互。比如在下面这段代码中,我们使用 asm 关键字嵌入了两行汇编代码,你能猜出它们做了什么吗?

#include <stdio.h>
int main( void )
{
    int	src = 1;
    int	dst;
    asm ("mov %1, %0\n\t"
         "add $1, %0"
         : "=r" (dst)
         : "r" (src) );
    printf( "%d\n", dst );
}

C 语言更贴近底层硬件的这一特征,就使得它非常适合被应用在需要细粒度控制资源,或与底层硬件打交道的场景中。实际上,这其中有很多优秀项目都是你所熟知,甚至在日常工作中都会经常使用到的。比如代码版本管理工具 Git、高性能 Web 服务器 Nginx、高性能 NoSQL 数据库 Redis,以及最知名、代码行数最多的开源项目 Linux 内核等。因此,如果你想要读懂它们的设计、搞懂它们的原理,那了解 C 语言便是一个必不可少的过程。

需要注意的是,C 语言也保持了高级语言的部分特性,比如提供了接近于自然语言的语法和关键字,且源代码可以做到独立于机器的分发和使用。因此,使用 C 语言,我们既能享受到它作为高级语言时的自然语法和特性,又能够做到同低级语言一样,精确地控制程序的执行细节,甚至直接与硬件交互。

高效的运行时性能

很明显,使用 C 语言正确实现的程序可以享受到最高的运行时性能。而通过内联汇编,它甚至可以与直接编写的汇编代码相比肩。

需要注意一点:和其他语言不同,C 并未提供语言内置的诸如垃圾回收(GC)等可能导致额外运行时开销的特性。在提升了性能的同时,需要正确处理内存的分配与回收过程,以避免出现诸如内存泄露等问题。在这门课里,会为你介绍如何正确地编写 C 代码,来避免类似的问题。

学习 C 语言,为什么是修炼编程内功的必经之路?

这时候,有人可能会问了:我并不想做嵌入式、操作系统这些底层开发,平时的开发工作感觉 Java、Go 这些语言也够用了,为什么还要学习 C 语言呢?

答案是:即使你不使用 C 语言进行开发,深入学习 C 语言,也是你修炼内功、成为编程和计算机高手的必经之路。

为什么这么说呢?主要有三个原因。

[danger]第一,C 语言作为一门简单通用的早期编程语言,是 Go、Objective-C、C#、Java 这些高级编程语言在设计时所参考的“原型”语言。可以说,C 语言就是众多编程语言中的“九阳神功”,相信在你深入了解 C 语言后,再去学习其他语言,也会变得轻松许多。[/danger]

[info]第二,上面也提到过,C 语言是目前众多流行操作系统、编译器、上层实用软件与各类系统组件,乃至嵌入式开发所使用的源语言。因此,学习 C 语言也让我们具有了能够去探索优秀软件内部实现细节的能力,而这通常也是优秀工程师提升自我实力的一种快捷方式。[/info]

[warning]第三,C 语言的抽象程度非常低,是最适合用来帮助理解计算机系统底层运作机制的语言。在学习如何高效使用 C 语言的过程中,你将会学习到有关高速缓存、内存、寄存器,以及函数调用等相关的内容,而这无疑对你提升自身实力有着巨大的帮助。总之,深入学习 C 语言之后,我们就拥有了从“更低纬度”理解计算机运作机制的能力。[/warning]

网上流传的一个不太恰当的比喻是:学习 C 语言正如我们通过学习营养学、健康学来为自己合理地制定饮食计划。当然我们也可以选择直接购买市面上已经装配好的各类营养餐品,但当身体状况并没有按预期发展时,我们并不清楚问题出在哪里。并且,当需要实现一些特殊的定制化需求时,可能市面上的产品功能总会与我们的目标有所出入。

编程也是如此,相较于直接使用诸如 Python、Java 等高抽象粒度的编程语言,学习 C 语言能够让你从基础层面了解程序是如何工作的。理解了计算机系统的底层运作机制,你在设计更复杂、性能更高的程序时,便能够得心应手、融会贯通。

所以,要想深入理解计算机系统的运行原理,学习 C 语言是一个必经之路。

这门课涉及的内容都是基于 x86-64 平台下的 Linux 系统进行介绍的。当然,对于 macOS和 Windows 系统来说,某些细节会有所不同,但基本原理是相通的。

时至今日,C 语言作为最“古老”的编程语言之一,仍然“老当益壮”、生生不息。这一切靠的不是巧合,而是绝对的实力。而要发挥 C 语言的最大威力,我们就不应该只简单了解它的语法,而应该在此基础上进一步了解代码如何被编译,程序如何被运行。只有当完整的“链路”建立在脑海中时,你才对程序有了最完全的把控。