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

作者分享了今年在C语言编程方面的重大突破,包括使用短名称的原始类型、改进的宏定义、抛弃零尾字符串和使用结构作为返回值等,旨在提高生产力和代码组织性。

b34ee17e5a7d3a331fb704ff12160bb1.gif

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

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

未经允许,禁止转载!

作者 | Chris Wellons       责编 | 弯月

责编 | 夏萌

出品 | 优快云(ID:优快云news)

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

2d099d2bbf22a1f388b942e0ea6f90dd.png

原始类型

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

17109d055547981365467ce1b8ef4c46.png

有些人更喜欢 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。

0b94f3c31cd84d0c0e2a61001568034a.png

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

b8b96c8d586d5b3da811cf51ab40cf46.png

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

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

3f3089ee2133991e4befb3898141d4ff.png

除了通常的优势之外,它还有如下特性:
  • 不需要为调试构建和发行构建分别定义。由“未定义行为检查器”(UBSan)的存在性控制,后者仅存在于调试构建中。

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

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

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

a67594f0baf17efcbb17aa79cf3a53fd.png

参数和函数

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

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

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

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

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

09e41d25cf12fbd61b6f514b1123c801.png

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

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

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

f50c7b2aa2b949131b60d934a6bcd748.png

字符串

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

d74784531fd77ddf8956e3d3b1ce4a10.png

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

0fc74da9cdaf702464d6655ac3bb8762.png

和宏结合使用:

3edbe27cce832e00454eaebb7bc593fa.png

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

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

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

6e157792ad8f5a5051975ce646e0891f.png

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

5f6c1837e53f5d64bec58b7a8501563e.png

更多结构

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

02e1fe1bbfb16b282fd3af8f08f813c2.png

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

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

d9db8dfb9a5ff31a6ed120723dbd58bf.png

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

bed8220fca50dfd420f3cab15298e91b.png

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

26b5fe2cc5908f3698e8b7f166918736.png

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

bc0dc33bae2ecec2e179b6f1bb5765ed.png

其他


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

2567717c877ee5d410662aa193e3a3c9.png

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

3b8cfdc18e6335f3f6ce0fa303d81104.png

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

f67734cdda45d8a3e73096acfbb6d3a6.png

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

推荐阅读:

那一次,我拯救了一家公司,结果只拿到了 625 美元的奖金”

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

一周可居家办公 3 天,去哪儿灵活办公制度出炉!网友:“快点让我的老板看到”

内容概要:本文详细介绍了“秒杀商城”微服务架构的设计与实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现与配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署和Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪与Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础和Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现和系统性能优化部分,结合代码调试与监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值