【C/C++】Code Style

命名规范

代码元素 命名风格 注释
Namespace under_scored 为了跟类名做区分
Class name CamelCase 为了跟标准库的类名做区分 (建议不要使用大写"C" 或者 "T" 作为前缀)
Function name camelCase 小写开头的函数名基本是通用的,除了.Net自成一格
Parameters/Locals under_scored 这个在c++世界里面是占大多数的
Member variables under_scored_with_ "_"作为前缀在c++标准里面是不建议的,所以使用后缀
Enums and its members CamelCase 除了很老的标准外,大多数都支持这种风格
Globals g_under_scored 你一开始就不该写全局变量!
File names 与类名相符 优劣参半

代码规范

条款1------重要的Specifiers

  1. override:重写基类的方法时,必须在函数后面加上override。
  2. const:类的方法没有修改类成员时,必须在函数后面加上const。比较典型的是所有的Getter函数。
  3. noexcept:当确定类的方法不会抛出异常时,尽可能加上noexcept。

条款2------try catch

当你不知道具体要catch什么异常时,就不要try了。

条款3------常量

定义常量,尤其是数字常量,禁止使用#define,一律用constexpr或者static constexpr。

cpp 复制代码
constexpr float STEP = 0.02f;
constexpr size_t MAX_SIZE = 100000;

条款4------尽可能使用显式类型转换

cpp 复制代码
int a = -10;
size_t b = 5;
auto c = a / b;
std::cout << "c = " << c << std::endl;
// c = 3689348814741910321

auto d = a / static_cast<int>(b);
std::cout << "d = " << d << std::endl;
// d = -2

条款5------auto关键字

能用auto的地方,多用auto,特别是很长的、一层套一层的类型,比如迭代器。

cpp 复制代码
std::vector<OpenMesh::Point<float, 4>>::Iterator iter = vec.begin(); // no good
// prefer
auto iter = vec.begin();

条款6------Type Alias

使用using关键词来创建别名,停止使用typedef,just for consistency.

cpp 复制代码
using AliasType = Type;
// typedef Type AliasType;

条款7------stl算法

尽可能使用stl里面的算法,不要自己手写相同功能的算法,不仅很难写的比stl好,还容易有bug,不好排查。

cpp 复制代码
// 如求和
int sum = 0;
for(auto i : vec){
	sum += i;
}
// maybe prefer std::accumulate or std::reduce
std::accumulate(vec.begin(), vec.end(), 0, [](auto value, auto item){return value + item;});
// or
std::reduce(vec.begin(), vec.end(), 0, [](auto value, auto item){return value + item;});
// or even parallelism
std::reduce(std::execution::par, vec.begin(), vec.end(), 0, [](auto value, auto item){return value + item;});

条款8------stl容器

  1. 使用合适的容器做合适的事情。
  2. 如果你不知道用哪个容器合适,就用vector。
  3. 如果知道vector将会用到的大小,先reserve。
  4. 尽量使用std::array,而不是原生数组,std::array可以直接使用stl算法,并且没有额外开销。
  5. 如果键值顺序无所谓,优先使用unordered_map。

条款9------传值

cpp 复制代码
// 需要修改
auto func(T& v) -> void;

// 只读,sizeof(T) >= 16,经验值
// 当T是float时,如果传const T&就很搞笑了,本身就只占4byte,现在变成了8byte(64位计算机)
auto func(const T& v) -> void; 

// 只读,sizeof(T) < 16
auto func(T v) -> void;

// 虽然没错,但没必要
auto func(const T v) -> void;

条款10------返回值

cpp 复制代码
// 返回局部变量,禁止返回引用。
auto func() -> T
{
	T val;
	// ...
	// RVO
	return val;
}

// 返回成员变量
struct A
{
	T val_;
	// 如果sizeof(T) < 16,经验值
	auto getValue() const noexcept -> T
	{
		return val_;
	}

	// 根据接收类型,会选择拷贝或者引用。
	auto getValue2() const noexcept -> const T&
	{
		return val_;
	}
};

条款11------默认参数

虽然个人不建议使用默认参数,但是不可否认默认参数的确一定程度上提供了不少方便。但是有一种情况下,坚决抵制使用默认参数,那就是基类虚函数。

cpp 复制代码
// 动态绑定会出现问题,不明白原理,会造成难以排查的错误。
class Base
{
public:
	virtual auto setValue(int v = 10) noexcept -> void
	{
		// ...
	}
}
class Derived: public Base
{
public:
	auto setValue(int v = 20) noexcept -> void override
	{
		// ...
	}
}

此外,可变引用不要带默认参数,虽然msvc能编过,但是标准不支持,是未定义行为。

cpp 复制代码
auto func(double& in_out = 5.f); // 达咩!!!

条款12------内存

  1. 优先栈分配,除非你能找到充分的理由向操作系统申请堆内存。
  2. 当申请堆内存时,尽量避免裸指针,除非你很强。
  3. 使用智能指针,unique_ptr优于shared_ptr,除非你找到充分的理由使用shared_ptr。

