经过这么多年,世界仍然由 C 编程驱动

已发表: 2022-03-11

今天存在的许多 C 项目都是在几十年前开始的。

UNIX 操作系统的开发始于 1969 年,其代码于 1972 年用 C 重写。创建 C 语言实际上是为了将 UNIX 内核代码从汇编语言转移到更高级别的语言,后者可以用更少的代码行完成相同的任务.

Oracle 数据库开发始于 1977 年,1983 年其代码由汇编改写为 C,成为世界上最流行的数据库之一。

1985 年发布了 Windows 1.0。 尽管 Windows 源代码不公开,但据说它的内核大部分是用 C 编写的,有些部分是汇编的。 Linux 内核开发始于 1991 年,也是用 C 语言编写的。第二年,它在 GNU 许可下发布,并被用作 GNU 操作系统的一部分。 GNU 操作系统本身是使用 C 和 Lisp 编程语言开始的,因此它的许多组件都是用 C 编写的。

但是 C 编程并不局限于几十年前开始的项目,当时还没有今天那么多的编程语言。 许多 C 项目今天仍在启动; 有一些很好的理由。

世界是如何由 C 驱动的?

尽管高级语言盛行,但 C 继续赋予世界权力。 以下是一些被数百万人使用并用 C 语言编程的系统。

微软Windows

微软的 Windows 内核主要是用 C 语言开发的,有些部分是用汇编语言开发的。 几十年来,世界上使用最多的操作系统,拥有大约 90% 的市场份额,由 C 语言编写的内核提供支持。

Linux

Linux 也主要是用 C 语言编写的,其中一些部分是汇编语言。 全球 500 台最强大的超级计算机中约有 97% 运行 Linux 内核。 它也用于许多个人计算机。

苹果

Mac 计算机也由 C 驱动,因为 OS X 内核主要是用 C 编写的。Mac 中的每个程序和驱动程序,就像在 Windows 和 Linux 计算机中一样,都在 C 驱动的内核上运行。

移动的

iOS、Android 和 Windows Phone 内核也是用 C 编写的。它们只是对现有 Mac OS、Linux 和 Windows 内核的移动改编。 因此,您每天使用的智能手机都在 C 内核上运行。

用 C 编写的操作系统内核

数据库

世界上最流行的数据库,包括 Oracle 数据库、MySQL、MS SQL Server 和 PostgreSQL,都是用 C 编码的(前三个实际上都是用 C 和 C++ 编写的)。

数据库用于各种系统:金融、政府、媒体、娱乐、电信、健康、教育、零售、社交网络、网络等。

由 C 驱动的数据库

3D电影

3D 电影是使用通常用 C 和 C++ 编写的应用程序创建的。 这些应用程序需要非常高效和快速,因为它们处理大量数据并每秒执行许多计算。 它们的效率越高,艺术家和动画师制作电影镜头所需的时间就越少,公司节省的钱就越多。

嵌入式系统

想象一下,有一天你醒来去购物。 叫醒你的闹钟很可能是用 C 语言编写的。然后你用微波炉或咖啡机做早餐。 它们也是嵌入式系统,因此可能是用 C 语言编程的。你一边吃早餐一边打开电视或收音机。 这些也是嵌入式系统,由 C 提供支持。当您使用遥控器打开车库门时,您也在使用最有可能用 C 编程的嵌入式系统。

然后你上你的车。 如果它具有以下特性,也可以用 C 语言编程:

  • 自动变速器
  • 胎压检测系统
  • 传感器(氧气、温度、油位等)
  • 记忆座椅和后视镜设置。
  • 仪表板显示
  • 防抱死刹车
  • 自动稳定控制
  • 巡航控制
  • 气候控制
  • 儿童锁
  • 无钥匙进入
  • 加热的座椅
  • 安全气囊控制

你到商店,停好车,然后去自动售货机买汽水。 他们用什么语言来编程这个自动售货机? 可能是 C。然后你在商店买东西。 收银机也是用 C 语言编写的。当你用信用卡付款时? 你猜对了:信用卡阅读器很可能是用 C 语言编写的。

