这些年来,世界仍然由C编程提供动力

本文概述

今天存在的许多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内核主要是用C语言开发的, 其中一些部分使用汇编语言。几十年来, 全球使用最多的操作系统(约90%的市场份额)一直由用C编写的内核提供支持。

的Linux

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

苹果电脑

Mac OS也由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电影

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

嵌入式系统

想象一下, 你有一天醒来购物。唤醒你的闹钟很可能是用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 Allain所说:

C是一种很棒的语言, 用于以大多数人都喜欢的方式表达编程中的常见想法。此外, C语言中使用的许多原理(例如, 用于命令行参数的argc和argv以及循环构造和变量类型)将以你学习的许多其他语言显示, 因此你可以进行交谈即使他们不以你们俩共同的方式了解C。

记忆操纵

任意存储器地址访问和指针算术是使C完美适合系统编程(操作系统和嵌入式系统)的重要功能。

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

例如, 可以设计一个微控制器, 以便每次设置地址0x40008001的第4位时, 通用异步接收器/发送器(或UART, 用于与外设通信的通用硬件组件)将发送存储器地址0x40008000中的字节。设置为1, 则在你将该位置1后, 外设将自动将其取消设置。

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

#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的指针, 然后使用该指针(使用最左边的*运算符)取消引用(给定指向的值), 最后将字节值分配给该取消引用的指针。换句话说:将变量字节的值写入存储器地址0x40008000。

下一行将扩展为:

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

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

确定性使用资源

系统编程不能依赖的公共语言功能是垃圾收集, 甚至只是对某些嵌入式系统的动态分配。嵌入式应用程序的时间和内存资源非常有限。它们通常用于实时系统, 在该系统中无法对垃圾收集器进行不确定的调用。而且, 如果由于内存不足而无法使用动态分配, 那么拥有其他内存管理机制(例如, 在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中的任何调用均引发异常, 则堆栈展开机制必须能够调用已构造对象的所有析构函数。堆栈展开机制的一个实现将使用此函数上次调用的返回地址来验证触发异常的调用的”检查点号”(这是简单的解释)。它通过使用辅助自动生成的函数(一种查找表)来实现此目的, 该函数将在该函数的主体引发异常的情况下用于堆栈展开, 该过程类似于以下内容:

// 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, 必须销毁b和a。对于检查点6, 必须销毁c和a。在任何情况下, 都必须遵守销毁顺序。对于检查点2、4、5、7和8, 只需破坏对象a。

此辅助功能会增加代码的大小。这是C ++增加到C的空间开销的一部分。许多嵌入式应用程序负担不起这些额外的空间。因此, 用于嵌入式系统的C ++编译器通常具有禁用异常的标志。在C ++中禁用异常不是免费的, 因为标准模板库在很大程度上依赖于异常来告知错误。无例外地使用此修改后的方案, 需要C ++开发人员进行更多的培训, 以检测可能的问题或发现错误。

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

学习C的理由

C语言不是一门难学的语言, 因此学习它的所有好处将变得很便宜。让我们来看看其中的一些好处。

林瓜·弗朗卡(Lingua Franca)寻找文字

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

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

理解机器(C语言思考)

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

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

此外, 如果你不能停下来对自己的工作方式进行思考, 那么最终可能会对程序的完成方式产生某种迷信。

用C像机器一样思考

在许多有趣的C项目上工作

许多有趣的项目都是用C语言完成的, 从大型数据库服务器或操作系统内核到小型嵌入式应用程序, 你甚至可以在家中为自己的满足和乐趣而做。没有理由停止做自己喜欢做的​​事情你不知道像C这样古老而又小巧但功能强大且久经考验的编程语言。

用C处理很酷的项目

总结

光明会并没有统治世界。 C程序员这样做。

鸣叫

C编程语言似乎没有到期日期。它与硬件的亲密性, 出色的可移植性和确定性的资源使用方式使其非常适合用于操作系统内核和嵌入式软件等底层开发。它的多功能性, 效率和良好的性能使其成为数据库或3D动画等高复杂度数据处理软件的绝佳选择。当今许多编程语言在预期用途方面都比C更好, 这一事实并不意味着它们在所有领域都胜过C。当性能为首要任务时, C仍不可超越。

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

相关:如何学习C和C ++语言:最终列表

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?