C++ Primer阅读心得(第十八章)

4 篇文章 0 订阅
本文详细介绍了C++中的异常处理机制,包括try-catch语句的使用、异常栈展开及处理技巧。同时,讨论了C++中的命名空间机制,用于解决命名冲突问题。此外,还深入探讨了C++的多重继承,包括构造顺序、菱形继承和虚继承,以及如何解决相关问题。最后,提到了函数重载和命名空间在函数查找中的作用。
摘要由CSDN通过智能技术生成
  1. C++中的异常处理:通过try语句限定可能发生异常并处理的语句块,通过throw抛出异常对象,通过catch语句捕获该异常对象。
try {
    // 业务代码
    throw exeception("error");
    // throw之后的所有语句都会被跳过
} catch (exception& e) {
    // 异常处理代码
}

异常机制的优势在于可以把异常处理的逻辑和正常的业务逻辑分开编写,而如果采用了返回错误码的方式,则不得不将异常处理的逻辑和正常的业务逻辑放在一起,代码显得有些乱(即使如此,大多数公司还是选择了放弃使用c++异常)。

  1. 异常处理的栈展开机制:当一个异常被throw出来之后,程序立即暂停当前函数执行(也就是会跳过后面的代码),按照函数调用的反方向,逐层查找第一个匹配的catch,然后执行其中的错误处理逻辑;如果未能找到,则执行terminate系统函数,终止程序的执行(也就是core出来)。这个过程被称为栈展开机制。需要特别注意的是:随着栈的不断展开,一个个函数/代码块被退出,其中的局部变量会全部被自动销毁。这个机制在一个class的对象被构造或者析构的过程中发生尤其危险,要确保构造函数即使抛出异常也能将构造到一半的对象正确的销毁,要确保析构函数能够接住自己执行过程中的所有异常并正确处理(接不住,让外层接住了就会造成资源泄漏,因为你只消毁了一半)。
  2. Catch的技巧:
  • 对于异常对象尽量catch引用:如果通过catch值捕获一个异常父类,那么当throw出来的是一个子类异常的时候,这个子类异常对象会被“截断”为父类对象,丢失很多信息;但是引用捕获不会有这个问题。
  • 多个Catch时子类异常在前(内),父类异常在后(外):按照前面介绍过的异常栈展开机制,第一个匹配的catch会被激活,所以如果父类在前,那么后面的子类将捕获不到任何异常!
  • 可以通过catch(…)告知编译器捕获任何异常
  • 在catch的语句块中可以通过不带任何参数的throw再次把这个异常抛出(个人不建议这么做,因为一旦外面没接住,core出来,那么core只会定位在再抛出那里,而不是首次发生异常的地方,给debug带来困难)
  1. 构造函数初始化列表中产生的异常无法通过构造函数体中的try/catch处理,我们需要使用函数try语句块(try function block)来处理。注意:这个catch只是给了你一个中间处理的机会,该异常会被rethrow出来,所以外面构造时还需要一个try/catch来接住。
int raise_exception() {
    throw std::runtime_error("Aha, exception!");
}

class CatchException {
public:
    //try在冒号前,catch在函数体之后
    CatchException() try : aaa(raise_exception()) {
    } catch (std::runtime_error& e) {
        std::cout << e.what() << std::endl;
    } //注意会被rethrow
private:
    int aaa;
};

try {
    CatchException ce;
} catch (std::runtime_error& e) {
    std::cout << e.what() << std::endl;
}
  1. C++98的异常声明太复杂,因此c++11增加了noexcept做了简化(只保留了throw(),等价于noexcept)。noexcept放在函数参数列表、const和引用声明之后,尾置返回类型和函数体之前。
void functionA(int x) noexcept {}         //等价于noexcept(true),不抛异常
void functionB(int x) throw() {}          //等价于noexcept,不抛异常
void functionC(int x) noexcept(true) {}   //不抛异常
void functionD(int x) noexcept(false) {}  //可能抛异常

带bool参数并且为true(不写默认为true)的noexcept是不抛出说明,表明此函数不会抛出异常,用户可以放心使用,编译器可以任意优化。但是如果做了这个声明的函数抛出异常,那程序就core给你看。

void functionE(int x) noexcept(noexcept(functionC(0))) {}   //异常绑定functionC
void functionF(int x) noexcept(noexcept(functionD(0))) {}   //异常绑定functionD

