C 语言的困境:为什么这些难题至今没有得到解决?
2025-01-21 08:45 阅读(116)

尽管标准 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.c
test.c:3:20: error: enumerator value for C is not an integer constant
enum 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.c
test.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.c
test.c:4:6: error: conflicting types for dex
char dex(char *s, int i) { return s[i]; }
^
test.c:2:35: note: previous implicit declaration of dex was here
int 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]; } // 定义

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

https://www.zuocode.com

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


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




导入声明问题


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

// floo.c
#include "dex.h"
int floo(int a, char *s) { return dex(s, a); }
// dex.h
char 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.c
char dexx(char *s, int i) { return s[i]; }

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


来源: https://www.csdn.net/