条款13------Trailing Comma

在数组或者枚举中,建议加上Trailing Comma。

cpp 复制代码
std::vector<int> v{0,1,2,3,};

// 特别是枚举,强烈建议加上Trailing Comma,会方便很多。
enum class Color
{
	Red,    ///< comment
	Green,  ///< comment
	Blue,   ///< comment
	Alpha,  ///< comment
}

条款14------使用nullptr而不是NULL

在c++代码中,空指针应该尽可能使用nullptr,而非NULL。

cpp 复制代码
#define NULL 1 // 使用NULL你就G了。

// 如果有两个重载函数,一个接收integer,一个接收指针。
auto fun(int) -> { std::print("{}", "This is an integer");}
auto fun(int*) -> { std::print("{}", "This is a pointer to an integer");}

auto p_int = NULL;
fun(p_int); // the first overload will be called.

auto p_int = nullptr;
fun(p_int); // the second overload will be called, which is generally expected.

条款15------不要把子线程detach掉,无论出于何种原因。

你找不到任何合情合理,并且负责任的理由,把子线程detach掉。将线程detach掉只会增加后续维护的困难度。C++ Core Guidelines也不建议使用detach。

CP.26: Don't detach() a thread

This rule sounds strange. The C++11 standard supports detaching a thread, but we should not do it! The reason is that detaching a thread can be quite challenging. As rule, C.25 said: CP.24: Think of an thread as a global container. Of course, this means you are magnificent if you use only variables with global scope in the detached threads. NO! Even objects with static duration can be critical. For example, look at this small program with undefined behavior.

cpp 复制代码
#include <iostream>  
#include <string>  
#include <thread>  
  
void func(){
  std::string s{"C++11"};
  std::thread t([&s]{ std::cout << s << std::endl;});
  t.detach();
}  
  
int main(){  
  func();  
}

This is easy. The lambda function takes s by reference. This is undefined behavior because the child thread t uses the variable s, which goes out of scope. STOP! This is the apparent problem but the hidden issue is std::cout. std::cout has a static duration. This means the lifetime of std::cout ends with the end of the program, and we have, additionally, a race condition: thread t may use std::cout at this time.

条款16------使用RAII的lock,不要直接lock和unlock

一般来说,要找到一个合适的unlock时机是不容易的,如果直接调用lock和unlock,很可能会导致程序死锁。

cpp 复制代码
// 不推荐的方式
auto fun() -> int
{
	mutex.lock();
	// your code
	// 此处可能会提前return,可能会抛出异常,这种情况下,mutex没有unlock,程序就死锁了。
	mutex.unlock();
	return 0;
}

// 推荐的方式
auto fun() -> int
{
	std::lock_guard<Mutex> guard(mutex);
	// your code
	// 此处无论是否提前return,或者抛出异常,都可以保证unlock会被调用。
	return 0;
}

// 需要手动解锁的时候
auto fun() -> int
{
	std::unique_lock<Mutex> u_lock(mutex);
	// your code
	u_lock.unlock();
	// your code
	u_lock.lock();
	// your code
	return 0;
}

// c++17 or later
auto fun() -> int
{
	// scoped_lock相较于lock_guard,支持多把锁。
	std::scoped_lock s_lock(mutex1, mutex2, mutex3);
	// your code

	return 0;
}

条款17------重视编译器警告

要特别重视编译器警告,不能当做没有看到。有些警告特别致命,相当于编译器告诉你,这里十有八九出错了,但是根据标准我不能管你,只能提示你,你自己看着办。这种类型的警告建议直接当做错误处理,我在这里列出了一些:

cmake 复制代码
/we4172 # 返回局部变量或临时变量的地址
/we4715 # 不是所有的控件路径都返回值
/we4265 # 类包含虚函数,但其不常用的析构函数不是虚函数;该类的实例可能无法进行正确析构
/we4390 # 找到空的受控语句;这是否是有意的?
/we4146 # 一元负运算符应用于无符号类型,结果仍为无符号类型
/we4308 # 负整型常量转换为无符号类型
/we4700 # 使用了未初始化的局部变量
/we4703 # 使用了可能未初始化的本地指针变量
/we4365 # "参数": 从"int"转换到"size_t",有符号/无符号不匹配
/we4245 # 从常量"int"转换到"size_t",有符号/无符号不匹配

举例:

cpp 复制代码
auto func() -> int&
{
	//we4172 # 返回局部变量或临时变量的地址
	int a = 0;
	return a;
}

auto func(bool b) -> int
{
	//we4715 # 不是所有的控件路径都返回值
	if(b){
		return 1;
	}
}

class VClass
{
public:
	virtual auto help() -> void = 0;
	~VClass(){}
	//we4265 # 类包含虚函数,但其不常用的析构函数不是虚函数;该类的实例可能无法进行正确析构
};

auto func(bool b)
{
	if(b);
	...
	//we4390 # 找到空的受控语句;这是否是有意的?
}

auto func()
{
	size_t a = 10;
	int b = -9;
	//we4146 # 一元负运算符应用于无符号类型,结果仍为无符号类型
	if(b > -a) 
	{
		
	}
}

