C++ 代码整洁之道

NOTICE: 这篇文章的框架条目来自《C++代码整洁之道:C++17可持续软件开发模式实践》,作者: [德] 斯提芬·罗特。书籍原名"Clean C++: Sustainable Software Development Patterns and Best Practices with C++ 17"。

NOTICE:未经作者允许禁止转载!

1. 编码基本原则

保持简单和直接原则(KISS)

Keep It Simple and Stupid.尽可能地采取简短、明了和直观的设计或编码方案,以避免过度工程化和不必要的复杂性。

不需要原则(YAGNI)

You Aren’t Gonna Need It. 这个原则的核心观点是:不要浪费时间在你可能永远不需要的特性上。在确定真的有必要的时候再写代码,那时再重构也来得及。

避免重复原则(DRY)

Don’t Repeat Yourself。应该尽量避免重复代码和冗余数据的出现,而应该采用抽象化、封装和代码重用等方式来避免重复。
遵循DRY原则可以带来以下几个好处:

a. 提高代码的可维护性、可读性和可重用性:通过避免重复代码和数据,可以使代码更具有模块化和可重用性,从而降低开发和维护成本,并且使代码更易于理解和修改。
b. 减少潜在的错误:重复代码很容易导致错误和不一致性,因为如果需要更新某个行为或者修复一个问题,需改的地方就会增加,这样可能会导致不必要的错误。
c. 优化性能:使用代码重用技术可以避免不必要的计算,因为不需要执行重复的代码,可以提高程序的性能。

为了遵循DRY原则,开发人员应该注意以下几点:

a. 提取共同的行为:在编写代码时,应该寻找可以被多次使用的代码块,并将它们提取出来作为公共函数或类库。
b. 采用抽象化和封装:通过对代码进行抽象化和封装,可以降低代码耦合性,并且可以避免重复代码的出现。
c. 避免分散数据:在编写代码时,应该避免将同一数据保存在多个地方,尽可能地将所有相关的数据都保存在一个地方,这样可以避免数据的不一致性。

信息隐藏原则

一段代码调用了另外一段代码,那么调用者不应该知道被调用者的内部实现。

a. 封装实现细节:每个组件应该将自己的实现细节尽可能地封装起来,以减少对外暴露的内容。这样可以有效地控制系统的复杂度,并降低修改组件时的风险。
b. 限制接口暴露:每个组件应该仅向外部暴露必要的接口,并尽可能地减少对外暴露的内容。这不仅可以提高组件间的独立性,还可以使系统更加灵活、可重用和易于维护。

高内聚原则

高内聚原则是指一个模块或类应该尽可能地将自己的功能和数据集中在一起,形成一个独立的单元,并且各个单元之间应该相互独立。高内聚的目的是将一个大模块分解为更小、更容易理解和维护的部分。

松耦合原则

不同的模块或组件之间应该尽量减少相互依赖。松耦合原则包括以下几个关键点:

a.接口稳定:模块之间的通信应该通过稳定的接口进行,接口的设计应该简单、清晰,并且能够满足各种场景的需求。
b. 最小依赖:每个模块或组件应该尽可能地减少对其他模块或组件的依赖,只与必要的组件进行交互,避免与其他模块或组件之间建立过多的耦合关系。
c. 松散耦合:各个模块或组件之间应该尽可能松散地耦合,也就是说它们之间的关系是可插拔、可替换的。这样可以方便地对系统进行扩展或改进,同时也更容易理解和维护。

小心优化原则

小步优化。

最少惊讶原则

保持一致性:在设计软件时,应该尽可能地保持一致的设计风格和交互方式,确保用户可以更容易地理解和使用软件。
满足用户期望:设计的功能和操作应该满足用户的习惯和预期,不应该出现让用户感到惊讶或困惑的行为。
明确反馈:当用户执行操作时,软件应该给予明确的反馈,确保用户可以得知操作的结果和状态。

童子军原则

当发现代码中有需要改进的或者风格不好的代码,应该立刻修改,不管是谁写的代码。

2. C++代码整洁的基本规范

