0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C++陷阱与套路

C语言与CPP编程 来源:码砖杂役 作者:码砖杂役 2022-12-12 10:26 次阅读
【导语】 C++是一门被广泛使用的系统级编程语言,更是高性能后端标准开发语言;C++虽功能强大,灵活巧妙,但却属于易学难精的专家型语言,不仅新手难以驾驭,就是老司机也容易掉进各种陷阱。本文结合作者的工作经验和学习心得,对C++语言的一些高级特性,做了简单介绍;对一些常见的误解,做了解释澄清;对比较容易犯错的地方,做了归纳总结;希望借此能增进大家对C++语言了解,减少编程出错,提升工作效率。

【1】我的程序里用了全局变量,但为什么进程正常停止的时候会莫名其妙的core掉

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

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

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

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

【2】编译器为什么不给局部变量和成员变量做默认初始化

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

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

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

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

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

  • 非自反性

  • 非对称性

  • 传递性

尽量对索引或者指针sort,而不是针对对象本身,因为如果对象比较大,交换(复制)对象比交换指针或索引更耗费。

【4】注意操作符短路

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

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

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

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

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

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

void GamePlayer::Heartbeat()
{
  bool hp = GenHP();
  bool mp = GenMP();
  if (hp || mp) 
  {  
    NotifyClientHPMP();
  }  
}

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

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

【5】理解std::vector的底层实现

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

(b) 运行过程中需要动态增删的vector,不宜存放大的对象本身 ,因为扩容会导致所有成员拷贝构造,消耗较大,可以通过保存对象指针替代。

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

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

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

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

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

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

【8】理解user stack空间很有限,不能在栈上定义过大的临时对象,递归函数要有退出条件且不能递归过深

一般而言,用户栈只有几兆(典型大小是4M,8M),所以栈上创建的对象不能太大;虽然递归函数能简化程序编写,但也常常带来运行速度变慢的问题,所以需要预估好递归深度,优先考虑非递归实现版本。

【9】内存拷贝小心内存越界;memcpy,memset有很强的限制,仅能用于POD结构,不能作用于stl容器或者带有虚函数的类

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

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

【10】用sprintf格式化字符串的时候,类型和格式化符号要严格匹配,因为sprintf的函数实现里是按格式化串从栈上取参数,任何不一致,都有可能引起不可预知的错误;/usr/include/inttypes.h里定义了跨平台的格式化符号,比如PRId64用于格式化int64_t

【11】stl容器的遍历删除要小心迭代器失效,vector,list,map,set等各有不同的写法

#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
  //vector遍历删除
  std::vector<int> v(8);
  std::generate(v.begin(), v.end(), std::rand);
  std::cout << "after vector generate...
";
  std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, "
"));

  for (auto x = v.begin(); x != v.end(); )
  {
    if (*x % 2)
      x = v.erase(x);
    else
      ++x;
  }

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

  //map遍历删除
  std::map<int, int> 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里保存下来,再走一趟单独的循环,删除待删元素。

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

【12】积极的使用const,理解const不仅仅是一种语法层面的保护机制,也会影响程序的编译和运行

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

【13】理解四种转型的含义和区别,避免用错,尽量少用向下转型(可以通过设计加以改进)

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

C++砖家说:一句话,尽量少用转型,强制类型转换是C Style,如果你的C++代码需要类型强转,你需要去考虑是否设计有问题。不管您信不信,我反正是信了。

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

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

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

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

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

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

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

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

构造函数里,对象并没有完全构建好,此时调用虚函数不一定能正确绑定,析构亦如此。

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

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

【19】协议尽量不要传float,如果传float要了解NaN的概念,要做好检查,避免恶意传播

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

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

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

【22】了解c++高阶特性:模板和泛型编程,union,bitfield,指向成员的指针,placement new,显式析构,异常机制,nested class,local class,namespace,多继承、虚继承,volatile,extern "C"等

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

【23】了解C++新标准,关注新技术,c++11/14/17、lambda,右值引用,move语义,多线程库等

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

【24】OOP设计原则并不是胡扯

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

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

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

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

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

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

【25】熟悉常用设计模式,活学活用,不生搬硬套

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

【26】了解延迟计算、COW和分散计算

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

而且很多情况下,我们可能需要在心跳里,把最新的战力值推送给客户端,这样的话,ModifyPropertyA(),ModifyPropertyB()里,我们其实只需要把战力置脏,延迟计算,这样就能避免不必要的计算。

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

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

延迟计算和分散计算都是常见的套路。

【27】理解字节对齐

自己对齐能让存储器访问速度更快。
自己对齐跟cpu架构相关,有些cpu访问特定类型的数据必须在一定地址对齐的储存器位置,否则会触发异常。

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

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

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

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

【30】减少依赖,注意隔离

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

了解pimpl技术。

头文件要自给自足,不要包含不必要的头文件,也不要把该包含的头文件推给user去包含,一句话,头文件包含要不多不少刚刚好。

【31】别让循环停不下来

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

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

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

【32】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),你认可吗?

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

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

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

【34】std::map还是std::unorder_map,我真的很纠结

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

而红黑树有更好的平均复杂度,所以如果数据量不是特别大,map是胜任的。

【35】整型一般用int,long就很好,用short,char需要很仔细,要防止溢出

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

很多时候,我们希望用short,char达到减少结构体大小的目的。但是由于字节对齐,可能并不能真正减少,而且1,2个字节的整型位数太少,一不小心就溢出了,需要特别注意。