所有这些设备都是嵌入式系统。 它们就像小型计算机,内部有一个微控制器/微处理器,在嵌入式设备上运行一个程序,也称为固件。 该程序必须检测按键并采取相应措施,并向用户显示信息。 例如,闹钟必须与用户交互,检测用户按下的是什么按钮,有时还要检测按下的时间长短,并相应地对设备进行编程,同时向用户显示相关信息。 例如,汽车的防抱死制动系统必须能够检测到轮胎的突然抱死,并采取措施在一小段时间内释放制动器上的压力,将其解锁,从而防止不受控制的打滑。 所有这些计算都是由编程的嵌入式系统完成的。

尽管嵌入式系统上使用的编程语言可能因品牌而异,但由于 C 语言具有灵活性、效率、性能和接近硬件的特性,它们最常使用 C 语言进行编程。

嵌入式系统通常用 C 语言编写

为什么仍然使用 C 编程语言?

今天,有许多编程语言允许开发人员在不同类型的项目中比使用 C 更有效率。 有更高级别的语言提供更大的内置库,可简化 JSON、XML、UI、网页、客户端请求、数据库连接、媒体操作等的工作。

但尽管如此,仍有很多理由相信 C 编程将在很长一段时间内保持活跃。

在编程语言中,一种尺寸并不适合所有人。 以下是 C 在某些应用程序中无与伦比且几乎是强制性的一些原因。

便携性和效率

C 几乎是一种可移植的汇编语言。 它尽可能地靠近机器,而它几乎普遍适用于现有的处理器架构。 几乎所有现有架构都至少有一个 C 编译器。 而如今,由于现代编译器生成了高度优化的二进制文件,用手写汇编来改进它们的输出并不是一件容易的事。

这就是它的可移植性和效率,“其他编程语言的编译器、库和解释器通常用 C 实现”。 Python、Ruby 和 PHP 等解释型语言的主要实现是用 C 编写的。它甚至被编译器用于其他语言与机器通信。 例如,C 是 Eiffel 和 Forth 的中间语言。 这意味着,这些语言的编译器不会为每个要支持的架构生成机器代码,而是只生成中间 C 代码,而 C 编译器会处理机器代码生成。

C 也已成为开发人员之间交流的通用语言。 正如 Dropbox 工程经理和 Cprogramming.com 的创建者 Alex Alllain 所说:

C 是一种很好的语言,可以以大多数人都习惯的方式表达编程中的常见想法。 此外,C 中使用的许多原则——例如,用于命令行参数的argcargv ,以及循环结构和变量类型——将出现在你学习的许多其他语言中,这样你就可以说话了对人们来说,即使他们不以你们俩共同的方式了解 C。

内存操作

任意内存地址访问和指针运算是使 C 语言非常适合系统编程(操作系统和嵌入式系统)的一个重要特性。

在硬件/软件边界,计算机系统和微控制器将其外围设备和 I/O 引脚映射到内存地址。 系统应用程序必须读取和写入这些自定义内存位置才能与世界通信。 因此,C 操作任意内存地址的能力对于系统编程来说是必不可少的。

例如,可以对微控制器进行架构,使得每次设置地址 0x40008001 的第 4 位时,通用异步接收器/发送器(或 UART,用于与外围设备通信的通用硬件组件)将发送内存地址 0x40008000 中的字节为 1,并且设置该位后,外围设备将自动取消设置。

这将是通过该 UART 发送字节的 C 函数的代码:

 #define UART_BYTE *(char *)0x40008000 #define UART_SEND *(volatile char *)0x40008001 |= 0x08 void send_uart(char byte) { UART_BYTE = byte; // write byte to 0x40008000 address UART_SEND; // set bit number 4 of address 0x40008001 }

函数的第一行将扩展为:

 *(char *)0x40008000 = byte;