2.1 良好的命名

源代码文件名、命名空间、类、函数、模板、参数、变量、常量等等等等,都应该具有有意义且比较有表现力的名字。

名称应该自注释

  • bad examples
int num;
bool flag;
std::vector<Customer> list;
Product data;
  • good examples
unsigned int number_of_customer;
bool is_changed;
std::vector<Customer> customers;
Product ordered_product;

自注释的命名也不应该过长,如果变量的上下文很清楚,只需要要简短的描述性名称。

避免冗余的名称

  • bad example
class Customer
{
// somde code
private:
    std::string customer_name_;
};

在Customer类中已经有足够的信息能够知道name表示customer的名字了,不必要重复,重复的话违背了"Don’t Repeat Yourself "原则.

std::string string_customer_name;

不要在变量中包含变量类型。

不要用匈牙利命名法

不要在变量前面带类型前缀,在变量里面带上类型前缀,当你更改类型后还需要更改变量名,忘记改还会误导读者,匈牙利命名法在上个世纪没有强大的IDE时可能有用,但是现在提示变量类型的功能在IDE中已经非常普遍。
另外模板类怎么可能提前指定变量类型。

避免晦涩的缩写

Car ctw;	// bad
Car car_to_wash;	// good

Polygon ply1;	// bad
Polygon first_polygon;	// good

const double GOE = 9.80665;	// bad
const double gravitation_acceleration_on_earth = 9.80665; // good

2.2 注释

不要为易懂的代码写注释

customer_index++;
Customer* customer = GetCustomerByIndex(customer_index);
CustomerAccount * account = customer->GetAccount();

像这样的代码完全不需要写注释,不要低估代码阅读者的智商。

不要用注释禁用代码

在代码里面不再需要的代码应该直接删除,有的想用注释的方式把一段代码注释掉,以防后面可能会用到,但是这种情况更应该做的的把代码放入代码版本管理工具中,如果有需要去版本工具中回溯。临时注释某些代码来debug是允许的,但是改完bug后应该删除。

特殊情况的注释

对于某些代码的特殊情况添加注释是很有用的。

2.3 函数

只做一件事

函数应该做一件事。做好这件事。只做这一件事

让函数尽可能小

函数短小的好处:

提高代码可维护性和可读性
提高代码的可复用性
方便调试和测试

如果以行数来计,一般不要超过15行,clean code 中说函数的第一要义是要短小,第二要义是要更短小,20行封顶最佳。

函数命名

函数的命名应该具有表达力和准确性,能够清楚地描述函数的功能和作用。使用动词或动词短语作为函数名,以描述函数执行的操作和结果。例如,getMax、setCount 等。

函数的参数和返回值

函数的参数应该尽可能少,最好没有参数,其次是一个参数,一般不要多于三个输入参数。需要返回的参数直接通过返回值返回。有些人可能会考虑在函数返回时会有临时对象的创建复制等过程,导致程序效率不足,但是现代C++体系发展了move语义,在返回的时候大多数情况下是直接move而不是创建临时对象再赋值,move语义的出现已经很大程度的改变了这种变成模式。

2.4 替换C++中的C风格代码

用string代替char*

string是C++中一个重要的组件,在用到字符串的时候首选string而不要使用c风格的char*。

使用标准容器而不是C风格的数组

C++提供很多标准容器,尽量使用标准容器而不要使用数组。最常用的例如使用vector替代c风格的数组。

使用C++的类型转换代替C的强制类型转换

C++ 中用static_cast(),来做强制类型转换。

float a = 1.0;
int b = (int)a; // c风格的强制类型转换,不推荐
int c = static_cast<int>(a); // C++ 风格的强制类型转换,推荐使用

避免使用宏

宏不利于调试:宏定义是在预处理阶段进行文本替换的,如果使用过多的宏定义,会使代码变得杂乱无章,不易于调试。
宏容易引起错误:宏定义中没有类型检查和作用域限制,这就容易导致一些错误的定义或使用。
宏不易于维护:宏定义的可读性较差,理解复杂的宏定义需要花费较大精力,也不易于修改和维护。

