zoukankan      html  css  js  c++  java
  • C++ primer plus读书笔记——第15章 友元、异常和其他

    第15章 友元、异常和其他

    1. 友元类的所有方法都可以访问原有类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数、或类为友元是由类定义的,而不能从外部强加友情。因此,尽管友元被授予从外部访问类的私有部分的权限,但它们并不与面向对象的编程思想相违背;相反,它们提高了公有接口的灵活性。

    2. 下面的语句使Remote成为一个友元类:friend class Remote;

      友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。先给出Tv类声明,再给出Remote类声明时,不需要在Tv类声明前前向声明Remote类,因为Tv类中的friend class Remote;已经指出Remote是一个类。

    3. P606-P609可以选择让特定的类成员称为另一个类的友元,而不必让整个类都成为友元,但这样做必须小心排列各种声明和定义的顺序。

    class Tv

    {

       friend void Remote::set_chan(Tv & t, int c);

       …

    }

    在编译器在Tv类的声明中看到Remote类的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和set_chan()方法的声明。这意味着Remote类的完整声明(数据成员和成员函数的声明)必须放在Tv类的完整声明之前,而Remote的方法中提到了Tv对象,所以必须在Remote的完整声明前使用前向声明。排列顺序为

    class Tv;        //前向声明

    class Remote    //Remote声明

    {

    };

    class Tv         //Tv声明+定义

    {

    };

    //Remote方法定义

    ……

    Remote方法中如果调用了Tv的方法,则Remote的声明中只能包含方法声明,并将实际的定义放在Tv类之后。

    4. 在类方法定义中使用inline关键字,可以使其成为内联方法,此时类声明中不需要inline。

    5. 在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类,包含类的成员函数可以创建和使用被嵌套类的对象,而仅当声明位于公有部分,才能在包含类的外面使用嵌套类。对类进行嵌套和包含不同。包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型。

    6. abort()函数在cstdlib中,其典型实现是向标准错误流发送消息abnormal program termination,然后终止程序。

    7. C++异常是对程序运行过程中发生的异常情况的一种响应。异常提供了将控制权从程序的一部分传递到另一个部分的途径。异常类型可以是字符串或其他C++类型,通常为类类型。

    8. 执行throw语句类似于执行返回语句,因为它将终止函数的执行,但函数不是将控制权返回给调用程序,而是导致程序沿函数调用序列后退,直到找到包含try块的函数。

    9. 如果函数引发了异常,而没有try块或没有匹配的处理程序时,程序默认将调用abort函数,但可以修改这种行为。

    10. 不建议使用异常规范,但至少应该知道它长什么样:

      double harm(double a) throw(bad_thing);//可能抛出bad_thing异常

      double marm(double a) throw();//不抛出异常

    异常规范的作用之一是告诉用户可能需要使用try块。然而,这项工作也可使用注释轻松地完成。异常规范的另一个作用是让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。

    C++11建议您忽略异常规范。然而,C++11确实支持一种特殊的异常规范:您可以使用新增的关键字noexcept指出函数不会引发异常:

    double marm() noexcept;//有关这种异常规范是否必要和有用存在一些争议。

    11. 假设try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,则程序流程将从引发异常的函数跳到包含try块和处理程序的函数。这涉及栈解退(unwinding the stack)。

    现在假设函数由于异常而终止,则程序也将释放栈中的内存,但不会在释放栈的第一个返回地址后停止,而是继续释放栈,直到找到一个位于try块中的返回地址。随后,控制权将转移到块尾的异常处理程序,而不是函数调用后面的第一条语句。这个过程称为栈解退。一个重要机制是,整个函数调用序列中的自动类对象的析构函数将被调用。

    12. 虽然throw-catch机制类似于函数参数和函数返回机制,但还是有些不同之处。其中之一是函数的返回语句将控制权返回到调用该函数的函数,但throw语句将控制权向上返回到第一个这样的函数:包含能够捕获相应异常的try-catch组合。另一个不同之处是,引发异常时编译器总是创建一个临时拷贝,即使catch块中指定的是引用。

    class problem{…};

    void super() throw (problem)

    {

       if(oh_no)

       {

          problem oops;

          throw oops;

       …

    }

    try{

       super();

    }

    catch(problem & p)

    {

    }

    p将指向oops的副本而不是oops本身。这是件好事,因为函数super()执行完毕后,oops将不复存在。顺便说一句,将引发异常和创建对象组合在一起将更简单:throw problem();

    13. 您可能会问,既然throw语句将生成副本,为何代码中使用引用呢?毕竟,将引用作为返回值的通常原因是避免创建副本以提高效率。答案是,引用还有另一个特征:基类引用可以指向派生类对象。假设有一组通过继承关联起来的异常类型,则在异常规范中只需列出一个基类引用,它将与任何派生类对象匹配。

    14. 如果有一个异常类继承层次结构,应该这样排列catch块,将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

    15. 使用省略号…来表示异常类型时,可以捕获任何异常:

      catch(…){//statement}

    类似于switch中的default语句,适用于当不知道被调用的函数会引发哪些异常的情况。

    16. 对于使用new导致的内存分配问题,C++最新的处理方式是让new引发bad_alloc异常。头文件new包含bad_alloc类的声明,它是从exception类公有派生而来的。但在以前,当无法分配请求的内存量时,new返回一个空指针。P633

    17. 为了确保new在失败时返回空指针而不是抛出异常,可以如下使用new:

    int *pi = new (std::nothrow) int;

    int *pa = new (std::nothrow) int[500];

    18. 异常被引发后,在两种情况下,会导致问题,即意外异常和未捕获异常。首先,如果它是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类极其派生类对象匹配),否则称为意外异常。如果异常不是在函数中引发的或者函数没有异常规范,则必须捕获它。如果没捕获(在没有try块或匹配的catch块时,将出现这种情况),则异常被称为未捕获异常。在默认情况下,都将导致程序异常终止。然而,可以修改程序对意外异常和未捕获异常的反应。

    19. 未捕获异常不会导致程序立刻异常终止。相反,程序将首先调用函数terminate()。在默认情况下,terminate()调用abort()。可以调用set_terminate()函数来修改terminate()的这种行为。set_terminate()和terminate()都是在头文件exception中声明的。如果发生意外异常,程序将调用unexpected()函数,这个函数将调用terminate(),后者在默认情况下将调用abort()。set_unexpected()函数可以修改unexpected()的行为。

    20. 与提供给set_terminate()的函数相比,提供给set_unexpected()的函数的行为受到更为严格的限制。具体地说,unexpected_handler函数可以为:

    • 通过调用terminate()、abort()或exit()来终止程序。
    • 引发异常(引发异常的结果见P640)

    21. 仅使用throw,而不指定异常将导致重新引发原来的异常。

    22. RTTI是运行阶段类型识别(Runtime Type Identification)的简称。这是新添加到C++的特性之一,很多老式实现不支持。另一些实现可能包含开关RTTI的编译器设置。RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。

    23. RTTI的用途:基类指针可以指向派生类对象,并调用类方法的正确版本,在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,则并不需要真正知道对象的类型。但派生对象可能包含不是继承而来的方法,在这种情况下,只有某些类型的对象可以使用该方法,所以必须使用RTTI确定该基类指针指向的对象是否可以使用该方法。

    24. C++有三个支持RTTI的元素。

    • 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0——空指针。
    • typeid运算符返回一个对type_info对象的引用。
    • type_info结构存储了有关特定类型的信息。

    只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。

    25. 通常,如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型,则下面的表达式将指针pt转换为Type类型的指针:

    dynamic_cast<Type *> (pt)

    否则,结果为0,即空指针。

    26. 也可以将dynamic_cast用于引用,其用法稍微有些不同:没有与空指针对应的引用值,因此无法使用特殊的引用值来指示失败。当请求不正确时,dynamic_cast将引发类型为bad_cast的异常,这种异常是从exception类派生而来的,它是在头文件typeinfo中定义的。因此,

    27. 可以像下面这样使用该运算符,其中rg是对Grand对象的引用:

    #include <typeinfo>

    class Grand{//has virtual method};

    class Superb : public Grand{…};

    class Magnificent : public Superb{…};

    try{

       Superb & rs = dynamic_cast<Superb &>(rg);

       …

    }

    catch(bad_cast &)

    {

       …

    };

    28. P647typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参数:

    • 类名
    • 结果为对象的表达式

    typeid运算符返回一个对type_info对象的引用,其中,type_info是在头文件typeinfo中定义的一个类。type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。如果pg指向的是一个Magnificent对象,则下述表达式的结果为true,否则为false:

       typeid(Magnificent) == typeid(*pg)

    如果pg是一个空指针,程序将引发bad_typeid异常。该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。

       type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串,通常是类的名称。例如,下面的语句显示指针pg指向的对象所属的类定义的字符串:

    cout << “Now processing type ” << typeid(*pg).name() << “. ”;

    29. P648误用RTTI的例子,如果在if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast。

    30. 类型转换运算符

      在C++的创始人Bjarne Stroustrup看来,C语言的类型转换太过松散,为了更严格地限制允许的类型转换,Stroustrop添加了4个类型转换运算符,使转换过程更加规范。

    • dynamic_cast
    • const_cast
    • static_cast
    • reinterpret_cast

    dynamic_cast使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换。

    const_cast只能改变const或volatile属性。

    const_cast <type_name> (expression)

    也就是说,除了const和volatile特征可以不同外,type_name和expression的类型必须相同。通常用它来删除指向const值的指针的const标签。

    注意:

       const_cast不是万能的,它可以修改指向一个值的指针,但修改const的结果是不确定的。

    #include <iostream>

     

    using std::cout;

    using std::endl;

     

    void change(const int *pt, int n);

     

    int main()

    {

        int pop1 = 38383;

        const int pop2 = 2000;

     

        cout << "pop1, pop2:" << pop1 << ", " << pop2 << endl;

        change(&pop1, -103);

        change(&pop2, -103);

        cout << "pop1, pop2:" << pop1 << ", " << pop2 << endl;

     

        return 0;

    }

     

    void change(const int *pt, int n)

    {

        int *pc;

       

        pc = const_cast<int *>(pt);

        *pc += n;

    }

    程序的运行结果为:

    pop1, pop2:38383, 2000

    pop1, pop2:38280, 2000

    请按任意键继续. . .

    可以看到,调用change()时,修改了pop1,但没有修改pop2。在change中,指针pc删除了const特征,因此可以用来修改指向的值,但仅当指向的值不是const才行。

    static_cast的语法也相同:

    static_cast<type-name> (expression)

    仅当type_name可被隐式转换为expression所属的类型expression所属的类型可被隐式转换为type_name时,上述转换才是合法的。因此,在基类和派生类之间,可以用static_cast来进行向上转换或向下转换。也可以用在枚举值和整型值之间的转换,浮点值和整型值之间的转换等等。

    reinterpret_cast运算符用于天生危险的类型转换P652。它不允许删除const,但会执行其他令人生厌的操作。

    然而,reinterprete_cast运算符并不支持所有的类型转换。例如,可以将指针类型转换为足以存储指针表示的整型,但不能将指针转换为更小的整型或浮点型。另一个限制是不能将函数指针转换为数据指针,反之亦然。

    31. dynamic_cast和static_cast的区别:dynamic_cast运算符只允许沿类层次结构向上转换,而static_cast运算符运行向上转换或向下转换。static_cast运算符还允许枚举型和整型之间以及数值类型之间的转换。

  • 相关阅读:
    ****ural 1141. RSA Attack(RSA加密,扩展欧几里得算法)
    ural 1356. Something Easier(数论,哥德巴赫猜想)
    ural 1261. Tips(进制运算)
    ural 1355. Bald Spot Revisited(数的素因子划分)
    ural 1049. Brave Balloonists(标准分解式,数论)
    jpa之hibernate4.2.4之tomcat6.0测试
    jpa之hibernate4.2.4之双向关联之一对一、一对多、多对多
    JPA之hibernate4.2.4之简单测试
    oracle之存储过程
    dom4j之创建xml、读取xml、修改xml
  • 原文地址:https://www.cnblogs.com/lakeone/p/5106570.html
Copyright © 2011-2022 走看看