这一行告诉编译器将值0x40008000解释为指向char的指针,然后取消引用(给出指向的值)该指针(使用最左边的*运算符),最后将byte值分配给该取消引用的指针。 换句话说:将变量byte的值写入内存地址0x40008000

下一行将扩展为:

 *(volatile char *)0x40008001 |= 0x08;

在这一行中,我们对地址0x40008001处的值和值0x08 (二进制中的00001000 ,即第 4 位中的 1)执行按位或运算,并将结果保存回地址0x40008001 。 换句话说:我们设置地址 0x40008001 的字节的第 4 位。 我们还声明地址0x40008001处的值是volatile 。 这告诉编译器该值可能会被我们代码外部的进程修改,因此编译器在写入该地址后不会对该地址中的值做出任何假设。 (在这种情况下,UART 硬件在我们通过软件设置它之后将其取消设置。)此信息对于编译器的优化器很重要。 例如,如果我们在for循环中执行此操作,但没有指定该值是 volatile,编译器可能会假设该值在设置后永远不会改变,并在第一个循环后跳过执行命令。

资源的确定性使用

系统编程不能依赖的一个通用语言特性是垃圾收集,甚至只是某些嵌入式系统的动态分配。 嵌入式应用程序在时间和内存资源方面非常有限。 它们通常用于实时系统,其中无法提供对垃圾收集器的非确定性调用。 如果由于内存不足而无法使用动态分配,那么拥有其他内存管理机制非常重要,例如将数据放置在自定义地址中,这是 C 指针允许的。 严重依赖动态分配和垃圾收集的语言不适合资源有限的系统。

代码大小

C 的运行时间非常短。 并且其代码的内存占用比大多数其他语言要小。

例如,与 C++ 相比,进入嵌入式设备的 C 生成的二进制文件的大小大约是由类似 C++ 代码生成的二进制文件的一半。 造成这种情况的主要原因之一是异常支持。

异常是 C++ 在 C 上添加的一个很好的工具,如果不触发和巧妙地实现,它们实际上没有执行时间开销(但以增加代码大小为代价)。

让我们看一个 C++ 的例子:

 // Class A declaration. Methods defined somewhere else; class A { public: A(); // Constructor ~A(); // Destructor (called when the object goes out of scope or is deleted) void myMethod(); // Just a method }; // Class B declaration. Methods defined somewhere else; class B { public: B(); // Constructor ~B(); // Destructor void myMethod(); // Just a method }; // Class C declaration. Methods defined somewhere else; class C { public: C(); // Constructor ~C(); // Destructor void myMethod(); // Just a method }; void myFunction() { A a; // Constructor aA() called. (Checkpoint 1) { B b; // Constructor bB() called. (Checkpoint 2) b.myMethod(); // (Checkpoint 3) } // b.~B() destructor called. (Checkpoint 4) { C c; // Constructor cC() called. (Checkpoint 5) c.myMethod(); // (Checkpoint 6) } // c.~C() destructor called. (Checkpoint 7) a.myMethod(); // (Checkpoint 8) } // a.~A() destructor called. (Checkpoint 9)

ABC类的方法在其他地方定义(例如在其他文件中)。 因此编译器无法分析它们,也无法知道它们是否会抛出异常。 因此它必须准备好处理从它们的任何构造函数、析构函数或其他方法调用中抛出的异常。 析构函数不应该抛出(非常糟糕的做法),但用户无论如何都可以抛出,或者他们可以通过调用一些抛出异常的函数或方法(显式或隐式)间接抛出。

如果myFunction中的任何调用引发异常,堆栈展开机制必须能够调用已构造对象的所有析构函数。 堆栈展开机制的一种实现将使用此函数的最后一次调用的返回地址来验证触发异常的调用的“检查点编号”(这是简单的解释)。 它通过使用一个辅助的自动生成函数(一种查找表)来实现这一点,该函数将用于堆栈展开,以防从该函数的主体中抛出异常,类似于以下内容:

 // Possible autogenerated function void autogeneratedStackUnwindingFor_myFunction(int checkpoint) { switch (checkpoint) { // case 1 and 9: do nothing; case 3: b.~B(); goto destroyA; // jumps to location of destroyA label case 6: c.~C(); // also goes to destroyA as that is the next line destroyA: // label case 2: case 4: case 5: case 7: case 8: a.~A(); } }

