开篇词|时至今日,如何更好地拥抱现代 C 语言?

你好,我是于航,目前在 PayPal 做软件研发与技术管理工作。

也许一些同学对我比较熟悉,我之前曾在极客时间开设过一门《WebAssembly 入门课》,并且还是多个 WebAssembly、C++ 相关每日一课视频的作者。而今天,我又设计了一门新课,想要带你从不同的视角来学习 “C” 这门语言。

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

作为 WebAssembly 技术的研究者和使用者,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 语言有着其自身所适合的应用场景。确实,我们无法使用它来编写 Web 应用,也无法使用它来高效快捷地构建深度学习应用。但你要知道的是,用于支持这些应用正常运行的系统组件,乃至操作系统内核,都是使用 C 语言编写的。现如今,这个世界上几乎所有重要的软件都与 C 有着直接或间接的关系。

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

图片

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

精确控制程序

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

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

图片

在某些特殊场景下,我们甚至可以直接在 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 语言,也是你修炼内功、成为编程和计算机高手的必经之路。

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

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

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

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

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

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

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

这门课是怎么设计的?

为了达到灵活、高效地使用 C 语言,并借此深入理解计算机系统运行原理的目的,只掌握 C 语言的基本语法是远远不够的。我们还需要深入到 C 语言的内部,去了解⼀个 C 程序从编写到编译,再到被运⾏的整个流程细节。只有做到“知其然”并“知其所以然”,方能运用自如,百战不殆。

为了做到这一点,我们将从 C 语言的核心语法开始,先来了解编译器是如何在机器指令层面实现它们的。紧接着,我们会把目光移到标准库。标准库是扩展 C 语言功能的一大利器,我们将会介绍现代 C 语言标准库中的一些重要功能,以及这些功能背后的运作机制。除此之外,如何利用计算机体系结构来编写高性能的 C 代码,也是工程化相关的重要内容。然后,随着 C 代码被编译,我们将会探讨二进制可执行程序是如何在与操作系统的协同工作下被运行的。经过这几个步骤后,你将会对一个 C 程序的完整生命周期有着更深刻的理解。

图片

基于这个思路,这门课主要分为四个模块。

第一个模块是“前置篇”,我将为你讲解一些学习这门课所需要的基础知识。我们的课程中涉及到了有关计算机体系结构、汇编语言等较为底层的内容,因此我为你设计了一讲“课前热身”,向你简单介绍与基本数据量单位、汇编语言,以及指令集寄存器有关的内容。在这一模块中,我还会用一段相对复杂的代码作为例子,来带你回顾 C 语言的核心语法,并介绍 C 程序从编写到运行的基本步骤。

第二个模块是“C 核心语法实现篇”。我会梳理 C 语言 7 大核心语法“背后的故事”,带你了解编译器如何在汇编层面实现这些语法。学完这个模块,你会对 C 程序的运行细节有着更深刻的理解,从而更好地掌握并优化程序运行。

第三个模块是“C 工程实战篇”。在这个模块中,你会学习到 C 语言在大型工程实战中的必备技巧,主要包括:快速掌握 C 标准库的重要功能,以及这些功能背后的实现原理;掌握编写高性能 C 代码、编码规范、结构化测试、结构化编译这些 C 项目工程化的实用技巧。

第四个模块是“C 程序运行原理篇”。我会为你介绍一个 C 程序是如何通过编译,并最终被操作系统运行的。程序的运行涉及到众多与操作系统的交互细节,你将在这个模块里详细了解它们。

图片

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

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

