C++ 的门门道道 | 技术头条

论坛 期权论坛 期权     
CSDN   2019-7-13 07:42   1912   0



作者 | 我不想种地
责编 | 郭芮
C++是一门被广泛使用的系统级编程语言,更是高性能后端标准开发语言;C++虽功能强大,灵活巧妙,但却属于易学难精的专家型语言,不仅新手难以驾驭,就是老司机也容易掉进各种陷阱。
本文结合作者的工作经验和学习心得,对C++语言的一些高级特性,做了简单介绍;对一些常见的误解,做了解释澄清;对比较容易犯错的地方,做了归纳总结;希望借此能增进大家对C++语言了解,减少编程出错,提升工作效率。
一、我的程序里用了全局变量,但为什么进程正常停止的时候会莫名其妙的core掉?
Rule:C++在不同模块(源文件)里定义的全局变量,不保证构造顺序;但保证在同一模块(源文件)里定义的全局变量,按定义的先后顺序构造,按定义的相反次序析构。
我们程序在a.cpp里定义了依次全局变量X和Y;
按照规则:X先构造,Y后构造;进程停止执行的时候,Y先析构,X后析构;但如果X的析构依赖于Y,那么core的事情就有可能发生。
结论:如果全局变量有依赖关系,那么就把它们放在同一个源文件定义,且按正确的顺序定义,确保依赖关系正确,而不是定义在不同源文件;对于系统中的单件,单件依赖也要注意这个问题。
二、编译器为什么不给局部变量和成员变量做默认初始化?
因为效率,C++被设计为系统级的编程语言,效率是优先考虑的方向,c++秉持的一个设计哲学是不为不必要的操作付出任何额外的代价,所以它有别于java,不给成员变量和局部变量做默认初始化,如果需要赋初值,那就由程序员自己去保证。
结论:从安全的角度出发,定义变量的时候赋初值是一个好的习惯,很多错误皆因未正确初始化而起,C++11支持成员变量定义的时候直接初始化,成员变量尽量在成员初始化列表里初始化,且要按定义的顺序初始化。
三、std::sort()的比较函数有很强的约束,不能乱来!
相信工作5年以上至少50%的C/C++程序员都被它坑过,我已经听到过了无数个悲伤的故事,《圣斗士星矢》,《仙剑》,还有别人家的项目《天天爱消除》,都有人掉坑,程序运行几天莫名奇妙的Crash掉,这锅好沉。
如果要用,要自己提供比较函数或者函数对象,一定搞清楚什么叫“严格弱排序”,一定要满足以下3个特性:
  • 非自反性
  • 非对称性
  • 传递性

尽量对索引或者指针sort,而不是针对对象本身,因为如果对象比较大,交换(复制)对象比交换指针或索引更耗费。
四、注意操作符短路
考虑游戏玩家回血回蓝(魔法)刷新给客户端的逻辑。玩家每3秒回一点血,玩家每5秒回一点蓝,回蓝回血共用一个协议通知客户端,也就是说只要有回血或者回蓝就要把新的血量和魔法值通知客户端。
玩家的心跳函数heartbeat()在主逻辑线程被循环调用:
  1. void  GamePlayer::Heartbeat(){    if (GenHP() || GenMP())    {        NotifyClientHPMP();    }}
复制代码
如果GenHP回血了,就返回true,否则false;不一定每次调用GenHP都会回血,取决于是否达到3秒间隔。
如果GenMP回蓝了,就返回true,否则false;不一定每次调用GenMP都会回血,取决于是否达到5秒间隔。
实际运行发现回血回蓝逻辑不对,Word麻,原来是操作符短路了,如果GenHP()返回true了,那GenMP()就不会被调用,就有可能失去回蓝的机会。OMG,你需要修改程序如下:
  1. void GamePlayer::Heartbeat(){    bool hp = GenHP();    bool mp = GenMP();    if (hp || mp)     {           NotifyClientHPMP();    }   }
