C 语言的困境:为什么这些难题至今没有得到解决?

B站影视 2025-01-18 22:14 2

摘要:然而,即便是广泛使用的标准 C 语言,至今仍存在一些令人困惑的问题亟待解决。

编程语言和编译器技术的发展,始终是推动软件工程进步的核心动力。

然而,即便是广泛使用的标准 C 语言,至今仍存在一些令人困惑的问题亟待解决。

在这篇文章中,我们将以编译器专家、D 编程语言的创造者和首位实现者 Walter Bright 的观点为切入点,探讨现代编译器技术如何弥补传统 C 语言的不足。

作者 | Walter Bright

翻译工具 | ChatGPT 责编 | 苏宓

出品 | CSDN(ID:CSDNnews)

尽管标准 C 语言在不断演进(当前版本为 C23),仍有一些令人费解的缺陷尚未修复。而此前 Dlang 社区在其 D 编程语言编译器中嵌入了一个全新的 C 编译器(称为 ImportC,https://dlang.org/spec/importc.html),以支持 C 代码编译。这一全新构建的编译器利用了现代编译技术,解决了一些传统 C 的问题。

问题来了:为什么标准 C 语言没有修复这些缺陷?譬如:

常量表达式的评估(Evaluating Constant Expressions)

编译时单元测试(Compile Time Unit Tests)

前向引用声明的问题(Forward Referencing of Declarations)

导入声明问题(Importing Declarations)

常量表达式的评估

来看以下 C 代码示例:

int sum(int a, int b) { return a + b; }enum E { A = 3, B = 4, C = sum(5, 6) };

使用 gcc 编译时会报错:

gcc -c test.ctest.c:3:20: error: enumerator value for C is not an integer constantenum E { A = 3, B, C = sum(5, 6) };^

通俗来说,虽然 C 语言能通过常量折叠(Constant Folding)在编译时计算出一个简单的表达式,但它无法在编译时执行函数调用。然而,ImportC 可以做到这一点。

建议改进:

在 C 语言语法中,凡是可以使用常量表达式的地方,编译器都应该能够在编译时执行函数,只要这些函数不涉及诸如 I/O 操作、访问可变的全局变量、进行系统调用等行为。

编译时单元测试

一旦 C 编译器具备了编译时函数求值(CTFE)的能力,许多新功能就变得可能。

在 C 代码中,很少见到单元测试,原因很简单:单元测试需要在构建系统中单独设置目标,并以独立的可执行文件形式运行。这种额外的复杂性导致开发者往往忽略它。也许你是一位每天坚持晨跑的人,同时认真设置单元测试的场景,但大多数开发者并非如此。

例如:

int sum(int a, int b) { return a + b; }_Static_assert(sum(3, 4) == 7, "test #1");

使用 gcc 编译时会报错:

gcc -c test.ctest.c:3:16: error: expression in static assertion is not constant_Static_assert(sum(3, 4) == 7, "test #1");^

然而,ImportC 可以成功编译这段代码。

潜在改进:

通过允许在编译时运行函数,C 语言可以轻松实现单元测试功能。无需额外的构建步骤或独立的测试可执行文件,每次代码编译时单元测试都会自动运行。例如,在 ImportC 的测试框架中,这种编译时测试得到了广泛应用。

前向引用声明的问题

在 C 语言中,函数的前向引用是一种常见的限制。以下代码展示了这个问题:

int floo(int a, char *s) { return dex(s, a); }char dex(char *s, int i) { return s[i]; }

使用 gcc 编译时会报错:

gcc -c test.ctest.c:4:6: error: conflicting types for dexchar dex(char *s, int i) { return s[i]; }^test.c:2:35: note: previous implicit declaration of dex was hereint floo(int a, char *s) { return dex(s, a); }

错误的原因是:编译器在解析 floo 时对 dex 的类型做了一个隐式假设(通常假设返回值为 int),但实际定义的类型与此假设不符。如果将 floo 和 dex 的顺序颠倒,这段代码就可以正常编译。这说明 C 编译器只能识别代码中“词法顺序上位于之前的声明”,而前向引用(即在定义之前调用函数)是不被允许的。

为什么这是一个问题?

是因为为了支持前向引用,每个函数都需要一个显式的声明。例如:

char dex(char *s, int i); // 声明int floo(int a, char *s) { return dex(s, a); }char dex(char *s, int i) { return s[i]; } // 定义

这种声明和定义的分离增加了无意义的工作量。

其次,这种限制迫使开发者调整代码顺序,比如将叶子函数(底层函数)放在最前面,而全局接口函数放在最后。这种代码组织方式类似于从报纸的底部开始阅读,非常不符合直觉。

ImportC 允许在任何顺序下编译全局声明,也就是说,开发者不必担心函数定义的前后顺序问题。

导入声明问题

在传统 C 编程中,处理多个文件模块时,通常需要单独创建 .h 头文件以声明模块接口。以下示例展示了常见的文件结构:

// floo.c#include "dex.h"int floo(int a, char *s) { return dex(s, a); }// dex.hchar dex(char *s, int i);// dex.c#include "dex.h"char dex(char *s, int i) { return s[i]; }

问题点:

1.繁琐:每个外部模块都需要一个单独的 .h 文件,这增加了开发者的负担。

2.易错:如果 .h 文件的声明和 .c 文件的定义不完全匹配,会导致各种难以排查的错误。

在 ImportC 中,直接导入模块的源文件即可,无需额外的头文件。例如:

// floo.c__import dex;int floo(int a, char *s) { return dexx(s, a); }// dex.cchar dexx(char *s, int i) { return s[i]; }

通过这种方式,开发者完全不需要编写 .h 文件,避免了繁琐的声明工作,同时减少了潜在的错误风险。

importC 文档:https://dlang.org/spec/importc.html

D 语言文档:https://dlang.org/

来源:CSDN

相关推荐