Menu Close

什么是 C 语言?——C 能做什么,与操作系统有什么关系?

一个计算器程序和一个操作系统,看起来似乎没有多少共同点。前者用来计算数字,后者负责管理整台计算机,但它们都可以用 C 语言编写。

C 是一种编程语言,可以用来描述计算过程、组织数据,以及控制程序的执行逻辑。与此同时,它也与管理内存、文件、处理器和设备的底层软件有着密切联系。

为什么 C 能够胜任这些不同的任务?如果操作系统本身就是用 C 编写的,另一个 C 程序又是如何在它里面运行的?

这篇教程将从基础讲起。你不需要有编程经验,也不需要事先了解操作系统的内部设计。

什么是 C 语言?——C 能做什么,与操作系统有什么关系?

1. 什么是 C 语言?

C 是一种通用编程语言。“通用”意味着它可以用来开发多种类型的软件,而不是只能完成某一种特定任务。

编程语言提供一套规则,让我们能够以人可以编写、软件工具可以处理的形式表达指令。程序员写下的这些文字称为源代码

使用 C,你可以描述各种操作,例如把两个数字相加、检查温度是否超过设定值、重复执行一项计算,或者处理文件中保存的信息。

C 程序通常通过函数来组织。函数是一段具有名称、负责完成特定任务的代码。例如,一个函数负责计算总金额,另一个函数负责显示结果。

把许多小函数组合起来,就可以构建较大的程序,让复杂任务更容易理解和管理。

C 是一种描述计算机应该做什么的语言。它本身不是操作系统,也不是处理器。

2. C 语言从哪里来?

20 世纪 70 年代初,丹尼斯·里奇(Dennis Ritchie)在贝尔实验室开发了 C 语言。C 的形成与 Unix 操作系统的发展密切相关。

早期 Unix 使用汇编语言编写。汇编语言使用符号名称来表示与特定处理器架构相关的机器指令。它能够让程序员细致地控制机器,但也会增加大型程序的开发难度,使程序更难适应不同的计算机。

C 提供了函数、数据类型和结构化的操作方式,使开发者能够更方便地编写系统软件,同时保留对底层资源的较强控制能力。

后来,Unix 的大部分代码被改用 C 编写。这使 Unix 更容易移植到不同的计算机上,不过,与具体硬件有关的适配工作仍然不可缺少。

C 和 Unix 一同成长,但 C 并不局限于 Unix。此后,它逐渐被用于许多其他系统和不同类型的软件开发。参考:丹尼斯·里奇撰写的 C 语言发展史

3. C 代码如何变成正在运行的程序?

处理器通常不会直接执行 C 源代码。它执行的是机器指令,也就是按照处理器能够识别的形式编码的操作。

一个 C 程序通常需要经过构建过程,才能运行。

首先,你在文本文件中编写源代码。C 源文件通常使用 .c 作为文件扩展名。

接下来,编译器把源代码转换成更底层的形式,通常会生成目标文件,其中包含机器代码以及后续处理所需的信息。

然后,链接器把所需的部分连接起来,并处理它们之间的引用关系。例如,某段代码调用了一个函数,链接器就需要帮助建立这个调用与函数实现之间的联系。有些库的连接工作也可能在程序加载或运行时完成。

最终得到的是可执行文件,也就是为特定运行环境准备好的程序文件。

当你启动一个普通桌面程序时,操作系统会把它加载到内存中,并为它准备所需的资源。随后,处理器开始执行程序中的机器指令。

编写程序、构建程序和运行程序,是不同的活动。 修改源代码之后,通常需要重新构建程序,修改才会体现在新的运行结果中。

4. C 语言能做什么?

你可以先用 C 编写一些小而实用的程序,例如温度转换器、计算器、猜数字游戏,或者统计文件中字数的工具。

同一种语言也可以用于规模大得多的项目。

C 可以用来开发处理数据、通过网络通信、管理存储信息或执行大量数值计算的程序。借助合适的库,它也可以支持图形、声音和交互界面。

是一组可以重复使用的代码。使用库中的函数,你就不必从头编写每一种操作。

例如,SQLite 是以 C 语言库形式实现的数据库引擎。数据库引擎是负责存储、查询和管理有组织数据的软件。应用程序可以借助 SQLite 管理自己的数据。参考:SQLite 官方介绍

因此,C 的用途远不止编写操作系统。它既可以用于用户直接操作的应用程序,也可以用于在后台提供支持的软件组件。

5. 为什么嵌入式系统经常使用 C?

嵌入式系统是安装在某种产品内部、负责完成特定任务的计算机系统,例如洗衣机控制器、温度监测设备或工业传感器

许多嵌入式系统使用微控制器微控制器是一种芯片,通常把处理器、内存以及连接外部设备的硬件接口集成在一起。