复制代码
逻辑与(&&)跟逻辑或(||)有同样的问题, if (a && b) 如果a的表达式求值为false,b表达式也不会被计算。
有时候,我们会写出 if (ptr != nullptr && ptr->Do())这样的代码,这正是利用了操作符短路的语法特征。
五、理解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的底层用数组实现,认清这一点并利用这一点。
六、用c标准库的安全版本(带n标识)替换非安全版本
比如用strncpy替代strcpy,用snprintf替代sprintf,用strncat代替strcat,用strncmp代替strcmp,memcpy(dst, src, n)要确保[dst,dst+n]和[src, src+n]都有有效的虚拟内存地址空间。
多线程环境下,要用系统调用或者库函数的安全版本代替非安全版本(_r版本),谨记strtok,gmtime等标准c函数都不是线程安全的。
七、理解函数调用的性能开销(栈帧建立和销毁,参数传递,控制转移),性能敏感函数考虑inline
X86_64体系结构因为通用寄存器数目增加到16个,所以64位系统下参数数目不多的函数调用,将会由寄存器传递代替压栈方式传递参数,但栈帧建立、撤销和控制转移依然会对性能有所影响。
八、理解user stack空间很有限,不能在栈上定义过大的临时对象,递归函数要有退出条件且不能递归过深
一般而言,用户栈只有几兆(典型大小是4M,8M),所以栈上创建的对象不能太大;虽然递归函数能简化程序编写,但也常常带来运行速度变慢的问题,所以需要预估好递归深度,优先考虑非递归实现版本。
九、内存拷贝小心内存越界
memcpy,memset有很强的限制,仅能用于POD结构,不能作用于stl容器或者带有虚函数的类。
带虚函数的类对象会有一个虚函数表的指针,memcpy将破坏该指针指向。
对非POD执行memset/memcpy,免费送你四个字:自求多福。
十、用sprintf格式化字符串时,类型和格式化符号要严格匹配
因为sprintf的函数实现里是按格式化串从栈上取参数,任何不一致,都有可能引起不可预知的错误; /usr/include/inttypes.h里定义了跨平台的格式化符号,比如PRId64用于格式化int64_t
十一、stl容器的遍历删除要小心迭代器失效,vector、list、map、set等各有不同的写法
  1. #include #include #include #include #include int main(int argc, char *argv[]){    //vector遍历删除    std::vector v(8);    std::generate(v.begin(), v.end(), std::rand);    std::cout =0; --i){    ...}
复制代码
程序跑到这,纳尼?根本停不下来啊?问题很简单,unsigned永远>=0,是不是心中一万只马奔腾?
解决这个问题很简单,但是有时候这一类的错误却没这么明显,你需要罩子放亮点,多个心眼。
三十二、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。
比如:
  1. template  class vector{    T& operator(size_t index) {}};
复制代码
标准库给出了最好的示范,因为如果是有符号的话,你需要这样判断
  1. if (index < 0 || index >= max_num) throw out_of_bound();
复制代码
而如果是无符号整数,你只需要判断 if (index >= max_num),你认可吗?
三十三、对于在启动时加载好,运行中不变化的查询结构,可以考虑用sorted array替代map,hash表等
因为有序数组支持二分查找,效率跟map差不多。对于只需要在程序启动的时候构建(排序)一次的查询结构,有序数组相比map和hash可能有更好的内存命中性(局部命中性)。
运行过程中,稳定的查询结构(比如配置表,需要根据id查找配置表项,运行过程中不增删),有序数组是个不错的选择;如果不稳定,则有序数组的插入删除效率比map,hashtable差,所以选用有序数组需要注意适用场合。
三十四、std::map还是std::unorder_map,我真的很纠结
想清楚他们的利弊,map是用红黑树做的,unorder_map底层是hash表做的,hash表相对于红黑树有更高的查找性能。hash表的效率取决于hash算法和冲突解决方法(一般是拉链法,hash桶),以及数据分布,如果负载因子高,就会降低命中率,为了提高命中率,就需要扩容,重新hash,而重新hash是很慢的,相当于卡一下。
而红黑树有更好的平均复杂度,所以如果数据量不是特别大,map是胜任的。
三十五、整型一般用int,long就很好,用short,char需要很仔细,要防止溢出
大多数情况下,用int,long就很好,long一般等于机器字长,long能直接放到寄存器,硬件处理起来速度也更快。
很多时候,我们希望用short,char达到减少结构体大小的目的。但是由于字节对齐,可能并不能真正减少,而且1,2个字节的整型位数太少,一不小心就溢出了,需要特别注意。
所以,除非在db、网络这些对存储大小非常敏感的场合,我们才需要考虑是否以short,char替代int,long。
作者:我不想种地,欢迎关注作者公众号【码砖杂役】。
声明:本文为作者投稿,版权归其个人所有,欢迎更多开发者朋友通过下方联系投稿。


热 文 推 荐
程序员求职新思路:互联网巨鳄瓜分 ToB 资源全公开

移动开发还有未来吗?

滴滴裁员补偿丰厚,员工称裁出幸福感?

那些简历造假拿 Offer 的程序员,后来都怎么样了?
被V神点赞, 我是如何用五子棋打败以太坊排名最高的应用的? |人物志
50个最有价值的数据可视化图表(推荐收藏)
一键免费自动AI抠图,效果连PS大哥也点赞!

史上最难的一道Java面试题!
print_r('点个好看吧!');
var_dump('点个好看吧!');
NSLog(@"点个好看吧!");
System.out.println("点个好看吧!");
console.log("点个好看吧!");
print("点个好看吧!");
printf("点个好看吧!\n");
cout
分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:65
帖子:240
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP