C++ primer 笔记(五)

第17章 用于大型程序的工具

17.1异常处理

  • 异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个.
  • 不存在数组或函数类型的异常类型。因为与函数传递实参一样,如果抛出一个数组,被抛出的对象自动转换为指向数组首元素的指针;如果抛出一个函数,函数被转换为指向该函数的指针。
  • 因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch,并且在完全处理了异常之后撤销。
  • 异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。
  • 执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch,该 catch 可以是同一函数中局部的 catch,也可以在直接或间接调用发生异常的函数的另一个函数中。
  • 当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分.
  • 抛出异常的时候,将暂停当前函数的执行,开始查找匹配的 catch 子句。首先检查 throw 本身是否在 try 块内部,如果是,就找与抛出对象相匹配的最近的catch子句。如果找到匹配的 catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内在并撤销局部对象),并且继续在调用函数中查找。如果对抛出异常的函数的调用是在 try 块中,则检查与该 try 相关的 catch 子句。如果找到匹配的 catch,就处理异常;如果找不到匹配的 catch,调用函数也退出,并且继续在调用这个函数的函数中查找。这个过程,称之为栈展开(stack unwinding),沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句。只要找到能够处理异常的 catch 子句,就进入该 catch 子句,并在该处理代码中继续执行。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。
  • 栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。
  • 析构函数应该从不抛出异常。栈展开期间会经常执行析构函数。在执行析构函数的时候,已经引发了异常但还没有处理它。如果在这个过程中析构函数本身抛出新的异常。该先处理哪个异常呢?答案是:在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。一般而言,terminate 函数将调用 abort 函数,强制从整个程序非正常退出。
  • 如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员。
  • 不能不处理异常。异常是足够重要的、使程序不能继续正常执行的事件。如果找不到匹配的 catch,程序就调用库函数 terminate
  • catch 子句中的异常说明符的类型决定了处理代码能够捕获的异常种类。类型必须是完全类型,即必须是内置类型或者是已经定义的程序员自定义类型。类型的前向声明不行。当 catch 为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名。
  • 在查找匹配的 catch 期间,找到的 catch 不必是与异常最匹配的那个 catch,相反,将选中第一个找到的可以处理该异常的 catch。因此,在 catch 子句列表中,最特殊的 catch 必须最先出现。带有因继承而相关的类型的多个 catch 子句,必须从最低派生类类到最高基类类型排序。
  • 异常与 catch 异常说明符匹配的规则比匹配实参和形参类型的规则更严格,大多数转换都不允许。既不允许标准算术转换,也不允许为类类型定义的转换。只允许以下3种转换

       1)允许从非 constconst 的转换

       2)允许从派生类型型到基类类型的转换。

       3)将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。

  • 进入 catch 的时候,用异常对象初始化 catch 的形参。像函数形参一样,异常说明符类型可以是引用。异常对象本身是被抛出对象的副本。是否再次将异常对象复制到 catch 位置取决于异常说明符类型。
  • 像形参声明一样,基类的异常说明符可以用于捕获派生类型的异常对象,而且,异常说明符的静态类型决定 catch 子句可以执行的动作。如果被抛出的异常对象是派生类类型的,但由接受基类类型的 catch 处理,那么,catch 不能使用派生类特有的任何成员。如果 catch 子句处理因继承而相关的类型的异常,它就应该将自己的形参定义为引用。
  • 有可能单个 catch 不能完全处理一个异常。在进行了一些校正行动之后,catch 可能确定该异常必须由函数调用链中更上层的函数来处理,catch 可以通过空throw语句重新抛出将异常传递函数调用链中更上层的函数。
  • 虽然重新抛出不指定自己的异常,但仍然将一个异常对象沿链向上传递,被抛出的异常是原来的异常对象,而不是 catch 形参。catch 在改变它的形参之后,如果 catch 重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变。当 catch 形参是基类类型的时候,我们不知道由重新抛出表达式抛出的实际类型,该类型取决于异常对象的动态类型。例如,来自带基类类型形参 catch 的重新抛出,可能实际抛出一个派生类型的对象。
  • 捕获所有异常catch (...) { }
  • 在进入构造函数函数体之前处理构造函数初始化式,构造函数函数体内部的 catch 子句不能处理在处理构造函数初始化时可能发生的异常。为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数 try 块,可以使用函数测试块将一组 catch 子句与函数联成一个整体
template <class T> Handle<T>::Handle(T *p)
    try : ptr(p), use(new size_t(1))
    {
         // empty function body
    }  catch(const std::bad_alloc &e)
           { handle_out_of_memory(e); }

image

  • exception 类型所定义的唯一操作是一个名为 what 的虚成员,该函数返回 const char* 对象,它一般返回用来在抛出位置构造异常对象的信息。因为 what 是虚函数,如果捕获了基类类型引用,对 what 函数的调用将执行适合异常对象的动态类型的版本。
    //应用程序还经常通过从 exception 类或者中间基类派生附加类型来扩充 exception 层次。这些新派生的类可以表示特定于应用程序领域的异常类型。
    
    class isbn_mismatch: public std::logic_error {
        public:
            explicit isbn_mismatch(const std::string &s):
                                  std::logic_error(s) { }
            isbn_mismatch(const std::string &s,
                const std::string &lhs, const std::string &rhs):
                std::logic_error(s), left(lhs), right(rhs) { }
            const std::string left, right;
            virtual ~isbn_mismatch() throw() { }
    //合成析构函数调用string析构函数,C++ 标准保证,string 析构函数像任意其他标准库类析构函数一样,不抛出异常。
    //但标准库的析构函数没有定义异常说明,我们知道,但编译器不知道,string析构函数将不抛出异常。我们必须定义自己的析构函数来恢复析构函数不抛出异常的承诺。    
    };
    Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs) {
             if (!lhs.same_isbn(rhs))
                 throw isbn_mismatch("isbn mismatch",lhs.book(), rhs.book());
             Sales_item ret(lhs);  // copy lhs into a local object that we'll return
             ret += rhs;           // add in the contents of rhs
             return ret;           // return ret by value
         }
    // use hypothetical bookstore exceptions
         Sales_item item1, item2, sum;
         while (cin >> item1 >> item2) { // read two transactions
             try {
                 sum = item1 + item2;       // calculate their sum
    	} catch (const isbn_mismatch &e) {
               cerr << e.what() << ": left isbn(" << e.left
                    << ") right isbn(" << e.right << ")"
                    << endl;}
  • new 之后但在 delete 之前发生的异常使得资源没有被撤销,从而引入异常安全的概念保证“如果发生异常,被分配的任何资源都适当地释放”。

    通过定义一个类来封闭资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”,简称 RAII。

    应该设计资源管理类,以便构造函数分配资源而析构函数释放资源。想要分配资源的时候,就定义该类类型的对象。如果不发生异常,就在获得资源的对象超出作用域后释放资源。更为重要的是,如果在创建了对象之后但在它超出作用域之前发生异常,那么,编译器保证撤销该对象,作为展开定义对象的作用域的一部分。

    class Resource {
        public:
            Resource(parms p): r(allocate(p)) { }
            ~Resource() { release(r); }
            // also need to define copy and assignment
        private:
            resource_type *r;           // resource managed by this type
            resource_type *allocate(parms p);     // allocate this resource
            void release(resource_type*);         // free this resource
        };
    
  • auto_ptr 类,是异常安全的“资源分配即初始化 RAII的例子。它接受一个类型形参的模板,为动态分配的对象提供异常安全。在头文件 memory 中定义。

    auto_ptr<T> ap; //创建名为 ap 的未绑定的 auto_ptr 对象

    auto_ptr<T> ap(p);//创建名为 apauto_ptr 对象,ap 拥有指针 p 指向的对象。该构造函数为 explicit

    auto_ptr<T> ap1(ap2);//创建名为 ap1auto_ptr 对象,ap1 保存原来存储在 ap2 中的指针。将所有权转给 ap1ap2 成为未绑定的 auto_ptr 对象

    ap1 = ap2;//将所有权 ap2 转给 ap1。删除 ap1 指向的对象并且使 ap1 指向 ap2 指向的对象,使 ap2 成为未绑定的

    ~ap  //析构函数。删除 ap 指向的对象

    *ap //返回对 ap 所绑定的对象的引用

    ap-> //返回 ap 保存的指针

    ap.reset(p);//如果 pap 的值不同,则删除 ap 指向的对象并且将 ap 绑定到 p

    ap.release();//返回 ap 所保存的指针并且使 ap 成为未绑定的

    ap.get();// 返回 ap 保存的指针

  • auto_ptr 只能用于管理从 new 返回的一个对象,它不能管理动态分配的数组.
  • auto_ptr 对象的复制和赋值是破坏性操作,基础对象的所有权从原来的 auto_ptr 对象转给副本,原来的 auto_ptr 对象重置为未绑定状态。因此,赋值的左右操作数必须都是可修改的左值。除了将所有权从右操作数转给左操作数之外,赋值还删除左操作数原来指向的对象。

  • 不能将 auto_ptrs 存储在标准库容器类型中。标准库的容器类要求在复制或赋值之后两个对象相等。因为如我们所见,当 auto_ptr 被复制或赋值的时候,有不寻常的行为。
     void f() {
            auto_ptr<int> ap(new int(42)); // allocate a new object
            // code that throws an exception that is not caught inside f
         }//编译器保证在展开栈越过 f 之前运行 ap 的析构函数。
    
     // error: constructor that takes a pointer is explicit and can't be used implicitly
        auto_ptr<int> pi = new int(1024);
        auto_ptr<int> pi(new int(1024)); // ok: uses direct initialization
    
  • auto_ptr类型没有定义到可用作条件的类型的转换

if (p_auto) // error: cannot use an auto_ptr as a condition

if (p_auto.get()) //OK

  • 应该只用 get 询问 auto_ptr 对象或者使用返回的指针值,不能用 get 作为创建其他 auto_ptr 对象的实参。

    使用 get 成员初始化其他 auto_ptr 对象违反 auto_ptr 类设计原则:在任意时刻只有一个 auto_ptrs 对象保存给定指针,如果两个 auto_ptrs 对象保存相同的指针,该指针就会被 delete 两次。

  • p_auto = new int(1024); // error: cannot assign a pointer to an auto_ptr

         p_auto.reset(new int(1024)); //不能直接将一个地址(或者其他指针)赋给 auto_ptr 对象,必须使用reset。

         要复位 auto_ptr 对象,可以将 0 传给 reset 函数。

        调用 auto_ptr 对象的 reset 函数时,在将 auto_ptr 对象绑定到其他对象之前,会删除 auto_ptr 对象所指向的对象(如果存在)。但是,正如自身赋值是没有效果的一样,如果调用该 auto_ptr 对象已经保存的同一指针的 reset 函数,也没有效果,不会删除对象。

  • 使用auto_ptr要注意

          1)不要使用 auto_ptr 对象保存指向静态分配对象的指针

          2)永远不要使用两个 auto_ptrs 对象指向同一对象。如用同一指针来初始化或者 reset 两个不同的 auto_ptr 对象,或使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset 另一个 auto_ptr 对象。

          3)不要使用 auto_ptr 对象保存指向动态分配数组的指针。当 auto_ptr 对象被删除的时候,它只释放一个对象——它使用 delete 操作符,而不用 delete []

          4)不要将 auto_ptr 对象存储在容器中

 

  • 定义异常说明

      void recoup(int) throw(runtime_error); //指出recoup函数如果抛出一个异常,该异常将是 runtime_error 对象,或者是由 runtime_error 派生的类型的异常。

      void no_problem() throw(); // 空说明列表指出函数不抛出任何异常

      在编译的时候,编译器不能也不会试图验证异常说明,不可能在编译时知道程序是否抛出异常以及会抛出哪些异常,只有在运行时才能检测是否违反函数异常说明。

      如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数 unexpected。默认情况下,unexpected 函数调用 terminate 函数,terminate 函数一般会终止程序。

      在 const 成员函数声明中,异常说明跟在 const 限定符之后。

      确定函数将不抛出任何异常,对函数的用户和编译器都有所帮助:知道函数不抛出异常会简化编写调用该函数的异常安全的代码的工作,我们可以知道在调用函数时不必担心异常,而且,如果编译器知道不会抛出异常,它就可以执行被可能抛出异常的代码所抑制的优化。

  • 异常说明与虚函数

      基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。

      这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常。