精选留言

  • EC-hero

    2021-12-09 11:06:04

    老师,“如下图所示,这里,左右两个窗口中相同背景颜色的代码行,表示了 C 代码与其对应的汇编代码。可以看到,左侧 C 代码中第三行变量 x 的值被存放到了 ebx 寄存器中” 是用什么软件看的?
    作者回复

    是这个网站哈 https://godbolt.org/,可以调整主题。

    2021-12-09 14:32:31

  • cc

    2021-12-08 22:46:12

    想了解一些关于 「C 语言为什么设计成现在这样」的内容。

    之前学过 Java 和 Go,自己看了一段时间的 C 语言,比如对 C 语言的函数声明就觉得很难读,后来看了一些资料,只要掌握“声明的语法和使用的语法类似”这一点,就比较容易看懂函数声明了。

    最近还在继续学 C 语言,遇到的一个困惑就是,为什么 C 语言需要有头文件,而其他接触到的语言都没有这个概念
    作者回复

    为什么 C 语言会有头文件这种设计?你可以这样简单理解:对于一个大型 C 项目,如何做到可以多人协作,分别编译,然后再把各自编写好的产物汇聚到一起,生成最终的可执行文件?我们通常会将一个模块可以对外使用的接口以原型的方式定义在头文件中,而将函数体实现隐藏。通过这种方式是不是就可以进行协作呢?但为什么其他语言没有借鉴类似的方式,这就说明这种方式并非一种好的设计。具体可以看看网上大家的讨论,比如这篇:https://softwareengineering.stackexchange.com/questions/233484/why-are-header-files-bad-design

    2021-12-09 20:15:41

  • 沉默王二

    2021-12-06 18:17:58

    好了,我来学习C语言了,永远滴神
  • Luke

    2021-12-16 20:59:49

    好早以前搞的os实验,看到那段at&t 汇编好亲切。
    老师的代码等价于:

    dst =src;
    dst = dst + 1;

    结果打印出来就是2

    %1表示第二个参数,$1是立即数1。
    rbp是栈的基指针,rsp是栈顶指针。
    但愿没记错,哈哈。
    作者回复

    正解!

    2021-12-18 11:41:01

  • 希望在南门

    2021-12-07 09:29:19

    感觉C不太容易学好
    作者回复

    其实也不太用纠结什么才是“学好”哈。作为语言,C 的语法就很简单很好学。但实际上,C 又由于应用的领域比较多,在写某些项目的时候又需要了解很多领域知识(比如编译器、数据库、协议等等)。所以总体还是看你学习 C 语言的目的是什么。

    2021-12-07 16:22:34

  • Y

    2021-12-08 11:40:46

    使用C语言很多年, 一直向深入理解C和编译和运行底层原理,这个课程早上点就更好了
  • 卢承灏

    2021-12-07 09:10:03

    我正是因为学了go,所以我想学好C
  • Geek_5b2ab1

    2021-12-07 14:41:26

    老师你好,请问由c生成的汇编代码是平台无关的吗?之前学过arm汇编,对于arm汇编,有专门的pdf文件,讲解每条指令的作用。我看c生成的汇编代码好像不是arm汇编。
    那由c生成的汇编代码是基于什么指令集的呢,有没有什么文档可以查看每条指令的介绍?
    作者回复

    汇编代码本身就是平台相关的了,我们这门课中的汇编都基于 x86-64 指令集,你可以在这里找到有关这些指令的细节哈:https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html

    2021-12-07 16:17:51

  • 罗耀龙@坐忘

    2021-12-06 23:14:54

    茶艺师学编程

    来极客时间学的第一门语言就是C。

    现在出了这门课,闭着眼睛就冲进来了。
    作者回复

    感谢支持,一起学习!

    2021-12-07 13:01:24

  • 龍蝦

    2022-03-04 11:04:31

    老师,关于最近 Linux 5.18 将升级到 C11 的消息,能否详细解释下呢?
    这个升级,具体要如何实施呢?
    作者回复

    我大致看了一下那封邮件,主要原因是 Linus 希望使用“可以在 for 循环内部声明变量”,也就是 “for loop initial declaration” 这个特性。但这个语言特性只在 C99 或者 C11 下才支持。所以,才有了想要做升级的讨论。而之所以选择 C11,是由于编译器实现比较稳定,相较于 C99,也可以进一步利用更多的语言新特性。

    升级做法应该就是把编译器选项从 -std=gnu89 改成 -std=gnu11。详情可以参考邮件列表链接:https://lore.kernel.org/lkml/20220308215615.14183-4-arnd@kernel.org/

    2022-03-15 10:19:47

  • 杨宇

    2022-04-21 21:42:03

    看到C89、C11这种命名,想起了千年虫。到了2089年,会怎样?
    作者回复

    hhhh这是一个好问题,希望 2089 年的时候 C 语言还在。

    2022-05-05 11:39:11

  • Alan_Hwang

    2021-12-08 16:42:03

    于老师,散发着艺术的气息。我选C,和大家一起学C
    作者回复

    哈哈哈,感谢支持,一起学习!

    2021-12-08 23:37:59

  • 糊糊

    2021-12-07 17:43:23

    这次一定坚持下去,学懂
    作者回复

    小编给你加油!

    2021-12-07 19:42:49

  • 森林

    2021-12-07 07:54:18

    对于我们而言,C是基本功。
  • GO

    2022-02-13 01:29:25

    老师,我想问问,因为要对C语言的本质进行相应的分析,所以汇编语言也会相应的了解,那么这门课程讲的汇编语言会涉及到哪种程度呢?
    因为我虽然是计算机专业,但是学校不开汇编这门课程了,请老师解答,谢谢!
    作者回复

    实际上跟着课程来逐渐理解汇编语言就可以,虽然我们不会系统地讲汇编语言的相关知识,但实际在课程中用到的汇编指令也不是很多。遇到不太理解的可以再 Google 一下,不需要单独学习汇编的课程。

    2022-02-15 22:47:10

  • 杰良

    2021-12-14 08:16:01

    深入系统与芯片的精确控制能力,从而获得高效的运行性能。简洁的语法设计,也实现了精确控制的同时拥有高层次抽血的能力。
  • 再不睡觉就秃了

    2021-12-06 19:10:16

    作者回复

    冲鸭!

    2021-12-06 21:41:56

  • Geek_b80486

    2022-09-22 00:21:39

    个人拙见,之前一直在学习arm汇编,对asm()那段代码始终理解不了,原来linux/unix下内联汇编为AT&T格式,%0为dst,1%为src,mov 1% 0%是将将1%的内容赋给0%,即dst = src,在此处纠结了许久,查阅资料才搞清楚,后面的代码就容易理解了。
  • 代码界的小白

    2022-05-26 11:42:24

    小白要开始学习C语言了
    作者回复

    加油!

    2022-06-26 13:47:54

  • 2021-12-11 17:36:25

    汇编所做的事情:
    1.dst = src
    2.dst = src + 1
    所以输出 dst 为 2
    作者回复

    没错!

    2021-12-12 23:48:23