【SDU Chart Team - Core】关于本项目C++开发中的常见问题汇总

本项目C++开发中的常见问题汇总

编译时

OOP中的声明与定义问题

  • 定义与声明文件

    在cpp项目开发中都知道需要将声明与定义分离,这是由于include机制造成的。include机制只是简单的将目标位置嵌入对应文件中的代码,这意味着A.cpp中嵌入B.cpp,在链接过程中将会出现两次B.cpp的定义(一次在A.cpp的include处,另一次在链接B.cpp中)。将声明和定义分离后,尽管可能存在多次声明,但它们保持一致,是合法的;链接时只会链接一份定义。

    以上做法既是一种规范也是一种惯例,如果有意打破会发生什么?比如使用hpp文件包含全部内容,更具体而言就是在头文件中给予类的成员函数的实现。答案是部分合法,仅当该文件在整个上下文中只出现一次。hpp是C++中的头文件,既能声明又能定义。但是如果出现多次,仍然会出现重定义问题。

    所以遵循规范是一种最保险的做法。

  • Include Guard

    为了解决循环引用问题,诞生了使用宏来保护整个文件的做法。具体方法如下:

    #ifndef _XXX_H
    #define _XXX_H
    //...
    #endif
    

    编译过程中如果已出现该部分,则ifndef使得这段重复内容将被自动跳过,从而避免了循环引用。

    需要注意Include Guard的保护范围是整个文件,包括所有引入的其他头文件和声明。否则仍然可能出现循环引用问题。

  • 相互依赖

    如果类A和类B具有相互依赖(你中有我,我中有你),则必然需要循环引用。尽管Include Guard解决了循环栈过深的问题,但它解决得太彻底以至于任何循环都不允许存在。因此极有可能出现的情况是A.h能引用B.h,但B.h再引用A.h时被Include Guard打断了。

    解决方案是在B.h中显式声明类A,但不提供细节。这是合法的因为声明在完全一致的前提下可以重复。同时A和B各自的依赖只能为指针,因为指针在编译时不依赖细节。

  • 命名空间

    如果循环依赖涉及命名空间,也可以用重声明的方法解决。但是重声明不只是声明命名空间,还必须声明完整被该文件所使用的全部内容,因为命名空间并不是主体,命名空间中的内容才是主体,例如你无法使用指针指向命名空间。一种简化的做法是使用静态类,这样也只用声明类而不提供细节。

模板元编程中的声明与定义问题

  • 定义与声明文件

    对于模板函数以及单纯的模板类,使用分离的方式仍然是规范。但是对于模板成员函数,规则就被打破了,原因在于模板成员函数需要具体的类型的细节。当使用到模板成员函数,则必须将其就地实现。因为模板的特殊性,这并不会产生重定义问题。

OOP中的组织问题

  • 继承、虚继承

    普通的继承,也就是静态的继承,保留基类字段,类型转换时进行截取。虚继承解决基类重复问题,对于重复的基类仅保留一个备份。

    如果设计出现菱形继承,不管是从规范上还是语义上都应当使用虚继承。

  • 虚函数、纯虚函数

    虚函数和纯虚函数使用的是vtable,使得子类可重写父类。如果编译中出现new_allocator的问题,大概率是直接使用了纯虚函数未被实现的虚类。

模板元编程中的组织问题

  • 自动推导

    很多情况下无法对模板类型进行自动推导。如返回值推导、局部变量推导。c++11中可以使用decltype进行显式推导,但是lambda作为类函数暂不能被推导。(c++中类似的反射机制都是静态机制,编译时完成,一切泛型设计都要遵照静态设计)

  • 以字符串泛型

    需要static const char *来使字符串泛型成立。可能引发一系列警告,例如使用了未命名的命名空间(修复方法未知)

  • 继承

    模板类不允许被继承,除非限定了类型。模板类可以继承一个基类来实现泛型多态。

指针与引用

  • 类型转换

    静态类型转换在编译时完成,要求非多态。动态类型转化在运行时进行,要求多态。一个强制多态的方法是显式定义虚析构函数。

    类型转换中的模板类型要带上量词,同时要注意转换的是指针还是引用。智能指针需要std::pointer_static_caststd::pointer_dynamic_cast

  • 量词

    在量词为const的成员函数中,成员变量都是const。如果需要使用非const形式的成员,则需要进行const类型转换。

