华为程序员总结:C/C++的陷阱与套路,当年就是折在这些地儿…

本文结合做者的工做经验和学习心得,对C++语言的一些高级特性,作了简单介绍;对一些常见的误解,作了解释澄清;对比较容易犯错的地方,作了概括总结;但愿借此能增进你们对C++语言了解,减小编程出错,提高工做效率。java

1、导语

C++是一门被普遍使用的系统级编程语言,更是高性能后端标准开发语言;C++虽功能强大,灵活巧妙,但却属于易学难精的专家型语言,不只新手难以驾驭,就是老司机也容易掉进各类陷阱。c++

本文结合做者的工做经验和学习心得,对C++语言的一些高级特性,作了简单介绍;对一些常见的误解,作了解释澄清;对比较容易犯错的地方,作了概括总结;但愿借此能增进你们对C++语言了解,减小编程出错,提高工做效率。程序员

 

2、陷阱

个人程序里用了全局变量,为什么进程退出会莫名其妙的core掉?算法

Rule:C++在不一样模块(源文件)里定义的全局变量,不保证构造顺序;但保证在同一模块(源文件)里定义的全局变量,按定义的前后顺序构造,按定义的相反次序析构。数据库

咱们程序在a.cpp里定义了依次全局变量X和Y;编程

按照规则:X先构造,Y后构造;进程中止执行的时候,Y先析构,X后析构;但若是X的析构依赖于Y,那么core的事情就有可能发生。后端

结论:若是全局变量有依赖关系,那么就把它们放在同一个源文件定义,且按正确的顺序定义,确保依赖关系正确,而不是定义在不一样源文件;对于系统中的单件,单件依赖也要注意这个问题。设计模式

 

std::sort()的比较函数有很强的约束,不能乱来数组

相信工做5年以上至少50%的C/C++程序员都被它坑过,我已经听到过了无数个悲伤的故事,《圣斗士星矢》,《仙剑》,还有别人家的项目《每天爱消除》,都有人掉坑,程序运行几天莫名奇妙的Crash掉,一脸懵逼。安全

若是要用,要本身提供比较函数或者函数对象,必定搞清楚什么叫“严格弱排序”,必定要知足如下3个特性:

非自反性

非对称性

传递性

尽可能对索引或者指针sort,而不是针对对象自己,由于若是对象比较大,交换(复制)对象比交换指针或索引更耗费。

 

注意操做符短路

考虑游戏玩家回血回蓝(魔法)刷新给客户端的逻辑。玩家每3秒回一点血,玩家每5秒回一点蓝,回蓝回血共用一个协议通知客户端,也就是说只要有回血或者回蓝就要把新的血量和魔法值通知客户端。

玩家的心跳函数heartbeat()在主逻辑线程被循环调用

 

若是GenHP回血了,就返回true,不然false;不必定每次调用GenHP都会回血,取决因而否达到3秒间隔。

若是GenMP回蓝了,就返回true,不然false;不必定每次调用GenMP都会回血,取决因而否达到5秒间隔。

实际运行发现回血回蓝逻辑不对,Word麻,原来是操做符短路了,若是GenHP()返回true了,那GenMP()就不会被调用,就有可能失去回蓝的机会。你须要修改程序以下:

 

逻辑与(&&)跟逻辑或(||)有一样的问题, if (a && b) 若是a的表达式求值为false,b表达式也不会被计算。

有时候,咱们会写出 if (ptr != nullptr && ptr->Do())这样的代码,这正是利用了操做符短路的语法特征。

 

别让循环停不下来

 

程序跑到这,WTF?根本停不下来啊?问题很简单,unsigned永远>=0,是否是心中一万只马奔腾?

解决这个问题很简单,可是有时候这一类的错误却没这么明显,你须要罩子放亮点。

 

内存拷贝当心内存越界

memcpy,memset有很强的限制,仅能用于POD结构,不能做用于stl容器或者带有虚函数的类。

带虚函数的类对象会有一个虚函数表的指针,memcpy将破坏该指针指向。

对非POD执行memset/memcpy,免费送你四个字:自求多福

注意内存重叠

内存拷贝的时候,若是src和dst有重叠,须要用memmov替代memcpy。

 

理解user stack空间颇有限

不能在栈上定义过大的临时对象。通常而言,用户栈只有几兆(典型大小是4M,8M),因此栈上建立的对象不能太大。

 

用sprintf格式化字符串的时候,类型和符号要严格匹配

由于sprintf的函数实现里是按格式化串从栈上取参数,任何不一致,都有可能引发不可预知的错误; /usr/include/inttypes.h里定义了跨平台的格式化符号,好比PRId64用于格式化int64_t

 

用c标准库的安全版本(带n标识)替换非安全版本