这类设备的内存、处理能力和供电条件可能比较有限。开发者往往需要清楚地了解程序如何使用这些资源。

C 能够帮助程序员组织数据、操作单独的二进制位,并使用与具体设备相关的接口。是一个二进制数字,取值为零或一。硬件经常使用一组二进制位来表示设置或状态信息。

例如,一个控制器可以不断读取温度传感器,把测量值与设定上限进行比较,并在需要降温时打开风扇。

C 语言负责表达判断和重复执行的逻辑,与具体硬件有关的代码则负责连接传感器和风扇。

掌握 C 是嵌入式编程的一部分,理解目标硬件则是另一部分。

6. 操作系统负责什么?

操作系统,英文简称 OS,负责管理计算机资源,并向程序提供可以使用的服务。

这些资源包括处理器时间、内存、存储空间以及连接到计算机的设备。

当多个程序同时运行时,操作系统会协调它们对处理器的使用。当程序需要内存或访问文件时,操作系统会管理相关请求,并执行相应的访问限制。

内核操作系统的核心部分,负责管理程序执行、保护内存以及协调设备访问等重要任务。

不过,操作系统并不只有内核。它还包括系统服务、工具等其他软件。有些操作系统还提供图形桌面环境。

理解这一点很重要,因为“编写操作系统”可能涉及许多不同的组成部分,而这些部分不一定全部使用同一种编程语言。

7. 为什么操作系统会使用 C 编写?

操作系统需要谨慎管理数据、与硬件交互,并高效执行大量频繁发生的操作。

C 提供了几种适合这类工作的能力。

它支持结构体,可以把相关的数据组织在一起。例如,操作系统可以用一个结构体保存某个正在运行的程序或某个已打开文件的相关信息。

它支持指针。指针是一种用于引用对象或函数的值。通过指针,代码可以访问已有的数据,而不必总是复制一份数据。

C 还提供二进制位操作,让程序员能够较细致地控制数据的表示方式和管理方式。

这些能力有利于开发者构建系统软件,并减少对复杂运行时机制的依赖。

不过,使用 C 并不意味着程序一定运行得快,也不意味着代码一定正确。性能还取决于程序设计、编译器、硬件和实际任务。有些硬件操作仍然需要汇编语言,或者编译器提供的特殊功能。

Linux 内核就是一个主要使用 C 编写的内核。它的官方文档也介绍了对 Rust 的支持,说明内核开发可以涉及多种语言。参考:Linux 内核编程语言文档

8. 每个 C 程序都会成为操作系统的一部分吗?

不会。程序使用什么语言编写,并不能决定它拥有什么权限。

一个使用 C 编写的计算器,通常只是普通应用程序。它不会因为内核也使用了 C,就自动成为内核的一部分。

现代桌面操作系统通常会把普通应用程序的执行与内核的特权执行分开。

用户态是普通应用程序使用的一种受限制的执行模式。内核态则允许操作系统的核心代码执行具有特殊权限的操作。

一个运行在用户态的 C 应用程序,不能随意读取其他程序受到保护的内存,也不能直接接管存储设备。

指针不会消除这些限制。如果程序尝试访问不允许访问的内存位置,操作系统可能会终止这个程序。

决定程序能力的是它如何被加载和执行,以及它拥有的权限,而不是它是否使用 C 编写。

9. C 程序如何使用操作系统服务?

假设一个 C 程序需要读取文本文件。

程序通常会调用库函数来打开文件并读取内容。库函数的实现再与操作系统配合,完成相关请求。

当需要内核协助时,执行过程会通过系统调用跨越受控制的边界。系统调用是程序向内核请求服务的一种机制。

内核检查请求,执行必要的操作,然后返回结果。

例如,内核可能会检查程序是否有权访问这个文件。如果请求失败,应用程序就需要处理这种失败情况。

并不是每个 C 函数都会调用内核。一个把两个数字相加或比较数值的函数,可以在不请求操作系统服务的情况下完成工作。

此外,一次库函数调用也不一定对应一次系统调用。库可能会先把数据暂存在内存中,把多个操作合并处理,或者不进入内核就完成部分工作。

C 负责描述程序的逻辑,库和操作系统接口则让程序能够使用这些逻辑之外的系统服务。

10. C 可以在没有操作系统的情况下运行吗?

可以。C 能够用于没有传统操作系统的环境。

这在小型嵌入式设备中很常见。设备通电之后,程序完成硬件初始化,然后不断执行预先设定的任务。

这种方式通常称为裸机编程,意思是程序下面没有通用操作系统提供管理和服务。

不过,必要的准备工作并不会因此消失。启动代码需要建立基本的执行环境,与硬件相关的代码则需要提供程序所需的设备访问能力。

一个在桌面电脑上输出文字的函数,不能被直接假定为也能在没有屏幕、没有操作系统输出服务的小设备上工作。

这也解释了为什么 C 可以用来编写操作系统执行编译后的 C 代码,本身并不一定需要一个已经存在的操作系统 它需要的是合适的执行环境,而系统开发者可以自行提供这个环境。

11. C 是高级语言,还是低级语言?

相对于汇编语言,C 属于高级语言。它允许程序员使用函数、具有名称的变量、条件判断和循环,而不必逐条编写处理器指令。

变量是用于保存数值的、有名称的对象。条件判断让程序根据测试结果选择行动。循环则用于重复执行一组操作。

与此同时,C 也让程序员接触到一些在其他语言中可能由系统自动处理的细节。例如,程序员需要关注数据大小、内存访问以及对象在什么时间内有效。

这就是为什么人们经常说 C“接近硬件”。

但这句话并不意味着每条 C 语句都会生成一条机器指令。编译器可能会转换、合并,甚至删除某些操作,只要最终仍然符合语言所要求的程序行为。

它也不意味着普通 C 程序可以绕过操作系统的保护机制。

C 提供了方便表达程序逻辑的抽象,同时也让程序员承担较多的资源管理和正确性责任。

12. 同一个 C 程序可以在不同计算机上运行吗?

很多时候,同一份 C 源代码可以经过适配或重新编译,用于不同系统。这种能力称为可移植性

例如,一个主要使用标准 C 功能进行计算的程序,可能只需要很少修改,就能在多个平台上工作。

但是,源代码的可移植性与可执行文件的兼容性并不是一回事。

为某种处理器架构或操作系统生成的可执行文件,通常不能被直接假定为也能在另一种环境中运行。你一般需要针对目标环境重新构建程序。

如果代码依赖某个操作系统特有的接口或特定硬件设备,还可能需要修改相关部分。

即使一些看似简单的假设也可能影响移植。例如,程序不能假定每个系统上的各种整数类型都具有完全相同的大小。

具有良好可移植性的 C 代码,需要遵守语言规则,并谨慎处理对外部系统的依赖。 可移植性需要开发者主动设计,不能仅仅因为程序使用了 C 就自动获得。

13. 使用 C 需要承担哪些责任?

C 给程序员较大的控制能力,但它不会自动阻止许多常见错误。

数组用于保存一组按顺序排列的元素。如果一个数组只能容纳十个元素,尝试访问第十一个元素就是错误。C 通常不会要求对每次数组访问都进行自动越界检查。

向一块内存缓冲区写入过多数据,超出它能够容纳的范围,可能造成缓冲区溢出。缓冲区是用于暂时保存数据的一块内存区域,溢出可能破坏其他数据,甚至产生安全漏洞。

C 也允许程序在运行过程中申请内存,这称为动态内存分配。程序员需要管理这块内存由谁负责,并在不再需要时释放它。

如果已经不再使用的动态内存没有被释放,就可能造成内存泄漏。而在释放之后继续访问那块内存,则是另一种严重错误。

某些错误会导致未定义行为,意思是 C 语言不再为这次执行规定可靠的结果。

对于初学者,最实用的做法是建立良好习惯:在使用之前正确初始化变量,遵守数组边界,检查操作是否成功,并认真对待编译器警告。

你不需要立即掌握所有内存概念,但随着程序逐渐复杂,这些知识需要一步一步补上。

14. 初学者应该从哪里开始?

你不需要一开始就编写操作系统,也不需要立即控制硬件。

先写一个能够显示文字的程序,然后学习保存数值、执行计算、根据条件选择行动,以及重复执行任务。

接下来,学习使用函数把程序分成容易理解的部分。等这些基础逐渐熟悉之后,再学习数组、文本、文件和指针。

温度转换器就是一个不错的入门练习。它有明确的输入、简单的计算过程,以及可以检查的输出结果。

之后,你可以逐步扩展它,让它处理多个温度值、拒绝不合适的输入,或者把结果保存到文件中。每次增加功能,都能在熟悉的任务中引入一个新概念。

学习 C 有助于你理解程序如何表示数据、使用内存,以及与外部系统交互。即使将来使用其他编程语言,这些基础仍然有价值。

结论

C 是一种通用编程语言,可以用于应用程序、可复用的库、嵌入式设备以及操作系统组件。

它与操作系统的关系有两个方面:开发者可以使用 C 构建操作系统本身,也可以使用 C 编写应用程序,向操作系统请求服务。

普通 C 应用程序仍然受到操作系统权限和保护规则的约束。在嵌入式环境中,只要具备必要的启动代码和硬件支持,编译后的 C 程序也可以在没有传统操作系统的情况下运行。

你不需要在编写第一个程序之前,就理解所有这些层次。先从简单任务开始,学习如何清楚地表达步骤,再逐步建立对程序和计算机系统的理解。

Posted in C语言教程