带表达式参数的noexcept是运算符,如果表达式可能会抛出异常则返回false,否则返回true。通过将两个noexcept结合可以让一个函数是否抛异常参考另外一个函数。

void (*pf1) (int) noexcept = functionA; //ok
pf1 = functionD;                        //error,不抛异常不兼容可能抛异常
void (*pf2) (int) noexcept(false) = functionD; //ok
pf2 = functionD;                        //ok,可能抛异常兼容不抛异常

class Base {
public:
    virtual void f1() noexcept {};         //不抛异常
    virtual void f2() noexcept(false) {};  //可能抛异常
    virtual void f3() {};                  //可能抛异常
};

class Derived : public Base {
public:
    virtual void f1() override {};                 //错误,不抛异常不兼容可能抛异常
    virtual void f2() noexcept(false) override {}; //正确,异常类型一致
    virtual void f3() noexcept override {};        //正确,可能抛异常兼容不抛异常
};

注意:在函数指针、虚函数中,可能抛异常兼容不抛异常,反之则不行。

  1. C++提供了命名空间机制来解决名字冲突的问题。命名空间以namespace加上自定义名称开始,后面接上一个语句块{}(与class/struct不同,语句块的结尾不需要分号)。同一命名空间的成员之间可以直接使用名称相互访问,不同命名空间互相访问必须指明所属命名空间名称。
namespace ns1 { //定义一个命名空间ns1
    class A {...};
    void functionB() {
        A a; //同一命名空间,直接使用
    }
}
ns1::functionB(); //不同命名空间,需要指明空间名称
  1. 命名空间可以嵌套,它们之间的可见性与语句块类似:
    • 内层空间可以直接使用外层空间已定义的成员
    • 内层空间内部,一个成员将覆盖外层空间同名成员
    • 外层空间使用内层空间成员时必须指明内层空间名称
  2. 命名空间可以是不连续的,所以可以通过将各个定义拆分到多个.h,实现写在对应的多个.cc中,并将这些.h中的定义装在同一个命名空间实现项目开发的互不干扰(话说我只知道该这么做,原理其实并不知道)。这样在用户在使用时可以只include自己需要的.h,链接时也只链接自己需要的部分,而不用把所有的.h和整个lib都包含进来。注意不要把.h放在namespace之中,这样就意味着你把这个.h中的所有内容都定义在了这个namespace内部,编译时会产生很多问题。
  3. C++11新增加了内联命名空间来解决library versioning的问题。内联命名空间的成员可以直接被外层使用,而无需指明空间名称。
#define VER2

namespace GlobalLib {
#ifdef VER1
inline
#endif
namespace LibVer1 {
    void functionA() {...}//版本1的实现
}
#ifdef VER2
inline
#endif
namespace LibVer2 {
    void functionA() {...}//版本2的实现
}
}
GlobalLib::functionA(); //调用version 2
GlobalLib::LibVer1::functionA(); //调用version1
  1. 命名空间定义的其他注意事项:
    • 没有加入任何命名空间的全局变量/函数/class其实是加入了默认的全局命名空间,可以使用::name直接引用它们
    • 未命名的namespace中的变量拥有静态生命周期,其作用范围是文件内部,这个特性是c++用来取代c风格的static声明的(居然是干这个用的,从来没用过,秀起来啊!)。
    • 模板特化/偏特化时的定义必须与原模板在同一个命名空间内部
  2. 可以通过给复杂的命名空间名称指定一个别名来简化代码:
namespace shortname = GlobalLib::LibVer1;
shortname::functionA(); //等价于GlobalLib::LibVer1::functionA()
  1. using声明与using指示:using声明每次从指定的namespace引入一个成员,不易产生冲突(类比于python的from xxx import yyy);using指示一次性引入目标namespace所有成员,较容易产生冲突(类比于python的import xxx)。