链接时

重定义

除了真的重定义外,还有可能:

  • 不规范的声明和定义

    即多次重引用了hpp,引用了cpp等。

  • 定义中用了错误的作用域运算符

    定义时使用了基类或者其他类的作用域运算符。

  • .o文件未更新

    如果在新文件中引入过去的声明,且过去声明的定义生成.o文件未被更新,则make自动跳过该文件。此时链接中出现多次定义,造成重定义问题。

  • 引入了版本不对应的库

    静态库的源文件和库文件不匹配。原因和.o未更新相似。

未定义

除了真的未定义外,还有可能:

  • Include Guard

    尤其可能出现在模板成员函数中,错误的include结构导致无法看见具体的定义。需要更改include入口或重新组织include结构。

  • 定义中忘了加作用域运算符

    定义时使用了基类或者其他类的作用域运算符。

  • .o文件未更新

    如果在新文件中取消过去的声明,且过去声明的定义生成.o文件未被更新,则make自动跳过该文件。此时链接中未出现定义,造成重定义问题。

  • 引入了版本不对应的库

    静态库的源文件和库文件不匹配。原因和.o未更新相似。

运行时

拷贝、引用、指针

在C++的OOP中格外关注三者的问题。普通的赋值,是拷贝,拷贝实际上是一个新的与右值不同的对象,类型转换还会带来Slicing的问题。引用则解决上述问题,是对象本身,但处理起来较复杂,引发左值右值引用的概念相关的问题。指针是一种动态引用机制,概念上简单,但是管理复杂。

  • 拷贝

    首先是比较难发现的拷贝误用。如果要对对象进行写操作,却使用了拷贝,那么被写的对象并不是对象本身,而是被新创建的对象。

    再就是剪裁问题。父类的拷贝并不包含子类的字段,复制后全部为空,这使得多态产生问题。

  • 引用

    首先是返回值返回引用是一种不好的规范。返回引用当且仅当返回的内容在内存中持久化,例如对象的成员变量。返回临时变量必然会造成问题。如果目的是避免拷贝,解决方法是std::move。

    对于右值引用,不能滥用。右值引用使得资源拥有权被转移,用来解决拷贝的额外开销问题。但是滥用则可能违背了程序本身的设计初衷,使得共享的资源被独占或被错误占有。

  • 指针

    只讨论智能指针。尽管智能指针将指针管理方便化,但并未完全解决内存问题。一个最全面的对问题的概括就是生命周期问题,具体而言就是希望对象应当在合适被释放。

    不当的设计使得对象过早释放,产生空指针。解决方法是持久化的数据使用共享指针。

    过晚的释放带来内存泄漏。例如循环共享指针会使得指针计数器错误,具体而言就是存在自己持有自己。这样对象自始至终无法被自动回收,造成内存泄露。

核心转储
  • 空指针

    使用智能指针前,先进行空指针判断。弱指针可以使用if null表达式:

    if (auto p = wp.lock)
    //...
    

    判断指针是否相同可以使用owner_before:

    !(a.owner_before(b) || b.owner_before(a))
    

    这是判断指针是否指向相同的对象。

  • 错误的类型转换

    动态类型转换在运行时进行了转换,但是转换发生了错误。

bad weak_ptr

主要来自enable_shared_from_this。

  • 不是指针创建
    对象不是指针创建的,就没有持有者,因此没有共享指针。

  • 非public继承

    enable_shared_from_this的原理是在make_shared等初始化时生成一个指向自己的虚指针。非public将这些内容隐藏了。

  • 多继承

    建议基类继承enable_shared_from_this。子类同时继承enable_shared_from_this则会造成冲突。

  • 构造中

    构造中不能使用shared_from_this(),原因是指针还没被创建。

bad std::function

使用了一个未初始化的std::function。解决方法也是提前判空,或者提供一个空的初值。

堆栈溢出

具体情况具体分析,一般来自于递归、while。

初始化
  • 构造中的虚函数

    构造中使用虚函数是危险行为。构造中还没进行动态绑定。

  • 随机初始化

    如果没有显式初始化,很可能初始化值不是零值,造成一些错误。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值