如果是定义常量,可以用 constexpr 来修饰。使用 constexpr 关键字可以优化代码性能,因为编译器可以在编译时将常量表达式计算出来,而无需运行时计算,从而避免了运行时开销。

#define PI 3.141592653 // 不推荐
constexpr long long PI = 3.141592653; //推荐

用一般函数替换宏函数。

2.5 资源管理

智能指针

智能指针是一种 RAII(资源获取即初始化)技术的应用,它自动管理对象的生命周期,解决了动态内存分配和回收的问题。C++11 提供了三种智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr(不推荐使用)。

std::unique_ptr:是一个独占式所有权的智能指针,保证同一时间只有一个指针可以访问其所指向的对象。当 unique_ptr 被销毁时,它所指向的对象也会被销毁。

std::shared_ptr:是一个共享所有权的智能指针,可被多个指针同时访问其所指向的对象,在最后一个 shared_ptr 被销毁时才会销毁所指向的对象。

避免显示的使用new和delete

make_unique 和 make_shared 是 C++11 引入的两个模板函数,用于创建智能指针的对象。它们都遵循 RAII 技术的原则,即对象的生命周期与智能指针的生命周期绑定在一起,从而有效避免了内存泄漏、重复释放等问题。
示例:

auto p = std::make_unique<int>(42);
auto p2 = std::make_shared<int>(42);

3. 面向对象

类的设计原则

让类尽可能的小

像函数一样小的类容易测试,容易理解,容易测试,容易复用。

单一职责原则

一个类应该只有一个单一的职责。

开闭原则

类应该对扩展开放,对修改关闭。

里氏替换原则(LSP)

子类对象应该能够替换掉程序中任何父类对象,并且保证不会产生任何负面影响。也就是说,子类继承父类时不能改变父类已有的行为,而是应该遵循父类的约束。

接口隔离原则(ISP)

“客户端不应该依赖于它不需要的接口”,也就是说,一个类不应该被强迫依赖它不使用的方法。简单来说,一个类对另一个类的依赖性,应该建立在最小化的接口上。

接口隔离原则可以减少系统的耦合度,提高系统的内聚性,从而增加系统的可维护性和可扩展性。

无环依赖原则

依赖倒置原则(DIP)

高层模块不应该依赖于低层模块的实现细节,而是应该依赖于抽象。同时,抽象不应该依赖于具体实现,而具体实现应该依赖于抽象。

不要和陌生人说话(迪米特法则)(LoD)

迪米特法则也被称为最少知道原则(LKP)。一个模块应该对外部模块产生最少依赖关系,即每个模块只应该关注与其密切相关的对象。迪米特法则的目的是为了减少系统中的耦合度,提高系统的可维护性、可扩展性和可测试性。

避免类的静态成员

4. 设计模式和习惯用法

依赖注入模式

Dependency Injection
将组件与其需要的服务分离,这样组件就不必知道这些服务的名称,也不必知道如何获取它们。
例如:日志记录器
在这里插入图片描述
常见的注入方式有构造器注入和setter注入。

Adapter模式

把一个类的接口转为期望的另一个接口,让接口不兼容的类可以适配

Strategy模式

定义一组算法,然后封装每个算法,使它们可以相互替换,策略模式允许算法独立于使用它的客户端而变化。
例如:在需要排序的地方定义许多排序算法,快排,堆排,冒泡,希尔排序等等的,在用的时候可以选择。或者在格式化输出的地方,用纯文本、xml、json格式输出文本。

Command模式

常用在client/server架构中

Command处理器模式

Composite模式

将对象组合成树结构来表示“部分-整体”的层次结构。

Observer模式

Factory模式

Strategy模式

Facade模式

Money Class模式

特例用法

参考

[1] https://www.clean-cpp.com/contact/
[2] 《Clean Code》Robert C. Martin
[3] 《C++代码整洁之道:C++17可持续软件开发模式实践》
[4] https://en.cppreference.com
[5] https://refactoringguru.cn/design-patterns/catalog
[6] https://google.github.io/styleguide/

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值