class Base {
     public:
         virtual double f1(double) throw ();
         virtual int f2(int) throw (std::logic_error);
         virtual std::string f3() throw
               (std::logic_error, std::runtime_error);
     };
class Derived : public Base {
     public:
         // error: exception specification is less restrictive than Base::f1's
         double f1(double) throw (std::underflow_error);

         // ok: same exception specification as Base::f2
         int f2(int) throw (std::logic_error);
         // ok: Derived f3 is more restrictive
         std::string f3() throw ();
     };
//通过派生类抛出的异常限制为由基类所列出的那些,在编写代码时就可以知道必须处理哪些异常。
//在确定可能需要捕获什么异常的时候compute 函数使用基类中的异常说明
void compute(Base *pb) throw(){
         try {
             // may throw exception of type std::logic_error
             // or std::runtime_error
             pb->f3();
         } catch (const logic_error &le)   { /* ... */ }
           catch (const runtime_error &re) { /* ... */ }
     }
  • 函数指针的异常说明
void (*pf)(int) throw(runtime_error); //pf指向接受int值的函数,该函数返回void对象,该函数只能抛出runtime_error类型的异常. 在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格。
     void recoup(int) throw(runtime_error);
     // ok: recoup is as restrictive as pf1
     void (*pf1)(int) throw(runtime_error) = recoup;
     // ok: recoup is more restrictive than pf2
     void (*pf2)(int) throw(runtime_error, logic_error) = recoup;
     // error: recoup is less restrictive than pf3
     void (*pf3)(int) throw() = recoup;
     // ok: recoup is more restrictive than pf4
     void (*pf4)(int) = recoup;
17.2 命名空间命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。命名空间作用域不能以分号结束。既可以定义新的命名空间,也可以添加到现在命名空间中。接口和实现的分离
// ---- Sales_item.h ----
     namespace cplusplus_primer {
         class Sales_item { /* ... */};
         Sales_item operator+(const Sales_item&,
                              const Sales_item&);
         // declarations for remaining functions in the Sales_item interface
     }
     // ---- Query.h ----
     namespace cplusplus_primer {
         class Query {
         public:
             Query(const std::string&);
             std::ostream &display(std::ostream&) const;
             // ...
         };
         class Query_base { /* ... */};
     }
     // ---- Sales_item.cc ----
     #include "Sales_item.h"
     namespace cplusplus_primer {
     // definitions for Sales_item members and overloaded operators
     }
     // ---- Query.cc ----
     #include "Query.h"
     namespace cplusplus_primer {
         // definitions for Query members and related functions
     }
// ---- user.cc ----
     // defines the cplusplus_primer::Sales_item class
     #include "Sales_item.h"
     int main() {
         // ...
         cplusplus_primer::Sales_item trans1, trans2;
         // ...
         return 0;
     }
全局命名空间(在任意类、函数或命名空间外) ::member_name 引用全局命名空间的成员。
  • 未命名的命名空间可以在给定文件中不连续,但不能跨越文件,每个文件有自己的未命名的命名空间。未命名的命名空间用于声明局部于文件的实体。在未命名的命名空间中定义的变量在程序开始时创建,在程序结束之前一直存在。
  • 未命名的命名空间中定义的名字可直接使用。不能使用作用域操作符来引用未命名的命名空间的成员。
      int i;   // global declaration for i
         namespace {
             int i;
         }
         // error: ambiguous defined globally and in an unnested, unnamed namespace
         i = 10;
    
  • 如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体。
  • 未命名的命名空间取代文件中的静态声明。
  • 除了在函数或其他作用域内部,头文件不应该包含 using 指示或 using 声明。头文件应该只定义作为其接口的一部分的名字,不要定义在其实现中使用的名字。
  • 类作用域中的 using 声明局限于被定义类的基类中定义的名字。
  • 命名空间别名。
    namespace cplusplus_primer { /* ... */ };
    namespace primer = cplusplus_primer;
    
  • using 指示(using directive )   using namespace namespace_name;   使得特定命名空间所有名字可见

          可以尝试用 using 指示编写程序,但在使用多个库的时候,这样做会重新引入名字冲突的所有问题。

    using 指示有用的一种情况是,用在命名空间本身的实现文件中。

    namespace blip {
        int bi = 16, bj = 15, bk = 23;
        // other declarations
    }
    int bj = 0; // ok: bj inside blip is hidden inside a namespace
    void manip(){
         using namespace blip;
         ++bi;           // sets blip::bi to 17
         ++bj;           // error: ambiguous
         ++::bj;         // ok: sets global bj to 1
         ++blip::bj;     // ok: sets blip::bj to 16
         int bk = 97;    // local bk hides blip::bk
         ++bk;           // sets local bk to 98
    }
  • 屏蔽命名空间名字规则的一个重要例外:

    接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。

    std::string s;

    // ok: calls std::getline(std::istream&, const std::string&)

    getline(std::cin, s); //它在当前作用域,包含调用的作用域以及定义 cin 的类型和 string 类型的命名空间中查找匹配的函数。因此,它在命名空间 std 中查找并找到由 string 类型定义的 getline 函数。

    std::string s;

    cin >> s;

    //如果没有这个例外
    using std::operator>>;        // need to allow cin >> s
    std::operator>>(std::cin, s); // ok: explicitly use std::>>
    
  • 隐式友元声明与命名空间:如果不存在可见的声明,那么,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。