好比用strncpy替代strcpy,用snprintf替代sprintf,用strncat代替strcat,用strncmp代替strcmp,memcpy(dst, src, n)要确保[dst,dst+n]和[src, src+n]都有有效的虚拟内存地址空间。多线程环境下,要用系统调用或者库函数的安全版本代替非安全版本(_r版本),谨记strtok,gmtime等标准c函数都不是线程安全的。

 

STL容器的遍历删除要当心迭代器失效

vector,list,map,set等各有不一样的写法:

 

有时候遍历删除的逻辑不是这么明显,可能循环里调了另外一个函数,而该函数在某种特定的状况下才会删除当前元素,这样的话,就是很长一段时间,程序都运行得好好的,而当你正跟别人谈笑风生的时候,突然crash,这就尴尬了。

圣斗士星矢项目曾经遭遇过这个问题,基本规律是一个礼拜game server crash一次,折磨团队将近一个月。

比较low的处理方式能够把待删元素放到另外一个容器WaitEraseContainer里保存下来,再走一趟单独的循环,删除待删元素。

固然,咱们推荐在遍历的同时删除,由于这样效率更高,也显得行家里手。

 

3、性能

空间换取时间

经过空间换取时间是提升性能的惯用法,bitmap,int map[]这些惯用法要了然于胸。

 

减小拷贝 & COW

了解Copy On Write。

只要可能就应该减小拷贝,好比经过共享,好比经过引用指针的形式传递参数和返回值。

 

延迟计算和预计算

好比游戏服务器端玩家的战力,由属性a,b决定,也就是说属性a,b任何一个变化,都须要重算战力;但若是ModifyPropertyA(),ModifyPropertyB()以后,都重算战力却并不是真正必要,由于修改属性A以后有可能立刻修改B,两次重算战力,显然第一次重算的结果会很快被第二次的重算覆盖。

并且不少状况下,咱们可能须要在心跳里,把最新的战力值推送给客户端,这样的话,ModifyPropertyA(),ModifyPropertyB()里,咱们其实只须要把战力置脏,延迟计算,这样就能避免没必要要的计算。

在GetFightValue()里判断FightValueDirtyFlag,若是脏,则重算,清脏标记;若是不脏,直接返回以前计算的结果。

预计算的思想相似。

 

分散计算

分散计算是把任务分散,打碎,避免一次大计算量,卡住程序。

 

哈希

减小字符串比较,构建hash,可能会多费一点存储空间,但收益可观,信我。

 

日志节制

日志的开销不容忽视,要分级,能够把日志做为debug手段,但要release干净。

 

编译器为何不给局部变量和成员变量作默认初始化

由于效率,C++被设计为系统级的编程语言,效率是优先考虑的方向,c++秉持的一个设计哲学是“不为没必要要的操做付出任何额外的代价”。因此它有别于java,不给成员变量和局部变量作默认初始化,若是须要赋初值,那就由程序员本身去保证。

结论:从安全的角度出发,不该使用未初始化的变量,定义变量的时候赋初值是一个好的习惯,不少错误皆因未正确初始化而起,C++11支持成员变量定义的时候直接初始化,成员变量尽可能在成员初始化列表里初始化,且要按定义的顺序初始化。

 

理解函数调用的性能开销(栈帧创建和销毁,参数传递,控制转移),性能敏感函数考虑inline

X86_64体系结构由于通用寄存器数目增长到16个,因此64位系统下参数数目很少的函数调用,将会由寄存器传递代替压栈方式传递参数,但栈帧创建、撤销和控制转移依然会对性能有所影响。

 

递归的优势、缺点

虽然递归函数能简化程序编写,但也经常带来运行速度变慢的问题,因此须要预估好递归深度,优先考虑非递归实现版本。

递归函数要有退出条件且不能递归过深,否则有爆栈危险。

 

4、数据结构和容器

了解std::vector的方方面面和底层实现

vector是动态扩容的,2的次方往上翻,为了确保数据保存在连续空间,每次扩充,会将原member悉数拷贝到新的内存块; 不要保存vector内对象的指针,扩容会致使其失效 ;能够经过保存其下标index替代。

运行过程当中须要动态增删的vector,不宜存放大的对象自己 ,由于扩容会致使全部成员拷贝构造,消耗较大,能够经过保存对象指针替代。

resize()是重置大小;reserve()是预留空间,并未改变size(),可避免屡次扩容; clear()并不会致使空间收缩 ,若是须要释放空间,能够跟空的vector交换,std::vector .swap(v),c++11里shrink_to_fit()也能收缩内存。

理解at()和operator[]的区别 :at()会作下标越界检查,operator[]提供数组索引级的访问,在release版本下不会检查下标,VC会在Debug版本会检查;c++标准规定:operator[]不提供下标安全性检查。

C++标准规定了std::vector的底层用数组实现,认清这一点并利用这一点。

 

