最近写C++的一些想法
最近一直在写C++, 以前没在工程中那么仔细的研究过C++的代码, 读书的时候也是Python写的比较多, 突然写起来感觉自己真的菜的一B,赶紧的读一读<Google 开源项目风格指南>找补找补,现在读完了,收获还是挺多的, 就来聊一聊. 博客的是原创总结性的文章,学习笔记都放在了黑鲸知识库中的C++目录里了,有兴趣的同学可以去看看.
C++简单说明
C++语言既保留了C语言的有效性、灵活性、便于移植等全部精华和特点,又添加了面向对象编程的支持,具有强大的编程功能,可方便地构造出模拟现实问题的实体和操作;编写出的程序具有结构清晰、易于扩充等优良特性,适合于各种应用软件、系统软件的程序设计。用C++编写的程序可读性好,生成的代码质量高,运行效率仅比汇编语言慢10%~20%。
刚入大学的计算机科班生,一般都是从C/C++学起,C/C++更接近底层, 能够让了解到程序的底层机制,对后面理解整个计算机基础体系都有非常巨大的帮助。 而且很多高校的课程也是C/C++,很多比赛如ACM也是推荐C/C++语言。学好后再学其他语言则是比较容易的事情了。但是C ++的学习深度与难度其实还挺大的, C++具有复杂的语法以支持多功能性.
我对C++的看法
简单来说我对C++的看法是真他妈的复杂,学起来完全不像其他的语言查查文档就能用个七七八八了,C++是真的要去理解,学习那些乱七八糟的特性,什么多编程范式支持,模板支持元编程,模板元编程特性图灵完备啥的,乱七八糟的名词听了就慌, 还有性能与抽象的权衡等等,不了解这些东西非常容易在写代码的时候掉坑里.
元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。 很多情况下与手工编写全部代码相比工作效率更高。 编写元程序的语言称之为元语言,被操作的语言称之为目标语言。
图灵完备是指在可计算性理论里,如果一系列操作数据的规则(如指令集、编程语言、细胞自动机)可以用来模拟单带图灵机。 这个词源于引入图灵机概念的数学家艾伦·图灵。 虽然图灵机会受到储存能力的物理限制,图灵完全性通常指“具有无限存储能力的通用物理机器或编程语言”。
但是现在做自动驾驶相关肯定还是C++最普遍,车上平台可能是基于Linux和C++的,自动驾驶的算法还处于百花齐放的状态,用C/C++最顺手,最容易迭代和比较(copy)。所以还是要硬着头皮学习(但是我也不会放弃的My Love–Python的 ^_^),争取强迫自己爱上C++😂. 和Python相比,对于我这样的菜狗来说最大的困难就是C++很难把代码安排的规规矩矩的,整整齐齐的,如果写出乱七八糟的东西瞬间”阳痿”,特别痛苦,代码审查也不会过关,所以一定要找一些规范的代码研究研究,才能在大工程里把代码组织起来.<<Google 开源项目风格指南>>相关规范目前看来是市面最被认可的C++规范,于是就去学习了一下,还有一些是跟大佬学来的规范.
Google 开源项目风格指南
这里就简单总结一下重点把内容,更详细的技术笔记请前往的: C++.
头文件
- 头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法.
- 所有头文件都应该有 #define 保护来防止头文件被多重包含, 命名格式当是:
H . 防止文件中的内容被重复定义 - 尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。
- 只有当函数只有 10 行甚至更少时才将其定义为内联函数.
- 使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.
函数规范
- 我们倾向于按值返回, 否则按引用返回。 避免返回指针, 除非它可以为空.
- 我们倾向于编写简短, 凝练的函数.
- 若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种. 这一规则也适用于构造函数.
- 只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 函数重载 遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下.
- 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
来自Google的奇技
- 动态分配出的对象最好有单一且固定的所有主, 并通过智能指针传递所有权.
- 使用 cpplint.py 检查风格错误.
其他 C++ 特性
- 所有按引用传递的参数必须加上 const.
- 只在定义移动构造函数与移动赋值操作时使用右值引用. 不要使用 std::forward.
- 要用好函数重载,最好能让读者一看调用点(call site)就胸有成竹,不用花心思猜测调用的重载函数到底是哪一种。该规则适用于构造函数。
- 我们不允许使用缺省函数参数,少数极端情况除外。尽可能改用函数重载。
- 我们不允许使用变长数组和 alloca().
- 我们允许合理的使用友元类及友元函数.
- 我们不使用 C++ 异常.
- 我们禁止使用 RTTI.
- 使用 C++ 的类型转换, 如 static_cast<>(). 不要使用 int y = (int)x 或 int y = int(x) 等转换方式;
- 只在记录日志时使用流.
- 对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.
- 我们强烈建议你在任何可能的情况下都要使用 const. 此外有时改用 C++11 推出的 constexpr 更好。
- C++ 内建整型中, 仅使用 int. 如果程序中需要不同大小的变量, 可以使用 <stdint.h> 中长度精确的整型, 如 int16_t.如果您的变量可能不小于 2^31 (2GiB), 就用 64 位变量比如 int64_t. 此外要留意,哪怕您的值并不会超出 int 所能够表示的范围,在计算过程中也可能会溢出。所以拿不准时,干脆用更大的类型。
- 代码应该对 64 位和 32 位系统友好. 处理打印, 比较, 结构体对齐时应切记:
- 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之
- 整数用 0, 实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 ‘\0’.
- 尽可能用 sizeof(varname) 代替 sizeof(type).
- 用 auto 绕过烦琐的类型名,只要可读性好就继续用,别用在局部变量之外的地方。
- 你可以用列表初始化。
- 适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。
- 不要使用复杂的模板编程
- 只使用 Boost 中被认可的库.
- 适当用 C++11(前身是 C++0x)的库和语言扩展,在贵项目用 C++11 特性前三思可移植性。
命名约定
- 函数命名, 变量命名, 文件命名要有描述性; 少用缩写.
- 文件名要全部小写, 可以包含下划线 (_) 或连字符 (-), 依照项目的约定. 如果没有约定, 那么 “_” 更好.
- 类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum.
- 变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接. 类的成员变量以下划线结尾, 但结构体的就不用, 如: a_local_variable, a_struct_data_member, a_class_data_member_.
- 声明为
constexpr
或const
的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合. - 常规函数使用大小写混合, 取值和设值函数则要求与变量名匹配: MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().
- 命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.
- 枚举的命名应当和 常量 或 宏 一致: kEnumName 或是 ENUM_NAME.
- 你并不打算 使用宏, 对吧? 如果你一定要用, 像这样命名: MY_MACRO_THAT_SCARES_SMALL_CHILDREN.
- 如果你命名的实体与已有 C/C++ 实体相似, 可参考现有命名策略.
类
- 不要在构造函数中调用虚函数, 也不要在无法报出错误时进行可能失败的初始化.
- 要定义隐式类型转换. 对于转换运算符和单参数构造函数, 请使用 ‘explicit’ 关键字.
- 如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用.
- 仅当只有数据成员时使用 struct, 其它一概使用 class.
- 使用组合常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.
- 真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的纯接口类.
- 接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制).
- 所有数据成员声明为 private, 除非是 static const 类型成员 (遵循 常量命名规则). 出于技术上的原因, 在使用 Google Test 时我们允许测试固件类中的数据成员为 protected.
- 声明次序: public -> protected -> private;
- 函数体尽量短小, 紧凑, 功能单一;
注释规则
- 在每一个文件开头加入版权公告,然后是文件内容描述.
- 每个类的定义都要附带一份注释,描述类的功能和用法.
- 函数声明处注释描述函数功能;定义处描述函数实现.
- 通常变量名本身足以很好说明变量用途. 某些情况下, 也需要额外的注释说明.
- 对于代码中巧妙的, 晦涩的, 有趣的, 重要的地方加以注释.
- 注意标点, 拼写和语法; 写的好的注释比差的要易读的多.
- 对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释.
- 通过弃用注释(DEPRECATED comments)以标记某接口点已弃用.
函数
- 倾向于按值返回, 否则按引用返回。 避免返回指针, 除非它可以为空.
- 如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
- 所有按引用传递的参数必须加上 const.
- 如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append(). 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用 列表初始化 指定参数.
- 只允许在非虚函数中使用缺省参数, 且必须保证缺省参数的值始终一致. 缺省参数与 函数重载 遵循同样的规则. 一般情况下建议使用函数重载, 尤其是在缺省函数带来的可读性提升不能弥补下文中所提到的缺点的情况下.
- 只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
格式
- 每一行代码字符数不超过 80.
- 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
- 只使用空格, 每次缩进 2 个空格.
- 返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致.
- Lambda 表达式对形参和函数体的格式化和其他函数一致; 捕获列表同理, 表项用逗号隔开.
- 要么一行写完函数调用, 要么在圆括号里对参数分行, 要么参数另起一行且缩进四格. 如果没有其它顾虑的话, 尽可能精简行数, 比如把多个参数适当地放在同一行里.
- 您平时怎么格式化函数调用, 就怎么格式化 列表初始化.
- 倾向于不在圆括号内使用空格. 关键字 if 和 else 另起一行.
- switch 语句可以使用大括号分段, 以表明 cases 之间不是连在一起的. 在单语句循环里, 括号可用可不用. 空循环体应使用 {} 或 continue.
- 句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.
- 如果一个布尔表达式超过 标准行宽, 断行方式要统一一下.
- 不要在 return 表达式里加上非必须的圆括号.
- 用 =, () 和 {} 均可.
- 预处理指令不要缩进, 从行首开始.
- 访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 1 个空格.
- 构造函数初始化列表放在同一行或按四格缩进并排多行.
- 命名空间内容不缩进.
- 水平留白的使用根据在代码中的位置决定. 永远不要在行尾添加没意义的留白.
- 垂直留白越少越好.
规则特例
- 对于现有不符合既定编程风格的代码可以网开一面.
- Windows 程序员有自己的编程习惯, 主要源于 Windows 头文件和其它 Microsoft 代码. 我们希望任何人都可以顺利读懂你的代码, 所以针对所有平台的 C++ 编程只给出一个单独的指南.
结束语
运用常识和判断力, 并且 保持一致.
编辑代码时, 花点时间看看项目中的其它代码, 并熟悉其风格. 如果其它代码中 if
语句使用空格, 那么你也要使用. 如果其中的注释用星号 (*) 围成一个盒子状, 那么你同样要这么做.
风格指南的重点在于提供一个通用的编程规范, 这样大家可以把精力集中在实现内容而不是表现形式上. 我们展示的是一个总体的的风格规范, 但局部风格也很重要, 如果你在一个文件中新加的代码和原有代码风格相去甚远, 这就破坏了文件本身的整体美观, 也让打乱读者在阅读代码时的节奏, 所以要尽量避免.
好了, 关于编码风格写的够多了; 代码本身才更有趣. 尽情享受吧!