namespace A {
        class C {
            friend void f(const C&); // makes f a member of namespace A
        };
}
//因为该友元接受类类型实参并与类隐式声明在同一命名空间中,所以使用它时可以无须使用显式命名空间限定符:

// f2 defined at global scope
void f2() {
         A::C cobj;
         f(cobj); // calls A::f
}


  • 命名空间对函数匹配有两个影响。一个影响是明显的:using 声明或 using 指示可以将函数加到候选集合。另一个则是如果该函数的形参有类类型则在该类类型(及定义其基类)的命名空间中查找,将匹配的函数加入候选.
namespace NS {
        class Item_base { /* ... */ };
        void display(const Item_base&) { }
    }
    // Bulk_item's base class is declared in namespace NS
    class Bulk_item : public NS::Item_base { };
    int main() {
        Bulk_item book1;
        display(book1);
        return 0;
    }
  • 如果命名空间内部的函数是重载的,那么,该函数名字的 using 声明声明了所有具有该名字的函数。如果 using 声明在已经有同名且带相同形参表的函数的作用域中引入函数,则 using 声明出错,否则,using 定义给定名字的另一重载实例,效果是增大候选函数集合。
  • 命名空间与模板:模板的显式特化必须在定义通用模板的命名空间中声明.

       有两种定义特化的方式:一种是重新打开命名空间并加入特化的定义,可以这样做是因为命名空间定义是不连续的;或者使用由命名空间名字限定的模板名定义特化。

 

17.3. 多重继承与虚继承

  • 构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。
  • 总是按构造函数运行的逆序调用析构函数。
  • 派生类的指针或引用可以转换为其任意基类(直接或间接)的指针或引用。但多重继承情况下,遇到二义性转换的可能性更大。
  • 像单继承一样,用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员或从其他基类继承的成员。
  • 多个基类含有同名函数,派生类对象调用该函数时可能导致二义性。即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个基类中是私有的而在另一个基类中是公用或受保护的,也是错误的。最后,如果在其中一个基类的基类中定义了该函数而直接基类中没有定义,调用仍是错误的。

          因为首先发生名字查找。然后编译器才确定所找到的声明是否合法。

  • 每个 IO 库类都继承了一个共同的抽象基类,那个抽象基类管理流的条件状态并保存流所读写的缓冲区。如果 IO 类型使用常规继承,则每个 iostream 对象可能包含两个 ios 子对象,通过使用虚继承解决这类问题。

image

虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。

class istream : public virtual ios { ... };
class ostream : virtual public ios { ... };
// iostream inherits only one copy of its ios base class
class iostream: public istream, public ostream { ... };
  • 即使基类是虚基类,也照常可以通过基类类型的指针或引用操纵派生类的对象。
  • 特定派生类实例的优先级高于共享虚基类实例
//从 VMI 类内部可以限定地访问哪些继承成员?哪些继承成员需要限定?
class Base {
    public:
        bar(int);
    protected:
        int ival;
    };
    class Derived1 : virtual public Base {
    public:
        bar(char);
        foo(char);
    protected:
        char cval;
    };
    class Derived2 : virtual public Base {
    public:
        foo(int);
    protected:
        int ival;
        char cval;
    };
    class VMI : public Derived1, public Derived2 { };
//bar和ival不用限定,且特定派生类实例的优先级高于共享虚基类实例
//不加限定访问的分别是Derived1::bar和Derived2::ival
//foo需要限定,不然有二义性
  • 派生类对虚基类的初始化。如果使用常规规则,就可能会多次初始化虚基类。

          在虚派生中,由最低层派生类的构造函数初始化虚基类。

          总是先调用虚基类的构造函数,再调用非虚基类的构造函数,分别都按声明的次序。

image

Bear::Bear(std::string name, bool onExhibit):
             ZooAnimal(name, onExhibit, "Bear") { }
Raccoon::Raccoon(std::string name, bool onExhibit)
           : ZooAnimal(name, onExhibit, "Raccoon") { }
Panda::Panda(std::string name, bool onExhibit)   //Panda虚派生自Bear和Raccoon
          : ZooAnimal(name, onExhibit, "Panda"), //首先使用构造函数初始化列表中指定的初始化式构造ZooAnimal部分
            Bear(name, onExhibit), //接下来,构造 Bear部分。忽略Bear的用于ZooAnimal构造函数初始化列表的初始化式。
            Raccoon(name, onExhibit),//然后,构造 Raccoon部分,再次忽略ZooAnimal初始化式。
            Endangered(Endangered::critical),
            sleeping_flag(false) { } //最后,构造 Panda 部分。
class TeddyBear : public BookCharacter, public Bear, public virtual ToyAnimal { /* ... */ }; 
要创建TeddyBear对象,调用构造函数的次序为ZooAnimal(); ToyAnimal();Character();BookCharacter(); Bear();TeddyBear(); 在这里,由最低层派生类TeddyBear指定用于 ZooAnimal和 ToyAnimal的初始化式

在合成复制构造函数中使用同样的构造次序,在合成赋值操作符中也是按这个次序给基类赋值。保证调用基类析构函数的次序与构造函数的调用次序相反。

 

第十八章 特殊工具与技术

18.1. 优化内存分配

  • new 基于每个对象分配内存的事实可能会对某些类强加不可接受的运行时开销,这样的类可能需要使用用户级的类类型对象分配能够更快一些。这样的类使用的通用策略是,预先分配用于创建新对象的内存,需要时在预先分配的内存中构造每个新对象。如vector
  • 分配原始内存时,必须在该内存中构造对象;对未构造的内存中的对象进行赋值而不是初始化,其行为是未定义的。对许多类而言,这样做引起运行时崩溃。赋值涉及删除现存对象,如果没有现存对象,赋值操作符中的动作就会有灾难性效果。
  • C++ 提供下面两种方法分配和释放未构造的原始内存。

       1)allocator,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。

       2)标准库中的 operator newoperator delete,它们分配和释放需要大小的原始的、未类型化的内存。

  • C++ 还提供不同的方法在原始内存中构造和撤销对象。

       1)allocator 类定义了名为 constructdestroy 的成员。construct 成员在未构造内存中初始化对象,destroy 成员在对象上运行适当的析构函数。

       2) 定位 new 表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。

       3)可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象所在的内存。

       4)算法 uninitialized_filluninitialized_copyfillcopy 算法一样执行,除了它们的目的地构造对象而不是给对象赋值之外。

  • allocator 类是一个模板,allocator 类将内存分配和对象构造分开。当 allocator 对象分配内存的时候,它分配适当大小并排列成保存给定类型对象的空间。但是,它分配的内存是未构造的,allocator 的用户必须分别 constructdestroy 放置在该内存中的对象。

allocator<T> a; //定义名为 aallocator 对象,可以分配内存或构造 T 类型的对象

a.allocate(n)//分配原始的未构造内存以保存 T 类型的 n 个对象

a.deallocate(p, n)//释放内存,在名为 pT* 指针中包含的地址处保存 T 类型的 n 个对象。运行调用 deallocate 之前在该内存中构造的任意对象的 destroy 是用户的责任

a.construct(p, t)//T* 指针 p 所指内存中构造一个新元素。运行 T 类型的复制构造函数用 t 初始化该对象

a.destroy(p)//运行 T* 指针 p 所指对象的析构函数

uninitialized_copy(b, e, b2)//从迭代器 be 指出的输入范围将元素复制到从迭代器 b2 开始的未构造的原始内存中。该函数在目的地构造元素,而不是给它们赋值。假定由 b2 指出的目的地足以保存输入范围中元素的副本

uninitialized_fill(b, e, t)//将由迭代器 be 指出的范围中的对象初始化为 t 的副本。假定该范围是未构造的原始内存。使用复制构造函数构造对象

uninitialized_fill_n(b, e, t, n)//将由迭代器 be 指出的范围中至多 n 个对象初始化为 t 的副本。假定范围至少为 n 个元素大小。使用复制构造函数构造对象

 

