现代C++构造函数总结及异常处理问题

继承构造函数

  1. C++11引入using,可以在派生类中直接使用using来声明继承基类的构造函数。而且,C++11标准继承构造函数被设计为跟派生类中的各种类默认函数(默认构造、析构、拷贝构造等)一样,是隐式声明的。这意味着如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。这比“透传”方案总是生成派生类的各种构造函数更加节省目标代码空间。
  2. 使用继承构造函数时,基类的构造函数如果有默认参数值,会导致派生类多个构造函数版本的产生,因此在使用有默认参数值的构造函数的基类时,程序员要小心。
  3. 当派生类有多个基类时,继承构造函数可能会有冲突,多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名、参数都相同。这种情况,可以通过显示定义继承类的冲突的构造函数,阻止隐式生成相应的继承构造函数来解决冲突。
  4. 一旦使用了继承构造函数,编译器就不会再为派生类生成默认构造函数了。

委派构造函数

  1. C++11的委派构造函数可以降低某些情况下的代码重复,可以将一个构造函数作为基准版本,然后在其他构造函数通过委派基准版本来进行初始化。
  2. 委派构造函数是在构造函数的初始化列表位置进行构造的、委派的。
  3. 称呼:一般我们把在初始化列表中调用基准版本的构造函数称为委派构造函数,而被调用的基准版本则为目标构造函数。所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成这样一种类构造的方式。
  4. 委派和初始化列表不可兼得。委派构造函数如果要给变量赋初值,初始化代码必须放在函数体中。
  5. 一个构造函数既可以是委派构造函数,也可以是目标构造函数。所以,可以有链状委托构造,但是一定要注意,不要形成委托环。
  6. 委派构造的一个实际应用:使用构造模板函数产生目标构造函数。
  7. 异常处理。在委派构造函数中使用try可以捕捉到目标构造函数中产生的异常。

移动构造函数

  1. 对于移动构造函数,抛出异常有时是件危险的事情。因为可能移动语义还没完成,一个异常却抛出来了,这就会导致一些指针成为悬挂指针。因此程序员应该尽量编写不抛出异常的移动构造函数,通过为其添加一个noexcept关键字,可以保证移动构造函数中抛出来的异常会直接调用terminate程序终止运行,而不是造成指针悬挂的状态。

左值、右值与右值引用
C++11中所有的值必须属于左值、将亡值、纯右值三者之一。
可以取地址的、有名字的是左值,不能取地址的、没有名字的就是右值。
C++中的右值又分为将亡值和纯右值。
纯右值:例如非引用返回的函数返回的临时变量值、一些运算表达式、不跟对象关联的字面量值、类型转换函数的返回值、lambda表达式等。
将亡值:将亡值是C++11新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象,比如返回右值引用T&&的函数返回值、std::move的返回值、转换为T&&的类型转换函数的返回值。

拷贝构造、拷贝赋值、移动构造、移动赋值 Helper

// Helper to delete copy constructor & copy-assignment operator
#define DISABLE_COPY_MOVE_ASSIGN(T)   \
  T(const T&) = delete;            \
  T& operator=(const T&) = delete; \
  T(T&&) = delete;                 \
  T& operator=(T&&) = delete
  
// Helper to declare copy constructor & copy-assignment operator default
#define DEFAULT_COPY_MOVE_ASSIGN(name)    \
  name(const name&) = default;            \
  name& operator=(const name&) = default; \
  name(name&&) = default;                 \
  name& operator=(name&&) = default

问题1:可以在构造函数中抛出异常吗?析构函数呢?
这是一个抛出异常与栈展开(stack unwinding)相关的问题。
答案:
ref: Can I throw an exception from a constructor? From a destructor?
构造函数是可以的。当无法正确初始化(构造)对象时,都应该从构造函数中抛出异常。 除了通过抛出异常退出构造函数之外,没有真正令人满意的替代方法。
对于析构函数,最好别。虽然可以在析构函数中抛出异常,但该异常不能离开析构函数,因为如果析构函数通过抛出异常退出,那么很可能会发生各种不好的事情,因为会违反标准库和语言本身的基本规则。 不要这样做。

More1:
ref: How can I handle a constructor that fails?
抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句。首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否可以处理该异常。如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch。这个过程称为栈展开(stack unwinding)。当处理该异常的catch结束之后,紧接着该catch之后的点继续执行。
在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。通常terminate函数将调用abort函数,导致程序的非正常退出。所以析构函数应该从不抛出异常。

More2:
为局部对象调用析构函数。在栈展开的过程中,会释放局部对象所占用的内存并运行类类型局部对象的析构函数。但需要注意的是,如果一个块通过new动态分配内存,并且在释放该资源之前发生异常,该块因异常而退出,那么在栈展开期间不会释放该资源,编译器不会删除该指针,这样就会造成内存泄露。

问题2:构造函数抛出异常时如何清理资源?
ref: How should I handle resources if my constructors may throw exceptions?
对象内的每个数据成员都应该清理自己带来的混乱。
如果构造函数抛出异常,则不会运行对象的析构函数。 如果你的对象已经做了一些需要撤消的事情(比如分配一些内存、打开文件或锁定信号量),那么这个“需要撤消的东西”必须被对象内部的数据成员记住。
例如,不是将内存分配到原始 Fred* 数据成员中,而是将分配的内存放入“智能指针”成员对象中,当智能指针死亡时,该智能指针的析构函数将删除 Fred 对象。 模板 std::unique_ptr 是诸如“智能指针”之类的示例。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值