C++最佳实践之编程建议

本文介绍C++的编程建议基于C++之父Bjarne Stroustrup编写的《A Tour of C++》,包括通用指南、命名空间、异常处理、成员函数、虚函数、构造函数、模板、容器、stl标准库、线程与并发控制。

目录

一、C++通用指南

二、智能指针与内存

三、命名空间与异常处理

四、成员函数与虚函数

五、构造函数与类型转换

1、构造函数

2、default默认操作

3、delete禁止操作

4、单参数的构造函数

5、禁止不同类型的拷贝

六、template模板

七、stl标准库

八、容器

九、线程与并发控制

1、线程初始化

2、mutex与lock


一、C++通用指南

通用的编程指南建议如下:

  • 保持函数简洁,保持代码简洁;
  • 关注编程技术,而不是语言特点;
  • 函数应该执行单个逻辑操作, 不要把多个功能放在同一个函数实现;
  • 当函数在不同类型执行相同任务时,使用函数重载;
  • 如果函数在编译期能确定,那么使用constexpr关键字修饰;
  • 避免复杂表达式;
  • 避免变窄转换,防止精度丢失,比如float类型转为int类型;
  • 变量的作用域最小化,能用局部变量就不要声明为全局变量;
  • 避免魔术常量,使用符号常量;
  • 优先考虑不可变的数据,比如参数不用修改,就声明为const;
  • 避免相似的命名,防止产生歧义;
  • 对于具有命名类型的声明,首选大括号{}来初始化;
  • 使用auto关键字避免重复类型命名;
  • 避免未初始化的变量;
  • 仅在位运算时使用unsigned修饰符;
  • 保持指针的使用简单直接;
  • 使用nullptr,而不用0或者NULL;
  • 能用代码表达,就不用过多注释;
  • 保持一致的缩进样式;
  • 避免使用goto跳转;
  • 使用==代替strcmp来判断字符串是否相等;
  • 使用new代替malloc来申请内存;
  • 不要用longjmp(),不要用exit(),而是抛出异常;
  • 使用<chrono>代替<ctime>,如果用到时间;

二、智能指针与内存

C++提供三种智能指针:shared_ptr、unique_ptr和weak_ptr,而auto_ptr已经过时不建议使用。智能指针用于管理内存分配与释放,其中shared_ptr采用引用计数法,当计数为0就会释放内存,对象共享一段内存;而unique_ptr独自拥有管理内存的所有权;weak_ptr作为弱指针,观察shared_ptr管理的内存对象,可以避免shared_ptr的循环引用导致的内存泄漏。

观察下面这段代码有什么问题:

void circle(int x)
{
    Shape∗ p = new Circle{Point{0,0},10};
    // ...
    if (x<0) throw Exception(); // potential leak
    if (x==0) return;           // potential leak
    // ...
    delete p;
}

当x < 0时,抛出异常;当x == 0时,程序返回。这两种情况没有调用delete p来释放内存对象,导致内存泄漏。建议的解决方案使用智能指针,来避免忘记释放内存。

再看另一段代码:

Shape∗ circle(int x)
{
    Shape∗ p = new Circle{Point{0,0},10};
    // ...
    return p;
}

这里返回本地对象(局部变量)的引用,函数执行完毕,对象已经释放,返回的指针地址成为野指针 。所以,不能返回本地对象的引用。

智能指针的使用与避免内存泄漏的建议如下:

  • 使用unique_ptr或shared_ptr避免忘记释放new创建的对象;
  • 当需要拷贝锁或者更小粒度的同步控制,优先使用unique_ptr;
  • 当与condition_variable结合使用时,优先使用unique_ptr;
  • 尽量使用make_shared代替shared_ptr;

三、命名空间与异常处理

命名空间用于防止命名冲突、模块隔离。关于命名空间与异常处理建议如下:

  • 使用头文件来表示接口和强调逻辑结构;
  • 源文件使用#include头文件,要实现它声明的函数;
  • 避免在头文件写非内联函数,头文件仅支持内联函数;
  • 不要在头文件使用using命名空间,最小化使用命名空间;
  • 函数执行出错,而且错误影响到调用函数,那么抛出异常;
  • 如果不确定使用异常还是错误码,那么优先使用异常;
  • 能够在编译期检查就在编译期检查,不要等到运行期;
  • 小量数据使用值传递,大量数据使用引用传递;
  • 优先采用常量引用,而不是普通引用;

四、成员函数与虚函数

关于成员函数与虚函数的使用建议如下:

  • 对称运算符使用非成员函数,仅当需要被类直接访问时才设为成员函数;
  • 把成员函数声明为对象状态不可修改;
  • 如果构造函数需要资源,那么它的类需要释构函数来释放资源;
  • 如果类是一个容器,赋予它一个初始化列表的构造函数;
  • 当接口和实现完全分离时,使用抽象类作为接口;
  • 通过指针或引用来访问多态对象;
  • 抽象类不需要构造函数;
  • 有虚函数的类,需要虚的释构函数;
  • 设计类的层次结构时,区分实现继承和接口继承;

五、构造函数与类型转换

1、构造函数

构造函数包括默认构造、拷贝构造、移动构造、拷贝赋值构造、移动赋值构造。而释构函数只有一个,并且无参数无返回值。示例代码如下:

class X {
public:
    X(param);               // normal constructor: create an object
    X();                    // default constructor
    X(const X&);            // copy constr uctor
    X(X&&);                 // move constr uctor
    X& operator=(const X&); // copy assignment: clean up target and copy
    X& operator=(X&&);      // move assignment: clean up target and move
    ~X();                   // destructor: clean up
};

对象可以被拷贝或移动有以下五种情况:

  • 作为赋值的来源;
  • 作为对象的初始化;
  • 作为函数参数;
  • 作为函数返回值;
  • 作为一个异常;

2、default默认操作

如果期望某个构造函数作为默认构造,那么使用=default。示例代码如下:

class Y {
public:
    Y(Sometype);
    Y(const Y&) = default; // default copy constructor
    Y(Y&&) = default;      // default move constructor
};

3、delete禁止操作

为了防止调用者不恰当调用,可以在构造函数加上=delete表示禁止某个操作。如果调用=delete修饰的操作,会在编译期报错。示例代码如下:

class Shape {
public:
    Shape(const Shape&) =delete; // no copy operation
    Shape& operator=(const Shape&) =delete; // no assign operation
};

void copy(Shape& s1, const Shape& s2)
{
    s1 = s2; // error: Shape copy is deleted
}

4、单参数的构造函数

默认把单个参数的构造函数声明为explicit,防止被隐式调用。示例代码如下:

class Hello {
public:
    explicit Hello(int a);
}

5、禁止不同类型的拷贝

如果默认值不适用某个类型,禁止拷贝。

六、template模板

为了定义优秀的模版,我们需要遵循以下机制:

  • 数值依赖类型:可变模版;
  • 编译期选择机制:if constexpr;
  • 查询类型和表达式属性的机制;

template模板有如下特点:

  • 能够将类型作为参数传递,而不丢失信息;
  • 在模版实例化时,把不同上下文信息组织在一起;
  • 将常量值作为参数传递,这意味着能够在编译期执行计算;

关于模板的建议如下:

  1. 使用模板表达适用多种参数类型的算法;
  2. 使用模板表达容器;
  3. 使用模板来提升抽象代码的等级;
  4. 让构造函数或函数模板来推断类模板参数类型;
  5. 虚成员函数不能作为模板成员函数;
  6. 使用模板别名来简化表示并隐藏细节;
  7. 模板提供编译期的动态类型;

七、stl标准库

C++提供stl标准库,包括:algorithm、array、vector、map、string、deque、list、thread等。关于stl标准库使用建议如下:

  1. 尽量使用标准库,不要自己造轮子;
  2. 不要认为标准库能使用所有处理情况;
  3. 使用标准库时,前面加std来引用,比如字符串std::string;
  4. 使用substr()来读子字符串,replace()写子字符串;
  5. 当进行范围检查时使用at(),当需要优化速度时使用iterator或[];
  6. 使用c_str()来表示C语言风格的字符串;

八、容器

C++提供的标准容器包括:vector、list、forward_list、deque、set、multiset、map、

multimap、unordered_map、unordered_multimap、unordered_set、unordered_multiset。

关于容器的使用建议如下:

扩容操作使用resize()而不是realloc();

插入操作使用push_back()或者insert(),效率比较高;

map基于红黑树,有序但效率较低。unordered_map基于哈希表,无序但效率较高;

九、线程与并发控制

1、线程初始化

使用函数或结构体作为执行任务,线程初始化的示例代码如下:

void f();  // 函数
struct F { // 函数对象
    void operator();
};

void hello()
{
    thread t1 {f};
    thread t2 {F()};
    t1.join(); // 等待线程执行完毕
    t2.join();
}

2、mutex与lock

多线程并发控制,需要用到mutex互斥锁与lock结合使用,或者condition_variable条件变量。其中,lock有scoped_lock、unique_lock、shared_lock、lock_guard。一般情况下,读数据用shared_lock,写数据用unique_lock()。示例代码如下:

shared_mutex mtx; // 互斥锁
void read_data()
{
    shared_lock lock {mtx};
// 读操作
}
void writer()
{
    unique_lock lock {mtx};
// 写操作
}

关于线程与并发控制的建议如下:

  1. 使用condition_variable条件变量进行多线程通信;
  2. 不要在条件变量情况下等待(容易死锁);
  3. 线程任务使用promise返回结果,从future获取结果;
  4. 使用async()启动简单任务;

至此,C++语言的编程建议介绍完毕。没有严格约束,但是遵循可以提高编程效率,提高代码可读性,有利于团队协同编程。如有错漏,欢迎伙伴们指出纠正。

猜你喜欢

转载自blog.csdn.net/u011686167/article/details/124540949