auto func()
{
	unsigned int a = -10;
	//we4308 # 负整型常量转换为无符号类型
}

auto func()
{
	int a;
	++a;
	//we4700 # 使用了未初始化的局部变量
}

auto func(size_t size)
{
	int* p;
	if(size > 255)
	{
		p = new int(10);
	}
	if(p)
	{
		delete p;
	}
	//we4703 # 使用了可能未初始化的本地指针变量
	// 解决:int* p{nullptr};
}

auto func()
{
	int a = -10;
	unsigned int b = a;
	//we4365 # "参数": 从"int"转换到"size_t",有符号/无符号不匹配
}

auto func()
{
	const int a = -10;
	unsigned int b = a;
	//we4245 # 从常量"int"转换到"size_t",有符号/无符号不匹配
}

条款18------尽量使用标准c++,避免使用编译器的扩展功能,除非几个主流的编译器都支持的扩展功能。

使用MSVC的时候,可以加上/permissive-编译选项,强制让编译器遵守c++标准。

条款19------重写虚函数必须使用override关键字

c++ 复制代码
class Base
{
public:
	virtual auto listen(int k) -> void = 0;
	virtual ~Base(){}
};

class Derived: public Base
{
public:
	auto listen(int k) -> void override
	{
		// do something
	}
};

如果不写override,那么当Base类把listen的接口改变的时候,编译器不会报错,甚至多数编译器连警告都不会有,这个时候程序运行的结果就会出错。一旦写override,那么当Base类把listen的接口改变的时候,编译就会失败,就很容易修改了。

代码优化建议

条款1------Ahmdal定律

S p e e d U p = 1 ( 1 − f u n c c o s t ) + f u n c c o s t / f u n c s p e e d u p SpeedUp=\frac{1}{(1-func_{cost})+func_{cost}/func_{speedup}} SpeedUp=(1−funccost)+funccost/funcspeedup1

也就是说,如果某个函数占50%的运行时间,你把他速度提升到了原来的两倍,那么根据这个公式, S p e e d U p = 1 ( 1 − 0.5 ) + 0.5 / 2 = 1 0.75 = 4 3 = 1.33 3 ‾ SpeedUp = \frac{1}{(1-0.5)+0.5/2}=\frac{1}{0.75}=\frac{4}{3}=1.33\overline{3} SpeedUp=(1−0.5)+0.5/21=0.751=34=1.333也就是整体提速了1.333倍。

条款2------先保证正确,再进行优化

Premature optimization is the root of all evil. ------ Donald Knuth

过早优化是万恶之源

条款3------使用likely和unlikely

如果无法避免地要使用条件分支,那么做好用上likely和unlikely。

c++ 复制代码
if( a > 3 ) [[likely]] {
    std::cout<<"a is greater then 3"<<std::endl;
}else[[unlikely]]{
    std::cout<<"a is smaller then 3"<<std::endl;
}

上面的例子中,如果a>3的概率远大于a<=3的概率,那么加上likely和unlikely会帮助编译器更好地优化。

条款4------把多次访问的本地变量放到register中

c++ 复制代码
auto func()
{
	register int a = 1;
	while(condition)
	{
		++a;
	}
}

当然,编译器可能已经帮你优化了,don't bother.

消除重复

PS:机能一次,使用多次

cpp 复制代码
// .h
virtual bool hasFormat(const QString &mimetype) const;

//.cpp
bool QMimeData::hasFormat(const QString &mimeType) const
{
    return formats().contains(mimeType);
}

bool QMimeData::hasUrls() const
{
    return hasFormat(textUriListLiteral());
}

bool QMimeData::hasText() const
{
    return hasFormat(textPlainLiteral()) || hasUrls();
}

bool QMimeData::hasHtml() const
{
    return hasFormat(textHtmlLiteral());
}

bool QMimeData::hasImage() const
{
    return hasFormat(applicationXQtImageLiteral());
}
相关推荐
Navigator_Z8 分钟前
C++ //练习 14.23 为你的StrVec类定义一个initializer_list赋值运算符。
开发语言·c++·算法
EleganceJiaBao19 分钟前
【C语言】static 关键字
c语言·关键字·语法·static·类型
LSG_Dawn21 分钟前
C++ 99 之 容器存取
开发语言·c++
老花眼猫30 分钟前
简明万年历编制(C语言)
c语言·经验分享·青少年编程
EleganceJiaBao1 小时前
【C语言】union 关键字
c语言·关键字·语法·union·类型
C or Cpp2 小时前
C++协程
java·数据结构·c++
程序喵阿呆2 小时前
leetcode 162 寻找峰值
数据结构·c++·算法·leetcode
十五年专注C++开发2 小时前
深入理解C++中的锁
开发语言·c++·高质量编程
mq白2 小时前
zxing-cpp+OpenCV根据字符串生成条形码
c++·人工智能·opencv·计算机视觉
托马斯-木2 小时前
配置Eclipse的C++环境
c++·eclipse