如果从检查点 1 和 9 抛出异常,则不需要销毁任何对象。 对于检查点 3, ba必须被破坏。 对于检查点 6,必须破坏ca 。 在所有情况下,都必须遵守销毁命令。 对于检查点 2、4、5、7 和 8,只需要销毁对象a

这个辅助函数增加了代码的大小。 这是 C++ 为 C 添加的空间开销的一部分。许多嵌入式应用程序无法承受这些额外空间。 因此,嵌入式系统的 C++ 编译器通常具有禁用异常的标志。 在 C++ 中禁用异常不是免费的,因为标准模板库严重依赖异常来通知错误。 毫无例外地,使用这种修改后的方案需要对 C++ 开发人员进行更多培训,以检测可能的问题或发现错误。

而且,我们谈论的是 C++,它的原则是:“不用为不用的东西付费”。 这种二进制大小的增加对于其他语言来说会变得更糟,这些语言会增加额外的开销,而其他功能非常有用,但嵌入式系统无法提供。 虽然 C 没有让您使用这些额外的功能,但它允许比其他语言更紧凑的代码占用空间。

学习 C 的理由

C 不是一门难学的语言,所以学习它的所有好处都会很便宜。 让我们看看其中的一些好处。

通用语

如前所述,C 是开发人员的通用语言。 书籍或互联网上的许多新算法的实现首先(或仅)由其作者以 C 语言提供。 这为实现提供了最大可能的可移植性。 我看到程序员在互联网上努力将 C 算法重写为其他编程语言,因为他或她不知道 C 的非常基本的概念。

请注意,C 是一种古老且广泛使用的语言,因此您可以在网络上找到各种用 C 编写的算法。 因此,您很可能会从了解这种语言中受益。

理解机器(用 C 语言思考)

当我们与同事讨论某些代码部分的行为或其他语言的某些特性时,我们最终会“用 C 语言交谈”:这部分是在传递指向对象的“指针”还是复制整个对象? 这里会发生任何“演员”吗? 等等。

在分析高级语言的一部分代码的行为时,我们很少讨论(或思考)一部分代码正在执行的汇编指令。 相反,在讨论机器正在做什么时,我们用 C 语言非常清楚地说话(或思考)。

此外,如果你不能停下来思考你正在做的事情,你最终可能会对如何(神奇地)完成事情产生某种迷信。

用 C 像机器一样思考

从事许多有趣的 C 项目

许多有趣的项目,从大型数据库服务器或操作系统内核,到小型嵌入式应用程序,您甚至可以在家里为您的个人满意度和乐趣进行操作,都是用 C 语言完成的。没有理由因为一个原因而停止做您可能喜欢的事情你不知道像 C 这样古老而小巧但功能强大且经过时间验证的编程语言。

使用 C 进行酷项目

结论

光明会不会统治世界。 C程序员做的。
鸣叫

C 编程语言似乎没有到期日期。 它与硬件的接近性、出色的可移植性和资源的确定性使用使其成为操作系统内核和嵌入式软件等低级开发的理想选择。 它的多功能性、效率和良好的性能使其成为数据库或 3D 动画等高复杂性数据操作软件的绝佳选择。 今天的许多编程语言在预期用途上都比 C 更好,但这并不意味着它们在所有领域都击败了 C。 当性能是优先级时,C 仍然是无与伦比的。

世界在 C 驱动的设备上运行。 无论我们是否意识到,我们每天都在使用这些设备。 对于软件的许多领域,C 是过去、现在,而且,据我们所知,仍然是未来。

相关:如何学习 C 和 C++ 语言:终极清单