C++ 编程技巧笔记记录(持续更新)
2019-09-30 06:38:21来源:博客园 阅读 ()
C++ 编程技巧笔记记录(持续更新)
目录
- 类/对象
- 1.多态基类的析构函数应总是public virtual,否则应为protected
- 2.编译器会隐式生成默认构造,复制构造,复制赋值,析构,(C++11)移动构造,(C++11)移动赋值的inline函数
- 3.不要在析构函数抛出异常,也尽量避免在构造函数抛出异常
- 模板
- 1. 不要偏特化模板函数,而是选择重载函数。
- 2.(C++11)不要重载转发引用的函数,否则使用其它替代方案
- 函数
- 1.(C++11)禁用某个函数时,使用 = delete而非private
- 2.(C++11)lambda表达式一般是函数对象。特殊地,在无捕获时是函数指针。
- 3.(C++11)尽可能使用lamada表达式代替std::bind
- 4.(C++11)使用lambda表达式时,避免默认捕获模式
- 内存相关
- 1.检查new是否失败通常是无意义的。
- 2.尽量避免多次new同一种轻量级类型,而是先new一个大区域再分配多次。
- STL标准库
- 1.(C++11)使用emplace/emplace_back/emplace_front而不是insert/push_back/push_front
- 2.在遍历容器时删除迭代器需谨慎
- 3.容器的at()会检查边界,[]则不检查边界
- 4.sort()的< 比较操作符,若两者相等则必须返还失败。
- 5.永远记住,更低的时间复杂度并不意味着更高的效率
- 6.需要深度优化时,使用自定义STL分配器
- 优化与效率
- 1.尽可能使用 ++i 而不是 i++
- 2.在后期遇到性能瓶颈,万不得已时才使用inline
- 3.尽量不使用dynamic_cast并且禁用RTTI
- 4.(C++11)只要潜在编译期可计算的函数/变量,就使用constexpr
- 异常
- 1.(C++11)若保证异常不会抛出,应使用noexpect异常规格,否则不要声明异常规格。
- 杂项
- 1.(C++11)使用nullptr而不是NULL或0
- 2.(C++11)使用enum class语法为枚举类型提供限定范围
- 3.(C++11)auto只能推导出类型型别,而decltype能够推导出声明型别
- 4.(C++17)需要用到任意可变的类型时,使用std::any,std::variant而不是union
- 参考
前言:C++是博大精深的语言,特性复杂得跟北京二环一样,继承乱得跟乱伦似的。
不过它仍然是我最熟悉且必须用在游戏开发上的语言,这篇文章用于挑选出一些个人觉得重要的条款/经验/技巧进行记录总结。
文章最后列出一些我看过的C++书籍/博客等,方便参考。
其实以前也写过相同的笔记博文,现在用markdown”重置“一下。
类/对象
1.多态基类的析构函数应总是public virtual,否则应为protected
当要释放多态基类指针指向的对象时,为了按正确顺序析构,必须得借助virtual从而先执行析构派生类再析构基类。
当基类没有多态性质时,可将基类析构函数声明protected,并且也无需耗费使用virtual。
2.编译器会隐式生成默认构造,复制构造,复制赋值,析构,(C++11)移动构造,(C++11)移动赋值的inline函数
当你在代码中用到以上函数时且没有声明该函数时,就会默认生成相应的函数。
特殊的,当你声明了构造函数(无论有无参数),都不会隐式生成默认构造函数。
不过隐式生成的函数比自己手写的函数(即使行为一样)效率要高,因为经过了编译器特殊优化。
(c++11)当你需要显式禁用生成以上某个函数时,可在函数声明尾部加上 = delete ,例如:
Type(const Type& t) = delete;
(c++11)当你需要显式默认生成以上某个函数时,可在函数声明尾部加上 = default ,例如:
Type(Tpye && t) = default;
3.不要在析构函数抛出异常,也尽量避免在构造函数抛出异常
析构函数若抛出异常,可能会使析构函数过早结束,从而可能导致一些资源未能正确释放。
构造函数若抛出异常,则无法调用析构函数,这可能导致异常发生前部分资源成功分配,却没能执行析构函数的正确释放行为。
模板
1. 不要偏特化模板函数,而是选择重载函数。
编译器匹配函数时优先选择非模板函数(重载函数),再选择模板函数,最后再选择偏特化模板函数。
当匹配到某个模板函数时,就不会再匹配选择其他模板函数,即使另一个模板函数旗下有更适合的偏特化函数。
所以这很可能导致编译器没有选择你想要的偏特化模板函数。
2.(C++11)不要重载转发引用的函数,否则使用其它替代方案
转发引用的函数是C++中最贪婪的函数,容易让需要隐式转换的实参匹配到不希望的转发引用函数。(例如下面)
template<class T>
void f(T&& value);
void f(int a);
//当使用f(long类型的参数)或者f(short类型的参数),则不会匹配int版本而是匹配到转发引用的版本
替代方案:
- 舍弃重载。换个函数名或者改成传递const T&形参。
- 使用更复杂的标签分派或模板限制(不推荐)。
函数
1.(C++11)禁用某个函数时,使用 = delete而非private
原因有4个:
- private函数仍需要写定义(即使那是空的实现),
- 派生类潜在覆盖禁用函数名的可能性,
- “=delete”语法比private语法更直观体现函数被禁用的特点,
- 在编写非类函数的时候,无法提供private属性。
一般 = delete的类函数应为public,因为编译器先检测可访问性再检验禁用性
2.(C++11)lambda表达式一般是函数对象。特殊地,在无捕获时是函数指针。
编译器编译lambda表达式时实际上都会对每个表达式生成一种函数对象类型,然后构造出函数对象出来。
特殊地,lambda表达式在无任何捕获时,会被编译成函数,其表达式值为该函数指针(毕竟函数比函数对象更效率)。
因此在一些老旧的C++API只接受函数指针而不接受std::function的时候,可以使用无捕获的lamdba表达式。
3.(C++11)尽可能使用lamada表达式代替std::bind
直接举例说明,假设有如下Func函数:
void Func(int a, float b);
现在我们让Func绑定上2.0f作为参数b,转化一个void(int a)的函数对象。
std::function<void(int)> f;
float b = 2.0f;
//std::bind写法
f = std::bind(Func, std::placeholders::_1, b);
f(100);
//lambda表达式写法
f = [b](int a) {Func(a, b); };
f(100);
可以看到使用std::bind会十分不美观不直观,还得注意占位符位置顺序。
而使用lambda表达式可以让代码变得十分简洁优雅。
4.(C++11)使用lambda表达式时,避免默认捕获模式
按引用默认捕获容易造成引用空悬,而显示的引用捕获更能容易提醒我们捕获的是哪个变量的引用,从而更容易理清该引用的生命周期。
按值默认捕获容易让人误解lambda式是自洽的(即不依赖外部)。下面是一个典型例子:
void test() {
static int a = 0;
auto func = [=]() {
return a + 2;
};
a++;
int result = func();
}
由于默认捕获,你以为a是以按值拷贝过去,所以期待result总会会是2。但是实际上你是调用了同一个作用域的静态变量,没有拷贝的行为。
所以,无论是按值还是引用,都尽量指定变量,而不是用默认捕获。
内存相关
1.检查new是否失败通常是无意义的。
new几乎总是成功的,现代大部分操作系统采取进程的惰式内存分配(即请求内存时不会立即分配内存,当使用时才慢慢吞吞分配)。
所以当使用new时,通常不会立即分配内存,从而无法真正检测到是否内存将会耗尽。
2.尽量避免多次new同一种轻量级类型,而是先new一个大区域再分配多次。
每次new的时候,实际上还会额外分配出一个存放内存信息的区域,而多次分配内存给轻量级类型时,会造成臃肿的内存信息。
而且在删除这些区域时,很容易造成很多块内存碎片,导致内存利用率不高。
所以应当使用内存池的方式,先new一大块区域,再从区域分配内存给轻量级类型。
STL标准库
1.(C++11)使用emplace/emplace_back/emplace_front而不是insert/push_back/push_front
emplace 最大的作用是避免产生不必要的临时变量,因为它可以直接在容器相应的位置根据参数来构造变量。
而 insert / push_back / push_front 操作是会先通过参数构造一个临时变量,然后将临时变量移动到容器相应的位置。
2.在遍历容器时删除迭代器需谨慎
顺序式容器删除迭代器会破坏本身和后面的迭代器,节点式容器删除迭代器会破坏本身,导致循环遍历崩溃(循环遍历依赖于容器原有的迭代器)。
两个值得借鉴的正确做法:
auto it = vec.begin();
while (it != vec.end()){
if (...){
// 顺序式容器的erase()会返回紧随被删除元素的下一个元素的有效迭代器
it = vec.erase(it);
}
else{
it++;
}
}
auto it = list.begin();
while (it != list.end()){
if (...) {
t.erase(it++);
}
else {
it++;
}
}
3.容器的at()会检查边界,[]则不检查边界
STL小细节。另外std::vector<bool>和std::bitset的[]提供的是值拷贝,而不是引用。
4.sort()的< 比较操作符,若两者相等则必须返还失败。
STL的sort算法基本是快排,是不稳定的排序。
若比较的两者相等时返还成功,则不稳定排序容易出现死循环,从而导致程序崩溃。
5.永远记住,更低的时间复杂度并不意味着更高的效率
STL容器,特别是set,map,有着很多O(logN)的操作速度,但并不意味着是最佳选择,因为这种复杂度表示往往隐藏了常数很大的事实。
例如说,集合的主流实现是基于红黑树,基于节点存储的,而每次插入/删除节点都意味着调用一次系统分配内存/释放内存函数。这相比vector等矢量容器所有操作仅一次系统分配内存(理想情况来说),实际上就慢了不少。
此外,矢量容器对CPU缓存更加友好,遍历该种容器容易命中缓存,而节点式容器则相对容易命中失败。
综合上述,如果要选择一个最适合的容器,那么不要过度信赖时间复杂度,除非你十分彻底的了解STL容器,或对各容器进行多次效率测试。
6.需要深度优化时,使用自定义STL分配器
每个STL容器都会要求提供一个Allocator类型作为该容器的节点分配器,不提供时使用STL默认的缺省分配器。
template<typename T, class Allocator = allocator<T>>
class list {...};
默认缺省分配器的行为往往是简单粗暴的new delete,这可能带来一些效率问题和内存碎片问题。
而通过自己定制分配器,我们可以把STL容器的内存分配达到如下策略:
类型 | 策略描述 |
---|---|
固定大小的缓冲池 | 所有内存分配都是一样大小,减少每次分配内存浪费。 |
共享内存 | 分配使用共享内存。 |
多个堆 | 分配使用不同的堆,试分配大小和类型而定。 |
单线程的 | 分配和释放均不保证线程安全。 |
垃圾回收 | 调用释放的时候并不立即释放,调用垃圾回收函数时才释放。 |
基于栈的策略 | 所有内存都是在栈上,适用于短生命期的容器对象。 |
静态内存 | 分配的内存位于程序的静态内存区里。 |
从不删除 | 调用释放的时候不释放内存,程序结束时才回收内存。 |
一次性删除 | 调用释放的时候不释放内存,通过定制函数来释放内存。 |
边界对齐策略 | 为了满足某些条件,内存边界总是对齐分配。例如在SSE中使用指令对齐内存的时候。 |
调试 | 分配记录、检查内存泄漏、检查内存覆盖情况、峰值分配大小等等。 |
优化与效率
1.尽可能使用 ++i 而不是 i++
这个是老生常谈的C++经典问题,对于int/unsigned等内置类型时,++i与i++似乎在效率上没有区别。
然而在使用迭代器或其他自定义类型时,i++往往还得创建一个额外的副本来用于返还值,而++i则直接返还它本身。
2.在后期遇到性能瓶颈,万不得已时才使用inline
现代编译器已经十分智能,很多时候该写成inline的函数编译器会自动帮你inline,不该inline的时候即使你显式写了inline编译器也有可能认为不该inline。
也就是说显式的写出inline只是给编译器一个建议,它不一定会采纳。
因此在开发时不用过早优化,过早考虑inline,而是遇到性能瓶颈时才考虑使用显式写出inline,不过大部分这时候你更应该考虑的是你写的算法效率。
3.尽量不使用dynamic_cast并且禁用RTTI
依靠dynamic_cast的代码往往可以用多态虚函数解决,而且多态虚函数更加优雅。因此,尽可能避免编写dynamic_cast。
另外可以随之禁用与dynamic_cast相关的RTTI特性,禁用该特性可以提升程序效率(每个类少一些臃肿的RTTI信息)。
4.(C++11)只要潜在编译期可计算的函数/变量,就使用constexpr
constexpr能让一些函数/变量在编译期就可计算,可减少运行期运算。(可视作模板元运算的美化语法)
此外,constexpr如果接受的是运行期变量/参数,则会变成运行期计算。
也就是说它既可用作编译期运算,也可运行期运算,语境作用域比非constexpr更广。
异常
1.(C++11)若保证异常不会抛出,应使用noexpect异常规格,否则不要声明异常规格。
无声明异常规格,意思是可能抛出任何异常。
相比无声明异常规格的函数,noexpect函数能得到编译器的优化(发生异常时不必解开栈),且能清晰表示自己的无异常保证。
杂项
1.(C++11)使用nullptr而不是NULL或0
NULL是C语言遗留的东西,是将宏定义成0的,容易造成指针和整数的二义性。
而nullptr很好的避免了整数的性质。
2.(C++11)使用enum class语法为枚举类型提供限定范围
C带来的enum语法是允许枚举类进行隐式转换的,潜在造成程序员不希望发生的转换。
而C++11的enum class会阻止隐式转换,需要程序员显示转换
enum class Color{Red,Blue,Green};
Color color = Color::Red;
int i = static<int>(color);
3.(C++11)auto只能推导出类型型别,而decltype能够推导出声明型别
int& value = 233;
auto a = value;//auto推导出是int类型
decltype(auto) b = value; //decltype(auto)是int&类型
也就是说auto推导出的类型会抛弃引用性质,而decltype能够推导出完整的声明类型。
此外一提,auto是声明类型的语法,而decltype()是一个表达式(类似于sizeof()),表达式的值是类型。
4.(C++17)需要用到任意可变的类型时,使用std::any,std::variant而不是union
union是从c继承来的特性,它的成员不可以是带构造函数/析构函数/自定义复制构造函数的c++类。
因此在需要万能变量的时候最好不要使用union,而是用std::any或std::variant ,目前C++17已引入<any>库和<variant>库。
万能变量是指可以转换任意类型(可扩展,如metadata)的变量,如果只固定在几个类型之间转换的使用union是个效率更优的选择。
参考
- 《C++ Primer Plus》:当初入门C++语言的书籍。
- 《C++程序设计语言(特别版)》:C++之父编写的入门教材,但实际上更应该算为介于入门与进阶之间的工具书(用于查询语法)。
- 《Effective C++》:C++ 进阶书,深入理解与经验
- 《More Effective C++》:C++ 进阶书,深入理解与经验
- 《深度探索C++对象模型》:C++ 进阶书,深入理解
- 《Expectional C++》:C++ 进阶书,深入理解与经验
- 《高速上手 C++11/14/17》:C++11/14/17 入门书,介绍C++11/14/17各项新特性的基础用法,它目前只有电子版本: https://github.com/changkun/modern-cpp-tutorial/blob/master/book/zh-cn/toc.md
- 《Effective Modern C++》:C++11/14 进阶书,介绍C++11/14部分新特性的深入理解与经验。
- 《游戏编程精粹》2/3/7:游戏编程综合技术书,有部分章节讲C++的经验。
C++是非常非常复杂的语言,了解得越多就越发觉得自己的无知(例如C++ Boost)。
但是在学习C++的中途也必须认识到,C++是一门工具,不要过多钻C++语言的牛角尖。
谨记:程序员是要成为工程师而不是语言学家。
原文链接:https://www.cnblogs.com/KillerAery/p/11601229.html
如有疑问请与原作者联系
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:数据结构、算法及应用
下一篇:C++类拷贝控制 深拷贝 浅拷贝
- C++ 转换函数搭配友元函数 2020-06-10
- C++ 自动转换和强制类型转换(用户自定义类类型) 2020-06-10
- C++ rand函数 2020-06-10
- C++ 友元函数 2020-06-10
- C++ 运算符重载 2020-06-10
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash