C++最佳实践之编程建议
本文介绍C++的编程建议基于C++之父Bjarne Stroustrup编写的《A Tour of C++》,包括通用指南、命名空间、异常处理、成员函数、虚函数、构造函数、模板、容器、stl标准库、线程与并发控制。
本文介绍C++的编程建议基于C++之父Bjarne Stroustrup编写的《A Tour of C++》,包括通用指南、命名空间、异常处理、成员函数、虚函数、构造函数、模板、容器、stl标准库、线程与并发控制。
目录
一、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模板有如下特点:
- 能够将类型作为参数传递,而不丢失信息;
- 在模版实例化时,把不同上下文信息组织在一起;
- 将常量值作为参数传递,这意味着能够在编译期执行计算;
关于模板的建议如下:
- 使用模板表达适用多种参数类型的算法;
- 使用模板表达容器;
- 使用模板来提升抽象代码的等级;
- 让构造函数或函数模板来推断类模板参数类型;
- 虚成员函数不能作为模板成员函数;
- 使用模板别名来简化表示并隐藏细节;
- 模板提供编译期的动态类型;
七、stl标准库
C++提供stl标准库,包括:algorithm、array、vector、map、string、deque、list、thread等。关于stl标准库使用建议如下:
- 尽量使用标准库,不要自己造轮子;
- 不要认为标准库能使用所有处理情况;
- 使用标准库时,前面加std来引用,比如字符串std::string;
- 使用substr()来读子字符串,replace()写子字符串;
- 当进行范围检查时使用at(),当需要优化速度时使用iterator或[];
- 使用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};
// 写操作
}
关于线程与并发控制的建议如下:
- 使用condition_variable条件变量进行多线程通信;
- 不要在条件变量情况下等待(容易死锁);
- 线程任务使用promise返回结果,从future获取结果;
- 使用async()启动简单任务;
至此,C++语言的编程建议介绍完毕。没有严格约束,但是遵循可以提高编程效率,提高代码可读性,有利于团队协同编程。如有错漏,欢迎伙伴们指出纠正。
更多推荐
所有评论(0)