经常使用数据结构

数组:内存连续,随机访问,性能高,局部性好,不支持动态扩展,最经常使用。

链表:动态伸缩,脱离插入极快,特别是带先后驱指针,内存一般不连续(固然能够经过从固定内存池分配规避),不支持随机访问。

查找:3种:bst,hashtable,基于有序数组的bsearch。二叉搜索树(RBTree),这个从begin到end有序,最坏查找速度logN,坏处内存不连续,节点有额外空间浪费;hashtable,好的hash函数很差选,搜索最坏退化成链表,难以估计捅数量,开大了浪费内存,扩容会卡一下,无序;基于有序数组的bsearch,局部性好,insert/delete慢。

 

5、最佳实践

对于在启动时加载好,运行中不变化的查询结构,能够考虑用sorted array替代map,hash表等

由于有序数组支持二分查找,效率跟map差很少。对于只须要在程序启动的时候构建(排序)一次的查询结构,有序数组相比map和hash可能有更好的内存命中性(局部命中性)。

运行过程当中,稳定的查询结构(好比配置表,须要根据id查找配置表项,运行过程当中不增删),有序数组是个不错的选择;若是不稳定,则有序数组的插入删除效率比map,hashtable差,因此选用有序数组须要注意适用场合。

 

std::map or std::unorder_map?

想清楚他们的利弊,map是用红黑树作的,unorder_map底层是hash表作的,hash表相对于红黑树有更高的查找性能。hash表的效率取决于hash算法和冲突解决方法(通常是拉链法,hash桶),以及数据分布,若是负载因子高,就会下降命中率,为了提升命中率,就须要扩容,从新hash,而从新hash是很慢的,至关于卡一下。

而红黑树有更好的平均复杂度,因此若是数据量不是特别大,map是胜任的。

 

积极的使用const

理解const不只仅是一种语法层面的保护机制,也会影响程序的编译和运行。

const常量会被编码到机器指令。

 

理解四种转型的含义和区别

避免用错,尽可能少用向下转型(能够经过设计加以改进)

static_cast, dynamic_cast,const_cast,reinterpret_cast,傻傻分不清?

C++砖家说:一句话,尽可能少用转型,强制类型转换是C Style,若是你的C++代码须要类型强转,你须要去考虑是否设计有问题。

 

理解字节对齐

字节对齐能让存储器访问速度更快。

字节对齐跟cpu架构相关,有些cpu访问特定类型的数据必须在必定地址对齐的储存器位置,不然会触发异常。

字节对齐的另外一个影响是调整结构体成员变量的定义顺序,有可能减小结构体大小,这在某些状况下,能节省内存。

 

牢记3 rules和5 rules,固然C++11又多了&&的copy ctor和op=版本

只在须要接管的时候才自定义operator=和copy constructor,若是编译器提供的默认版本工做的很好,不要去自找麻烦,自定义的版本勿忘拷贝每个成分,若是要接管就要处理好。

 

组合优先于继承,继承是一种最强的类间关系

典型的适配器模式有类适配器和对象适配器,通常而言,建议用对象适配的方式,而非用基于继承的类适配方式。

 

减小依赖,注意隔离

最大限度的减小文件间的依赖关系,用前向声明拆解相互依赖。

了解pimpl技术。

头文件要自给自足,不要图省事all.h,不要包含没必要要的头文件,也不要把该包含的头文件推给user去包含,一句话,头文件包含要很少很多刚恰好。

 

严格配对

打开的句柄要关闭,加锁/解锁,new/delete,new[]/delete[],malloc/free要配对,可使用RAII技术防止资源泄露,编写符合规范的代码

Valgrind对程序的内存使用方式有指望,须要干净的释放,因此规范编程才能写出valgrind干净的代码,否则再好的工具碰到不按规划写的代码也是武功尽废啊。

 

理解多继承潜在的问题,慎用多继承

多继承会存在菱形继承的问题,多个基类有相同成员变量会有问题,须要谨慎对待。

 

有多态用法抽象基类的析构函数要加virtual关键字

主要是为了基类的析构函数能获得正确的调用。

virtual dtor跟普通虚函数同样,基类指针指向子类对象的时候,delete ptr,根据虚函数特征,若是析构函数是普通函数,那么就调用ptr显式(基类)类型的析构函数;若是析构函数是virtual,则会调用子类的析构函数,而后再调用基类析构函数。

 

避免在构造函数和析构函数里调用虚函数

构造函数里,对象并无彻底构建好,此时调用虚函数不必定能正确绑定,析构亦如此。

从输入流获取数据,要作好数据不够的处理,要加try catch;没有被吞咽的exception,会被传播

从网络数据流读取数据,从数据库恢复数据都须要注意这个问题。

 

协议尽可能不要传float,若是传float要了解NaN的概念,要作好检查,避免恶意传播

