首先要明白的是,如今世界上很多大型的C语言程序项目,在几十年前就开始了。
Unix 操作系统的开发始于 1969 年,在 1972 年 Unix 开发团队使用C语言重写了它的几乎所有代码。实际上,创建C语言的一个很大的原因就是为了将 Unix 内核代码从程序集移动到更高级别的语言,这样可以用更简洁的代码量行执行相同的任务。
Oracle 数据库的开发始于 1977 年,当时的开发语言是 assembly, 不过在 1983 年开发团队使用 C语言重构了Oracle 数据库的代码,如今它已经成为世界上最流行的数据库之一。
1985年,Windows 1.0 发布。虽然 Windows 源代码不是公开的,但有人说它的内核大部分是用 C语言编写的,小部分使用了汇编。Linux 内核开发始于 1991 年,也是用 C 语言编写的,1992 年,它以 GNU 许可证发布,并作为 GNU 操作系统的一部分使用。GNU 操作系统本身是使用 C语言 和 Lisp 编程语言开发的,它的许多组件都是用 C语言编写的。
但是C语言编程并不局限于上面列举的这些几十年前就开始的项目,当时没有今天那么多的编程语言。事实上,许多C语言项目今天仍在不断进行。
C 是驱动世界科技的基石
尽管现在其他高级语言(如 java、python)看起来似乎更加流行,但C语言仍在世界科技的发展中,具有不可磨灭的作用。下面这些产品或者服务都离不开C语言,几乎每一个人每天都在使用。
微软的 windows 操作系统
就像上面说的,微软的 Windows 内核主要是用C语言实现的,只有极少部分是用汇编语言开发的。几十年来,windows 操作系统是世界上使用最多的操作系统,约占市场份额的 90%,要明白这离不开C语言编写的内核提供支持。
Linux
Linux 也主要是用C语言编写的,其中小部分使用了汇编语言。世界上前 500 名最强大的超级计算机中,大约 97% 运行Linux内核。当然了,现在 Linux 也用于许多个人电脑。
苹果的 Mac
苹果的 Mac 电脑的操作系统 OS X 内核也是由 C语言编写的。Mac 电脑中的每个程序和驱动程序,就和 Windows 和 Linux 电脑一样,可以说都是由 C语言程序提供动力的。
手机
iOS、Android 和 Windows Phone 内核也是用C语言编写的。手机上常用的这几个系统只是对现有 Mac OS、Linux 和Windows 内核做了一定的适应性移植,所以你每天使用的智能手机其实都是运行在C语言开发的内核上的。
数据库
现在世界上最流行的几大数据库,包括 Oracle 数据库、MySQL、MS SQL Server 和 PostgreSQL,基本上都是使用 C语言开发的(前三个同时使用了 C语言和 C++)。数据库广泛用于各种系统:金融、政府、媒体、娱乐、电信、卫生、教育、零售、社会网络、物联网等。
3D电影
3D电影通常是用 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语言也成为了开发人员之间交流的通用语言。C是一种很好的语言,可以用大多数人都能接受的方式表达编程中的共同想法。相当一部分编程语言都能看到 C语言的影子,因此即使一些程序员不了解C,C语言程序员也能与他们交谈。
内存操作
能够进行任意内存地址访问和指针算法,是C语言非常适合系统编程(操作系统和嵌入式系统)的一个重要原因。在硬件/软件边界,计算机系统和微控制器将其外围设备和I/O管脚映射到内存地址。系统应用程序必须读写这些自定义内存,才能与外界通信。因此,C语言处理任意内存地址的能力对于系统编程是必不可少的。
例如,微控制器的结构可以使存储器地址 0x40008000 中的字节在地址 0x40008001 的位 4 设置为 1 时由通用异步收发器(或 UART 串口通信,用于与外设通信的通用硬件组件)发送,并且在设置该位之后自动执行。这是通过该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 解释为指向一个字符的指针,然后对该指针(使用最左边的* 运算符)取消引用(给出指向的值),最后将字节值赋给该未引用的指针。换句话说:将变量 byte 的值写入内存地址 0x40008000。
下一行将扩展为:
*(volatile char *)0x40008001 |= 0x08;
在这一行中,我们对地址 0x40008001 处的值和 0x08(二进制形式为000001000,即位号4中的1)执行按位或运算,并将结果保存回地址 0x40008001。换句话说:这行C语言代码设置了地址为 0x40008001 的字节的第4位。程序还声明地址 0x40008001 处的值是 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 a.A() called. (Checkpoint 1)
{
B b; // Constructor b.B() called. (Checkpoint 2)
b.myMethod(); // (Checkpoint 3)
} // b.~B() destructor called. (Checkpoint 4)
{
C c; // Constructor c.C() called. (Checkpoint 5)
c.myMethod(); // (Checkpoint 6)
} // c.~C() destructor called. (Checkpoint 7)
a.myMethod(); // (Checkpoint 8)
} // a.~A() destructor called. (Checkpoint 9)
A、B 和 C 类的方法在其他地方定义(例如在其他文件中),编译器不能分析它们,也不知道它们是否会抛出异常。因此,编译器必须准备处理来自其任何构造函数、析构函数或其他方法调用的异常。
如果MyFunction中的任何调用引发异常,则堆栈展开机制必须能够为已构造的对象调用所有析构函数。堆栈展开机制的一个实现将使用该函数最后一次调用的返回地址来验证触发异常的调用的“Checkpoint num”。它通过使用一个辅助的自动生成函数(一种查找表)来实现这一点,如果从函数体中抛出异常,该函数将用于堆栈展开,类似于:
// 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();
}
}
如果从 Checkpoint 1 和 Checkpoint 9 抛出异常,则不需要销毁任何对象。对于 Checkpoint 3,必须销毁 B 和 A。对于 Checkpoint 6,必须销毁 C 和 A 。在任何情况下,都必须遵守销毁令。对于 Checkpoint 2、4、5、7 和 8,只需要销毁对象 A。
此辅助功能会增加代码大小,这是 C++ 增加的空间开销的一部分,许多嵌入式应用程序不能负担这个额外的空间。因此,嵌入式系统的 C++ 编译器常常有一个禁用异常的标志。禁用 C++中 的异常不是免费的,因为标准模板库很大程度上依赖于异常来通知错误。使用这种修改的方案,对C++程序开发人员的要求自然就比较高了。
对于其他语言来说,二进制大小的增加会使其他额外的开销变得更为糟糕,这些特性是非常有用但不能被嵌入式系统提供的。C语言没有提供这些额外特性的使用,因此它能够实现比其他编程语言更紧凑的代码。
为什么学习C语言
C语言不是一门难学的语言,如前所述,C语言是程序开发人员的通用语言。许多新算法在书中或互联网上的实现首先(或仅)由作者以C语言提供。这提供了最大可能的可移植性。我见过程序员在互联网上为把C语言算法改写成其他编程语言而苦苦挣扎,因为他们不知道C语言的基本概念。
C语言是一种古老而广泛的语言,因此可以在网上找到用C语言编写的所有算法,从各位前辈的经验中获益匪浅。
当我与同事讨论代码的某些部分或其他语言的某些特性的行为时,我们最终会“用C语言交谈”:这部分是向对象传递“指针”还是复制整个对象?这里会有 cast 吗?等等。
在分析高级语言的一部分代码时,我们很少讨论汇编指令。相反,当讨论机器在做什么时,用C语言基本上就能非常清楚地表达。许多有趣的项目,从大型数据库服务器或操作系统内核,到小型嵌入式应用程序,都是能以C语言为出发点深入了解的。
小结
光明会不会统治世界不知道,但C语言程序员一定会。C语言似乎永远不会过时,它与硬件非常接近,可移植性强,资源使用确定性强,是操作系统内核、嵌入式软件等低层次开发的理想选择。它的多功能性、高效性和良好的性能使它成为高复杂度数据处理软件(如数据库或3D动画)的最佳选择。
今天的许多编程语言在其预期用途上都优于C语言,但这并不意味着它们在所有领域都优于C语言。当性能是优先考虑的时候,C语言仍然是无与伦比的。
不管人们是否能够意识到,全世界都在使用C语言驱动给的设备。C语言从过去到现在都是人类科技不可缺少的,而且,对于软件开发的许多领域来说,C语言仍然是未来。