// pseudo-implementation of memory allocation strategy for a vector-like class
     template <class T> class Vector {
     public:
         Vector(): elements(0), first_free(0), end(0) { }
         void push_back(const T&);
          // ...
     private:
         static std::allocator<T> alloc; // object to get raw memory
         void reallocate(); // get more space and copy existing elements
         T* elements;       // pointer to first element in the array
         T* first_free;     // pointer to first free element in the array
         T* end;            // pointer to one past the end of the array
         // ...
     };
template <class T> void Vector<T>::push_back(const T& t) {
         // are we out of space?
         if (first_free == end)
           reallocate(); // gets more space and copies existing elements to it
         alloc.construct(first_free, t); // new (first_free) T(t); 
         ++first_free;
     }
template <class T> void Vector<T>::reallocate() {
         // compute size of current array and allocate space for twice as many elements
         std::ptrdiff_t size = first_free - elements;
         std::ptrdiff_t newcapacity = 2 * max(size, 1);
         // allocate space to hold newcapacity number of elements of type T
         T* newelements = alloc.allocate(newcapacity);  //T* newelements = static_cast<T*> (operator new[](newcapacity * sizeof(T))); 
         // construct copies of the existing elements in the new space
         uninitialized_copy(elements, first_free, newelements);
         // destroy the old elements in reverse order
         for (T *p = first_free; p != elements; /* empty */ )
            alloc.destroy(—p);                     //  p->~T(); 
         // deallocate cannot be called on a 0 pointer
         if (elements)    //传给 deallocate 一个零指针是不合法的。
             // return the memory that held the elements
             alloc.deallocate(elements, end - elements); // operator delete[](elements); 
         // make our data structure point to the new elements
         elements = newelements;
         first_free = elements + size;
         end = elements + newcapacity;
     }
  • new表达式调用名为 operator new的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。

         delete表达式对指向的对象运行适当的析构函数;然后,通过调用名为 operator delete 的标准库函数释放该对象所用内存。

        调用 operator delete 函数不会运行析构函数,它只释放指定的内存

  • 标准库函数operator new和operator delete是 allocator 的 allocate和deallocate成员的低级版本,它们都分配但不初始化内存.
  •      void *operator new(size_t);       // allocate an object
         void *operator new[](size_t);     // allocate an array
    
         void *operator delete(void*);     // free an object
         void *operator delete[](void*);   // free an array
    
  • allocator 的成员 constructdestroy 也有两个低级选择

       定位 new 表达式在已分配的原始内存中初始化一个对象,它不分配内存。比construct 更灵活,它可以使用任何构造函数。construct 函数总是使用复制构造函数。

      使用析构函数的显式调用作为调用 destroy 函数的低级选择

         new (place_address) type

         new (place_address) type (initializer-list)

     allocator<string> alloc;
     string *sp = alloc.allocate(2); // allocate space to hold 2 strings
     // two ways to construct a string from a pair of iterators
     new (sp) string(b, e);                    // construct directly in place
     alloc.construct(sp + 1, string(b, e));   // build and copy a temporary
  • 相对都使用低级版本而言,使用allocate类更好,因为operator new分配空间时要对指针强制类型转换,而且allocate类提供可感知类型的内存管理更安全灵活,但也要注意 定位new表达式比construct要灵活。
  • 编译器看到类类型的 newdelete 表达式的时候,它查看该类是否有 operator newoperator delete 成员,如果类定义(或继承)了自己的成员 newdelete 函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本。
  • 成员 newdelete 函数:如果类定义了这两个成员中的一个,它也应该定义另一个。

    这些函数隐式地为静态static函数.因为它们要么在构造对象之前使用operator new,要么在撤销对象之后使用(operator delete),

         void* operator new(size_t); //类成员 operator new必须返回类型 void* 并接受 size_t 类型的形参。用以字节计算的分配内存量初始化函数的 size_t 形参。

void operator delete(void*);//必须具有返回类型void。它可以定义为接受单个void*类型形参,也可以定义为接受两个形参

void operator delete(void*,size_t);//void*可以是空指针。若提供了 size_t 形参,就由编译器用第一个形参所指对象的字节大小自动初始化 size_t

当基类有virtual析构函数,size_t则传给 operator delete 的大小将根据被删除指针所指对象的动态类型而变化.

类成员 operator new[] 必须具有返回类型 void*,并且接受的第一个形参类型为 size_t。用表示存储特定类型给定数目元素的数组的字节数值自动初始化操作符的 size_t 形参。

成员操作符 operator delete[] 必须具有返回类型 void,并且第一个形参为 void* 类型。用表示数组存储起始位置的值自动初始化操作符的 void*形参。也可以有两个形参,第二个形参为 size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参。

 

  • 覆盖类特定的内存分配: 如果类定义了自己的成员 newdelete,类的用户就可以通过使用全局作用域确定操作符,用::new 或 ::delete 表达式使用全局的库函数。

          如果用 new 表达式调用全局 operator new 函数分配内存,则 delete 表达式也应该调用全局 operator delete 函数。

  •    一个内存分配器基类    

            改进内置库的 newdelete 函数。一个通用策略是预先分配一场原始内存来保存未构造的对象,创建新元素的时候,可以在一个预先分配的对象中构造;释放元素的时候,将它们放回预先分配对象的块中,而不是将内存实际返还给系统。这种策略常被称为维持一个自由列表freelist。可以将自由列表实现为已分配但未构造的对象的链表。

template <class Type>
     class QueueItem: public CachedObj< QueueItem<Type> > {
          // remainder of class declaration and all member definitions unchanged
     };
template <class T> class CachedObj {
     public:
         void *operator new(std::size_t);
         void operator delete(void *, std::size_t);
         virtual ~CachedObj() { }
     protected:
         T *next;
     private:
         static void add_to_freelist(T*);
         static std::allocator<T> alloc_mem;
         static T *freeStore;
         static const std::size_t chunk;
     };
template <class T>
         void *CachedObj<T>::operator new(size_t sz)
         {
              // new should only be asked to build a T, not an object
              // derived from T; check that right size is requested
              if (sz != sizeof(T))
                  throw std::runtime_error
                   ("CachedObj: wrong size object in operator new");
              if (!freeStore) {
                  // the list is empty: grab a new chunk of memory
                  // allocate allocates chunk number of objects of type T
                  T * array = alloc_mem.allocate(chunk);

                  // now set the next pointers in each object in the allocated memory
                  for (size_t i = 0; i != chunk; ++i)
                        add_to_freelist(&array[i]);
              }
              T *p = freeStore;
              freeStore = freeStore->CachedObj<T>::next;
              return p;   // constructor of T will construct the T part of the object
         }
template <class T>
     void CachedObj<T>::operator delete(void *p, size_t)
     {
        if (p != 0)
            // put the "deleted" object back at head of freelist
            add_to_freelist(static_cast<T*>(p));
     }
template <class T>
     void CachedObj<T>::add_to_freelist(T *p)
     {
        p->CachedObj<T>::next = freeStore;
        freeStore = p;
     }
template <class T> allocator< T > CachedObj< T >::alloc_mem;
template <class T> T *CachedObj< T >::freeStore = 0;
template <class T> const size_t CachedObj< T >::chunk = 24;
QueueItem<Type> *pt = new QueueItem<Type>(val); //使用 QueueItem<T>::operator new 函数从自由列表分配一个对象。 //再为类型 T 使用元素类型的复制构造函数,在该内存中构造一个对象。

CachedObj 只能用于不包含在继承层次中类型。与成员 newdelete 操作不同,CachedObj 类没有办法根据对象的实际类型分配不同大小的对象:它的自由列表保存单一大小的对象。因此,它只能用于不作基类使用的类,如 QueueItem 类。

 

18.2. 运行时类型识别RTTI  (Run-time Type Identification):  程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型。

  • 通过下面两个操作符提供 RTTI: 对于带虚函数的类,在运行时执行 RTTI 操作符,但对于其他类型,在编译时计算 RTTI 操作符。

         1) typeid 操作符,返回指针或引用所指对象的实际类型。

         2) dynamic_cast 操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。

  

  • dynamic_cast 操作符: 可将基类类型对象的引用或指针转换为同一继承层次中其他类型的引用或指针。

         与其他强制类型转换不同,dynamic_cast 涉及运行时类型检查。如果绑定到引用或指针的对象不是目标类型的对象,则 dynamic_cast 失败。如果转换到指针类型的 dynamic_cast 失败,则 dynamic_cast 的结果是 0 值;如果转换到引用类型的 dynamic_cast 失败,则抛出一个 bad_cast 类型的异常。