能够考虑用整数替代浮点,好比万分之五(5%%),就保存5。

 

定义宏要遵循常规

要对每一个变量加括弧,有时候须要加do {} while(0)或者{},以便能将一条宏当成一个语句。要理解宏在预处理阶段被替换,不用的时候要#undef,要防止污染别人的代码。

 

了解智能指针和指针的误用

理解基于引用计数法的智能指针实现方式,了解全部权转移的概念,理解shared_ptr和unique_ptr的区别和适用场景

 

考虑用std::shared_ptr管理动态分配的对象。

指针能带来弹性,但不要误用,它的弹性指一方面它能在运行时改变指向,能够用来作多态,另外一方面对于不能固定大小的数组能够动态伸缩,但不少时候,咱们对固定大小的array,也在init里new/malloc出来,其实不必,并且会多占用sizeof(void*)字节,并且增长一层间接访问。

 

size_t究竟是个什么?我该用有符号仍是无符号整数?

size_t类型是被设计来保存系统存储器上能保存的对象的最大个数。

32位系统,一个对象最小的单位是一个字节,那2的32次方内存,最多能保存的对象数目就是4G/1字节,正好一个unsigned int能保存下来(typedef unsigned int size_t)。

一样,64位系统,unsigned long是8字节,因此size_t就是unsigned long的类型别名。

对于像索引,位置这样的变量,是用有符号仍是无符号呢?像money这样的属性呢?

一句话:要讲道理,用最天然,最瓜熟蒂落的类型。好比索引不可能为负用size_t,帐户可能欠钱,则money用int。好比:

 

标准库给出了最好的示范,由于若是是有符号的话,你须要这样判断

if (index < 0 || index >= max_num) throw out_of_bound();

而若是是无符号整数,你只须要判断 if (index >= max_num),你承认吗?

 

整型通常用int,long就很好,用short,char须要很谨慎,要防止溢出

整型包括int,short,long,long long和char,没错,char也是整型,float是实型。

绝大多数状况下,用int,long就很好,long通常等于机器字长,能直接放到寄存器,硬件处理起来速度也一般更快。

不少时候,咱们但愿用short,char达到减小结构体大小的目的。可是因为字节对齐的缘由,可能并不能真正减小大小,并且1,2个字节的整型位数太少,一不当心就溢出了,须要特别注意。

因此,除非在db、网络这些对存储大小很是敏感的场合,咱们才须要考虑是否以short,char替代int,long。其余状况下,就至关于为省电而不开楼道的灯,省不了多少钱却冒着摔断腿的危险。

局部变量更没有必要用(unsigned) short,char等,栈是自动伸缩的,它既不节省空间,还危险,还慢。

 

6、扩展

了解c++高阶特性

模板和泛型编程,union,bitfield,指向成员的指针,placement new,显式析构,异常机制,nested class,local class,namespace,多继承、虚继承,volatile,extern "C"等

有些高级特性只有在特定状况下才会被用到,但技多不压身,平时仍是须要积累和了解,这样在需求出现时,才能从本身的知识库里拿出工具来对付它。

 

了解C++新标准

关注新技术,c++11/14/1七、lambda,右值引用,move语义,多线程库等

c++98/03标准到c++11标准的推出历经13年,13年来程序设计语言的思想获得了很大的发展,c++11新标准吸取了不少其余语言的新特性,虽然c++11新标准主要是靠引入新的库来支持新特征,核心语言的变化较少,但新标准仍是引入了move语义等核心语法层面的修改,每一个CPPer都应该了解新标准。

 

OOD设计原则并非胡扯

设计模式六大原则(1):单一职责原则

设计模式六大原则(2):里氏替换原则

设计模式六大原则(3):依赖倒置原则

设计模式六大原则(4):接口隔离原则

设计模式六大原则(5):迪米特法则

设计模式六大原则(6):开闭原则

 

熟悉经常使用设计模式,活学活用,不生搬硬套

神化设计模式和反设计模式,都不是科学的态度,设计模式是软件设计的经验总结,有必定的价值;GOF书上对每个设计模式,都用专门的段落讲它的应用场景和适用性,限制和缺陷,在正确评估得失的状况下,是鼓励使用的,但显然,你首先须要准确get到她。

以上为本次所有分享,但愿对你们有帮助!避免常见陷阱与套路,提升编码效率。

另外若是你想更好的提高你的编程能力,学好C语言C++编程!弯道超车,快人一步!

C语言C++编程学习交流圈子,QQ群1030652847点击进入】微信公众号:C语言编程学习基地

分享(源码、项目实战视频、项目笔记,基础入门教程)

欢迎转行和学习编程的伙伴,利用更多的资料学习成长比本身琢磨更快哦!

编程学习书籍分享:

编程学习视频分享: