Starguard
开发笔记
Toggle navigation
Starguard
全部笔记
Unity
大话存储笔记
C语言
MongoDB
About Me
归档
标签
C++代码整洁的基本规范
2022-02-20 16:27:55
42
0
0
admin
# C++代码整洁的基本规范 [TOC] ## 说明 * 该博客由《C++代码整洁之道-C++17可持续软件开发模式实践》总结而来,部分内容有异议所以加上了自己的看法。 ## 良好的命名 * 源代码文件、命名空间、类、模板、函数、参数、变量和常量等,都应该具有有意义且富有表现力的名字。 ### 名称应该自注释 * 不好的命名就像打包搬家的箱子上写“东西”,而不是“餐具”等有意义的分类,浪费时间且没有意义。 * 不好的命名: ```C++ unsigned int num; bool flag; std::vector<Customer> list; Product data; ``` * 好的命名: ```C++ unsigned int numberOfArticles; bool isChanged; std::vector<Custom> customers; Product orderedProduct; ``` * 命名也不应该过长,否则也会影响可读性,不要在函数、变量等的命名中包含文件名、类名等上下文信息。 ### 使用域中的名称 * 领域驱动设计(Domain-Driven Design,DDD)是试图将业务领域的事物和概念映射到代码中,使软件系统成为一个真实系统的模型。 * 类、方法、参数、返回类型等命名都使用业务领域内典型的术语,具有领域知识的非技术相关人员也有很大的可能理解代码。 ### 选择适当抽象层次的名称 * 软件系统通常是分层的,更高抽象级别的模块要完成的任务,应该通过与下一层较低抽象级别的模块的交互来完成。 * 例如一个网上商店从上到下层的命名变得越来详细: * Billing:创建单据的大型组件 * DiscountCalculator、LineItemFactory:计算折扣、创建单据条目的模块 * calculateReducedValueAddedTax:更深层次具体的任务 ### 避免冗余的名称 * 不要在成员名称中包含类的名称,也不要在名称中包含类型。 ### 避免晦涩难懂的缩写 * 好的和不好的命名: ```C++ const double GOE = 9.80665; // Bad! constexpr Acceleration gravitationalAccelerationOnEarth = 9.80665_ms2; // Good! ``` ### 避免匈牙利命名和命名前缀 * 像C语言这样的弱类型语言(例如float变量的值可以赋值给int变量),使用简单编辑器时匈牙利命名法可能有用,但现代具有“IntelliSense”(智能提示)的IDE中匈牙利命名法没有用途,反而会带来危害,例如更换类型时未修改名称会导致误解,命名也会比通常长一点。 ### 避免相同的名称用于不同的目的 * 相同的名称用于不同的目的是很不容易理解的。 ## 注释 ### 让写代码像讲故事一样 * 好的代码无需过多的解释,应该专注于写更清晰且富有表现力的代码,而不是解释代码。 ### 不要为易懂的代码写注释 * 太多没必要的代码注释是侮辱代码阅读者的智商,浪费人的注意力,而且修改了代码没修改相应的注释还容易引起误导。 ### 不要通过注释禁用代码 * 通过版本控制可以很方便地找回代码,注释掉的的代码可能之后都不会用到。 ### 不要写块注释 * 不要在代码注释中包含Change log、版权信息、装饰性的注释等,这些都没有太大用处。版权信息可以写在license.txt、copyright.txt等文件中。 ### 特殊情况的注释是有用的 * 如果为了性能等原因违反了设计原则,可以用注释解释为什么这样做。 * 还有一些非常特殊的情况,比如数学公式或算法是没有深入专业知识的人无法理解的,就需要精心编写注释。 * 使用Doxygen等工具生成文档时,建议只给公共API编写Doxygen风格的注释,否则会产生很多没有意义的注释,甚至注释和代码的比例达到1:1。另外,添加Doxygen注释的时候还有一些窍门: * 不要使用`@file [<name>]`标签将文件名写入文件本身,因为Doxygen会自动读取文件名称,而且违反了DRY原则,文件重命名后还要记得修改`@file`标签。 * 不要写`@version`、`@author`、`@date`标签,版本控制能更好地管理和跟踪这些信息。 * 强烈建议使用`@mainpage`标签提供描述性项目主页,作为不熟悉项目的开发人员的入门指南和帮助。 * 用精心设计的单元测试代替`@example`标签编写的示例,因为编写示例会增加额外负担,而且单元测试一般要保持最新(否则会测试失败)。 * 项目增长到一定大小,建议在Doxygen的分组机制(`@defgroup <name>`、`@addogroup <name>`、`@ingroup <name>`)的帮助下汇集某些类别的软件单元。 * 不要写`@bug`、`@todo`标签,应立即修复缺陷,或使用问题跟踪软件来提交缺陷。但凭我的个人经验,最初开发的软件会有比较多的TODO,没法立即全部修复,使用缺陷提交软件又不能快速定位到代码位置,所以TODO预留在代码中,直到TODO被完成在新项目还是有用的,jetbrains的IDE对TODO也有良好的管理,双击TODO项就能立刻跳转到代码位置: ![title](/api/file/getImage?fileId=6212045ec73b33aaff4d9ea9) ## 函数 ### 只做一件事情 * 函数可能做了太多事情的标志: * 行数过多 * 试图为函数起一有意义和有表现力的名称以描述该函数的功能时,无法避免使用连词(“和”、“或”等) * 函数体用空行垂直分隔成代表后续步骤的几个片段(通常这些片段的开头使用注释说明这些代码片段的功能) * 圈复杂度过高 * 函数的入参过多 ### 让函数尽可能小 * 理想情况下是4~5行,最多12~15行,不能再多了。 * 不用担心函数调用开销,现代编译器已经足够智能将许多结构简化为功能上类似的高效机器代码,真正的性能问题往往是糟糕的架构和设计引起的。 ### 函数命名 * 以动词开头,例如: ```C++ void CustomerAccount::grantDiscount(DiscountValue discount); void Subject::attachObserver(const Observer& observer); void Subject::notifyAllObservers() const; int Bottling::getTotalAmountOfFilledBottles() const; bool AutomaticDoor::isOpen() const; bool CardReader::isEnabled() const; bool DoubleLinkedList::hasMoreElements() const; ``` ### 使用容易理解的名称 * 下面的代码行看起来也能知道大概意思,但还不够直观: ```C++ std::string head = html.substr(startOfHeader, lengthOfHeader); ``` * 可以通过更容易理解的名称将它封装起来: ```C++ std::string ReportRenderer::extractHtmlHeader(const std::string& html) { return html.substr(startOfHeader, lengthOfHeader); } ``` * 这样做既容易理解,又将提取HTML头这样的一个功能隔离了,我们要查这个功能的时候只需要搜索“extractHtmlHeader”,而不是搜索“substr”在大量的结果中寻找。 ### 函数的参数和返回值 #### 参数的个数 * 理想的参数个数是0个(指的是方法,函数的理想参数个数是1个),不应超过3个。 #### 避免使用标志参数 * 标志参数是指向函数传递一个bool变量指示函数根据该值执行不同的操作,这说明函数可能是低内聚的,违反了单一职责原则。 * 如果函数通过标志参数决定执行不同的操作,应该将其拆分成两个类或两个成员函数来处理。如果拆分的两个类一个需要调用另一个,通过组合一个类到另一个类的成员来减少重复代码。 * 改进前的代码: ```C++ Invoice Billing::createInvoice(const BookingItems& items, const bool withDetails) { if (withDetails) { // ... } else { // ... } } ``` * 改进后的代码: ```C++ class Billing { public: virtual Invoice createInvoice(const BookingItems& items) = 0; }; class SimpleBilling : public Billing { public: virtual Invoice createInvoice(const BookingItems& items) override; }; class DetailedBilling : public Billing { public: virtual Invoice createInvoice(const BookingItems& items) override; private: SimpleBilling simpleBilling; }; ``` * C++11开始可以通过override标识符指定类的一个虚函数覆盖了基类虚函数。 #### 避免使用输出参数 * 输出参数通常起到“返回多个值”的效果,应避免使用输出参数。 * 输出参数很不直观,有时调用者不容易发现哪些是输出参数,而且开发人员还要处理将保存结果值的所有变量,调用这些函数的代码会很快变得混乱。 * 解决方法是返回一个包含多个值的单个对象,或使用std::tuple或std::pair。但建议std::tuple只被用于哪些临时组合的对象,这些对象无论如何都不属于同一类别。而一旦数据必须被捆绑在一起时,由于它们的内聚力很高,通常说明我们要为这一组数据创建类。 #### 不要传递或返回0(NULL,nullptr) * 返回一个对象的指针时,如果返回0(NULL,nullptr),可能会令人困惑,例如一个函数用来查找一个对象是否存在,返回nullptr可能是对象不存在,也可能是查找出错。 * 而且返回nullptr意味着代码里要充斥着空检查,降低代码的可读性并增加复杂度,如果忘记了空检查,可能会导致严重的运行时错误。 * C++还需要考虑一个重要的问题:对象的所有权。对返回指针的函数的调用者来说,指针指向的资源是否需要释放、应该由谁来释放,都是要考虑的问题,所以应该尽量避免返回指针。 * 解决方法是尽量在栈上构造对象而不是在堆上,如果要将在函数或方法中创建的对象返回给调用者,过去通常返回一个指针避免大型对象昂贵的拷贝构造,但C++11开始支持Move语义,允许资源从一个对象“移动”到另一个对象而不是复制它们,非常快速,所以不需要再担心昂贵的拷贝构造代价了。C++11标准中,所有标准库容器类都已扩展为支持Move语义。 * 另外其他的解决方法有: * 使用const引用代替指针,不需要解指针操作符(*)有利于写出更清晰的代码。 * 如果不可避免地处理指向资源的指针,例如API返回的指针,可以使用智能指将其包装并使用RAII习惯用(资源申请即初始化) #### 正确使用const * 使用const可以省去很多麻烦并节省调试时间,因为违反const会直接导致编译错误。另外使用const可以支持编译器的一些优化算法,提高性能。 * const可以防止传递给函数的参数被改变: ```C++ unsigned int determineWeightOfCar(const Car* car); // 指针指向的Car对象不可变 unsigned int determineWeightOfCar(Car const* car); // 指针指向的Car对象不可变,和上一行等同,但推荐这一种写法 void lacquerCar(Car* const car); // 指针指向的Car对象可修改,但指针的指向不可修改,即不能让指针指向一个新创建的Car对象(不能执行car = new Car()) unsigned int determineWeightOfCar(Car const* const car); // 指针指向的Car对象以及指针的指向都不可变 void printMessage(const std::string& message); // 不允许修改被引用的字符串message void printMessage(std::string const& message); // 不允许修改被引用的字符串message,和上一行等同,但推荐这一种写法 ``` * 还可以将类的(非静态)成员函数声明为const,则该方法不能修改类的成员变量,通常只用来查询。 ```C++ class Car { public: void printColor() const; private: string _color; }; void Car::printColor() { // _color = "white"; // 会编译报错 cout << _color << endl; } ``` ## C++工程中的C风格代码 ### 使用C++的string和stream替代C风格的char* * C++11中已不允许将字符串字面量赋值给char*(ISO C++11 does not allow conversion from string literal to 'char *'),例如: ```C++ char *s = "world"; // 需要写成 const char *s = "world"; 且s的值不可修改 ``` * C++的string相比C的char*有以下优点: * C++的string对象自己管理字符串的内存,可以轻松地复制、创建和销毁它们,免于管理字符串数据的声明周期。 * string是可变的,可以通过索引修改某个字符,但`char *s = "world;"`定义的字符串这样做会程序崩溃,string对象还可以轻松地添加字符串或单个字符、连接字符串、替换字符串的某些部分等。 * string提供了方便的迭代器接口来遍历其元素,这也意味着可以将`<algorithm>`中定义的所有合适的算法应用于string对象。 * string与C++ I/O stream能完美配合使用。 * C++11开始,Move语义也适用于string,即string可以作为一个函数的返回值赋值给调用者中的变量,却没有昂贵的拷贝开销。 * 存在例外的情况可以使用`char*`,例如只需要定义一个固定长度的固定字符串,那么string就没有什么优势,可以通过以下方式定义字符串常量: ```C++ const char* const PUBLISHER = "Apress Media LLC"; ``` * 为了与C风格的API库兼容,也不得不使用`char*`。注意`char*`可以传递给string类型的变量,但string不能传递给`char*`类型的变量。 ### 避免使用printf()、sprintf()和gets()等 * printf等C语言函数不安全,容易产生安全漏洞。 * C++的输出流的插入操作符(`<<`)可以被任何类重载,非常简单地打印类中的许多值,要注意将这个重载函数定义为类的友元函数,因为它能在不创建对象的情况下被调用: ```C++ class Invoice { ... private: // 这里返回ostream的引用是可以的,因为ostream是通过引用传参的,如果将非引用的形参作为引用返回,或者将局部变量作为引用返回是会出错的,因为形参或局部变量释放后引用就无效了 friend std::ostream& operator<<(std::ostream& outstream, const Invoice& invoice); ... std::ostream& operator<<(std::ostream& outstream, const Invoice& invoice) { outstream << "Invoice No.: " << invoice.invoiceNumber << std::endl; ... return outstream; } ``` ### 使用标准库的容器而不是C风格的数组 * 一方面C风格的数组不安全,容易产生越界访问,另一方面C风格的数组只能通过指向数组首元素的指针来传递,作为函数参数时还需要一个额外的参数传数组的长度,无法满足“函数参数的个数尽可能少”以及“高内聚”的原则。 * std::array实例有size()方法来查询大小,且具有STL兼容接口,`<algorithm>`中的算法可以应用于std::array。 ### 用C++类型转换代替C风格的强制转换 * 应该尽可能消除强制类型转换,如果必须要转换,使用C++的类型转换代替C风格的强制转换。 * C++的类型转换会在编译期间进行检查,C风格的类型转换不会在编译期间检查出来,可能会在运行时出错。 ```C++ double d { 3.1415 }; int i = (int)i; // 不应使用 int i = static_cast<int>(d); // 应该使用 ``` * 下面的例子可能造成运行时错误,即使使用非常保守的设置(`g++ -std=c++17 -pedantic -pedantic-errors -Wall -Wextra -Werror -Wconversion`): ```C++ int32_t i { 200 }; // 占用4字节 int64_t* pointerToI = (int64_t*)&i; // 指针指向的变量占用8字节 *pointerToI = 9223372036854775807; // 将8字节的值写入仅4字节大小的存储区域,可能造成运行时错误 // 如果使用C++的类型转换,编译时就会报错 int64_t* pointerToI = static_cast<int64_t*>(&i); ``` * 另外C++的类型转换也比C风格的类型转换更容易搜索。 ### 避免使用宏 * 宏可能产生非预期的问题,例如类似函数的宏如果少加了括号,就可能导致语义错误,如果不是必要,不需要使用宏。
上一篇:
现代C++的高级概念
下一篇:
CentOS7安装与初次配置
0
赞
42 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
文档导航