//假定 Base 是至少带一个虚函数的类,Derived 类派生于 Base 类。basePtr是指向 Base的指针
    if (Derived *derivedPtr = dynamic_cast<Derived*>(basePtr))  //非0指针转换成功
     {
         // use the Derived object to which derivedPtr points
     } else { // BasePtr points at a Base object
         // use the Base object to which basePtr points
     }

因为不存在空引用,所以不可能对引用使用用于指针强制类型转换的检查策略,相反,当转换失败的时候,它抛出一个 std::bad_cast 异常,该异常在库头文件 typeinfo 中定义。

void f(const Base &b){
        try {
            const Derived &d = dynamic_cast<const Derived&>(b);
        // use the Derived object to which b referred
        } catch (std::bad_cast) {
            // handle the fact that the cast failed
        }
     }
  • dynamic_cast失败条件:如果运行时实际绑定到引用或指针的对象不是目标类型的对象(或其派生类的对象),则dynamic_cast失败。
  • typeid 操作符:如果操作数是定义了至少一个虚函数的类类型,则在运行时计算类型
typeid(e)     //e 是任意表达式或者是类型名
                //结果为std::type_info&    在头文件typeinfo中
  • typeid 最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较
         Base *bp;
         Derived *dp;
         // compare type at run time of two objects
         if (typeid(*bp) == typeid(*dp)) {
             // bp and dp point to objects of the same type
         }
         // test whether run time type is a specific type
         if (typeid(*bp) == typeid(Derived)) {
             // bp actually points to a Derived
         }
    
  • 如果指针 p 的值是 0,那么,如果 p 的类型是带虚函数的类型,则 typeid(*p) 抛出一个 bad_typeid 异常;如果 p 的类型没有定义任何虚函数,则结果与 p 的值是不相关的。
  • RTTI 的使用
//比较基类和派生,3个类型就要9个操作符重载,4个类型要16个,太多。。。
     bool operator==(const Base&, const Base&)
     bool operator==(const Derived&, const Derived&)
     bool operator==(const Derived&, const Base&);
     bool operator==(const Base&, const Derived&);

//考虑用虚函数解决时,无法访问派生类特有成员

//使用RTTI dynamic_cast来访问运行时动态绑定的派生类私有成员
class Base {
         friend bool operator==(const Base&, const Base&);
     public:
         // interface members for Base
     protected:
         virtual bool equal(const Base&) const;
         // data and other implementation members of Base
     };
     class Derived: public Base {
         friend bool operator==(const Base&, const Base&);
     public:
         // other interface members for Derived
     private:
         bool equal(const Base&) const;
         // data and other implementation members of Derived
     };
 bool operator==(const Base &lhs, const Base &rhs)
     {
        // returns false if typeids are different otherwise
        // returns lhs.equal(rhs)
        return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
     }
bool Derived::equal(const Base &rhs) const
     {
        if (const Derived *dp
                   = dynamic_cast<const Derived*>(&rhs)) {
           // do work to compare two Derived objects and return result
        } else
           return false;
     }
bool Base::equal(const Base &rhs) const
     {
          // do whatever is required to compare to Base objects
     }
  • type_info 类:确切定义随编译器而变化,但是,标准保证所有的实现将至少提供以下操作

t1 == t2   如果两个对象 t1t2 类型相同,就返回 true;否则,返回 false

t1 != t2   如果两个对象 t1t2 类型不同,就返回 true;否则,返回 false

t.name()   返回 C 风格字符串,这是类型名字的可显示版本。类型名字用系统相关的方法产生

t1.before(t2) 返回指出 t1 是否出现在 t2 之前的 bool 值。before 强制的次序与编译器有关

  • 默认构造函数和复制构造函数以及赋值操作符都定义为 private,所以不能定义或复制 type_info 类型的对象。程序中创建 type_info 对象的唯一方法是使用 typeid 操作符。

 

18.3. 类成员的指针:只应用于类的非 static 成员

class Screen {
     public:
         typedef std::string::size_type index;
         char get() const;
         char get(index ht, index wd) const;
     private:
         std::string contents;
         index cursor;
         index height, width;
     };

//定义数据成员的指针
 Screen::index Screen::*pindex = &Screen::width;

//定义成员函数的指针:所属类的类型,返回类型,函数形参的类型和数目,包括成员是否为 const
char (Screen::*pmf2)(Screen::index, Screen::index) const = &Screen::get;

//为成员指针使用类型别名
 typedef
     char (Screen::*Action)(Screen::index, Screen::index) const;
 Action get = &Screen::get;

//使用类成员指针
//成员指针解引用操作符(.*)从对象或引用获取成员。
//成员指针箭头操作符(->*)通过对象的指针获取成员。

 // pmf points to the Screen get member that takes no arguments
     char (Screen::*pmf)() const = &Screen::get;
     Screen myScreen;
     char c1 = myScreen.get();      // call get on myScreen
     char c2 = (myScreen.*pmf)();   // equivalent call to get
     Screen *pScreen = &myScreen;
     c1 = pScreen->get();     // call get on object to which pScreen points
     c2 = (pScreen->*pmf)();  // equivalent call to get
     char (Screen::*pmf2)(Screen::index, Screen::index) const = &Screen::get;
     char c3 = myScreen.get(0,0);     // call two-parameter version of get
     char c4 = (myScreen.*pmf2)(0,0); // equivalent call to get

//使用数据成员的指针
Screen::index Screen::*pindex = &Screen::width;
     Screen myScreen;
     Screen::index ind1 = myScreen.width;      // directly
     Screen::index ind2 = myScreen.*pindex;    // dereference to get width
     Screen *pScreen;
     ind1 = pScreen->width;        // directly
     ind2 = pScreen->*pindex;      // dereference pindex to get width

//成员指针函数表

class Screen {
     public:
         Screen& home();         // cursor movement functions
         Screen& forward();
         Screen& back();
         Screen& up();
         Screen& down();
         
         typedef Screen& (Screen::*Action)();
         static Action Menu[];        // function table

         enum Directions { HOME, FORWARD, BACK, UP, DOWN };
         Screen& move(Directions);
     };
Screen::Action Screen::Menu[] = { &Screen::home,
                                       &Screen::forward,
                                       &Screen::back,
                                       &Screen::up,
                                       &Screen::down,
                                     };

Screen& Screen::move(Directions cm) {
          // fetch the element in Menu indexed by cm
          // run that member on behalf of this object
          (this->*Menu[cm])();
          return *this;
     }

     Screen myScreen;
     myScreen.move(Screen::HOME);    // invokes myScreen.home
     myScreen.move(Screen::DOWN);    // invokes myScreen.down

 

18.4. 嵌套类 Nested Classes:名字不是全局可见的

template <class Type> class Queue {
         // interface functions to Queue are unchanged
     private:
         //只有 Queue 或 Queue 的友元可以访问 QueueItem 类型,
         //所以可以用struct定义QueueItem使成员为 public 成员
         struct QueueItem {
             QueueItem(const Type &);
             Type item;            // value stored in this element
             QueueItem *next;      // pointer to next element in the Queue
         };
         QueueItem *head;      // pointer to first element in Queue
         QueueItem *tail;      // pointer to last element in Queue
     };//在类外部定义的嵌套类成员,构造函数template <class Type> Queue<Type>::QueueItem::QueueItem(const Type &t): item(t), next(0) { } 

因为 Queue 类是一个模板,它的成员也隐含地是模板。具体而言,嵌套类 QueueItem 隐含地是一个类模板。

Queue 类的每次实例化用对应于 Type 的适当模板实参产生自己的 QueueItem 类。QueueItem 类模板的实例化与外围 Queue 类模板的实例化之间的映射是一对一的。

  • 在外围类外部定义嵌套类
template <class Type> class Queue { 
     private:
         struct QueueItem; // 嵌套类的前向声明,在实际定义前,该类为不完全类型,不能创建对象,
                       //只能定义指针或引用,或声明(而非定义)用该类型作为形参或返回类型的函数
         QueueItem *head;  // pointer to first element in Queue
         QueueItem *tail;  // pointer to last element in Queue
     };
     template <class Type>
     struct Queue<Type>::QueueItem {
         QueueItem(const Type &t): item(t), next(0) { }
         Type item;        // value stored in this element
         QueueItem *next; // pointer to next element in the Queue
     };
  • 嵌套类中的非静态函数具有隐含的 this 指针,指向嵌套类型的对象。同样,外围类中的非静态成员函数也具有 this 指针,它指向外围类型的对象。

          嵌套类可以直接引用外围类的静态成员、类型名和枚举成员。

  • 实例化外围类模板的时候,不会自动实例化类模板的嵌套类。像任何成员函数一样,只有当在需要完整类类型的情况下使用嵌套类本身的时候,才会实例化嵌套类。
  • 处理类成员声明的时候,所用的任意名字必须在使用之前出现。当处理定义的时候,整个嵌套类和外围类均在作用域中。
class Outer {
     public:
         struct Inner {
             // ok: reference to incomplete class
             void process(const Outer&);
             Inner2 val; // error: Outer::Inner2 not in scope
         };
         class Inner2 {
         public:
             // ok: Inner2::val used in definition
             Inner2(int i = 0): val(i) { }
             // ok: definition of process compiled after enclosing class is complete
             void process(const Outer &out) { out.handle(); }
         private:
             int val;
         };
         void handle() const; // member of class Outer
     };

 

18.5. 联合:节省空间的类

  • 一个 union 对象可以有多个数据成员,但在任何时刻,只有一个成员可以有值。当将一个值赋给 union 对象的一个成员的时候,其他所有都变为未定义的。

        每个 union 对象的大小在编译时固定的:它至少与 union 的最大数据成员一样大。

  • union 可以指定保护标记使成员成为公用的、私有的或受保护的。默认情况下,union 表现得像 struct

    union 也可以定义成员函数,包括构造函数和析构函数。但是,union 不能作为基类使用,所以成员函数不能为虚数。

    union 不能具有静态数据成员或引用成员,而且,union 不能具有定义了构造函数、析构函数或赋值操作符的类类型的成员

    union illegal_members {
             Screen s;      // error: has constructor
             static int is; // error: static member
             int &rfi;      // error: reference member
             Screen *ps;    // ok: ordinary built-in pointer type
         };
    
    TokenValue first_token = {'a'};  // 显式初始化只能为第一个成员提供初始化式。
    TokenValue last_token;           // uninitialized TokenValue object
    TokenValue *pt = new TokenValue; // pointer to a TokenValue object
    last_token.cval = 'z';
    pt->ival = 42;
    

union 对象的某个数据成员一个值使得其他数据成员变为未定义的。通过错误的数据成员检索保存在 union 对象中的值,可能会导致程序崩溃或者其他不正确的程序行为。

避免通过错误成员访问 union 值的最佳办法是,定义一个单独的对象跟踪 union 中存储了什么值。这个附加对象称为 union判别式discriminant

  • 匿名联合:  不能有私有成员或受保护成员,也不能定义成员函数
class Token {
     public:
         enum TokenKind {INT, CHAR, DBL};
         TokenKind tok;   //判别式,跟踪 union 
         union {                 // anonymous union
             char   cval;
             int    ival;
             double dval;
         };
     };
Token token;
     switch (token.tok) {
         case Token::INT: 
             token.ival = 42; break; //匿名 union 的成员的名字出现在外围作用域中
     	 case Token::CHAR:
             token.cval = 'a'; break;
         case Token::DBL:
             token.dval = 3.14; break;
     }

18.6. 局部类(Local Classes): 在函数体内部定义的类

  • 局部类的所有成员(包括函数)必须完全定义在类定义体内部,因此,局部类远不如嵌套类有用。

          不允许局部类声明 static 数据成员,没有办法定义它们。

  • 局部类只能访问在外围作用域中定义的类型名、static 变量和枚举成员,不能使用定义该类的函数中的变量
     int a, val;
     void foo(int val)
     {
        static int si;
        enum Loc { a = 1024, b };
        // Bar is local to foo
        class Bar {
        public:
            Loc locVal; // ok: uses local type name
            int barVal;
            void fooBar(Loc l = a)         // ok: default argument is Loc::a
            {
               barVal = val;      // error: val is local to foo
               barVal = ::val;    // ok: uses global object
               barVal = si;       // ok: uses static local object
               locVal = b;        // ok: uses enumerator
            }
        };
        // ...
     }

可以将一个类嵌套在局部类内部。这种情况下,嵌套类定义可以出现在局部类定义体之外,但是,嵌套类必须在定义局部类的同一作用域中定义。照常,嵌套类的名字必须用外围类的名字进行限定,并且嵌套类的声明必须出现在局部类的定义中

void foo()
     {
        class Bar {
        public:
            // ...
            class Nested;    // declares class Nested
        };
        //  definition of Nested
        class Bar::Nested {
            // ...
        };
     }
//嵌套在局部类中的类本身是一个带有所有附加限制的局部类。
//嵌套类的所有成员必须在嵌套类本身定义体内部定义

 

18.7. 固有的不可移植的特征 (Inherently Nonportable Features)

算术类型的大小随机器不同而变化,位域和 volatile 限定符,链接指示(它使得可以链接到用其他语言编写的程序)

  • 18.7.1. 位域 (Bit-fields)

          保存特定的位数。当程序需要将二进制数据传递给另一程序或硬件设备的时候,通常使用位域。

         位域在内存中的布局是机器相关的。

         位域必须是整型数据类型,可以是 signedunsigned。通过在成员名后面接一个冒号以及指定位数的常量表达式,指出成员是一个位域

         地址操作符(&)不能应用于位域,所以不可能有引用类位域的指针,位域也不能是类的静态成员.

typedef unsigned int Bit;

     class File {
         Bit mode: 2;
         Bit modified: 1;
         Bit prot_owner: 3;
         Bit prot_group: 3;
         Bit prot_world: 3;
         
     public:                         //通常使用内置按位操作符操纵超过一位的位域
        enum { READ = 01, WRITE = 02 }; // File modes
	inline int isRead() { return mode & READ; }
     	inline int isWrite() { return mode & WRITE; }
	void setRead() { mode |= READ;} // set the READ bit

     };

(如果可能)将类定义体中按相邻次序定义的位域压缩在同一整数的相邻位,从而提供存储压缩。例如,在前面的声明中,5 个位域将存储在一个首先与位域 mode 关联的 unsigned int 中。位是否压缩到整数以及如何压缩与机器有关.

通常最好将位域设为 unsigned 类型。存储在 signed 类型中的位域的行为由实现定义。

 

18.7.2. volatile 限定符

volatile 的确切含义与机器相关,只能通过阅读编译器文档来理解。使用 volatile 的程序在移到新的机器或编译器时通常必须改变。

当可以用编译器的控制或检测之外的方式改变对象值的时候,应该将对象声明为 volatile。关键字 volatile 是给编译器的指示,指出对这样的对象不应该执行优化。

类也可以将成员函数定义为 volatilevolatile 对象只能调用 volatile 成员函数。

可以声明 volatile 指针、指向 volatile 对象的指针,以及指向 volatile 对象的 volatile 指针:

像用 const 一样,只能将 volatile 对象的地址赋给指向 volatile 的指针,或者将指向 volatile 类型的指针复制给指向 volatile 的指针。只有当引用为 volatile 时,我们才可以使用 volatile 对象对引用进行初始化。

合成的复制控制不适用于 volatile 对象,因为不能将 volatile 对象传递给普通引用或 const 引用。

如果类希望允许复制 volatile 对象,或者,类希望允许从 volatile 操作数或对 volatile 操作数进行赋值,它必须定义自己的复制构造函数和/或赋值操作符版本

 class Foo {
     public:
         Foo(const volatile Foo&);    // copy from a volatile object
         // assign from a volatile object to a non volatile objet
         Foo& operator=(volatile const Foo&);
         // assign from a volatile object to a volatile object
         Foo& operator=(volatile const Foo&) volatile;
         // remainder of class Foo
     };
//通过将复制控制成员的形参定义为 const volatile 引用,我们可以从任何各类的 Foo 对象进行复制或赋值:普通 Foo 对象、const Foo 对象、volatile Foo 对象或 const volatile Foo 对象。

 

18.7.3. 链接指示 extern "C"  指出任意非 C++ 函数所用的语言。

  • 链接指示有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上。
// illustrative linkage directives that might appear in the C++ header <cstring>
     // single statement linkage directive
     extern "C" size_t strlen(const char *);
     // compound statement linkage directive
     extern "C" {
         int strcmp(const char*, const char*);
         char *strcat(char*, const char*);
     }
// compound statement linkage directive
     extern "C" {
     #include <string.h>     // C functions that manipulate C-style strings
     }

有时需要在 C 和 C++ 中编译同一源文件。当编译 C++ 时,自动定义预处理器名字 __cplusplus(两个下划线),所以,可以根据是否正在编译 C++ 有条件地包含代码。

#ifdef __cplusplus
     // ok: we're compiling C++
     extern "C"
     #endif
     int strcmp(const char*, const char*);
C 语言不支持函数重载,在一组重载函数中只能为一个 C 函数指定链接指示
// error: two extern "C" functions in set of overloaded functions
     extern "C" void print(const char*);
     extern "C" void print(int);
  • extern "C" 函数和指针

      C 函数的指针与 C++ 函数的指针具有不同的类型,不能将 C 函数的指针初始化或赋值为 C++ 函数的指针(反之亦然)。

     void (*pf1)(int);            // points to a C++ function
     extern "C" void (*pf2)(int); // points to a C function
     pf1 = pf2; // error: pf1 and pf2 have different types

因为链接指示应用于一个声明中的所有函数,所以必须使用类型别名,以便将 C 函数的指针传递给 C++ 函数

// FC is a pointer to C function
     extern "C" typedef void FC(int);
     // f2 is a C++ function with a parameter that is a pointer to a C function
     void f2(FC *);

A.1

abort      <cstdlib>           ios_base  <ios_base>

accumulate <numeric>           isalpha   <cctype>

allocator  <memory>            islower   <cctype>

auto_ptr   <memory>            ispunct   <cctype>

back_inserter  <iterator>      isspace   <cctype>

bad_alloc   <new>              istream_iterator <iterator>

bad_cast <typeinfo>            istringstream <sstream>

bind2nd  <functional>          isupper <cctype>

boolalpha <iostream>           left     <iostream>

less_equal <functional>        copy      <algorithm>

logic_error <stdexcept>        count     <algorithm>

lower_bound <algorithm>        count_if  <algorithm>

make_pair  <utility>           max  <algorithm>

dec       <iostream>           min  <algorithm>

ends      <iostream>           equal_range <algorithm>

negate    <functional>         exception <exception>

noboolalpha <iostream>         fill  <algorithm>

noshowbase  <iostream>         fill_n <algorithm>

noshowpoint <iostream>         find  <algorithm> 

noskipws    <iostream>         find_end <algorithm>

not1        <functional>       find_first_of <algorithm>

nounitbuf   <iostream>         fixed  <iostream>

nouppercase  <iostream>        flush  <iostream>

nth_element  <algorithm>       for_each  <algorithm>

oct          <iostream>        front_inserter <iterator>

ofstream     <fstream>         fstream <fstream>

getline      <string>          ostream_iterator <iterator>

hex          <iostream>        ostringstream <sstream>

ifstream    <fstream>          out_of_range <stdexcept>

inner_product <numeric>        pair        <utility>

inserter     <iterator>        partial_sort <algorithm>

internal      <iostream>       plus       <functional> 

priority_queue <queue>         sqrt        <cmath>

ptrdiff_t      <cstddef>       stable_sort  <algorithm>

range_error   <stdexcept>      strcmp       <cstring>

replace       <algorithm>      strcpy       <cstring>

replace_copy  <algorithm>      reverse_iterator  <iterator>

stringstream  <sstream>        right     <iostream>

strlen        <cstring>        runtime_error <stdexcept>

strncpy       <cstring>        scientific   <iostream>

terminate <exception>          tolower    <cctype>

set_difference <algorithm>     toupper    <cctype>

set_intersection <algorithm>   type_info   <typeinfo>

set_union   <algorithm>        unexpected<exception>

setfill     <iomanip>          uninitialized_copy<memory>

setprecision<iomanip>          unitbuf<iostream>

setw<iomanip>                  unique<algorithm>

showbase<iostream>             unique_copy<algorithm>

showpoint<iostream>            upper_bound<algorithm>

size_t<cstddef>                uppercase<iostream>

skipws<iostream>               sort<algorithm>

 

 

A.2. 算法简介

A.2.1. 查找对象的算法

find(beg, end, val)  

count(beg, end, val)

find_if(beg, end, unaryPred)

count_if(beg, end, unaryPred)

查找许多值中的一个的算法

find_first_of(beg1, end1, beg2, end2)
find_first_of(beg1, end1, beg2, end2, binaryPred)
find_end(beg1, end1, beg2, end2)
find_end(beg1, end1, beg2, end2, binaryPred)

查找子序列的算法

adjacent_find(beg, end)
adjacent_find(beg, end, binaryPred)
search(beg1, end1, beg2, end2)
search(beg1, end1, beg2, end2, binaryPred)
search_n(beg, end, count, val)
search_n(beg, end, count, val, binaryPred)
A.2.2. 其他只读算法
for_each(beg, end, f)
mismatch(beg1, end1, beg2)
mismatch(beg1, end1, beg2, binaryPred)
equal(beg1, end1, beg2)
equal(beg1, end1, beg2, binaryPred)
A.2.3. 二分查找算法
lower_bound(beg, end, val)
lower_bound(beg, end, val, comp)
upper_bound(beg, end, val)
upper_bound(beg, end, val, comp)
equal_range(beg, end, val)
equal_range(beg, end, val, comp)
binary_search(beg, end, val)
binary_search(beg, end, val, comp)

A.2.4. 写容器元素的算法

只写元素不读元素的算法

fill_n(dest, cnt, val)
generate_n(dest, cnt, Gen)

使用输入迭代器写元素的算法

copy(beg, end, dest)
transform(beg, end, dest, unaryOp)
transform(beg, end, beg2, dest, binaryOp)

replace_copy(beg, end, dest, old_val, new_val)
replace_copy_if(beg, end, dest, unaryPred, new_val)

merge(beg1, end1, beg2, end2, dest)
merge(beg1, end1, beg2, end2, dest, comp)

使用前向迭代器写元素的算法

swap(elem1, elem2)
iter_swap(iter1, iter2)
swap_ranges(beg1, end1, beg2)
fill(beg, end, val)
generate(beg, end, Gen)
replace(beg, end, old_val, new_val)
replace_if(beg, end, unaryPred, new_val)

使用双向迭代器写元素的算法

copy_backward(beg, end, dest)
inplace_merge(beg, mid, end)
inplace_merge(beg, mid, end, comp)

A.2.5. 划分与排序算法

划分算法:要求双向迭代器

stable_partition(beg, end, unaryPred)
partition(beg, end, unaryPred)

排序算法

sort(beg, end)
stable_sort(beg, end)
sort(beg, end, comp)
stable_sort(beg, end, comp)
partial_sort(beg, mid, end)
partial_sort(beg, mid, end, comp)
partial_sort_copy(beg, end, destBeg, destEnd)
partial_sort_copy(beg, end, destBeg, destEnd, comp)
nth_element(beg, nth, end)
nth_element(beg, nth, end, comp)

A.2.6. 通用重新排序操作

使用前向迭代器的重新排序算法

remove(beg, end, val)
remove_if(beg, end, unaryPred)
unique(beg, end)unique(beg, end, binaryPred)
rotate(beg, mid, end)
使用双向迭代器的重新排序算法
reverse(beg, end)
reverse_copy(beg, end, dest)

写至输出迭代器的重新排序算法

remove_copy(beg, end, dest, val)
remove_copy_if(beg, end, dest, unaryPred)
unique_copy(beg, end, dest)
unique_copy(beg, end, dest, binaryPred)
rotate_copy(beg, mid, end, dest)
random_shuffle(beg, end)
random_shuffle(beg, end, rand)
A.2.7. 排列算法
要求双向迭代器的排列算法
next_permutation(beg, end)
next_permutation(beg, end, comp)
prev_permutation(beg, end)
prev_permutation(beg, end, comp)

A.2.8. 有序序列的集合算法

要求输入迭代器集合算法

includes(beg, end, beg2, end2)
includes(beg, end, beg2, end2, comp)
set_union(beg, end, beg2, end2, dest)
set_union(beg, end, beg2, end2, dest, comp)
set_union(beg, end, beg2, end2, dest)
set_union(beg, end, beg2, end2, dest, comp)

set_difference(beg, end, beg2, end2, dest)
set_difference(beg, end, beg2, end2, dest, comp)

set_symmetric_difference(beg, end, beg2, end2, dest)
set_symmetric_difference(beg, end, beg2, end2, dest, comp)
A.2.9. 最大值和最小值
min(val1, val2) 
min(val1, val2, comp) 
max(val1, val2) 
max(val1, val2, comp)

min_element(beg, end)min_element(beg, end, comp)max_element(beg, end)
max_element(beg, end, comp)

lexicographical_compare(beg1, end1, beg2, end2)
lexicographical_compare(beg1, end1, beg2, end2, comp)
A.2.10. 算术算法
accumulate(beg, end, init)accumulate(beg, end, init, BinaryOp)
inner_product(beg1, end1, beg2, init)
inner_product(beg1, end1, beg2, init, BinOp1, BinOp2)
partial_sum(beg, end, dest) partial_sum(beg, end, dest, BinaryOp)
adjacent_difference(beg, end, dest)
adjacent_difference(beg, end, dest, BinaryOp)

 