所以,除非在db、网络这些对存储大小非常敏感的场合,我们才需要考虑是否以short,char替代int,long。

审核编辑 :李倩

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C++
    C++
    +关注

    关注

    22

    文章

    2104

    浏览量

    73482
  • 编译器
    +关注

    关注

    1

    文章

    1618

    浏览量

    49043

原文标题:C++陷阱与套路

文章出处:【微信号:C语言与CPP编程,微信公众号:C语言与CPP编程】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    C7000 C/C++优化指南用户手册

    电子发烧友网站提供《C7000 C/C++优化指南用户手册.pdf》资料免费下载
    发表于 11-09 15:00 0次下载
    <b class='flag-5'>C</b>7000 <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>优化指南用户手册

    C语言和C++中结构体的区别

    同样是结构体,看看在C语言和C++中有什么区别?
    的头像 发表于 10-30 15:11 133次阅读

    C7000优化C/C++编译器

    电子发烧友网站提供《C7000优化C/C++编译器.pdf》资料免费下载
    发表于 10-30 09:45 0次下载
    <b class='flag-5'>C</b>7000优化<b class='flag-5'>C</b>/<b class='flag-5'>C++</b>编译器

    OpenVINO2024 C++推理使用技巧

    很多人都使用OpenVINO新版的C++ 或者Python的SDK,都觉得非常好用,OpenVINO2022之后的版本C++ SDK做了大量的优化与整理,已经是非常贴近开发的使用习惯与推理方式。与OpenCV的Mat对象对接方式更是几乎无缝对接,非常的方便好用。
    的头像 发表于 07-26 09:20 771次阅读

    C++语言基础知识

    电子发烧友网站提供《C++语言基础知识.pdf》资料免费下载
    发表于 07-19 10:58 7次下载

    C++中实现类似instanceof的方法

    C++有多态与继承,但是很多人开始学习C++,有时候会面临一个常见问题,就是如何向下转型,特别是不知道具体类型的时候,这个时候就希望C++ 可以向Java或者Python中有instanceof这个
    的头像 发表于 07-18 10:16 528次阅读
    <b class='flag-5'>C++</b>中实现类似instanceof的方法

    鸿蒙OS开发实例:【Native C++

    使用DevEco Studio创建一个Native C++应用。应用采用Native C++模板,实现使用NAPI调用C标准库的功能。使用C标准库hypot接口计算两个给定数平方和的平
    的头像 发表于 04-14 11:43 2491次阅读
    鸿蒙OS开发实例:【Native <b class='flag-5'>C++</b>】

    使用 MISRA C++:2023® 避免基于范围的 for 循环中的错误

    在前两篇博客中,我们 向您介绍了新的 MISRA C++ 标准 和 C++ 的历史 。在这篇博客中,我们将仔细研究以 C++ 中 for 循环为中心的特定规则。
    的头像 发表于 03-28 13:53 733次阅读
    使用 MISRA <b class='flag-5'>C++</b>:2023® 避免基于范围的 for 循环中的错误

    c语言,c++,java,python区别

    C语言、C++、Java和Python是四种常见的编程语言,各有优点和特点。 C语言: C语言是一种面向过程的编程语言。它具有底层的特性,能够对计算机硬件进行直接操作。
    的头像 发表于 02-05 14:11 2219次阅读

    vb语言和c++语言的区别

    VB语言和C++语言是两种不同的编程语言,虽然它们都属于高级编程语言,但在设计和用途上有很多区别。下面将详细比较VB语言和C++语言的区别。 设计目标: VB语言(Visual Basic)是由
    的头像 发表于 02-01 10:20 2047次阅读

    C++简史:C++是如何开始的

    MISRA C++:2023,MISRA® C++ 标准的下一个版本,来了!为了帮助您做好准备,我们介绍了 Perforce 首席技术支持工程师 Frank van den Beuken 博士撰写
    的头像 发表于 01-11 09:00 528次阅读
    <b class='flag-5'>C++</b>简史:<b class='flag-5'>C++</b>是如何开始的

    C语言和C++中那些不同的地方

    C语言虽说经常和C++在一起被大家提起,但可千万不要以为它们是一个东西。现在我们常用的C语言是C89标准,C++
    的头像 发表于 12-07 14:29 902次阅读
    <b class='flag-5'>C</b>语言和<b class='flag-5'>C++</b>中那些不同的地方

    开箱即用!教你如何正确使用华为云CodeArts IDE for C/C++

    华为云CodeArts IDE 定位华为云开发者桌面 ,是华为云面向开发者提供的一款智能化桌面集成开发环境。CodeArts IDE for C/C++集成了华为自研的C/C++语言服
    的头像 发表于 11-29 17:40 750次阅读
    开箱即用!教你如何正确使用华为云CodeArts IDE for <b class='flag-5'>C</b>/<b class='flag-5'>C++</b>!

    如何选择创建c语言和c++

    选择创建 C 语言和 C++ 都需要综合考虑多个因素。在决定使用哪种语言之前,我们需要对这两种语言的特点、优缺点、适用场景、学习成本等进行全面的了解和对比。下面是关于选择创建 C 语言和 C+
    的头像 发表于 11-27 15:58 567次阅读

    c++怎么开始编程

    C++是一种高级的、通用的编程语言,用于开发各种类型的应用程序。它是从C语言演变而来,也是一种静态类型语言,可以在不同的平台上进行开发。C++具有高度的灵活性和性能,并且广泛应用于游戏开发、桌面
    的头像 发表于 11-27 15:56 888次阅读