在众多高级编程语言中,C 语言历史悠久,且生命力旺盛,系统开发和应用开发兼具,是信息技术发展的一把利器。这里简单介绍一下 C 语言的发展及其对跨平台开发的影响。
C语言
C 语言是在 1969 到 1973 年间,由贝尔实验室的 Dennis Ritchie 最初为重写 unix 操作系统而开发的,它成功替代了汇编语言开发操作系统的模式,随后得到了广泛飞速的发展。由于几大流行操作系统的内核(Linux、Windows 等)都是由 C 开发的,所以称之为系统编程语言,其能力不局限于系统开发。常见的高级编程语言或脚本语言,像 Java、Python、Perl 和 PHP 等都是应用类编程语言,对开发人员来说,由这些语言编写的代码,不存在运行平台的问题,很多高级语言也是由 C 来编写的。
而与众多流行的高级编程语言相比,C 语言是一种与平台真正相关的编程语言(C++ 可以认为是 C 的超集)。编译工具将 C 源代码翻译成某种机器指令集的二进制程序,这种程序只能在相应的操作系统和硬件平台上运行。Java 程序则仅需一次编译,就可到处运行,与具体的硬件平台无关,唯一条件就是该平台上得有 java 虚拟机。
跨平台开发
跨平台开发,是指一套代码(或者一种业务)在多个平台上运行的编程方式,也是一种开发技巧。平台就是业务运行的环境,Windows、Linux 和 Unix 等就是最典型的计算机操作系统平台,还有像浏览器 IE、Chrome 和 Firefox 等是一类应用平台;这些 “平台” 也有自己的运行 “平台”,Windows 可以运行在 x86、amd64 和 arm 等硬件平台上,Linux 可以跑的更多;这里讨论的平台指操作系统,涉及的平台分 Windows 和 Unix-like。各种 Unix 和各种 Linux 视为同宗,Portable Operating System Interface (POSIX) 这套规范在 Unix-like 上表现的较为一致,Windows 上也有支持,但其上的 Win32 API 功能更为丰富。
跨平台开发当然是为了满足业务发展的需要而进行的,当你的软件在 Windows 上已运行良好,但随 Linux 市场的兴起,你不得不开发 Linux 上的产品,在 Linux 平台上重造一个 “轮子”,业务与 Windows 上运行的软件没有差异,只是换了个平台而已。由于平台的差异,操作系统提供的接口不同,开发人员根据不同的系统调用实现相同的业务需求。在开发过程中,自然而然地出现一种抽象层,将业务和运行平台进行分离。
像 Java 这样的高级语言可以算是高级抽象,使用这些应用类语言来编写软件不用考虑平台,只需关注业务,这是一种比较常用的开发模式。这样似乎没有必要使用 C 来做应用开发,但在实践当中,许许多多的基础部件:数据库 MySql、WEB 服务器 Apache 等都是 C 来开发的,因为 C 开发的软件开销少、运行效率高。
跨平台问题
C 语言本应该是跨平台的,几乎每个平台都原生支持 C 开发环境。由于 C 编译器实现的差异性和操作系统的多样性,导致用 C 开发应用时存在跨平台运行问题。
有必要说一下 C 语言的几个主要标准的进化
- K&R C
经典 C,事实标准,许多编译器的最低标准要求 - C89
标准 C,大部分 C 代码都是 C89 兼容的 - C99
引入了非常多的新特性,有较多的 c 编译器提供支持,gcc 就支持的很好,但微软公司对这个标准不那么热心,其集成开发工具 Visual Studio 2013 才开始比较良好地支持 C99 特性,这也成了软件从 Linux 系统移植到 Windows 平台的一个障碍。
新特性有:
- 宏定义支持取可变参数 #define Macro(…) _VAARGS
- 使用宏定义时,允许省略参数,被省略的参数会被扩展成空串
- 增加了内联函数
- 支持不定长的数组,即数组长度可以在运行时决定,比如利用变量作为数组长度。声明时使用 int a[var] 的形式。
- 变量声明不必放在语句块的开头,随用随定义;for 语句常写成 for(int i=0;i<100;++i) 的形式,即 i 只在 for 语句块内部有效;微软的一些编译器不支持这样的书写方式。
- 允许在 struct 的最后定义的数组不指定其长度,写做 type name[] 的形式,主要用在不定长结构体的定义中,这个特性在应用中较为常见;
结构定义
struct vectord {
size_t len;
double arr[];
};
这样使用
struct vectord *vector = sizeof(struct vectord) + array_len*sizeof(double);
vector->len = ...;
for (int i = 0; i < vector->len; i++)
vector[i] = ...
- 初始化结构的时候允许对特定的元素赋值,形式为:
(微软的一些编译器同样不支持。)
struct test{int a[3],b;} foo[] = { [0].a = {1}, [1].a = 2 };
struct test{int a, b, c, d;} foo = { .a = 1, .c = 3, 4, .b = 5 };
- 其他标准
C11 等,如果是跨平台开发,似乎可以无视最近标准引入的新特性了。
我们在用 C 进行开发时,尽量使用 C89 标准和部分 C99 特性,在需要依赖操作系统平台特性时,通过宏来控制相应平台上的特殊代码——
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <unistd.h>
#if defined(unix)
#include <sys/param.h>
#endif
#endif
#if defined(_WIN32)
#elif defined(_AIX)
#include <fcntl.h>
#include <sys/procfs.h> ...
#elif defined(linux) ...
#elif defined(__sun) && defined(__SVR4)...
#endif
宏定义是 C 语言的一个特色,功能很多;可以利用它针对特定平台编译特定代码,其他平台的代码不会编译连接到执行文件中,这样产生的程序规模就会小很多,同时产生了平台依赖。而 java 程序,如果要执行特定平台的业务,需要在运行时来检查当前运行的环境,再来做出选择。
当然,我们是站在巨人肩膀上进行软件开发的,不用亲自实现每项功能,在开源世界里有许许多多通用的、成熟的工具库可以使用。
- NSPR (NetScape Portable Runtime)
它为非 GUI(图形界面) 开发提供了一套平台独立的系统工具库,涉及的内容包括:
NSPR 的目标是在各个操作系统环境提供统一的 API,它不是努力输出各个操作系统的最广泛特性,而是提供最优解或者说是最佳实践,这些功能是现代操作系统的共有特性。如果出现新的操作系统,将 NSPR 移植到新平台的成功率是非常高的,主流系统 NSPR 均有支持。浏览器 Firfox 就用到了它。
该库虽历史悠久,但生命力强盛。接口设计的比较稳定,具有很好的二进制兼容性。- 线程
- 线程同步
- 文件和网络 IO
- 时间
- 内存管理
- 共享库处理
- APR(Apache Portable Runtime)
Apache 的跨平台库,除了基本的操作系统抽象外,还提供了比较丰富的工具。 - OpenSSL
网络安全通讯库 - libcurl
客户端网络通信开发库,支持非常多的网络协议,HTTP(S)、FTP(S)、POP3、SCP 和 SMTP 等等。
很多工具库首先以 C(或 C++) 的形式出现,然后再为其他高级语言提供功能扩展。
跨平台开发,除了语言层面上的,还有编译工具链的问题,涉及如何建立工程文件,使用什么编译器等等。CMake 系统可以帮助解决跨平台工程文件构建问题,先为平台生成对应开发环境的工程文件,再由平台上的编译工具进行编译;为可以生成 visual studio 工程文件,也可以为 Unix-like 系统生成 Makefile。
小结
C 语言既可进行操作系统开发,也可进行应用开发,适用范围广泛,对 C 开发人员来说,想象力限制了开发能力。但它不是马斯洛大锤,所要解决的问题也不都是钉子。在实践中,需要在软件运行速度和开发效率等问题上取得平衡。(徐品华 | 天存信息)