C语言存在的问题及Zig语言如何改进,差异对比全在这

张开发
2026/4/18 6:53:39 15 分钟阅读

分享文章

C语言存在的问题及Zig语言如何改进,差异对比全在这
C 是一种底层的系统编程语言几乎不存在对内存的抽象因而内存管理完全得依靠你自身对汇编的抽象同样很少可是足以支撑一些诸如类型系统等通用概念。它还是一种适应性极强的编程语言。要是编写得恰当哪怕你的厨房烤箱具备一些奇特的架构它也能够在其上运行。C语言因其设计特点故而非常适宜用于底层系统编程然而这并不表明其设计决策于当下标准而言毫无瑕疵在这篇博客里头我们会探讨一些C语言所存在的问题这些问题致使人们多次尝试去创建用于替代C语言的备选语言。被定为改进版C语言的新系统编程语言Zig引发了相当多的关注它是如何达成这一目的的呢在这篇博客里我们要研究一些和C语言有联系的问题还要探究Zig是怎样处理这些问题的。差异对比表Comptime 取代文本替换预处理把文本在源代码里用预处理器去替换这可不是C语言独有的在C语言诞生以前它就存在了在IBM 704计算机的SAP汇编器里早就有类似的例子了下面是一个AMD64汇编片段的例子它定义了一个pushr宏会依据其参数把它替换成push或者pushf。C语言作为汇编的那种最小程度的抽象运用了同样的办法去支持宏然而这非常容易引发问题下面是一个小例子。%macro pushr 1%ifidn %1, rflagspushf%elsepush %1%endif%endmacro%define regname rcxpushr raxpushr rflagspushr regname或许会期待这般代码把 result 的值设定成 (2 3)^2 25。可是呢鉴于 SQUARE 宏函数具备的文本替换特性展开之后的结果是 2 3 * 2 3 其计算得出的结果是 11 并非 25。要让这段代码可以正常运行极为关键的一点是保证所有的宏均被准确无误地添加上了括号。逗号。#define SQUARE(x) x * xint result SQUARE(2 3)C语言不会宽容这种错误也不可能温和地提示你这些错误错误依然有可能在程序的别的位置甚至是于后续的输入里出现。然而Zig用以处理此类任务的方法更为直观它引入了执行函数于编译时而非运行时的comptime参数和函数。以下即为Zig中的C SQUARE宏。fn square(x: anytype) TypeOf(x) {return x * x;}const result comptime square(2 3); // result 25, at compile-timeZig编译器的又一优点在于可针对输入开展类型检查哪怕其为anytype。于Zig内调用square函数之际要是运用了不支持*操作符的类型便会引致编译时的类型错误const result comptime square(hello); // compile time error: type mismatchComptime 允许在编译时执行任意代码const std import(std);fn fibonacci(index: u32) u32 {if (index 2) return index;return fibonacci(index - 1) fibonacci(index - 2);}pub fn main void {const foo comptime fibonacci(7);std.debug.print({}, .{ foo });}有一个Zig程序它对一个fibonacci函数做了定义之后在进行编译这个行为的时候调用了该函数去设置foo的值在运行的时候没有对fibonacci进行调用。Zig的编译器时计算也能够涵盖一些小型的C语言特性比如说于一个平台之上最小的有符号值竟是 -2的15次方等于-32768之数最大值乃是(2的15次方)-1等于32767这样的数值在C语言里面没办法把有符号类型的最小值写成一个字面常数的说。signed x -32768; // not possible in C这是由于于C语言里-32768在实际上是-1乘以32768然而32768并不处于signed类型的边界范围之内呢。可是呀在Zig中-1乘以32768属于一个编译时期进行的计算。const x: i16 -1 * 32768; // Valid in Zig内存管理与 Zig Allocator我曾经提到C语言对内存几乎没有抽象。这既有利也有弊有一种有着极大之力并也伴随着极大之责的一种情况。在诸如像C这样采取手动内存管理方式的计算机编程语言之下要是管理的状况不够妥善那么如此一来便说不定会带来相当严重的安全方面的问题。在这种情况下最好的情形或许仅仅只会致使服务变为抗拒执行然而最糟糕的情形则极有可能会让攻击之人得以去执行任意的代码。有许多种语言尝试通过施加编码所存在的限制或者运用垃圾收集器这种方式来规避这一问题。可是呢Zig却采用了不一样的方式。Zig 同时提供了几个优势Zig 不会如同 Rust 那般对编码方式加以限制帮你维持安全状态防止泄露情况发生然而依旧能让你如同在 C 里一样去自由行事我个人觉得这或许是一种便利的折中办法。const std import(std);test detect leak {var list std.ArrayList(u21).init(std.testing.allocator);// defer list.deinit; - 这行缺失了try list.append();try std.testing.expect(list.items.len 1);}上述 Zig 代码使用内置的 std.testing.allocator 来初始化一个 ArrayList并让你 allocate 和 free并测试你是否在泄漏内存zig test testing_detect_leak.zig1/1 test.detect leak... OK[gpa] (err): memory address 0x7f23a1c3c000 leaked:.../lib/zig/std/array_list.zig:403:67: 0x21ef54 in ensureTotalCapacityPrecise (test)const new_memory try self.allocator.alignedAlloc(T, alignment, new_capacity);^.../lib/zig/std/array_list.zig:379:51: 0x2158de in ensureTotalCapacity (test)return self.ensureTotalCapacityPrecise(better_capacity);^.../lib/zig/std/array_list.zig:426:41: 0x2130d7 in addOne (test)try self.ensureTotalCapacity(self.items.len 1);^.../lib/zig/std/array_list.zig:207:49: 0x20ef2d in append (test)const new_item_ptr try self.addOne;^.../testing_detect_leak.zig:6:20: 0x20ee52 in test.detect leak (test)try list.append();^.../lib/zig/test_runner.zig:175:28: 0x21c758 in mainTerminal (test)} else test_fn.func;^.../lib/zig/test_runner.zig:35:28: 0x213967 in main (test)return mainTerminal;^.../lib/zig/std/start.zig:598:22: 0x20f4e5 in posixCallMainAndExit (test)root.main;^All 1 tests passed.1 errors were logged.1 tests leaked memory.error: the following test command failed with exit code 1:.../testZig 内置的 Allocator 有哪些Zig 提供了几个内置的分配器包括但不限于Zig 还支持你自定义分配器。亿万美元的错误 vs Zig Optionals这段C代码会出现突然崩溃的情况除了一个SIGSEGV之外不会有任何线索这会让你陷入不知所措的局面struct MyStruct {int myField;};int main {struct MyStruct* myStructPtr ;int value;value myStructPtr-myField; // 访问未初始化结构的字段printf(Value: %d\n, value);return 0;}Zig不存在任何引用它具备将问号放在前面来表示的可选类型你仅能对其进行赋值归于这有着问号前导表现的可选类型而且唯有当你查证它们并非空值之时才能够援引它们运用orelse关键字或者单纯的if表达式即可达成此项不然的话便会引发编译错误。const Person struct {age: u8};const maybe_p: Person ; // 编译错误: 预期类型为 Person找到 Type(.)const maybe_p: ?Person ; // OKstd.debug.print({}, { maybe_p.age }); // 编译错误: 类型 ?Person 不支持字段访问std.debug.print({}, { (maybe_p orelse Person{ .age 25 }).age }); // OKif (maybe_p) |p| {std.debug.print({}, { p.age }); // OK}Zig 的技术保证指针运算 vs Zig Slice在C语言里地址是以一个数值予以表示的由此允许开发者针对指针开展算术运算这一特性致使C语言开发者能够借由操作地址去访问并修改任意内存位置。指针算术常常被应用于像操作或者访问数组那些特定的部分又或者是高效地遍历动态分配的内存块这类任务并且无需进行复制。然而因为C语言存在不宽容性这个情况指针算术很容易致使诸如段错误或者未定义行为等问题出现这就使得调试变成一种真正让人痛苦的事情了。大多数这类问题能够运用Slice予以解决Slice给出了一种更为安全、更为直观的方式用以操作以及访问数组或者内存区域var arr [_]u32{ 1, 2, 3, 4, 5, 6 }; // 1, 2, 3, 4, 5, 6const slice1 arr[1..5]; // 2, 3, 4, 5const slice2 slice1[1..3]; // 3, 4显式内存对齐任何一种类型都会存在一个对齐数此对齐数对该类型合法内存地址予以定义对齐是以字节作为单位的这确保了变量起始地址能够被对齐值整除举例来说CPU 强行执行这些对齐要求若一个变量的类型未正确对齐它有可能致使程序崩溃像段错误或者 illegal instruction 错误。当下我们经由一些手段特地于下面的代码里头去构建一个未处于对齐状态的 unsigned int 指针。这段代码在绝大多数 CPU 之上运行之际会出现崩溃状况int main {unsigned int* ptr;char* misaligned_ptr;char buffer[10];// 故意让指针未对齐使其不能被 4 整除misaligned_ptr buffer 3;ptr (unsigned int*)misaligned_ptr;unsigned int value *ptr;printf(Value: %u\n, value);return 0;}采用低级语言会引发些许挑战像是管控内存的对齐。一旦出现差错极有可能致使崩溃并且C不会为你实施检查。那么Zig又如何呢让我们用 Zig 写一段类似的代码pub fn main void {var buffer [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };// 故意让指针未对齐使其不能被 4 整除var misaligned_ptr buffer[3];var ptr: *u32 ptrCast(*u32, misaligned_ptr);const value: u32 ptr.*;std.debug.print(Value: {}\n, .{value});}要是你对上面的代码进行编译由于有着一个对齐方面的问题Zig就会报错进而阻止编译.\main.zig:61:21: error: cast increases pointer alignmentvar ptr: *u32 ptrCast(*u32, misaligned_ptr);^.\main.zig:61:36: note: *u8 has alignment 1var ptr: *u32 ptrCast(*u32, misaligned_ptr);^.\main.zig:61:30: note: *u32 has alignment 4var ptr: *u32 ptrCast(*u32, misaligned_ptr);^即便是你尝试借助一个显式的 alignCast 去哄骗 Zig在安全构建模式里的 Zig 也会于生成的代码当中增添一个指针对齐安全检查借此确保指针是以承诺的那种方式实现对齐的。故而要是运行时出现对齐错误它会凭借一条信息以及一个追踪来向你表明问题究竟出在何处。C 则不会pub fn main void {var buffer [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };// 故意让指针未对齐使其不能被 4 整除var misaligned_ptr buffer[3];var ptr: *u32 ptrCast(*u32, alignCast(4, misaligned_ptr));const value: u32 ptr.*;std.debug.print(Value: {}\n, .{value});}// 编译成功运行时你会收到main.zig:61:50: 0x7ff6f16933bd in ain (main.obj)var ptr: *u32 ptrCast(*u32, alignCast(4, misaligned_ptr));^...\zig\lib\std\start.zig:571:22: 0x7ff6f169248e in td.start.callMain (main.obj)root.main;^...\zig\lib\std\start.zig:349:65: 0x7ff6f1691d87 in td.start.WinStartup (main.obj)std.os.windows.kernel32.ExitProcess(initEventLoopAndCallMain);^酷毙了数组作为值C 语言的语义规定数组总是作为引用传递void f(int arr[100]) { ... } // 传递引用void f(int arr[]) { ... } // 传递引用C 语言的解决方案是创建一个 包装 结构体并传递结构体struct ArrayWrapper{int arr[SIZE];};void modify(struct ArrayWrapper temp) { // 使用包装结构体传递值// ...}而在 Zig 中这样就可以了fn foo(arr: [100]i32) void { // 传递数组值}fn foo(arr: *[100]i32) void { // 传递数组引用}错误处理好多C语言的API存在错误码这样子的概念也就是说函数的返回值用来表明成功状态或者是一个指示具体错误的整数。Zig同样采用相同的方式去处理错误不过是在类型系统里针对这个概念作出了更具用处以及更有表现力的改进。Zig里的错误集合好似一个枚举。然而整个编译过程中的每一个错误名都会被赋予一个大于0的无符号整数。错集类型与普通类型二者其可借由!运算符加以组合进而形成错联类型如FileOpenError!u16这般。此类型所具之值或为错误值又或为普通类型的值。const FileOpenError error{AccessDenied,OutOfMemory,FileNotFound,};const maybe_error: FileOpenError!u16 10;const no_error maybe_error catch 0;Zig 是有 try 和 catch 这两个关键字的然而它们跟其他语言里名为 try 和 catch 的东西并无关联这是由于 Zig 不存在异常这种情况。try x乃是x catch而当|err|出现时便返回err的简略写法一般是运用在并不适宜对错误进行处理的地方。总体而言Zig 的那种错误处理机制和 C 类似不过呢它是有着类型系统给予支持的。Zig 如何在运行时判断返回值是表示错误码还是实际输出!T 可以看作是struct {errorCode: GlobalErrorEnum, // u16result: T}“ok”情况被认定为是errorCode的属于0的那种情形。当一个函数返回的是!T的时候它事实上含有着两部分的含义一部分是u16 enum另一部分是T。Zig 错误的技术保证同一个错误名多次出现会被分配相同的整数值。const FileOpenError error {AccessDenied,OutOfMemory,FileNotFound,};const AllocationError error {OutOfMemory,};// AllocationError.OutOfMemory FileOpenError.OutOfMemory一切皆表达式假设你处在从别的高级语言转变至C语言的情况之下你大概会对某些诸如此类的特性怀有思念之情const firstName Tom;const lastName undefined;const displayName ( {if(firstName lastName)return ${firstName} ${lastName};if(firstName)return firstName;if(lastName)return lastName;return (no name);})Zig 的美妙之处在于可以把代码块当作表达式来使用。const result if (x) a else b;一个更复杂的例子const firstName: ?*const [3:0]u8 Tom;const lastName: ?*const [3:0]u8 ;var buf: [16]u8 undefined;const displayName blk: {if (firstName ! and lastName ! ) {const string std.fmt.bufPrint(buf, {s} {s}, .{ firstName, lastName }) catch unreachable;break :blk string;}if (firstName ! ) break :blk firstName;if (lastName ! ) break :blk lastName;break :blk (no name);};有的代码块能有一个标签像 :blk并且能够借由 break blk: 从那个代码块当中跳出进而返回一个特定的值。C 语言面临更复杂的语法处理看看这个 C 类型char * const (*(* const bar)[5])(int)如此声明了 bar 是成为一个常量指针其指向由 5 个指针所组合而成的数组而这些指针又指向一个函数该函数需要拿整数作为参数并最终返回一个常量指针该常量指针指向字符类型。不管这究竟属于何种含义。当然存在一些工具比如说cdecl.org它能够助力你去阅读C类型并且运用人类易于理解的语言来对其予以解释。我颇为确定对于实际从事C开发的人员而言处理这类类型或许并非那般艰难有些人在生来便具备阅读复杂语言的能力。然而对于像我这般喜爱简单明了的普通之人来讲Zig类型更便于阅读以及维护。一段有趣且合法的 C 代码inline int volatile long typedef _Atomic _Complex const long unsigned A;一段有趣且合法的 Zig 代码var x: *allowzero align(8) addrspace(.generic) const volatile u8 align(8)addrspace(.generic) linksection(unused_feature_section) undefined;结论这篇博客文章里我们探讨了一些C语言所存在的问题这些问题使得人们去寻觅或者创制替代方案。总而言之Zig 通过以下方式解决了这些问题感谢我的朋友 Thomas 对这篇博客进行了技术审查。本文的参考资料当你运用C语言之际可曾碰到过这般问题是否存在文中未曾提及的各类问题你有没有试着去使用Zig要是有的话你认为它的哪些特性是最为有用的欢迎于评论区去分享你的相关经验。

更多文章