该篇文章是我自己的经验总结,不可能100%适合读者,当然相关的C++工程实践书籍类似《Effective C++》,《More Effective C++》,《Modern Effective C++》,《Learning C++ Best Practices》 《Google C++ Coding Style》等等可能都有类似描述,我这篇文章可能也是从以上的书籍文章汲取了一些。
// Bad Idea
// 这么做虽然没错,能按照预想运行,但是会给后来人员造成迷惑
for (int i = 0; i < 15; ++i)
std::cout << i << std::endl;
// Bad Idea
// 这就有错了,std::cout没在循环内,变量i也不是循环内的,与预想不一致
int sum = 0;
for (int i = 0; i < 15; ++i)
++sum;
std::cout << i << std::endl;
// Good Idea
// 这个语义就完全正确了。
int sum = 0;
for (int i = 0; i < 15; ++i) {
++sum;
std::cout << i << std::endl;
}
11.限制代码列的字符数
一般推荐是80-100个字符之间,我自己是80。一般IDE和文本编辑器都可以强制限制。
// Bad Idea
// 难阅读
if (x && y && myFunctionThatReturnsBool() && caseNumber3 && (15 > 12 || 2 < 3)) {
}
// Good Idea
// 逻辑思路跟得上了,容易阅读
if (x && y && myFunctionThatReturnsBool()
&& caseNumber3
&& (15 > 12 || 2 < 3)) {
}
12.使用""包含本地头文件
<> 是保留给标准库和系统库头文件的,自己写的本地头文件#include "MyHeader.h"
13.初始化成员变量
最好用初始化成员列表来初始化。
// Bad Idea
class MyClass
{
public:
MyClass(int t_value)
{
m_value = t_value; //这是赋值,而不是初始化
}
private:
int m_value;
};
// Good Idea
// C++ 初始化成员列表是C++语言特有的,这样写代码更加清晰干净,
// 而且还有潜在的性能提升,因为初始化和赋值不是一个概念。
// 《Effective C++》也提到过
class MyClass
{
public:
MyClass(int t_value)
: m_value(t_value)
{
}
private:
int m_value;
};
// Best Idea
// ... //
private:
int m_value{ 0 }; // allowed
unsigned m_value_2 { -1 }; // compile-time error, narrowing from signed to unsigned.
// ... //
// If the parameter is input parameter, not output
// Bad Idea
class MyClass
{
public:
void do_something(int i);
void do_something(std::string str);
};
// Good Idea
class MyClass
{
public:
void do_something(const int i);
void do_something(const std::string &str);
};
// Very Bad Idea
class MyClass
{
public:
explicit MyClass(const int& t_int_value)
: m_int_value(t_int_value)
{
}
const int& get_int_value() const
{
return m_int_value;
}
private:
int m_int_value;
}
// Good Idea
class MyClass
{
public:
explicit MyClass(const int t_int_value)
: m_int_value(t_int_value)
{
}
int get_int_value() const
{
return m_int_value;
}
private:
int m_int_value;
}
// Bad Idea
MyClass *myobj = new MyClass;
// ...
delete myobj;
// Good Idea
auto myobj = std::make_unique<MyClass>(constructor_param1, constructor_param2); // C++14
auto myobj = std::unique_ptr<MyClass>(new MyClass(constructor_param1, constructor_param2)); // C++11
auto mybuffer = std::make_unique<char[]>(length); // C++14
auto mybuffer = std::unique_ptr<char[]>(new char[length]); // C++11
// or for reference counted objects
auto myobj = std::make_shared<MyClass>();
// ...
// myobj is automatically freed for you whenever it is no longer used.
std::vector<DeviceCollect::st_port> tcp_ports;
// Very Bad Idea
for(int i = 0; i < tcp_ports.size(); ++i)
{
//do_something
tcp_ports[i];
}
// range for , foreach semantics
// Not a Bad Idea
for(const auto & prot : tcp_ports)
{
//do_something
port;
}
// Very Good Idea
tcp_ports.erase(
std::remove_if(std::begin(tcp_ports), std::end(tcp_ports),
[](const DeviceCollect::st_port & o) -> bool { return o.processPath.compare(std::string("空")) == 0; }),
std::end(tcp_ports));
// This
std::vector<ModelObject> mos{mo1, mo2};
// -or-
auto mos = std::vector<ModelObject>{mo1, mo2};
// Don't do this
std::vector<ModelObject> mos;
mos.push_back(mo1);
mos.push_back(mo2);
std::shared_ptr<ModelObject_Impl>(new ModelObject_Impl());
// should become
std::make_shared<ModelObject_Impl>(); // (it's also more readable and concise)
// Good Idea
for (int i = 0; i < 15; ++i)
{
MyObject obj(i);
// do something with obj
}
// Bad Idea
MyObject obj; // 无意义的对象初始化
for (int i = 0; i < 15; ++i)
{
obj = MyObject(i); // 无必要的赋值操作
// do something with obj
}
// obj还继续占用内存,直到离开它自身的作用范围
// Bad Idea
auto f = std::bind(&my_function, "hello", std::placeholders::_1);
f("world");
// Good Idea
auto f = [](const std::string &s) { return my_function("hello", s); };
f("world");
title: C++最佳实践 date: 2017-02-22 08:58:23 tags:
软件工程
工欲善其事必先利其器
现在软件工程越来越发达,C++的标准也一直在改进更新,这门在大众来看的“古老”语言也在慢慢变得更加像现代编程语言一样了,现代软件工程持续交付,持续集成等工具概念层出不穷,这里我想推荐些工具给读者改进优化项目开发流程
Cmake,最好的C++跨平台构建工具,没有之一,automake,qmake在它面前黯然失色。
Travis CI, 这个是持续集成工具,能在Github上很好的工作。
CppCheck, C/C++ 静态分析工具,免费的,能查出很多类型缺陷,内存泄漏和资源泄漏。当然还有很多语言的静态分析工具,如果有兴趣,请看这里
另外,在个人项目和公司项目中,C++编译器,无论在g++,MSVC,或者clang上,请把警告级别调整到最高。MSVC我是调整到W4级别,g++上,由于本人不熟悉g++的警告类型,那么请开-Wall -Wextra警告并严格观察,另外g++上还可以开-Weffc++选项,编译器会按照《Effective C++》的实践规范来检查代码的隐患。这些都是很重要,编译器的警告很重要!!要好好利用静态类型语言带来的优点。最后把警告尽量消除到0 warnings为止! 最后的最后,用C++的静态分析工具检查一遍所有的源码,选择性的消除工具报告出来的缺陷。这样你会发现,后期的软件的运行时的BUG会少很多,特别是不明不白的crash。
正文
1.基本C++命名规范
另外C++标准库和Boost采用另一种规范,如果你的代码与标准库和Boost混合写契合度很高,推荐用以下的规范:
2.区分私有成员变量
当然,个别一些习惯,是在私有成员变量后加下划线后缀: object_
3.区分函数参数
当然,代码最重要的还是要与CodeBase一致,最终看公司的规范,这里只是一个样例,t可以认为是“the”的缩写。这只是区分函数参数与局部变量的一种策略。
4.任何命名不能是下划线开头
如果你这么做,那么可能会与编译器的扩展关键字造成冲突。如果好奇,那么请看stackoverflow的这个讨论。
5.一个良好的样例
6.空指针的表示请用nullptr
C++ 11 中的空指针是一个特定的值,用以代替0或NULL。 如果好奇,请看知乎的讨论, 不然值会有二义性。另外,知乎上还有讨论2。
7.注释
优先使用//来注释代码块,不要使用/**/
8.不要在头文件中使用using namespace
这会导致using的名字空间污染范围扩散,因为使用了这个头文件的源文件都隐式使用这个名字空间了,这将来容易造成名字空间的冲突,该错误查找困难。不利于后来开发人员维护。
9.头文件守护
这个想必很多人已经习惯了,不过如果不这样做的危害还是要说一下,这样可以防止头文件被重复包含多次而造成的问题,也能解决意外包含其他工程头文件的冲突。
10.代码块一定要用{}
如果不这么做,可能会导致一些语义错误。
11.限制代码列的字符数
一般推荐是80-100个字符之间,我自己是80。一般IDE和文本编辑器都可以强制限制。
12.使用""包含本地头文件
<> 是保留给标准库和系统库头文件的,自己写的本地头文件#include "MyHeader.h"
13.初始化成员变量
最好用初始化成员列表来初始化。
好奇请戳知乎的讨论。
当然,在C++ 11中,你可以考虑总是给成员变量一个默认值,
使用大括号初始化,因为它在编译时不允许数据收窄。
优先使用大括号初始化,除非有原因的特殊要求不那么做。
14.总是使用名字空间
在C语言时代,很多库的开发者,为了防止函数符号链接时冲突,就在函数名加入库名称的前缀,比如OpenCV的函数都是cv_xxxx。当然,这是历史原因,如果是采用C++ 编译器,就应该使用namespace防止符号冲突,采用boost库的方式。
15.使用标准库提供的正确的整型类型
在C++中,最好不要出现int类型,最好是intxxx_t , uintxxx_t。 表示大小请使用std::size_t。
可以看这里的参考,这样提高可移植性,因为在不同类型的平台上,这些类型会typedef到特定类型上去。
注意: signed char 保证至少 8 位,int 保证至少 16 位,long 保证至少 32 位,long long 保证至少 64 位。
如果还不明白,去看stackoverflow的讨论
16.Tab和空格不要混合使用
这个绝对禁止,应该从编辑器和IDE里面更改设置,比如让Tab等于4个空格。至于设置Tab等于多少个空格合理,这个是个人喜好问题,不然就是Emacs和Vim之争了。
17.不要害怕模版
这个,我对于C++的模版元编程不熟悉,就不做过多讨论了。模版可以说是另外一种语言,另一种“函数式”语言。它是图灵完备(Turing-Complete)的。
感兴趣可以在网络上看到各种玩法:
18.慎用操作符重载
重载操作符是个很方便的特性,比如用C++ 实现一个BigInteger类,一个类的实体表示一个大整数,如果给类实现一个add函数表示大整数做加法,a.add(b),那么太不直观了,a + b更加方便直观。另一个官方例子就是std::string重载了+号操作符,以便字符串的拼接。当然,重载操作符本质上还是调用函数。
虽然方便,但是如果随意重载操作符,可能导致许多诡异的错误,详情见这里的讨论。
尤其是,当需要编写重载操作符的时候,应该把以下几点时时刻刻记住在脑子里:
剩下给出一个重载操作符的参考资料
19.消除隐式转换
20.考虑零原则(Rule of Zero)
该原则声称,不要提供任何编译器能自动生成的函数(拷贝构造,拷贝赋值构造,移动构造,移动赋值构造,析构函数),除非该类需要构造一些关于所有权(ownership)的概念。
该原则的目的是让当更多的成员变量被添加到该类时,让编译器提供最佳的版本,换句话说,这些东西最好让编译器来维护。
该原则的背景在这里,然后这篇文章做了实现并解释了技术。
21.尽可能多的用const修饰
const 告诉编译器方法或变量是不可变(immutable)的,这也能帮助编译器优化代码,同时也能帮助开发者知道函数是否有副作用(side-effect)。另外,常引用(const &)也能阻止编译器进行不必要的拷贝代价。感兴趣的可以看这篇文章底下John Carmack的评论。
22.小心函数返回的类型
Getters:
临时和局部变量:
23.不要用常引用(const &)来传递或返回一个基本类型变量
为什么呢? 因为对于基本类型变量用引用传递或返回会导致指针操作,这会更加慢。如果通过值传递,是利用处理器寄存器直接传递的,会更加快。
24.避免裸指针访问内存
直接内存分配和释放在C++中做到完全消除内存错误和内存泄漏是很难的,要善于利用RAII,C++ 11提供了智能指针这种方便的工具,基本可能杜绝内存泄漏。
25.用std::array 或 std::vector 代替C风格的数组
这两个都使用内存连续分配的,可以完全代替C数组。如果想获得最原生C数组的性能,可以用std::array,它还可以结合STL的算法,非常方便。另外,避免使用std::shared_ptr来指向一个数组,用std::unique_ptr代替。
26.使用C++风格的转换代替C风格的转换
使用C++风格的转换(static_cast<> dynamic_cast<>)代替C风格的转换,C++风格的转换编译器会加入更多的检查,类型更加安全。
27.不要定义可变参函数
类似printf,可变参的函数不是类型安全的,错误的输入可能导致异常终止,或者让程序产生未定义行为,未定义行为会导致软件的安全问题,如果你有支持C++ 11的编译器,请使用变参模版来代替。
附加内容: 如何防止下一个类似HeartBleed的漏洞
28.避免使用宏
宏是预处理器(preprocessor)做的事,这个事件在编译器开始编译代码之前,这会让编译器少做了类型检查,因为宏本质是个文本替换,如果出现BUG,调试器很难找出问题源头所在。
29.函数参数尽量避免bool类型
这对于阅读代码来说,不会增加更多有意义的信息,有了bool变量,就说明函数的输出依赖了这个条件,导致做了过多的事情,一个函数尽量保证只做一件事并做好一件事。如果遇到需要这样写的函数,就把bool的条件分离出一个单独的函数来处理,或者传入枚举类型,让参数更有意义。
这篇文章讲述了更多的细节,以及如何解决。
30.尽量避免裸循环
使用C++ 新标准提供的更加高级的语义。
31.正确使用关键字override和final
这些关键字能帮助开发者更清晰的理解虚函数是怎样被使用的,如果虚函数的函数签名有变化,override关键字也能捕获一些潜在的错误,导致编译出错。也能提示编译器如何更好的优化虚函数。
32.了解使用的类型
这个在条款15也提到过,这里会给出一个更详细的参考。
33.避免全局数据
全局数据会导致函数间的无法意料的副作用,也会导致代码间难以并行化,或者不可能并行化。
34.避免静态变量
除了全局数据,静态数据的构造和析构可能不总是能按照你预想的进行。如果你的程序是跨平台的,那么这个噩梦可能成真,请看这么个例子,一个g++的BUG引出了共享静态数据析构顺序的一个问题。这个共享静态数据是从动态库中加载的。
35.shared_ptr
该指针几乎等同于全局变量,因为它可以让不同的代码访问相同的数据。
36.单例模式
单例模式一般都需要保证线程安全,因为单例一般实现是static变量和shared_ptr实现的。
37.互斥量和可变量要一起使用
更多详情,请看这里。
38.需要的时候就前置声明
用这段代码:
代替以下代码:
如果类是模版类,可以这么写:
这样在编译器重新构建依赖的时候,可以有效降低编译时间。
39.避免不必要的模版实例化
模版实例化不是没有代价的,过多的模版实例化,每种类型实例化为一份代码,那么会造成编译出的机器码膨胀,也会加大编译时间。
更多详情,请看这篇文章。
40.避免递归模版实例化
递归会让模版实例化瞬间变多,加重编译器负担,造成更加难以理解的代码。
问题解决请看这篇文章。
41.不要包含无用的头文件
降低编译依赖,减少编译时间。
42.使用初始化列表
初始化列表能带来更大性能的提升,减少对象的拷贝和容器的resize
43.使用move语义
move语义是C++ 11的最大革新了。
相关文章:
对于大多数代码,只要这样就可以了:
但是MSVC2013不支持,MSVC2015支持。
44.减少shared_ptr的拷贝
该智能指针允许共享,类似实现Java中的引用计数来保证无资源泄漏,但是拷贝的代价很高,yi你用计数必须是原子的和线程安全的。
45.尽可能降低拷贝或重新赋值
更加复杂的场景可以这样:
IIFE in C++ 11 请看这里。
46.不要大量使用异常
这个《Google C++ Coding Style》上有说,这里就不多做解释了。
47.避免使用new
48. 优先使用unique_ptr
如果能用unique_ptr解决的场景,就不要用shared_ptr。
当前的最佳实践就是,从一个工厂函数中返回一个unique_ptr,如果有需要,再从unique转成shared。
49.限制变量作用范围
变量应该尽可能的推迟声明,什么时候需要用到再声明,不能像C89那样,全部放在函数开头。这样也影响阅读性。降低变量作用范围同时降低内存的使用,帮助编译器生成更高效的代码。
50.优先使用double,而不是float
当然了,这也得看场景和编译器的优化水平了,double可能比float更高效。 使用float,因为精度更低,所以在转换中可能更低效。当然,如果在向量化操作中,如果你牺牲精度而换取高性能,float可能会更加高效。
double是C++浮点数的默认类型
51.优先使用++i而不是i++ (可以废弃)
当然,现代编译器优化水平相当高了,在for循环中,两者优化出来的机器码都是一样的,但是你无法保证在个别的编译器平台上能这样。
52. char是char,string是string
前者会被编译器解析成const char*, 当写入流的时候,会检查末尾'\0'结束符。后者被解释成单个字符,省去了CPU很多操作。
如果在特别场景下大量使用前者,可能前者会慢慢变成性能瓶颈。
53.不要使用std::bind
std::bind 在lambda表达式出现之前可以用,但是之后就没有多大用了。大多数情况下,可以用lambda表达式代替。
54.善用脚本,拥抱其他语言和框架
静态类型语言与动态类型语言结合才更强劲,参考使用boost::python。 该用什么语言完成的工作就用那种语言完成。写Web就用Java拉,前端就javascript啦,不要折腾没用的。
当你每次鄙视一种语言的时候,就失去了一次向它学习的机会。