A.3. 再谈 IO 库

A.3.1. 格式状态

    boolalpha            将真和假显示为字符串

x   noboolalpha          将真和假显示为 1, 0

    showbase             产生指出数的基数的前缀

x   noshowbase           不产生记数基数前缀

    showpoint            总是显示小数点

x   noshowpoint          有小数部分才显示小数点

    showpos              显示非负数中的 +

x   noshowpos            不显示非负数中的 +

    uppercase            在十六进制中打印 0X,科学记数法中打印 E

x   nouppercase          在十六进制中打印 0x,科学记数法中打印 e

x   dec                  用十进制显示

    hex                  用十六进制显示

    oct                  用八进制显示

    left                 在值的右边增加填充字符

    right                在值的左边增加填充字符

    internal             在符号和值之间增加填充字符

    fixed                用小数形式显示浮点数

    scientific           用科学记数法显示浮点数

    flush                刷新 ostream 缓冲区

    ends                 插入空字符,然后刷新 ostream 缓冲区

    endl                 插入换行符,然后刷新 ostream 缓冲区

    unitbuf              在每个输出操作之后刷新缓冲区

x   nounitbuf            恢复常规缓冲区刷新

x   skipws               为输入操作符跳过空白

    noskipws             不为输入操作符跳过空白

    ws                  “吃掉”空白

注:带 x 的是默认流状态。

 

iomanip 中定义的操纵符

setfill(ch)             ch 填充空白

setprecision(n)         将浮点精度置为 n

setw(w)                 读写 w 个字符的值

setbase(b)              按基数 b 输出整数

 

  • flags 操纵恢复格式状态

不带实参的 flags() 返回流的当前格式状态。返回值是名为 fmtflags 的标准库定义类型。

flags(arg) 接受一个实参并将流格式置为实参所指定的格式

 void display(ostream& os)
     {
          // remember the current format state
          ostream::fmtflags curr_fmt = os.flags();
          // do output that uses manipulators that change the format state of os
          os.flags(curr_fmt);              // restore the original format state of os
     }

A.3.3. 控制输出格式

控制布尔值和格式:boolalpha  取消:noboolalpha

指定整型值的基数 hexoctdec

指出输出的基数:需要打印八进制或十六进制值,可能应该也使用 showbase 操纵符         重置noshowbase

控制浮点值的格式:指定显示精度: 成员函数precision一个版本接受一个 int 值并将精度设置为那个新值,它返回先前的精度值;另一个版本不接受实参并返回当前精度值。

                           setprecision 操纵符接受一个实参,用来设置精度。

                                控制记数法:scientific 操纵符将流变为使用科学记数法         fixed 操纵符将流为使用固定位数小数表 

                                                   uppercase 操纵符控制科学记数法中的 e和16进制x的大小写

                                                  要将流恢复为浮点值的默认处理,必须把floatfield 的标准库定义值传给 unsetf 成员函数来取消 scientificfixed 所做的改变

                                                    // reset to default handling for notation

                                                   cout.unsetf(ostream::floatfield);     

                               显示小数点:默认情况下,当浮点值的小数部分为 0 的时候,不显示小数点。showpoint 操纵符强制显示小数点。noshowpoint 操纵符恢复默认行为

                               填充输出:        setw,指定下一个数值或字符串的最小间隔。

                          left,左对齐输出。              right,右对齐输出。输出默认为右对齐。

                                                         internal,控制负值的符号位置。internal 左对齐符号且右对齐值,用空格填充介于其间的空间

                         setfill,使我们能够指定填充输出时使用的另一个字符。默认情况下,值是空格。

A.3.4. 控制输入格式化

noskipws 操纵符导致输入操作符读(而不是跳过)空白(空格、制表符、换行符、进纸和回车)。要返回默认行为,应用 skipws 操纵符

A.3.6. 单字节操作

几个未格式化的操作一次一个字节地处理流,它们不忽略空白地读。例如,可以使用未格式化 IO 操作 getput 一次读一个字符

     char ch;
     while (cin.get(ch))   //与noskipws相同的输入
             cout.put(ch);

单字节低级 IO 操作:

is.get(ch)   istream is 的下一个字节放入 ch,返回 is

os.put(ch)   将字符 ch 放入 ostream,返回 os

is.get()     返回 is 的下一字节作为一个 int

is.putback(ch)  将字符 ch 放回 is,返回 is

is.unget()    is 退回一个字节,返回 is.  把已经从流读取出来的那个字符放回去.

is.peek()     将下一字节作为 int 值返回但不移出它.   返回输入流上下一字符的副本但不改变流

这些函数返回 int 值的原因是为了允许它们返回一个文件结束标记。允许给定字符集使用 char 范围的每一个值来表示实际字符,因此,该范围中没有额外值用来表示文件结束符。

相反,这些函数将字符转换为 unsigned char,然后将那个值提升为 int,因此,即使字符集有映射到负值的字符,从这些操作返回的值也将是一个正值. 通过将文件结束符作为负值返回,标准库保证文件结束符区别于任意合法字符值。为了不要求我们知道返回的实际值,头文件 iostream 定义了名为 EOFconst,可以使用它来测试 get 的返回值是否为文件结束符。实质上我们使用 int 对象来保存这些函数的返回值

 

A.3.7. 多字节操作:

is.get(sink, size, delim) is 中读 size 个字节并将它们存储到 sink 所指向的字符数组中。读操作直到遇到 delim 字符,或已经读入了 size 个字节,或遇到文件结束符才结束。如果出现了 delim,就将它留在输入流上,不读入到 sink 中。

is.getline(sink, size, delim)   与三个实参的 get 行为类似,但读并丢弃 delim

is.read(sink, size)size 个字节到数组 sink。返回 is

is.gcount()  返回最后一个未格式化读操作从流 is 中读到的字节数 . 如果在调用 gcount 之前调用 peekungetputback,则返回值将是 0!

os.write(source, size)  size 个字从数组 source 写至 os。返回 os

is.ignore(size, delim)  读并忽略至多 size 个字符,直到遇到 delim,但不包括 delim。默认情况下,size 是 1 而 delim 是文件结束符

 

get 或其他返回 int 值的函数的返回值赋给 char 对象而不是 int 对象,是常见的错误,但编译器不检测这样的错误,相反,发生什么取决于机器和输入数据。例如,在将 char 实现为 unsigned char 的机器上,这是一个死循环:

    char ch;    // Using a char here invites disaster!
     // return from cin.get is converted from int to char and then compared to an int
     while ((ch = cin.get()) != EOF)
              cout.put(ch);

A.3.8. 流的随机访问

为了支持随机访问,IO类型维持一个标记,该标记决定下一个读或写发生在哪里。IO 类型还提供两个函数:一个通过 seek 指定位置重新安置该标记,另一个 tell 我们标记的当前位置。标准库实际上定义了两对 seek 和 tell 函数, 一对由输入流使用,另一对由输出流使用。输入和输出版本由后缀 gp 区分,g 版本指出正在“获取”(读)数据,p 函数指出正在“放置”(写)数据。

seekg   重新定位输入流中的标记

tellg   返回输入流中标记的当前位置

seekp   重新定位输出流中的标记

tellp   返回输出流中标记的当前位置

普通 iostream 对象一般不允许随机访问,该认为本节其余部分只能应用于 fstreamsstream 类型

seek 函数有两个版本:一个移动到文件中的一个“绝对”地址,另一个移动到给定位置的字节偏移量处:

 // set the indicated marker a fixed position within a file or string
     seekg(new_position); // set read marker
     seekp(new_position); // set write marker
 // offset some distance from the indicated position
     seekg(offset, dir); // set read marker
     seekp(offset, dir); // set write marker

seek 实参的偏移量:     inOut.seekg(0, fstream::beg);   // reposition to start of the file

beg  流的开头

cur  流的当前位置

end  流的末尾

名为 pos_typeoff_type 的类型分别表示文件位置和从该位置的偏移量。off_type 类型的值可以为正也可以为负,在文件中可以进行前向或后向 seek

// remember current write position in mark
    ostringstream writeStr; // output stringstream
    ostringstream::pos_type mark = writeStr.tellp();
     // ...
     if (cancelEntry)
          // return to marked position
          writeStr.seekp(mark);

 

posted on 2011-04-12 16:28 Atela 阅读(823) 评论(0)  编辑 收藏 引用 所属分类: C/C++


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

<2011年4月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

统计

常用链接

留言簿

随笔分类

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