乐趣区

关于c++:CC陷阱与套路当年就是折在这些地儿…

摘要:本文联合作者的工作教训和学习心得,对 C ++ 语言的一些高级个性,做了简略介绍;对一些常见的误会,做了解释廓清;对比拟容易犯错的中央,做了演绎总结;心愿借此能增进大家对 C ++ 语言理解,缩小编程出错,晋升工作效率。

一、导语

C++ 是一门被宽泛应用的零碎级编程语言,更是高性能后端规范开发语言;C++ 虽功能强大,灵便奇妙,但却属于易学难精的专家型语言,不仅老手难以驾驭,就是老司机也容易掉进各种陷阱。

本文联合作者的工作教训和学习心得,对 C ++ 语言的一些高级个性,做了简略介绍;对一些常见的误会,做了解释廓清;对比拟容易犯错的中央,做了演绎总结;心愿借此能增进大家对 C ++ 语言理解,缩小编程出错,晋升工作效率。

二、陷阱

我的程序里用了全局变量,为何过程退出会莫名其妙的 core 掉?

Rule:C++ 在不同模块(源文件)里定义的全局变量,不保障结构程序;但保障在同一模块(源文件)里定义的全局变量,按定义的先后顺序结构,按定义的相同秩序析构。

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

依照规定:X 先结构,Y 后结构;过程进行执行的时候,Y 先析构,X 后析构;但如果 X 的析构依赖于 Y,那么 core 的事件就有可能产生。

论断:如果全局变量有依赖关系,那么就把它们放在同一个源文件定义,且按正确的程序定义,确保依赖关系正确,而不是定义在不同源文件;对于零碎中的单件,单件依赖也要留神这个问题。

std::sort()的比拟函数有很强的束缚,不能乱来

置信工作 5 年以上至多 50% 的 C /C++ 程序员都被它坑过,我曾经听到过了无数个悲伤的故事,《圣斗士星矢》,《仙剑》,还有他人家的我的项目《天天爱打消》,都有人掉坑,程序运行几天莫名微妙的 Crash 掉,一脸懵逼。

如果要用,要本人提供比拟函数或者函数对象,肯定搞清楚什么叫“严格弱排序”,肯定要满足以下 3 个个性:

  1. 非自反性
  2. 非对称性
  3. 传递性

尽量对索引或者指针 sort,而不是针对对象自身,因为如果对象比拟大,替换(复制)对象比替换指针或索引更消耗。

留神操作符短路

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

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

void  GamePlayer::Heartbeat()
{if (GenHP() || GenMP())
    {NotifyClientHPMP();
    }
}

如果 GenHP 回血了,就返回 true,否则 false;不肯定每次调用 GenHP 都会回血,取决于是否达到 3 秒距离。

如果 GenMP 回蓝了,就返回 true,否则 false;不肯定每次调用 GenMP 都会回血,取决于是否达到 5 秒距离。

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

void GamePlayer::Heartbeat()
{bool hp = GenHP();
    bool mp = GenMP();

    if (hp || mp)
    {NotifyClientHPMP();
    }  
}

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

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

别让循环停不下来

for (unsigned int i = 5; i >=0; --i)
{//...}

程序跑到这,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 等各有不同的写法:

int main(int argc, char *argv[])
{
    //vector 遍历删除
    std::vector v(8);
    std::generate(v.begin(), v.end(), std::rand);
    std::cout << "after vector generate...\n";
    std::copy(v.begin(), v.end(), std::ostream_iterator(std::cout, "\n"));
    for (auto x = v.begin(); x != v.end();)
    {if (*x % 2)
            x = v.erase(x);
        else
            ++x;
    }

    std::cout << "after vector erase...\n";
    std::copy(v.begin(), v.end(), std::ostream_iterator(std::cout, "\n"));

    //map 遍历删除
    std::map m = {{1,2}, {8,4}, {5,6}, {6,7}};
    for (auto x = m.begin(); x != m.end();)
    {if (x->first % 2)
            m.erase(x++);
        else
            ++x;
    }
    return 0;
}

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

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

比拟 low 的解决形式能够把待删元素放到另一个容器 WaitEraseContainer 里保留下来,再走一趟独自的循环,删除待删元素。

当然,咱们举荐在遍历的同时删除,因为这样效率更高,也显得行家里手。

三、性能

空间换取工夫

通过空间换取工夫是进步性能的习用法,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 位零碎下参数数目不多的函数调用,将会由寄存器传递代替压栈形式传递参数,但栈帧建设、撤销和管制转移仍然会对性能有所影响。

递归的长处、毛病

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

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

四、数据结构和容器

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

  1. vector 是动静扩容的,2 的次方往上翻,为了确保数据保留在间断空间,每次裁减,会将原 member 悉数拷贝到新的内存块;不要保留 vector 内对象的指针,扩容会导致其生效;能够通过保留其下标 index 代替。
  2. 运行过程中须要动静增删的 vector,不宜寄存大的对象自身,因为扩容会导致所有成员拷贝结构,耗费较大,能够通过保留对象指针代替。
  3. resize()是重置大小;reserve()是预留空间,并未扭转 size(),可防止屡次扩容;clear()并不会导致空间膨胀,如果须要开释空间,能够跟空的 vector 替换,std::vector .swap(v),c++11 里 shrink_to_fit()也能膨胀内存。
  4. 了解 at()和 operator[]的区别:at()会做下标越界查看,operator[]提供数组索引级的拜访,在 release 版本下不会查看下标,VC 会在 Debug 版本会查看;c++ 标准规定:operator[]不提供下标安全性查看。
  5. C++ 标准规定了 std::vector 的底层用数组实现,认清这一点并利用这一点。

罕用数据结构

数组:内存间断,随机拜访,性能高,局部性好,不反对动静扩大,最罕用。

链表:动静伸缩,脱离插入极快,特地是带前后驱指针,内存通常不间断(当然能够通过从固定内存池调配躲避),不反对随机拜访。

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

五、最佳实际

对于在启动时加载好,运行中不变动的查问构造,能够思考用 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。比方:

template <class T> class vector
{T& operator(size_t index) {}};

规范库给出了最好的示范,因为如果是有符号的话,你须要这样判断

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 等,栈是主动伸缩的,它既不节俭空间,还危险,还慢。

六、扩大

理解 c ++ 高阶个性

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

有些高级个性只有在特定状况下才会被用到,但技多不压身,平时还是须要积攒和理解,这样在需要呈现时,能力从本人的知识库里拿出工具来凑合它。

理解 C ++ 新规范

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

c++98/03 规范到 c ++11 规范的推出历经 13 年,13 年来程序设计语言的思维失去了很大的倒退,c++11 新规范排汇了很多其余语言的新个性,尽管 c ++11 新规范次要是靠引入新的库来反对新特色,外围语言的变动较少,但新规范还是引入了 move 语义等外围语法层面的批改,每个 CPPer 都应该理解新规范。

OOD 设计准则并不是胡扯

  • 设计模式六大准则(1):繁多职责准则
  • 设计模式六大准则(2):里氏替换准则
  • 设计模式六大准则(3):依赖倒置准则
  • 设计模式六大准则(4):接口隔离准则
  • 设计模式六大准则(5):迪米特法令
  • 设计模式六大准则(6):开闭准则

相熟罕用设计模式,活学活用,不生吞活剥

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

点击关注,第一工夫理解华为云陈腐技术~

退出移动版