阅读此文前,麻烦您点击一下“关注”,既方便您进行讨论与分享,还能为您带来不一样的参与感,感谢您的支持。
编程风格的转变,个人生产力的飞跃 我的C语言技术一直在提高,编程理念的改变促使我重新审视代码风格。这次改变对我来说是多年来最大的一次,它极大地影响了我的工作效率和组织能力。虽然部分原因是主观的,但也包含一些客观的提高。
这里记录的风格对我最有效,但不意味着每个人都应该如此,我会根据不同项目遵循其规范。 首先,基本类型我一直采用简短命名,因为它可以使代码更加清晰和易于检查。这些类型频繁出现,所以简洁很有好处。我不再使用_t后缀,它可能分散注意力。
虽然有人喜欢s前缀的有符号类型,但我更喜欢i,保留s用于其他用途。在指定类型大小时,size更统一,不占用前缀,更重要的是大小值应为有符号,所以我提供了特殊命名。usize使用非常特殊,主要与需要无符号大小的外部接口交互。 b32表示“32位布尔值”,意思很明确。
我本可以使用_Bool,但我更倾向于字母加大小写,远离一些奇怪的语义。对初学者来说,32位布尔值似乎“浪费内存”,但实际上不是。布尔值会存储在寄存器中或填充结构,真正需要注意大小的情况下,我会将布尔值打包到变量flags中,1字节的布尔值一般不会引起内存问题。
UTF-16在Win32下会带来许多问题,所以我经常使用c16(“16位字符”)。虽然uint16_t效果相同,但char16_t的命名可以给调试器提供信息,特别是GDB,表示这些变量保存字符值。Win32有wchar_t类型,但我更喜欢明确UTF-16的使用。
u8表示八位字节,一般用于UTF-8数据。它与byte不同,后者代表原始内存,是一种特殊别名类型。理论上,它们可以是不同类型,有不同语义,但我所知目前没有实现。不同的名字只表示用途不同。 那些不支持固定宽度类型的系统,只有学术意义,不值得花时间支持。
包括int_fast32_t等类型。几乎没有软件能在这种系统上正常工作,我相信没人测试过,似乎也没人关心。我不会单独使用这些命名,如果用,必须提供typedefs给读者信息。解释这些不值得精力。即使在最近的文章中,我也使用ptrdiff_t而不是size。
接下来是我的“标准”宏:。虽然我坚持常量全大写,但对看起来像函数的宏,我使用小写,因为更易读。它们没有其他宏的命名空间问题,比如同时有宏new()和变量new,后者看起来完全不像函数调用。对GCC和Clang,我最喜欢的assert宏如下:。除通常优势外,它还有:。
不需要为调试和发行构建分别定义。由“未定义行为检查器”控制,仅存在于调试构建。它提供诊断输出,带文件名和行号。在发行构建中,它变为优化提示。如果要在发行构建启用断言,可以通过-fsanitize-trap将其设置为陷阱模式,然后启用-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:。
另一个改变是,在返回值中,使用结构体来代替参数。实际上就是多返回值,只不过没有解构而已。这是一个巨大的组织性变化。例如,下面的函数返回了两个值,一个解析后的结果,一个状态:。
那么“额外的复制”怎么办别担心,因为在没有inline的情况下,这种调用会实际上变成一个隐藏的、带有restrict的输出参数,所以不会有额外的考校。使用这种返回方式,我不需要使用特殊值(比如)来表示错误,所以可以更清晰。
这也导致了一种在函数开头定义零值返回值的编程风格,即首先定义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)。
当您跟我有更多互动的时候,才会被认定为铁粉。如果您喜欢我的文章,可以点个“关注”,成为铁粉后能第一时间收到文章推送。本文仅在今日头条首发,请勿搬运。
,