using std::vector;   //只引入std::vector
using namespace std; //引入整个std的内容,强烈不建议真的工程中这么做
  1. 对于命名空间的名称查找有一个特殊规则:查找一个函数名称时,编译器也会沿着实参所属的namespace进行查找,这主要是方便操作符重载的使用(命名空间::操作符(参数1,参数2),想想就酸爽)。
  2. 命名空间与函数重载:
    • using声明只能接函数名,所以会引入所有可能的重载函数,这些函数会与已存在的函数发生作用,局部作用域中会覆盖外部同名函数,内部有完全一致的则引发一个错误,否则就是加入重载。
    • using指示会引入所有成员,如果出现完全一致的函数不会报错,你可以通过命名空间名称使用那个同名的函数
  3. C++中支持从多个基类派生子类,这被称为多重继承。多重继承把多个基类放在一个派生列表当中,每个基类只能出现一次,每个基类可以指定一个访问说明符(public/private),不指定的话class默认为private,struct为public。派生类对象内部含有所有基类的子对象,其基类构造顺序按照派生列表从左到右依次执行,析构过程正好相反。注意:多继承的派生类在继承基类构造时如果产生冲突,则需要解决。
class MultiDerived : public BaseA, private BaseB, Base c { //派生列表
                  // -----------------------------> 基类构造次序
                  // <----------------------------- 基类析构次序
}; 
  1. 派生类的指针/引用可以转化为继承树上的任何一个基类的指针/引用。派生类代码中的名称查找先找本地,然后沿着继承树一路向上。如果在多个基类中找到,则产生了二义性,需要你指定用哪个。解决这种二义性的最好办法是在派生类中也定义一个同名的覆盖产生冲突的名称。
  2. 为了解决多重继承中多次继承同一个基类的菱形继承问题,C++提供了虚继承机制。通过在继承声明的过程中增加virtual关键字,表明愿意共享此基类,使得此类型的所有派生类都只会拥有一个被共享的基类。注意:是父类声明虚继承祖类,然后子类才能避免菱形继承。
class Base {};
class DerivedBase1 : public virtual Base {}; //虚继承
class DerivedBase2 : public virtual Base {}; //虚继承
class Derived : public DerivedBase1, public DerivedBase2 {}; //Base只会有一个
  1. 需要注意虚继承情况下的二义性问题:
    • 一个名字如果只在祖类中出现,则孙类直接使用祖类的
    • 一个名字如果在祖类中出现,并且有一个父类覆盖了它,则孙类使用的是这个父类覆盖了的
    • 一个名字如果在祖类中出现,并且有多个父类覆盖了它,则孙类直接使用存在二义性
  2. 因为在孙类中只有一个祖类的子对象,所以虚继承的祖类对象由孙类负责初始化(而不是中间那层父类们)。总体的构造顺序是:先构造虚基类(祖类),再按照继承列表的顺序构造父类,最后构造自己;而析构的过程正好相反。
class BaseA {
public:
    BaseA(const std::string& derived_name) {
        std::cout << "BaseA constructed by " << derived_name << std::endl;
    }
};

class DerivedBase1 : public virtual BaseA {
public:
    DerivedBase1(const std::string& derived_name) : BaseA("DerivedBase1"){
        std::cout << "DerivedBase1 constructed by " << derived_name << std::endl;
    }
};

class DerivedBase2 : public virtual BaseA {
public:
    DerivedBase2(const std::string& derived_name) : BaseA("DerivedBase2"){
        std::cout << "DerivedBase2 constructed by " << derived_name << std::endl;
    }
};

class BaseB {
public:
    BaseB(const std::string& derived_name) {
        std::cout << "BaseB constructed by " << derived_name << std::endl;
    }
};

class MultiDerived : public BaseB, public DerivedBase1, public DerivedBase2 {
public:
    MultiDerived() : DerivedBase2("MultiDerived"),
                     DerivedBase1("MultiDerived"),
                     BaseA("MultiDerived"), //必须写这行,否则编译不通过(孙类初始化虚基类(祖类))
                     BaseB("MultiDerived"){ //构造顺序与初始化列表顺序无关,先初始化虚基类,
                                            //然后按照继承列表的顺序来初始化

    }
};

MultiDerived md;

//输出
BaseA constructed by MultiDerived
BaseB constructed by MultiDerived
DerivedBase1 constructed by MultiDerived
DerivedBase2 constructed by MultiDerived
  1. 多重继承的使用场景往往是这样的:一方面,继承一个接口基类,达成is-a的语义;另一方面,从另外一些基类获得一些能力(“基于基类实现功能”,实现层面的组合、private继承和CRTP等,例如:singleton)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值