截至 2023 年,我个人的 C 编程风格

【CSDN 编者按】今年C语言技术有了突破性进步,对作者影响较深,所以作者决定记录下当前的状态和个人理由。

原文链接:https://nullprogram.com/blog/2023/10/08/

未经允许,禁止转载!

作者 | Chris Wellons       责编 | 弯月
责编 | 夏萌出品 | CSDN(ID:CSDNnews)

今年,我的 C 语言技术有了突破性的进步,技术范式的转变促使我重新思考个人的编程习惯以及风格。这是多年来我的个人风格转变最大的一次,所以我决定记录下当前的状态和我个人的理由。这些变化对生产力和组织利益都产生了很大影响,虽然大多数都是主观看法,但也包括一些客观的改进。本文记录的是对我个人来说最有效的编程风格,我并不是说每个人都应该这样编写 C,在向某个项目贡献代码时,我依然会遵循他们的风格。

原始类型

首先,我们来看看基础知识,对于原始类型,我一直使用短名称,因为短名称可以让代码更加清晰,而且更加方便审查。这些名称在程序中频繁出现,因此简洁是有好处的。另外,后缀 _t 更加容易造成视觉上的注意力分散,因此我已经不使用了。

有些人更喜欢 s 开头的带符号类型。但我更喜欢前缀 i,我保留了 s 用于其他用途。在指定大小时,使用 size 会更加统一,不会占用前缀,而且重要的是表示大小的值应该是有符号的,所以我提供了特殊的名字。usize 的用法很特别,主要用于与需要无符号大小值的外部接口进行交互。

b32指的是“32 位布尔值”,意思很明确。我本可以使用 _Bool,但我还是希望使用字母加大小的方式,并远离一些奇怪的语义。对于初学者来说,使用 32 位的布尔值似乎是在“浪费内存”,但实际上并非如此。布尔值会存储在寄存器中(作为返回值、局部变量时),或者会被补齐(作为结构的字段时)。在确实需要注意布尔值大小的情况下,我会将布尔值打包到变量 flags 中,但 1 个字节的布尔值一般不会引发内存问题。

UTF-16看似很少使用,但在 Win32 下会带来许多问题,因此经常需要使用 c16(“16 位字符”)。虽然uint16_t 的效果也是一样的,但在“类型层次结构”中包含 chat16_t 的名称可以给调试器提供信息,特别是GDB,可以用来表示这些变量保存的是字符值。Win32 本身有一个名为 wchar_t 的类型,但我喜欢明确 UTF-16 的使用。

u8表示八位字节,一般用于处理 UTF-8 数据。它与 byte 不同,后者代表原始内存数据,是一种特殊的别名类型。理论上,它们可以是不同的类型,具有不同的语义,但据我所知,目前没有任何实现这么做。目前看来,不同的名字只是表明用途不同。

至于那些不支持固定宽度类型的系统,它们只有学术意义,不值得浪费太多时间支持。这包括 int_fast32_t 之类的类型。几乎没有任何软件能在这种系统上正常工作,我很确定没人测试过,所以似乎也没人关心它们。

我不会在单独的代码中使用这些名字(比如除了本文之外的代码片段等)。如果要用这些名字,就必须写出 typedefs 给读者一些额外的信息。不值得花费额外的精力去解释这些。即使在我最新的文章中,我也用了 ptrdiff_t 而不是 size。

接下来是我的“标准”宏:

虽然我坚持常量采用全大写,但对于看起来像函数的宏,我还是采用了小写,因为这样更容易阅读。它们不像其他宏定义有命名空间的问题,比如我可以同时有一个宏 new() 和一个变量或字段 new,后者看起来完全不像函数调用。

对于 GCC 和 Clang,我最喜欢的assert 宏如下:

除了通常的优势之外,它还有如下特性:

  • 不需要为调试构建和发行构建分别定义。由“未定义行为检查器”(UBSan)的存在性控制,后者仅存在于调试构建中。

  • libubsan 提供了诊断输出,自带文件名和行号。

  • 在发行构建中,它会变成优化提示。

如果想在发行构建中启用断言,可以通过 -fsanitize-trap 将 UBSan 设置为陷阱模式,然后启用 -fsanitize=unreachable。理论上这也可以通过-funreachable-traps 实现,但在本文撰写时,该方法无法在最近的 GCC 版本中使用。

参数和函数

不要使用 const。它对于优化没有任何作用,而且我不记得它曾经捕获过任何错误。在写原型文档的时候我用过一阵子,但回顾后发现,好的参数名就足够了。去掉 const 可以更整洁,从而提高生产力。我相信C语言中加入 const 是一个昂贵的错误。

(一个小小的例外:我依然会在静态表上使用 const,以提醒自己这是靠近代码的一段只读内存。如果有必要我会使用 const。这一点的重要性很低。)

空指针使用 0。短小精悍。这不是什么新技巧,我已经用了七年之久,之前所有的文章都提过这一点。理论上,在一些极端情况下这会引发问题,而且相关讨论也有很多,但我编写了十万行代码也没遇到过这种极端情况。

如有必要就使用 restrict,但最好是精心组织代码、避免使用 restrict,也就是说不要在循环中写“输出”参数,或者不要使用任何输出参数。我也不使用 inline,反正所有代码都是作为一个单元进行转换的。

所有结构都要 typedef。以前我不想使用,但能够省略关键字 struct 的确会提高代码简洁性。如果是递归结构,可以紧挨着使用前向定义,这样字段就可以使用较短的名字:

除了入口点之外,所有函数都定义成 static。同样,既然所有代码都编译成一个转换单元,那就没有理由不这样做。C 语言没有默认 static 很可能是个错误,不过我并不是太在意这一点。通过短类型名、无 const、无 struct 等手段,函数及其返回值类型可以更容易地写在同一行中。

有时我在其他文章中会省略 static,因为在完整的程序的语境之外,写不写 static 无所谓。但在本文中我不会省略 static,以强调这一点。

有一段时间,我坚持将类型名首字母大写,从而将其命名与函数和变量区分开,但后来就不这样做了。也许以后会尝试其他方式。

字符串

今年对于生产力提升最大的一个变动就是完全放弃使用以零结尾的字符串。这是C语言的另一个糟糕的错误。我开始使用如下 string 类型:

我曾用过几个不同的名字,但最喜欢这个。s 表示字符串,8 表示 UTF-8,或 u8。s8 宏(有时简写为 S)包裹一个 C 字符串字面量,然后生成一个 s8 字符串。s8 的处理方式类似于富指针,通过复制来传递或返回。与 str 相比,s8 非常适合作为函数名前缀,而 str 已经被许多库函数用作前缀了。一些示例:

和宏结合使用:

你也许想用可变长数组,并把大小和数组放在一起。我试过了。非常不灵活,完全不值得这么做。例如,从字面量创建字符串以及使用字符串都会很麻烦。

有时候我会想,“这个程序太简单了,不需要字符串。”但这种想法几乎总是错的。有了字符串,我就会更清楚地思考,也能更好地思考简单的程序。(C++ 多年前就有了 std::string_view 和std::span。)

此外,还有一个 UTF-16 版本的 s16:

我并不太确定应该把 u 放在宏内还是写在字符串字面量上。

更多结构

另一个改变是,在返回值中,使用结构来代替参数。实际上就是多返回值,只不过没有解构而已。这是一个巨大的组织性变更。例如,如下函数返回了两个值,一个解析后的结果,一个状态:

那“额外的复制”怎么办?别怕,因为在没有inline 的情况下,这种调用会实际上变成一个隐藏的、带有 restrict 的输出参数,所以不会有额外的考校。使用这种返回方式,我不需要用特殊值(比如null)来表示错误,所以可以更清晰。

这也导致了一种在函数开头定义零值返回值的编程风格,即首先定义 ok 为 false,然后在所有return 语句中返回 ok 的值。这样出错时就可以立即返回,而成功的路径将 ok 设置为 true 再返回。

除了静态数据之外,我也不再使用初始化器,除了方便的零初始化器之外。(例外:s8 和 s16 宏)。这也包括特定的初始化器。我转而采用赋值进行初始化。例如下面的“构造函数”:

我认为这样的代码很容易阅读,而且还消除了一个认知负担:赋值是用点分隔的,有明确的顺序。上例中的顺序无所谓,但有时顺序很重要:

上例中,即使是同一个种子,e 也有六种可能的值。我不喜欢思考这种可能性。

其他

使用__attribute 代替 __attribute__。__后缀很罗嗦,且没必要。

Win32系统编程通常只需要一部分定义和声明,不用包含整个 window.h,所以我决定通过自定义类型手动写出原型。这样可以减少构建时间,避免污染命名空间,而且接口更干净(没有 DWORD/BOOL/ULONG_PTR,只有 u32/b32/uptr)。

至于行内汇编,可以把外层括号当作大括号,在开括号之前加一个空格,就像 if 语句一样,然后每行之间用冒号分隔:

我的编程风格还有更多值得介绍的地方,但除了上面这些,其他方面今年并没有太多变化。具体的示例可以参见小程序 wordhist.c(https://github.com/skeeto/scratch/blob/master/misc/wordhist.c)。

推荐阅读:

倒计时 10 天!与图灵奖得主、大模型掌门人齐聚,看 AI 正在如何重塑世界!

成本降20%!OpenAI被爆将出“杀手锏”,用更低的价格开发专属ChatGPT

▶仅用 26 秒!AI 设计了一款可行走的机器人,网友:AI 已“成精”,只是审美还不行!

相关推荐

  • 微软超 5000 亿“天价”收购完成;AI 耗电相当于一个国家年用电量;Ubuntu 23.10 镜像遭下架 | 极客头条
  • 阿里内部首发前端开发手册,完整版开放下载了!
  • 学会 arthas,让你 3 年经验掌握 5 年功力!
  • Spring Batch 批处理框架优化实践,效率嘎嘎高!
  • 深入理解Java注解的实现原理,注解的本质
  • 当自动驾驶遇上GPT-4V:L4要解决了?
  • 超级干货 | 数据平滑9大妙招
  • 【深度学习】计算机视觉领域如何从别人的论文里获取自己的idea?
  • 保姆级教程:不到30行代码上手讯飞版ChatGPT-API
  • 巧用 Redis,实现微博 Feed 流功能!
  • 微软全力拥抱 Java !
  • 使用CSS圆锥渐变创建背景图案
  • 让 web 再次伟大:用 CanvasKit 实现超级丝滑的原神地图(已开源)!!!
  • 不满意网上的Token无感知刷新方案,自己琢磨了个解决方案~
  • 当个 PM 式程序员「GitHub 热点速览」
  • 「原生案例」如何在JavaScript中实现实时搜索功能
  • 基于开源项目或云产品构建属于自己的私域知识库问答系统
  • 字节跳动李航:对语言大模型的若干观察和思考
  • 还有这操作?这都什么公司?
  • [开源]高性能、高吞吐量、高扩展性物联网平台,单机支持百万链接