0%

大多数情况下,适当提出classes或class templates定义以及functions和function
templates声明,是最花费心力的两件事。太快定义变量可能造成效率上的拖延;过度使用转型可能导致代码变慢又难维护,又找来微妙的理解错误;返回对象内部数据的handle可能会破坏封装并留给客户dangling
handles;未考虑异常带来的冲击则可能导致资源泄露和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合(coupling)则可能导致让人不满意的冗长build
times。

条款26:尽可能延后变量定义式的出现时间

  1. 当定义了一个类型带有构造函数或析构函数,就得付出构造成本和析构成本。如果函数可能抛出异常,最好将未使用的变量尽量延后声明(为何不声明定义分割是因为将函数构造出来再赋值比直接构造时指定初值效率低)。
  2. 对于在循环结构中定义在里头还是外头需要权衡:如果赋值成本比“构造+析构”成本高,那么倒不如在循环结构中定义。

条款27:尽量少做转型动作

  1. 回顾转型语法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // old-style casts
    (T)expression
    T(expression)

    // C++-style casts
    /*
    const_cast通常用来将对象常量行转除(cast away he constness)
    */
    const_cast<T>(expression)

    /*
    dynamic_cast主要用来执行安全向下转型(safe downcasting),用来决定某对象是否归属继承体系中的某个类型,它是唯一无法由旧式语法执行的动作,也很耗费重大运行成本的转型动作
    */
    dynamic_cast<T>(expression)

    /*
    reinterpret_cast意图执行低级转型,它取决于编译器,不可移植。它可以这么用,将pointer to int转型为一个int
    */
    reinterpret_cast<T>(expression)

    /*
    static_cast用来强迫隐式转换(implicit conversions),它基本什么都能做,就是无法将const转为non-const,这只有const_cast办得到。
    */
    satic_cast<T>(expression)
    有人觉得转型起始什么都没做,其实这是错误的观念:底层转型表述几乎会产生一些代码:
    1
    2
    3
    4
    class Base { ... };
    class Derived : public Base { ... };
    Derived d;
    Base *pb = &d;
    这里不过是建立一个base class指针指向一个derived class对象,但有时候两个指针值并不相同。这种情况下会有个偏移量offset)在运行期被施行于Derived*指针身上,才取得正确Base*指针值。
    我们很容易写出似是而非的代码(在其他语言可能是真的)。例如derived classes内的virtual函数代码调用base class对应函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /* 
    这段程序将*this转型为Window,它调用的不是当前对象上的函数,
    而是*this对象的baseclass部分的一个临时副本。
    */
    class Window
    {
    public:
    virtual void onResize() { ... }
    ...
    };
    class SpecialWindow: public Window
    {
    public:
    virtual void onResize()
    {
    static_cast<Winddow>(*this).onResize();
    // 正确做法是:
    Window::onResize();
    ...
    }
    ...
    };

条款28:避免返回handles指向对象内部成分

  1. 一个成员函数返回referencespointersitertors,这些统统都是所谓的handles(号码牌,用来取得某个对象)。而一个返回“代表对象内部数据”的handle,随之而来的是降低对象封装性的风险。

条款29:为”异常安全“努力是值得的

  1. 当异常被抛出时,带有异常安全性Exception safety)的函数一般不泄露任何资源并不允许数据败坏。这样的函数可分为三种保证:基本型、强烈型和不抛出异常型。
  2. 强烈保证以copy-and-swap实现出来,保证一个动作要么全部完成,否则不进行任何变动。

条款30:透彻了解inlining的里里外外

  1. inline内嵌函数可以免除函数的调用成本,编译器最优化机制通常被设计来浓缩不含函数调用的代码。然而过多的inline可能导致增加目标码object coed)大小。在内存有限 的机器上,过度热衷inlining会使程序体积太大,即使拥有虚内存,造成的代码膨胀会导致额外的换页行为paging),降低指令高速缓存的命中率instruction cache hit rate),以及伴随这些问题而来的效率损失。
  2. Inline函数通常被置于头文件内,因为多数build environments在编译过程中进行inlining,在链接期完成inlining。inlining在多数C++程序中是编译期行为。
  3. inline是一个向编译器发出的申请,而不是强制命令。大部分编译器拒绝过于复杂(如有循环和递归)的函数inlining。对于virtual函数也会,因为virtual意味着”wait,知道runtime才确定调用哪个函数“。

条款31:将文件间的编译依存关系降至最低

  1. #include指示符提供的定义式和其含入文件之间形成了一种编译依存关系compilation dependency)。如果这些头文件中有任何一个被改变,或者这些头文件所依赖的其他头文件有任何改变,那么每个含有class的文件就得重新编译。解决方案是通过前置声明配合指针或引用类型声明来减少编译依赖。

条款18:让接口容易被正确使用,不易被误用

  1. 除非有好理由,否则应该尽量令你的types的行为与内置types一致。“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  2. “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除用户的资源管理责任。
  3. std::shared_ptr支持定制型删除其(custom deleter)。这可防范DLL问题(在这个dll创建对象,在另一边dll删除),可被用来自动解除互斥锁等等。

条款19:设计class犹如设计type

  1. C++程序员许多时间主要用来扩张类型系统(type system)。这意味着他不仅是class设计者,还是type设计者。包括重载(overloading)函数和操作符、控制内存的分配和归还、定义对象的初始化和终结……所以应该带着和“语言设计者当初设计语言内置类型时”一样的谨慎来研讨class设计。
  2. 设计高效的classes需要考虑:
    1. 新type的对象应该如何被创建和销毁?(operator new,operator new[],operator delete和operator delete[]的设计);
    2. 对象的初始化和对象的复制有什么样的差别?(构造函数和复制(assigment)操作符的行为;
    3. 新type的对象如果被passed by value(以值传递),意味着什么?
    4. 什么是新type的“合法值“? class必须维护的约束条件(invariants)(构造函数、赋值操作符和”setter“函数需要的错误检查工作。它影响函数的异常抛出、以及函数异常明细列(exception specifications)。
    5. 新type需要配合某个继承图系(inheritance graph)吗? (必然受到那些classes设计的束缚)
    6. 新type需要什么样的转换? (当需要隐式转换,必须写一个类型转换函数或者写一个non-explicit-one-argument(可被单一实参调用)构造函数)
    7. 什么样的操作符和函数对此新type而言是合理的?
    8. 什么样的标准函数应该驳回? (private)
    9. 谁该取用新type的成员? (public、protected、private、friends)
    10. 什么是新type的”未声明接口“(undeclared interface)?
    11. 新type有多么一般化? 如果是定义一整个types家族,就需要定义一个class template)
    12. 真的需要一个新type吗?

条款20:宁以pass-by-reference-to-const替换pass-by-value

  1. 当一个函数以pass by value方式接受对象,它的成本是很高的,很短的声明周期使它调用构造函数和析构函数。以by reference方式传递参数可以避免slicing(对象切割)问题(derived class对于base class来说的特化信息丢失)。
  2. 当使用内置类型有机会选择采用pass-by-value或pass-by-reference-to-const时,by value方式可能会效率高些,因为内置类型都相当小。一般可以合理假设”pass-by-value并不昂贵“的唯一对象就是内置类型和STL的迭代器和函数对象。

条款21:必须返回对象时,别妄想返回其reference

  1. 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个eap-allocated对象,或返回pointer或reference指向一个local sattic对象而有可能同时需要多个这样的对象。

条款22:将成员变量声明未private

  1. 成员变量的封装性与”成员变量的内容改变时所破坏的代码数量“成反比。取消一个public成员变量,所有使用它的客户码都会被破坏,;取消一个protected成员变量,所有使用它的derived classes都会被破坏。从封装的角度来看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。
  2. 将成员变量声明为private。可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
  3. protected并不比public更具封装性。

条款23:宁以non-member、non-friend替换member函数

  1. 如果某些东西被封装,越多东西被封装,就越多的弹性去改变它。
  2. 如果在一个member函数和一个non-member,non-friend函数之间做抉择,两者提供相同机能,那么较大封装性的是non-member non-friend函数,因为它不增加访问private成分的函数数量。
  3. 标准程序库并不是拥有单一、整体、庞大的<C++StandardLibrary>头文件并在其中包含std命名空间内的每一样东西,而是有数十个头文件,每个头文件声明std的某些机能。
  4. 将所有便利函数放在多个头文件但隶属同一个ing名空间,意味着客户可以轻松扩展这一组便利函数。它需要做的就是添加更多non-member non-friend函数到此命名空间内。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

1
2
3
4
5
6
7
8
9
10
class Rational
{
public:
Rationalint numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
const Rational oeprator*(const Rational &rhs) const;
private:
...
};

当尝试混合式算式,只有一般行的通;

1
2
3
4
5
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // ok
result = oneHalf * 2; // ok
result = 2 * oneHalf; // error

oneHalf是一个内含operator *函数的class对象,而整数2并没有相应的class,也就没有operator*成员函数。但为什么第二个参数是2时可被接受?这里发生了隐式类型转换implicit type conversion)。

1
2
const Rational temp(2);
result = oneHalf * temp;

而实际想要支持混合式算数运算,就让operator*称为一个non-member函数,允许编译器在每一个实参身上执行隐式类型转换:

1
2
3
4
const Rational operator*(const Rational &lhs, const Rational &rhs)
{
return Rational(lhs.numberator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

因此结论:如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是non-member。

条款25:考虑写出一个不抛出异常的swap函数

  1. 一旦要置换两个类对象值,唯一需要做的就是置换其pImpl指针,但default swap算法不知道这点。确切实践思路的一个做法是将std::swap针对该class对象特化total template specialization):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Widge
    {
    public:
    ...
    void swap(Widget &other)
    {
    using std::swap;
    swap(pImpl, other.pImpl); // 置换指针
    }
    };

    namespace std
    {
    temlate<>
    void swap<Widget>(Widget &a, Widget &b)
    {
    a.swap(b);
    }
    }
    假设WidgetWidgetImpl都是class templates而非classes,可以尝试将它们的数据类型参数化:
    1
    2
    3
    4
    5
    template<typename T>
    class WidgetImpl { ... };

    template<typename T>
    class Widget { ... };
    在类内放个swap成员函数很简单,却在特化std::swap时遇上乱流:
    1
    2
    3
    4
    5
    6
    7
    namespace std
    {
    // error
    template<typename T>
    void swap< Widget<T> >(Widget<T> &a, Widget<T> &b)
    { a.swap(b); }
    }
    看起来合理但不合法,这是企图偏特化partially specialize)一个function template,但C++只允许对class template偏特化。有时候std的内容是标准委员会决定,如果希望软件有预期行为,最好不加新东西到std里头。为此,还是声明non-member swap让它调用member swap,但不再将non-member swap声明为std::swap特别版本或重载版本。
    顺带一提,任何地点的任何代码打算置换两个Widget对象,因而调用swap,C++的名称查找法则(name lookup rules,或argument-dependent lookup和koenig lookup法则)会找到专属版本,那正是我们需要的:
    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T>
    void doSomething(T &obj1, T &obj2)
    {
    using std::swap; // 令std::swap在此函数可用
    ...
    swap(obj1, obj2); // 寻找最佳swap版本woc~
    ...
    }
    查找法则首先寻找global作用域或T所在命名空间内的T专属swap,找不到才使用std内的swap,这得感谢using声明式在函数内曝光。
  2. 成员版swap绝不可抛出异常,因为swap的一个最好的应用是帮助clasees或class templates提供异常安全性保障。

条款13:以对象管理资源

  1. 以对象管理资源的观念常被称为“资源取得时机便是初始化实际”(Resource Acquisition Is Initialization; RAII)。在获得的同时立刻被放进管理对象中。auto_ptrs有个不寻常的性质:若通过copy构造函数或copy assignment操作符复制,它们就会变成null,而复制所得的指针将取得资源的唯一拥有权,并在析构函数中释放资源。
  2. auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer;RCSP)。它提供的行为类似垃圾回收garbage collection)。

条款14:在资源管理类中小心copying行为???

  1. 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
  2. 普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。

条款15:在资源管理类中提供对原始资源的访问

  1. 除非发誓永不录用APIs,否则只得绕过资源管理对象(resource-managing objects)直接访问原始资源raw resources)。要将RAII class对象转换为其所包含的原始资源。有两个做法:显式转换和隐式转换。前者通过get成员函数可获得指针;后者可行是因为RAII重载了指针取值pointer dereferencing)操作符(operator->和operator*),它们允许隐式转化至底部原始指针。

条款16:成对使用new和delete时要采取相同形式

  1. 当你使用new,有两件事发生。第一,内存被分配出来(operator new);第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生;针对此内存会有一个(或更多)析构函数被调用,然后内存被释放(operator delete)。
  2. new时使用[],必须在相应调用delete时也使用[]。对于喜欢对数组形式做typedef动作的人,最好不要这样做,而C++ STL标准库中的容器templates,可将数组需求降至最低。

条款17:以独立语句将newed对象置入智能指针

对于processWidget(std::shared_ptr<Widget>(new Widget), priority());这行代码可以通过编译,然而上述调用可能泄露资源。因为它的执行顺序可能时

  1. 执行“new Widget”
  2. 调用priority删除
  3. 调用std::shared_ptr构造函数

      万一对priority的调用导致异常,“new Widget”返回的指针将会丢失,因为尚未置入std::shared_ptr内。解决方法是使用独立语句将创建Widget和置入指针两个步骤分割开来:

1
2
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

条款05:了解C++默默编写并调用哪些函数

  1. 如果自己没声明,编译器会自动声明(编译器版本)的copy构造函数copy assignment操作符析构函数。如果没有声明构造函数,编译器也会声明一个default构造函数
  2. 如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。
  3. 如果一个类中有reference或pointer类型的数据。那么对于copy assignment操作符需要自己定义,而不是使用编译器生成版本。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

  1. 为了组织copying动作可以设计一个base class并且不予以实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Uncopyable 
    {
    protected:
    Uncopyable() {}
    ~Uncopyable() {}
    private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
    };

    class HomeForSale : private Uncopyable
    {
    // class不再声明copy构造函数或copy assignment操作符
    ...
    };
            这行得通,甚至是member函数或friend函数——尝试拷贝HomeForSale对象,编译器便试图生成一个copy构造函数和一个copy assignment操作符,生成版会尝试调用base class的对应函数,而调用被拒绝,因为拷贝函数是private。

条款07:为多态基类声明virtual析构函数

  1. 给base class一个virtual析构函数,此后删除derived class对象就会销毁整个对象。如果class不含virtual函数,通常表示意图不被做一个base class。
  2. 想实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。它由一个vptrvirtual table pointer)指向一个由函数指针构成的数组,称为vtblvirtual table);每个带有virtual函数的class都有一个相应的vtbl。当对象调用virtual函数时,编译器在其中寻找适当的函数指针。给base classes一个virtual析构函数这个规则只适用于polymorphic带多态性质的)base classes身上。

条款08:别让异常逃离析构函数

  1. 对于析构函数中可能抛出的异常,由两种办法可以避免。一是如果抛出异常就结束程序,通常通过在析构函数调用abort抢先制不明确行为于死地;二是吞下它。
  2. 如果用户需要对某个操作函数运行期间的异常做出反应,应该提供一个普通函数来操作。然后在结束时析构函数来判断二重保险。

条款09:绝不在构造和析构过程中调用virtual函数

  1. 由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived classes阶层,那些成员变量尚未初始化,这将产生未定义行为。
  2. 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。

条款10:令operator=返回一个reference to *this

条款11:在operator=中处理“自我赋值”

  1. 如果operator=函数内的*this和赋值的来源是同一个对象,潜在的delete操作可能就摧毁了全部对象。因此需要注意在复制之前要么数据最好别删除,要么通过测试是否自我赋值。
  2. 在operator=函数内的替换方案最好是使用所谓的copy and swap技术。
    1
    2
    3
    4
    5
    Widget& Widget::operator=(Widget rhs)
    {
    swap(rhs);
    return *this;
    }

条款12:赋值对象时勿忘其每一个成分

  1. 当你编写一个copying函数,要确保复制所有local成员变量,且调用所有base calsses内的适当的copying函数。
  2. 对于为了消除复制代码的重复性,不该用copy assignment操作符调用copy构造函数,反之亦然。消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且被命名为init。

条款01:视C++为一个语言联邦

  1. C++是一个多重范式编程语言multiparadigm programming language),它是一个支持过程形式procedural)、面向对象形式object-oriented)、函数形式functional)、泛型形式generic)、元编程形式metaprogramming)的语言。
  2. 为了理解C++可以从四个方面入手:
    • C。区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)。
    • Object-Oriented C++。classes(构造函数和析构函数),封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)。
    • Template C++。
    • STL。容器(containers)、迭代器(iterators)、算法(algorithms)以及函数对象(function objects)。

条款02:尽量以const,enum,inline替换#define

  1. #define记号也许在编译器开始处理源码前就被预处理器移走了。于是可能记号没有进入记号表symbol table)内。
  2. 当我们以常量替换#define,如果要在头文件内定义一个常量char*-based字符串,必须写const两次(顶层const和底层const):const char* const authorName = "Scott Meyers";但通常用string对象更合宜;如果是class专属常量,为了确保常量至多只有一份,必须称为一个static成员。
  3. 如果不想别人获得一个pointer或reference指向整数常量,enum可以帮助实现这个约束。
  4. template<typename T>inline void callWithMax(cons T &a, const T &b);来代替#define定义的仿函数,这种template inline函数不仅获得宏带来的效率还有一般函数的所有可预料行为和类型安全性(type safety)。

条款03:尽可能使用const

  1. const多才多艺,可以在classes外部修饰global或namespace中的常量,或修饰文件、函数、或区块作用域(block scope)中被声明为static的对象。也可以修饰calsses内部的static和non-static成员变量。面对指针,可以指出指针自身、指针所指物,或两者都不是。
  2. STL迭代器系是以指针塑膜出来,作用跟T*指针一样。声明迭代器为const表示迭代器不指向不同的东西(T *const指针)。但是希望迭代器所指的东西不可改动(模拟const T*指针),需要的是const_iterator
  3. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class TextBlock {
    public:
    ...
    // operator[] for const 对象
    const char& operator[](std::size_t position) const
    { return text[position]; }
    // operator[] for non-const对象
    char& operator[](std::size_t position)
    { return text[position]; }
    private:
    std::string text;
    };
          TextBlock的operator[]可这么使用:
    1
    2
    3
    4
    5
    6
    7
    TextBlock tb("Hello");
    // 调用non-const TextBlock::operator[]
    std::cout << tb[0];

    const TextBlock ctb("World");
    // 调用const TextBlock::operator[]
    std::cout << ctb[0];
          注意:non-const operator[]的返回类型是reference to cahr,不是char。因为如果返回类型是个内置类型,那么改动返回值就不合法,即使合法,C++以by value返回对象意味着返回值是副本,而不是本身。
  4. mutable成员变量总是会被更改,即使在const成员函数内。
  5. 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class TextBlock {
    public:
    ...
    const char& operator[](std::size_t position) const
    {
    ...
    ...
    ...
    return text[position];
    }
    char& operator[](std::size_t position)
    {
    // 将op[]返回值的const转除;为*this加上const;调用const op[]
    return const_cast<char&>(
    static_cast<const TextBlock&>(*this)
    [position]
    );
    }
    ...
    }

条款04:确定对象被使用前已先被初始化

  1. 永远在使用对象之前将它初始化。对于无任何成员的内置类型,必须手工完成,因为C++不保证初始化它们。
  2. 构造函数最好使用成员初值列member initialization list
  3. non-local static对象被local static对象替换。以“函数调用”替换“直接访问non-local static对象“。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class FileSYstem { ... };
    FileSystem& tfs()
    {
    static FileSystem fs;
    return fs;
    }
    class Directory { ... };
    Directory::Directory(params)
    {
    ...
    std::size_t disks = tfs().numDisks();
    ...
    }
    Directory& tempDir()
    {
    static Directory td;
    return td;
    }
            这些reference-returning函数往往十分单纯,这些函数”内涵static对象“的事实使它们在多线程系统中带有不确定性。此时的做法是:在程序的单线程启动阶段(single-threaded startup portion)手工调用所有reference-returning函数,可消除与初始化有关的”</font color=red>竞速形势(race conditions)“。

测试工具

  • 三剑客grepsedawk的使用
  • 网络分析工具tcpdump的使用
  • 内存分析:valgrind
  • 其他:netstat、lsof、nc、tcpcopy、prerf、sysctl、strace、prove等
  • 性能检测工具:perf、ftrace、vmstat

Linux三剑客


grep

用法:grep [OPTION...] PATTERN [FILE...]
选项:
  • -b:每一行打印字符偏移量
  • -c:统计符合要求的行数
  • -E:使用egrep
  • -f:从文件中取匹配模板
  • -F:不使用正则快速搜索
  • -h:不显示文件名
  • -H:显式
  • -i:忽略大小写
  • -l:仅列出符合要求的文件,不列出具体行
  • -L:与-l相反
  • -n:显示符合要求的行及行号
  • -p:silence模式
  • -v:取反
  • -r:遍历子目录
  • -s:no-message
  • -w:匹配完整的词
  • -x:匹配完整的行
  • -An:
  • -Bn:
  • -Cn:
  • 正则规则:
    • []:表示集合中的内容,如[abc]匹配abc
      • [^]:取反
      • [a-z]:小写字母
      • [A-Z]:大写字母
      • [0-9]:数字
    • ^$:表示行首和行位,^$表示空行
    • *:通配符,表示多个重复
    • .:单个。.*表示0个或多个
    • {n, m}:个数范围,n个到m个之间,需要用/转义,egrep下不需要转义
    • egrep
      • +:类似.,1个或多个
      • ?:表示0个或多个
      • |:表示关系,如'ab|cd|def'表示abcddef的串
      • ():将部分内容合成单元

sed

用法:sed [OPTION]... [SCRIPTorCOMMAND] [FILE]...
选项:
  • -e:执行多条命令
  • -f:引导sed脚本
  • -i:直接修改文件
  • -n:silent,搭配s替换p可以打印修改过的行
  • -h:帮助
  • -V:版本信息
  • action:
    • a:后插,sed '/text/a\newline' OR sed '/text/a newline'
    • c:取代,sed '3,4c\replaceline'
    • d:删除,sed '2,4d' OR sed '/text/'d
    • g:获取缓冲区内容替换文本
    • G:获取缓冲区内容追加文本
    • h:拷贝文本到缓冲区
    • H:追加文本到缓冲区,保持和获取:sed -e '/text/h' -e '$G' file
    • i:前插,sed '/text/i\newthing'
    • l:列表不打印字符清单
    • n:读取下一个命令,用新命令处理新行
    • p:打印,sed '1p;3,4p'
    • q:打印到第n行退出
    • r:从file读内容,sed '/text/r file'
    • s:取代(正则)
      • ^$.*[][^]
      • (..):匹配子串,保存字符如s/(love)able/\1rsloveable -> lovers
      • &:拼接,s/love/sb& -> love -> sblove
      • <:匹配单词开始,如/<love/表示love开头的单词的行
      • >:匹配单词结束,如/love>/
      • x{m}:重复x有m次,/0{5}
      • x{m,}:重复x至少m次
      • x{m, n}:重复x至少m到n次
    • t:if分支
    • g:全面替换
    • w:把行写入file,sed '/text/w file'
    • x
    • y:把字符翻译为另外的字符,不做正则
    • !:后续命令作用于没有选中的行
    • #:注释扩展到下个换行符前

awk

awk简明教程

用法:
awk [-F SEPSTRING] [-v ASSIGNMENT]... program [ARGUMENT...]
awk [-F SEPSTRING] -f PROFILE [-f]... [-v ASSIGNMENT]... [ARGUMENT...]
选项:
  • -F fs:指定文件分隔符,fs是字符串或正则,如-F:
  • -v var=value:定义变量
  • -f scripfile:从脚本中读取awk
  • -W:
    • compact or traditional:兼容模式运行awk,gawk将同awk,忽略扩展
    • copyleft or copyright:版权信息
    • help or usage:选项简短说明
    • lint or lint-old:向传统unix移植结构警告
    • posix:兼容模式不识别:\x、函数关键字、func、换码序列以及当fs是一个空格时,将新行作为一个域分隔符;操作符=不能代替^和^=;fflush无效
    • re-interval:允许间隔正则使用,POSIX字符类,括号表达式[[:alpha:]]
    • source PROG:使用PROG作为源码,可搭配-f
      环境变量
      变量 描述
      $n 记录的第n个字段,由FS分隔
      $0 完整的输入记录
      ARGC 命令行参数数量
      ARGIND 命令行中当前文件位置
      ARGV 命令行参数数组
      CONVFMT 数字转换格式(%.6g)
      ENVIRON 环境变量关联数组
      ERRNO 系统错误
      FIELDWIDTHS 字段宽度列表
      FILENAME 当前文件名
      ENR 同NR
      FS 字段分隔符
      IGNORECASE 为真则忽略大小写匹配
      NF 记录中的字段数
      OFMT 数字输出格式
      OFS 输出字段分隔符(默认空格)
      ORS 输出记录分隔符(默认换行)
      RLENGTH match函数匹配的字符串长度
      RS 记录分隔符(默认换行)
      RSTART match函数匹配的第一个位置
      SUBSEP 数组下标分隔符(默认\034)
      通用正则表达式元字符
      符号 描述
      \Y 匹配单词开头或末尾的空字符串
      \B 匹配单词内空字符串
      < 匹配单词开头
      > 匹配单词末尾
      \w 匹配字母数字组成的单词
      \W 匹配非字母数字组成的单词
      ' 字符串开头空字符串
      字符换末尾空字符串

tcpdump

其他一些工具

strace

perf性能分析工具

调试关注点

  • 基于性能分析:可以算法优化代码优化
  • 硬件资源使用:如cache访问次数、cache丢失次数、流水线停顿周期、前端总线访问次数等
  • 操作系统资源:如系统调用次数、上下文切换次数、任务迁移次数
  • 事件以下几种:
    • Hardware Event:由PMU部件产生,特定条件下探测性能事件是否发生以及发生次数,如cache命中
    • Software Event:由内核产生的事件,分布各个功能模块,统计与操作系统相关的事件,如进程切换,tick数等
    • Tracepoint Event:由内核中静态tracepoint所触发的事件,tracepoint用来判断程序运行期间内核的行为细节,如slab分配器分配次数等
    • Tool Event:duration_time

      用法:perf [--version] [--help] [OPTIONS] COMMAND [ARGS]

  • The most commonly used perf commands are:
    • annotate Read perf.data (created by perf record) and display annotated code
    • archive Create archive with object files with build-ids found in perf.data file
    • bench General framework for benchmark suites
    • buildid-cache Manage build-id cache.
    • buildid-list List the buildids in a perf.data file
    • c2c Shared Data C2C/HITM Analyzer.
    • config Get and set variables in a configuration file.
    • data Data file related processing
    • diff Read perf.data files and display the differential profile
    • evlist List the event names in a perf.data file
    • ftrace simple wrapper for kernel’s ftrace functionality
    • inject Filter to augment the events stream with additional information
    • kallsyms Searches running kernel for symbols
    • kmem Tool to trace/measure kernel memory properties
    • kvm Tool to trace/measure kvm guest os
    • list List all symbolic event types
    • lock Analyze lock events
    • mem Profile memory accesses
    • record Run a command and record its profile into perf.data
    • report Read perf.data (created by perf record) and display the profile
    • sched Tool to trace/measure scheduler properties (latencies)
    • script Read perf.data (created by perf record) and display trace output
    • stat Run a command and gather performance counter statistics
    • test Runs sanity tests.
    • timechart Tool to visualize total system behavior during a workload
    • top System profiling tool.
    • version display the version of perf binary
    • probe Define new dynamic tracepoints
    • trace strace inspired tool

      功能分类

      全局性概况
  • list:查看系统支持的性能事件
  • bench:对系统性能摸底
  • test:对系统健全性测试
  • stat:对全局性能进行统计
    全局细节
  • top:实时查看系统进程函数占用率情况
  • probe:自定义动态事件
    特定功能分析
  • kmem:支队slab子系统性能分析
  • kvm:针对kvm虚拟化分析
  • lock:分析锁性能
  • mem:分析内核调度器性能
  • trace:记录系统调用轨迹
    记录与分析
  • record:记录信息到perf.data
    (FrameGraph)[https://gitee.com/mirrors/FlameGraph.git]
    1
    2
    3
    4
    5
    6
    7
    # 使用perf录制数据并使用FrameGraph查看火焰图
    # 录制程序十秒
    $ perf record -F 99 -a -g -- sleep 10
    # 数据展开
    $ perf script | ../stackcollapse-perf.pl > out.perf-folded
    # 生成svg矢量图
    $ ../flamegraph.pl out.perf-folded > perf-kernel.svg
  • report:生成报告
  • diff:对两个记录进行diff
  • evlist:列出记录的性能事件
  • annotate:显式perf.data函数代码
  • archive:将相关符号打包,方便其他机器进行分析
  • script:将perf.data输出可读性文本
    可视化工具
  • timechart record:记录事件
  • timechart:生成output.svg文档

一些技巧

1
2
3
4
5
# 查看程序的pid、ppid、pgid、sid、comm信息
$ ps -C program o pid,ppid,pgid,sid,comm
# 查看进程层次关系
$ ps -ejH
$ ps axjf

valgrind

使用手册

用法:valgrind [VALGRIND_OPTIONS] programing [PROG_OPTIONS]

基本选项
  • -h:帮助

  • -q:静默模式

  • -v:输出额外的信息,如加载的共享对象,检测和执行引擎进度,和异常行为警告,重复选项会增加详细程序

    要点

    开始编译
  • 使用-g有助于获得直接指向源代码行的消息

  • 如果使用C++,使用-fno-inline可以轻松看到函数调用链,或者使用valgrind选项--read-inline-info=yes让valgrind读取内联信息

  • 开启-O1以上程序优化会导致memcheck误报未定义等错误,尽量不优化,且开启-Wall

    注释输出
  • 默认valgrind将消息输出文件描述符2(stderr),如果要写入其他文件描述符,可以使用--log-fd=<n>来指定fd减少干扰项

  • 为了减少干扰项,最好通过-log-file=<filename>将调试日志写到指定文件

  • 最少干扰是通过--log-socket=<ip:port>指定网络套接字进行输出,可以省略端口号,将默认使用端口1500,默认值由VG_CLO_DEFAULT_LOGPORT定义。而valgrind监听端则使用valgrind-listener来进行监听,连接的切断listener并不会杀死valgrind,而是会自动切换回写入stderr

    • valgrind-listener接受多大50个valgrinded进程同时连接,它接收三个选项:
      • -e --exit-at-zero:当连接进程数为0时退出,否则将永远运行
      • --max-connect=<n>:进程上限,默认50,可以自动设限
      • portnumber:更改默认端口(1500)
  • 检测重复错误会有大量开销,valgrind会在发现1000个不同的错误或总共发现10000000个错误后停止错误收集,为了避免中断,可以使用--error-limit=no选项

  • 错误检查工具会检测系统中预装系统库的很多问题,如果不想看到这些错误,可以使用--gen-suppressions=yes选项进行消除。如果使用了-v选项,则在程序结束时打印已使用抑制行

  • --tool=<toolname>:可以决定运行哪个valgrind工具。默认为memcheck,除此之外还有cachegrindcallgrindhelgrinddrdmassifdhatboucheynoneexp-sgcheckexp-bbv

一些技巧记录

  • 显式本地IP:curl myip.ipip.net
  • 查找结构体:grep -Rn --include="*.h" --include="*.c" 'struct ether_header' /usr
  • 查找宏定义:grep -Rn --include="*.h" --include="*.c" '#define PCAP_ERRBUF_SIZE' /usr

  • 有三个著名的C++语言扩充性质,它们都会影响C++对象。分别是templateexception handing(EHruntime type identification(RTTI(RTTI可以想象成EH的一个副作用)。

7.1 Template

  • template原本被视为是对container classes的支持,但现在成为STL的基础。它也被用于属性混合(如内存分配机制)或互斥(mutual exclusion)机制(使用于线程同步化控制)。它设置被用于template metaprograms技术:class expression templates将在编译时期而非执行期被评估(evaluated),因而带来效率提升。
Template的“实例化”行为(Template Instantiation)
  • 下面的template Point class:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template<class Type>
    class Point {
    public:
    enum Status { unallocated, normalized };
    Point(Type x = 0.0, Type y = 0.0, Type z = 0.0);
    ~Point();
    void* operator new(size_t);
    void operator delete(void*, size_t);
    // ...
    private:
    static Piont<Type> *freeList;
    static int chunkSize;
    Type _x, _y, _z;
    };
    实际程序中,static data members并不可用,nested enum或enumerators也一样。它们每个只能通过template Point class的某个实例来存取或操作
    1
    2
    3
    4
    // ok
    Point<float>::Status s;
    // error
    Point::Status s;
    如果定义一个指针,指向特定实例:
    1
    Point<float> *ptr = 0;
    什么也没发生。因为指向class object的指针,本身并不是class object,编译器不需要直到与该class有关的任何members数据或object布局数据。如果不是pointer而是reference,又如何:
    1
    2
    3
    4
    const Point<float> &ref = 0;
    // 内部扩展
    Point<float> temporary(float(0));
    const Point<float> &ref = temporary;
    所以一个class object的定义,不论是由编译器暗中地做或是程序员显式地做,都会导致template class的实例化。也就是Point中的三个nonstatic members都会被绑定。然而member functions(未被使用过的)不应该被实例化。只有在需要时才实例化。但编译器不遵循这项要求,所以由使用者来主导实例化instantiation),主要有两个原因:
    1. 空间和时间效率的考虑
    2. 尚未实现的机能
Template的错误报告(Error Reporting within a Template)
Template中的名称决议法(Name Resolution within a Template)
  • template的两种意义:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // scope of the template definition
    extern double foo(double);
    template<class type>
    class ScopeRules
    {
    public:
    void invariant() {
    _member = foo(_val);
    }
    type type_dependent() {
    return foo(_member);
    }
    // ...
    private:
    int _val;
    type _member;
    };
    // scope of the template declareation
    extern double foo(double);
    // scope of the template instantiation
    extern int foo(int);
    // ....
    ScopeFules<int> sr0;
    Template中,一个nonmember name的决议结果,是根据name的使用是否与实例化该template的参数类型有关而决定的。如果无关,就以scope of the template declareation决定name,否则以scope of the template instantiation决定name。
    ScopeFules template中有两个foo()调用操作。它会调用foo(double)的版本,此外,_val的类型是int。因为函数的决议结果只和函数的原型(signature)有关,和函数的返回值没有关系。因此_member的类型不会影响哪一个foo()实例被选中。在scope中,只有一个foo()候选者。调用操作由scope of the template declareation决议。如果按以下方式:
    1
    sr0.type_dependent();
    它由scope of the template instantiation决议。它会调用int版本,被type把持。
    这意味着编译器必须保持两个scope contexts:
    1. “scope of the template “,用于专注一般的template class
    2. “scope of the template instantiation”,用于专注特定的实例
Member Function的实例化行为(Member Function Instantiation)
  • 对于template function的实例化(instantiation)。编译器提供了两个策略:一个是编译时期策略,程序代码在program text file中备妥可用;另一个是链接时期策略
  • 如果virtual function被实例化(instantiated),其实例化点紧跟在class的实例化点后。
  • template instantiation似乎拒绝全面自动化,虽然工作做对了,但产生出来的object files重新编译成本可能太高,以手动方式在个别object module中完成预先实例化操作(pre-instantiation)是唯一有效率的方法。

7.2 异常处理(Exception Handing)

  • 想要支持exception handing,编译器的主要工作就是找出catch子句,以处理被抛出的exception。这需要追踪程序堆栈中的每个函数的目前作用区域,同时编译器需要提供某种查询exception objects的方法,以直到实际类型(RTTI)。还需要某种机制管理被抛出的object,包括产生、存储、析构、清理和一般存取。
Exception Handing快速检阅
  • exception handing由三个主要组件构成:throw子句、catch子句和try区段。当一个exception被抛出去,控制权会从函数调用中释放出来,并寻找吻合的catch子句。如果没有,则调用terminate()中断例程。当控制权被放弃后,堆栈中每个函数调用也被推理(popped up)。
对Exception Handing的支持
  • 当exception发生时,编译系统完成以下事情:
    1. 检验发生throw操作的函数。
    2. 决定throw操作是否在try区段中。
    3. 把exception type拿来和每个catch子句比较。
    4. 流程控制交到吻合的匹配的catch子句手中。
    5. 如果throw不发生在try区段中,或没有catch子句吻合。那么:
      1. 摧毁active local objects。
      2. 堆栈中将函数unwind掉
      3. 进行堆栈的下一个函数,然后重复2·5。
决定throw是否发生在一个try区段中
  • 一个函数可以想象为好几个区域:
    • try区段以外的区域,没有active local objects。
    • try区段以外的区域,但有一个(或以上)的active local objects需要析构。
    • try区段以内的区域。
将exception的类型和每一个catch子句的类型做比较
  • 对于每个被抛出来的exception,编译器产生一个类型描述器,对exception的类型进行编码。如果那是derived type,编码内容包含所有base class的类型信息。
  • 类型描述器(type derscriptor)是必要的。因为exception是在执行期被处理的,object必须有自己的类型信息。RTTI正是因为支持EH而获得的副产品。
当一个实际对象在程序执行时被抛出,会发生什么事?

7.3 执行期类型识别(Runtime Type Identification,RTTI)

Type-Safe Downcast(保证安全的向下转换操作)
Type-Safe Dynamic Cast(保证安全的动态转换)
Reference并不是Pointers
Typeid运算符

7.4 效率有了,弹性呢?

动态共享函数库(Dynamic Shared Libraries)
共享内存(Shared Memory)

  • 有个简单的式子:
    1
    if (yy == xx.getValue())) ...
    其中xx和yy的定义:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Y {
    public:
    Y();
    ~Y();
    bool oeprator==(const Y&) const;
    // ...
    };

    class X {
    public:
    X();
    ~X();
    operator Y() const; // conversion运算符
    X getValue();
    // ...
    };
    当使用equality(等号)运算符时,xx被resolves为“被overloaded的Y成员实例”。下面是该式子的转换:
    1
    2
    // resolution of intended operator
    if (yy.operator==(xx.getValue()))
    Y的等号运算符需要一个类型为Y的参数,而getValue()传回的是一个类型为X的object。这时提供conversion运算符,把一个X object转换为一个Y object,它施行于getValue()的返回值身上。下面是第二次转换:
    1
    2
    // conversion of getValue()'s return value
    if (yy.operator==(xx.getalue().operator Y()))

6.1 对象的构造和析构(Object Construction and Destruction)

  • 一般constructor和destructor的安插都如下:
    1
    2
    3
    4
    5
    6
    {
    Point point;
    // point.point::Point();
    ...
    // point.point::~Point();
    }
    如果一个区段或函数有一个以上的离开点,情况就会混乱一些,Destructor必须被放在每一个离开点(object还存活)之前。
全局对象(Global Objects)
  • C++保证在第一次使用global数据前之前,将它构造出来,而在结束前把identity摧毁掉。如果global object如果有constructor和destructor的话,它需要静态的初始化操作和内存释放操作。所有的global obejct都被放置在程序的data segment中。如果不显式指定值,则内存内容为0(C略有不同,C并不自动设定初值)。C语言中一个global object只能被一个常量表达式(可在编译时期求值)设定初值。虽然class object在编译时期可以被放置data segment中并且内容为0,但constructor一直要到程序启动(startup)时才会实施。而必须对放置program data segment中的object的初始化表达式做评估(evaluate),这就是一个object需要静态初始化的原因
  • munch是一个可移植但成本颇高的静态初始化(以及内存释放)方法。munch策略:
    1. 为每个需要静态初始化的文件产生__sti()函数(sti是static initialization),内含constructor调用操作或inline expansions。一个global对象identity会在matrix.c中产生:
      1
      2
      3
      __sti_matrix_c__identity() {
      identity.Matrix::Matrix();
      }
      matrix_c是文件名编码,_identity表示文件中所定义的static object。
    2. 在每个需要静态的内存释放操作(static deallocation)的文件中,产生一个__std()函数(std是static deallocation),内含必要的destructor调用操作,或是inline expansions。比如以上identity对象会调用Matrix destructor。
    3. **提供一组rumtime library “munch”函数:一个_mani()函数(call __sti(),一个exit()函数(call __std())**。
  • 最后一个需要解决的问题是,如何收集程序中各个object files的__sti()函数和__std()函数。解决方法是使用nm命令,nm会dump出object file的符号表格项目symbol table entries)。nm施行于可执行文件上,输出piped into到munch程序中。munch程序则搜寻__sti或__std开头的名称,然后把函数名称加到jump table中,然后把表格写到program text文件中,CC命令被重新激活,将内含表格的文件编译,整个可执行文件被重新链接。_main()和exit()在各个表格上走访一遍,轮流调用每一个项目(代表一个函数地址)。但这方法离正统计科太远了……
  • 当特性平台的C++编译器出现,更有效率的方法也随之出现,因为各平台上扩充链接器和目的文件格式(object file format),以直接支持静态初始化和内存释放操作。例如System V的Executable and Linking Format(ELF)被扩充以增加支持.init和.fini两个sections,两个sections内含的对象所需的信息,分别对应静态初始化和释放操作。编译器特定的startup函数会完成平台特定的支持。
  • 使用被静态初始化的objects,有缺点。如果exception handing被支持,那些objects将不能被放置于try区段之内。因为任何的throw操作必然将触发exception handing library的默认terminate()函数以致于结束程序,这对于被静态调用的constructors可能是特别无法接受的。另一个缺点是为了控制跨模块静态初始化的objects的依赖顺序,而形成的复杂度。建议根本不要用需要静态初始化的global objects(虽然这建议不普遍为C程序员接受)。
局部静态对象(Local Static Objects)
  • 假设有以下程序片段:
    1
    2
    3
    4
    5
    const Matrix& identity() {
    static Matrix mat_identity;
    // ...
    return mat_identity;
    }
    Local static class object保证了什么样的语意?
    • mat_identity的constructor只能执行一次,虽然函数可能被调用多次
    • mat_identity的destructor只能施行一次,虽然函数可能会被调用多次
  • 编译器的无条件构造对象会导致即使函数不曾被调用过也被程序起始时被初始化。cfont的做法是,用一个临时性对象保护mat_identity的初始化操作。第一次调用时,临时对象被评估为false,constructor被调用,然后临时对象改为true。而destructor判断则反过来。困难的是,没办法在静态的内存释放函数中存取local对象。取出local object的地址就好了……
对象数组(Array of Objects)
  • 假如有数组定义:
    1
    Point knots[10];
    如果Point没有定义constructor也没有定义destructor,那么工作不会比(build-in)类型的数组更多。然而它的确定义了一个default destructor,所以这个操作轮流施行每个元素上。cfont中,使用一个叫vec_new()的函数,产生以class object构造成的数组。一些新编译器则提供了两个函数,一个分别处理有和没有内含virtual base class的class。vec_vnew()用于内含virtual base class函数的class。当然不同平台有差异:
    1
    2
    3
    4
    5
    6
    7
    void* vec_new (
    void *array, // 数组起始地址
    size_t elem_size, // class object的大小
    int elem_count, // 元素个数
    void (*constructor)(void*), // 函数指针
    void (*destructor)(void*, char)
    )
    参数array如果持有不具名数组地址(knots),就是0,意味着数组经由应用程序new运算符动态配置于heap中
    下面是编译器可能针对10个Point元素所做的vec_new()调用操作:
    1
    2
    Point knots[10];
    vec_new(&knots, sizeof(Point), 10, &Point::Point, 0);
    如果Point定义了一个destructor,当knots结束时destructor也施行于10个Point元素身上。这时经由一个类似vec_delete()的runtime library
    1
    2
    3
    4
    5
    6
    void* vec_delete(
    void *array,
    size_t elem_size,
    int elem_count,
    void (*destructor)(void*, char)
    )
    对于以下的组成方式:
    1
    2
    3
    4
    5
    Point knots[10] = {
    Point(),
    Point(1.0, 1.0, 0.5),
    -1.0
    };
    对于明显获得初值的元素,vec_new()不再有必要。对于尚未初始化的元素,施行方式则像没有explicit initialization list的class elemnts一样:
    1
    2
    3
    4
    5
    6
    7
    // C++伪码
    Point knots[10];
    Point::Point(&knots[0]);
    Point::Point(&knots[1], 1.0, 1.0, 0.5);
    Point::Point(&knots[2], -1.0, 0.0, 0.0);
    // 初始化剩余7个元素
    vec_new(&knots+3, sizeof(Point), 7, &Point:Piont, 0);
Default Constructors和数组

6.2 new和delete运算符

  • 运算符new的使用,看起来似乎是单一运算,像这样:
    1
    int *pi = new int(5);
    它由两个步骤完成
    1. 通过new运算符函数实例,分配内存
    2. 为配置的内存初始化
      1
      2
      3
      int *pi;
      if (pi = __new(sizeof(int)))
      *pi = 5;
      pi所指对象的生命因delete而结束。所有后继对pi的参考操作不再保证有良好的行为。虽然地址上的对象不再合法,地址本身仍然代表一个合法的程序空间。因此pi能继续被使用,但不是个合法的编程风格。
  • 以constructor分配一个class object,情况类似:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Ponit3d *origin = new Point3d;
    // 转换
    Point3d *origin;
    if (origin = __new(sizeof(Point3d)))
    origin = Point3d::Point3d(origin);
    // 如果有exception hading,情况会复杂
    if (origin = __new(sizeof(Point3d))) {
    try {
    origin = Point3d::Point3d(origin);
    } catch ( ... ) {
    __delete(origin);
    throw; // 继续抛出
    }
    }
    Destructor情况类似:
    1
    2
    3
    4
    5
    6
    7
    8
    delete origin;
    // 变成
    if (origin != 0) {
    Point3d::~Point3d(origin);
    __delete(origin);
    }
    // 如果有exception handing
    // ...
    new运算符实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    extern void* operator new(size_t size)
    {
    if (size == 0)
    size = 1;
    void *last_alloc;
    while (!(last_alloc = malloc(size)))
    {
    if (_new_handler)
    (*_new_handler)();
    else return 0;
    }
    return last_alloc;
    }
    虽然这么写是合法的:
    1
    new T[0];
    但语言要求每次new调用都返回独一无二的指针。解决问题的传统方法是传回一个指针,指向一个默认为1-byte的内存区块(这就是为什么程序代码中size被设为1的原因)。这个程序的有趣之处在于,它允许使用者提供一个属于自己的_new_handler()函数。
    1
    2
    3
    4
    5
    extern vodi operator edlete(void *ptr)
    {
    if (ptr)
    free((char*)ptr);
    }
针对数组的new语意
  • new一个数组;
    1
    2
    3
    int *p_array = new int[5];
    // 转化
    int *p_array = (int*)__new(5 * sizeof(int));
    vec_new()不会被调用,因为它主要功能是把default constructor施行于class objects所组成的数组的每个元素身上。
    1
    2
    3
    4
    Point3d *p_array = new Point3d[10];
    // 编译
    Point3d *p_array;
    p_array = vec_new(0, sizeof(Point3d), 10, &Point3d::Point3d, &Point3d::~Point3d);
  • 应该如何记录元素个数?一个方法是为vec_new()传回的每个内存块配置一个额外的word,然后元素个数藏在word中,这叫cookie。原始编译器有两个用来存取cookie:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    typedef void *pv;
    extern int __insert_new_array(PV array_key, int elem_count);
    // 表格中取出并移除array_key,传回不是elem_count则传回-1
    extern int __remove_old_array(PV array_key);
    // cfront vec_new()
    PV __vec_new(PV ptr_array, int elem_count, int size, PV construct)
    {
    // 如果ptr_array是0,从heap中配置数组。
    int alloc = 0;
    int array_sz = elem_count == 0)
    ptr_array = PV(new char[array_sz]);
    if (ptr_array == 0)
    return 0;
    // 把数组元素个数放到cache中
    int status = __insert_new_array(ptr_array, elem_count);
    if (status === -1) {
    if (alloc)
    delete ptr_array;
    return 0;
    }
    if (construct) {
    register char* elem = (char*)ptr_array;
    register char* lim = elem + array_sz;
    // PF是typedef,代表函数指针
    register PF fp = PF(constructor);
    while (elem < lim) {
    (*fp)((void*)elem);
    // 下一个元素
    elem += size;
    }
    }
    return PV(ptr_array);
    }
Placement Operator new的语意
  • 有一个预先定义好的重载的(overloaded)new运算符,称为placement oeprator new。它需要第二个参数,类型为void*,调用方式如下:
    1
    Point2w *ptw = new(arena) Point2w;
    arena指向内存中的区块,用来放置新的Point2w object。placement operator new的实现方法很平凡:
    1
    2
    3
    4
    void* operator new(size_t, void *p)
    {
    return p;
    }
    placement new operator的强大之处在于,编译系统保证object的constructor会施行于其上:
    1
    2
    3
    Point2w *ptw = (Point2w*)arena;
    if (ptw != 0)
    ptw->Point2w::Point2w();
    对于arena表现的真正指针的类型,derived class很明显不在支持之列。对于derived class,或是其他没有关联的类型,行为虽然并非不合法,却也未经定义:
    1
    2
    3
    4
    5
    6
    7
    // 可以这么配置
    char *arena = new char[sizeof(Point2w)];
    // 相同object可以这么获得
    Point2sw *arena = new Point2w;
    // 一般placement new operator并不支持多态。
    // 如果derived class比base class大,Point3w的constructor会导致严重破坏。
    Point2w *p2w = new (arena) Point3w;
    Placement new operator引入C++2.0时,最晦涩难懂的问题就是:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Base { int j; virtual void f(); };
    struct Derived : Base { void f(); };
    void fooBar() {
    Base b;
    b.f(); // Base::f()被调用
    b.~Base();
    new(&b) Derived;
    b.f(); // 哪个f()被调用
    }

6.3 临时性对象(Temporary Objects)

  • 如果有函数:

    1
    T operator+(const T&, const T&);

    那么两个此对象的a和b进行a+b;。会产生临时变量放置传回的对象。详细过程是产生临时性对象,放置结果,使用T的copy constructor,把临时性对象当作c的初始值。当然,视operator+()定义而定,NRV优化可能实施起来。这导致直接求表达式结果,避免执行copy constructor和具名对象的destructor。至于会用哪种方式初始化,C++ Standard允许编译器有临时性对象的完全自由度:

    1
    2
    3
    4
    5
    T c = a + b;
    // 而其中的加法运算符被定义为
    T operator+(const T&, const T&);
    // 或
    T T::operator+(const T&);

    那么实现根本不产生临时性变量。如果以:

    1
    c = a + b;

    不能够忽略,它会导致下面结果:

    1
    2
    3
    4
    T temp;
    temp.operator+(a, b); (1
    c.operator=(temp); (2
    temp.T::~T();

    (1)那行意味着为构造的临时对象赋值给operator+()。意思是要么表达式结果被copy constructed到临时对象中,要么以临时对象取代NRV。然而不管哪一种情况都有问题。运算符函数并不为外加参数调用destructor(它期望新内存),所以需要在调用前先调用destructor。然而,转换语意被用来把copy assignment运算符的隐式调用操作和destructor和copy constructor来代替assignment操作

    1
    2
    c.T::~T();
    c.T::T(a + b);

    而以上操作都可以由使用者供应,而且它还会产生临时变量。

    1
    2
    3
    T c = a + b;
    // 上面的比下面的操作更有效率被编译器转化
    c = a + b;
  • 第三种形式是,没有目标对象:

    1
    2
    3
    4
    5
    6
    a + b
    // 不论怎么种情况
    String v;
    v = s + t + u;
    // 或
    printf("%s\n", s + t);

    都会产生临时对象,与s+t相关联。上述的printf可能不保证安全,因为它的正确性和s+t何时被摧毁有关。(malloc(0))。例如对于该算式的一个可能的pre-Standard转换,可能造成重大灾难:

    1
    2
    3
    4
    5
    6
    // C++伪码
    // 临时性对象被摧毁得太快(太早)了。
    String temp1 = operator+(s, t);
    const char *temp2 = temp1.operator const char*();
    temp1.~String();
    printf("%s\n", temp2); // temp2来自何方?

    一种转换方式是在调用printf()之后实施String destructor。标准规格上说:临时性对象的被摧毁,应该是对完整表达式(full-expression)求值过程种的最后一个步骤。该完整表达式造成临时对象的产生完整表达式是被包裹的表达式种最外围的那个表达式

    1
    2
    // tertiary full expression with 5 sub-expressions
    ((objA > 1024) && (objB > 1024)) ? objA + objB : foo(objA, objB);

    五个子算式内含在“?:完整表达式”中。任何子表达式所产生的任何临时对象,都应该在完整表达式被求值完成后,才可以毁去。 p273

  • 临时性对象的生命规则有两个例外:

    • 第一个例外发生在表达式被用来初始化一个object时。如果某个对象progNameVersion的初始化需要调用copy constructor,那么临时性对象的析构就不是我们期望的。C++ Standard要求说:凡持有表达式执行结果的临时性对象,应该存留到object的初始化操作完成为止
      1
      2
      3
      4
      5
      6
      const char* progNameVersion = progName + progVersion;
      // C++ pseudo Code
      String temp;
      operator+(temp, progName, progVersion);
      progNmaeVersion = temp.String::operator char*();
      temp.String::~String();
      progNameVersion指向未定义的heap内存。
    • 第二个例外是当一个临时性对象被一个reference绑定时,例如:
      1
      2
      3
      4
      5
      6
      const String &space = " ";
      // 产生代码
      // C++ pseudo Code
      String temp;
      temp.String::String(" ");
      const String &space = temp;
      很明晰那,临时性对象被摧毁,reference就没什么用了。所以Standard说:
      如果临时性对象被绑定与reference,对象将残留,直到reference的生命结束,直到临时对象的生命范畴(scope)结束
临时性对象的迷思(神话、传说)

纯虚函数的存在(Presence of a Pure Virtal Function)
  • 考虑abstract base class声明:
    1
    2
    3
    4
    5
    6
    7
    8
    class Abstract_base {
    public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char* mumble() const { return _mumble; }
    protected:
    char *_mumble;
    };
    这个class被设计为一个抽象的base class(其中有pure virtual function,使得Abstract_base不可能有实例),但需要一个显式的构造函数以初始化data member _mumble。如果没有初始化操作,derived class的局部性对象_mumble及那个无法决定初值。
纯虚函数的存在(Presence of a Pure Virtual Function)
  • pure virtual function只能被静态地调用invoked statically),不能经由虚拟机制调用。而pure virtual destructor,class设计者一定得定义它。因为每个derived class class以及base class destructor。只要缺乏定义,就会导致链接失败
虚拟规格的存在(Presence of a Virtual Specification)
  • 如果决定把Abstract_base::mumble()设计为virtual function,那是糟糕的选择,因为其函数定义内容并不与类型有关,因而几乎不会被后继的derived class改写。
虚拟规格中const的存在
  • 决定要给virtual function是否需要const,是一件繁琐的事情。如果声明为const,意味着subclass实例可能被无穷次数地使用。不把函数声明为const,意味和函数不能获得一个const reference或const pointer。头大的是,声明函数为const,才发现derived instance必须修改某一data member。解决办法就是不再用const就是了。
重新考虑class的声明
  • 综上所述,这才是比较适当的一种设计:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Abstract_base {
    public:
    virtual ~Abstract_base(); // nonpure virtual
    virtual void interface() = 0; // nonconst
    const char* mumble() const { return _mumble; } // nonvirtual
    protected:
    Abstract_base(char *pc =ss 0);
    char *_mumble;
    }

5.1 ”无继承”情况下的对象构造

  • 考虑程序片段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Point global;                   //1
    //2
    Point foobar() //3
    { //4
    Point local; //5
    Point *heap = new Point; //6
    *heap = local; //7
    // ... stuff ... //8
    delete heap; //9
    return local; //10
    } //11

    L1、L5、L6分别为:global内存配置local内存配置heap内存配置。一个object的声明,是该object的一个执行期属性。local object的生命从L5定义开始,L10为止。global object的生命和这个程序的生命相同。heap object的生命从被new运算符配置出来开始,到delete运算符摧毁结束。
    下面是Point的第一次声明,写成C程序。这是一种Plain OI’ Data声明形式:

    1
    2
    3
    4
    5
    typedef struct
    {
    float x, y, z;
    Point;
    }

    如果用C++来编译这代码,编译器会为Point声明一个trivial default constructor、一个trivial destructor、一个trivial copy constructor,以及一个trivial copy assignment operator。但实际上,编译器会分析这个声明,并贴上POD标签

  • 当编译器遇到这样的定义:

    1
    Point global;                   //1

    观念上Point的trival constructor和destructor会被产生并调用,constructor在程序起始(startup)被调用,而destructor在程序的exit()处被调用。然而事实上trivial members要么没定义,要么没被调用。
    在C中,global被视为一个临时性的定义,因为它没有显式的初始化操作。它可以在程序中发生多次。实例会被折叠起来,只留下一个,放在程序data segment中。这块空间称为BSS,这是Block Started by Symbol缩写。
    C++并不支持临时性定义。虽然它可以判断class object或是POD。global在C++中被视为完全定义(会阻止多个定义)。C和C++的差异是,BSS data segment在C++中相对不重要。所有的全局变量都被以“初始化过的数据”来对待
    至于foobar()函数的L5,既没有构造也没有被析构,不过可能没有经过初始化就会成BUG(L7)。至于heap object:

    1
    2
    3
    4
    5
    Point *heap = new Point;    //6
    // 转化
    Point *heap = __new(sizeof(Point));
    // 有初始化过就没问题
    *heap = local; //7

    事实上L7会产生编译警告,观念上,这样的指定操作会触发trivial copy assignment operator做拷贝搬运操作。而实际上object是POD,所以assignment只是像C那样纯粹bitwise搬运。

抽象数据类型(Abstract Data Type)
  • 以下是Point声明,提供了完整封装性,但没有提供virtual function:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Point {
    public:
    Point(float x = 0.0, float y = 0.0, float z = 0.0) : _x(x), _y(y), _z(z) {}
    // no copy constructor, copy operator
    // or destructor defined ...
    // ...
    private:
    float _x, _y, _z;
    };
    这里还是三个连续的float,不论private或public存取层或是member function都不会占用额外的对象空间。没有定义copy constructor或copy operator,因为有default bitwise semantics足够了,也不需要destructor。对于实例:
    1
    Point global; // Point::Point(0.0, o.0, 0.0);
    有了default constructor,global被定义在全局范畴,初始化操作将延迟到程序启动(startup)。
    如果要将class中的所有成员设定常量初值,给予一个explicit initialization list会比较有效率(相比constructor的inline expansion而言)。但它有三项缺点:
    1. 只有当class members都是public,才会奏效。
    2. 只能指定常量,因为在编译器就能评估求值。
    3. 由于编译器并不自动施行,初始化行为可能性很高。
为继承做准备
  • 第三个Point声明,为继承性质和某些操作的动态决议dynamic resolution)做准备,目前限制z成员做存取操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Point {
    public:
    Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) {}
    // no destructor, copy constructor, or
    // copy operator defined ...
    virtual float z();
    // ...
    protected:
    float _x, _y;
    };
    virtual functions的导入在这里附带一个virtual destructor的声明在这个例子里,并无好处。除此之外,每个class object多负担一个vptr之外,virtual function的导入也引发编译器对Point class产生的膨胀作用:
    • 定义的constructor附带了一些代码,以便vptr初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // C++伪码
      Point* Point::Point(Point *this, float x, float y) : _x(x), _y(y)
      {
      // 设定object的vptr
      this->__vptr_Point = __vtbl__Point;
      // 扩展member initialization list
      this->_x = x;
      this->_y = y;
      // 传回this
      return this;
      }
    • 合成一个copy constructor和copy assignment operator,而且操作不是trivial(implicit destructor仍然是)。如果Point object被初始化或以derived class object赋值,那么以为基础(bitwise)的操作可能对vptr带来非法设定:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // C++伪码
      // copy constructor 内部合成
      inline Point* Point::Point(Point*this, const Point &rhs)
      {
      // object的vptr
      this->__vptr_Point = __vtbl__Point;
      // copy
      return this;
      }
      编译器在优化状态会把object内容连续拷贝到另一个object,而不是精准memberwise。L7的memberwise赋值操作可能出发copy assignment operator的合成,及调用inline expansion(行内扩张):以this取代heap,以rhs取代local
      戏剧性的冲击在L10。由于copy constructor的出现,foobar()可能转化为:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Point foobar(Point &__result)
      {
      Point local;
      local.Point::Point(0.0, 0.0);
      // heap
      // copy constructor
      __result.Point::Point(local);
      // local destructor
      // Point::derstructor
      // local.Point::~Point();
      return
      }
      如果NRV优化,转化为:
      1
      2
      3
      4
      5
      6
      Point foobar(Point &__result)
      {
      __result.Point::Point(0.0, 0.0);
      // heap
      return;
      }

5.2 继承体系下的对象构造

  • 当定义一个object:

    1
    T object;

    如果T有constructor,它会被调用。除此之外,constructor调用伴随了什么?constructor可能内含大量隐藏码,因为编译器会扩充每个constructor

    1. 在member initialization list中的data members初始化操作会被放进constructor函数本体,以声明顺序为顺序。
    2. 如果member没有在member initialization list中,但这个member有default constructor,那么被调用。
    3. 在那之前,如果class object有vptr,必须被设定初值指向适当vtbls。
    4. 在那之前,base class constructors必须被调用,以base class声明顺序为顺序。
      • 如果base class被列入member initialization list中,那么任何显式参数都传递进去
      • 如果base class没有被列入member initialization list中,而它有default constructor(或default memberwie copy constructor),那么调用它
      • 如果base class是多层继承下的第二或后继base class,那么调整this指针
    5. 在那之前,所有virtual base class constructors必须被调用,从左到右,从深到浅:
      • 如果class被列入member initialization list中,那么任何显式指定的参数,都传递进去。如果没有列入list中,而class有default constructor,调用它
      • class中的每个virtual base class subobject的偏移地址(offset)必须在执行期可被存取
      • 如果class object是最底层(most-derived)的class,其constructors可能被调用;用以支持者行为的机制必须放出来
  • 以Point为例,探讨constructors扩充的必要性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Point {
    public:
    Point(float x = 0.0, float y = 0.0);
    Point(const Point&); // copy constructor
    Point& operator=(const Point&); // copy assignment operator
    virtual ~Point(); // virtual derstructor
    virtual float z() { return 0.0; }
    // ...
    protected:
    float _x, _y;
    };

    class Line {
    Point _begin, _end;
    public:
    Line(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
    Line(const Point&, const Point&);
    draw();
    // ...
    };

    。。。。。。。。

虚拟继承(Virtual Inheritance)
  • 考虑下面虚拟继承,继承自Point:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Point3d : public virtual Point {
    public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : Point(x, y), _z(z) {}
    Point3d(const Point3d &rhs) : Point(rhs), _z(rhs._z) {}
    ~Point3();
    Point3d& operator=(const Point3d&);
    virtual float z() { return _z; }
    // ...
    protected:
    float _z;
    };
    传统的constructor扩充并没有用,因为virtual base class的共享性之故:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 不合法
    Point3d* Point3d::Point3d(Point3d *this, float x, float y, float z)
    {
    this->Point::Point(x, y);
    this->__vptr_Piont3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d_Point;
    this->_z = rhs._z;
    return this;
    }
    看出什么错误了吗?
    1
    2
    3
    4
    5
    6
    graph BT
    Point3d -- virtual --- Point
    Vertex -- virtual --- Point
    Vertex3d -- public --- Point3d
    Vertex3d -- public --- Vertex
    pVertex -- public --- Vertex3d
    Vertex的constructor必须调用Point constrcutor。然而当Point3d和Vertex为Vertex3d的subobjects时,则调用操作不一定发生。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Point3d的constructor扩充内容
    Point3d* Point3d::Point3d(Point3d *this, bool __most_derived, float x, float y, float z)
    {
    if (__most_derived != false)
    this->Point::Point(x, y);
    this->__vptr_Point3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
    this->_Z = ths._z;
    return this;
    }
    Vertex3d调用Point3d和Vertex的constructor时,会把__most_derived参数设为false,于是就压制了对两个constructors中对Point constructor的调用操作。
    1
    2
    3
    4
    5
    6
    7
    8
    Vertex3d* Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, float x, float y, float z)
    {
    if (__most_derived != false)
    this->Point::Point(x, y);
    this->Point3d::Point3d(false, x, y, z);
    this->Vertex::Vertex(false, x, y);
    return this;
    }
    这个策略得以保持语义正确。当定义一个Point3d时,Point3d constructor可以正确调用Point virtual base class subobject。当定义Vertex3d时时,Vertex3d constructor正确调用Point constructor。Point3d和Vertex的constructor不会对Point调用。这种把一个constructor分裂为二的做法,可以带来速度的提升。??
vptr初始化语意学(The Semantics of the vptr Initialization)
  • 当我们定义PVertex object时,constructors的调用顺序是:
    1
    2
    3
    4
    5
    Point(x, y);
    Point3d(x, y, z);
    Vertex(x, y, z);
    Vertex3d(x, y, z);
    PVertex(x, y, z);
  • 假设继承体系中每个class都定义一个virtual function size(),返回class的大小。而这个继承体系中的每个constructors内含调用操作:
    1
    2
    3
    4
    5
    Point3d::Point3d(float x, float y, float z) : _x(x), _y(y), _z(z)
    {
    if (spyOn)
    cerr << "Within Point3d::Piont3d()" << "size: " << size() << endl;
    }
    在一个class的constructor(和destructor)中,经由构造中的对象来调用virtual function,其函数实例是在此class中有作用的那个。如果调用操作限制必须在constructor(或destructor)中直接调用,那么答案十分明显;将每个调用以静态方式决议它,不要用到虚拟机制。如果是在Point3d constructor中,就显式调用Point3d::size()。
  • 如果size()之中又调用一个virtual function,这个调用也必须决议为Point3d的函数实例。因此在执行constructor时,必须限制一组virtual functions候选名单。为了控制class中有所作用的函数,编译系统需要控制住vptr的初始化和设定操作,vptr初始化操作怎么处理,得视vptr在constructor中应该在什么时候被初始化而定。 在base class constructors调用操作之后,在member iniaialization list中members初始化操作前。它解决了class中限制一组virtual functions名单的问题。如果每个constructor都一直等待其base class constructors执行完毕之后才设定对象的vptr,那么每次都能调用正确的virtual function实例。
    constructor的执行算法通常如下:
    1. 在derived class constructor中,所有virtual base classes及上层base class的constructors会被调用
    2. **对象的vptr(s)会被初始化,指向相关virtual table(s)**。
    3. 如果有member initialization list,将在constructor内扩展开来。这在vptr被设定之后才做,以免virtual member function被调用。
    4. 最后才执行程序员所提供的代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      PVertex::PVertex(float x, float y, float z) : _next(0), Vertex3d(x, y, z), Point(x, y)
      {
      if (spyOn)
      cerr << "Within PVertex::PVertex()" << "size: " << size() << endl;
      }
      // 扩展
      // C++伪码
      PVertex* PVertex::PVertex(PVertex *this, bool __most__derived, float x, float y, float z)
      {
      // 条件式地调用virtual base constructor
      if (__most__derived != false)
      this->Point::Point(x, y);

      // 无条件调用上一层base
      this->Vertex3d::Vertex3d(x, y, z);

      // 将相关的vptr初始化
      this->__vptr_PVertex = __vtbl_PVertex;
      this->__vptr_Point__PVertex = __vtbl_Point__PVertex;

      // 程序员缩写的代码
      if (spyOn)
      cerr << "Within PVertex::PVertex()"
      << "size: "
      // 经由虚拟机制调用
      << (*this->__vptr__PVertex[3].faddr)(this)
      << endl;
      // 传回被构造的对象
      return this;
      }
  • 下面是vptr必须被设定的两种情况:
    1. 当一个完整的对象被构造起来时。如果我们声明一个Point对象,则Point constructor必须设定其vptr。
    2. 当一个subobject constructor调用一个virtual function(不论是直接调用还是间接调用)时。
      p218

5.3 对象复制语意学(Object Copy Semantics)

  • 当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择

    1. 什么都不做,施行默认行为
    2. 提供一个explicit copy assignment operator
    3. 显式地拒绝把一个class object指定给另一个class object

    当选择不准将一个class object指定给另一个class object时,只要把copy assignment operator声明为priavte,并且不提供其定义就可以了。设为private,就不再允许于任何地点(除了在member functions以及该class的friends之中)做赋值assign)操作。

  • 需要验证copy assignment operator的语意,利用Point class来帮助讨论:

    1
    2
    3
    4
    5
    6
    7
    class Piont {
    public:
    Point(float x = 0.0, float y = 0.0);
    // ... 没有virtual function
    protected:
    float _x, _y;
    };

    对于默认行为是否足够,如果要支持的只是一个简单的拷贝操作,那么默认行为不但足够而且有效率,而且没有理由再自己提供一个copy assignment operator。
    如果不对Point供应一个copy assignment operator,光是以来memberwise copy,编译器不会产生实例。编译器不会产生出一个实例,因为class有了bitwise copy语义,所以implicit copy assignment operator被视为毫无用处,也不会被合成出来。copy assignment operators并不表示bitwisecopy semantics是nontrivial,只有nontrivial instances才会被合成出来。因此,对于Point class,这样的赋值操作:

    1
    2
    3
    Point a, b;
    ...
    a = b;

    由bitwise copy完成,期间并没有copy assignment oeprator被调用。注意,我们还是可能提供一个copy constructor,为的是把name return valueNRV)优化打开。copy constructor的出现不意味着也也要提供一个copy assignment operator。

  • 如果该operator在继承之下呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    inline Point& Point::operator=(const Point &p)
    {
    _x = p._x;
    _y = p._y;
    return *this;
    }
    // 虚拟继承
    class Point3d : virtual public Point {
    public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
    // ...
    protected:
    float _z;
    };
    // 如果没有定义copy assignmnet operator,根据合成规则的第二项和第四项。合成的东西看起来这样:
    inline Point3d& Point3d::operator=(Point3d *const this, const Piont3d &p)
    {
    // call base class
    this->Point::operator=(p);

    // memberwise copy the derived class members
    _z = p._z;
    return *this;
    }

    它缺乏一个member assignment list(平行于member initialization list的东西)。缺少copy assignment list,看起来是小事,但如果没有它,编译器一般就没有办法压抑上一层base class的copy operators被调用。例如Vertex copy operator,Vertex虚拟继承自Point:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // class Vertex : virtual public Point
    inline Vertex& Vertex::operator=(const Vertex &v)
    {
    this->Point::operator=(v);
    _next = v._next;
    return *this;
    }
    // 从Point3d和Vertex派生Vertex3d,下面是其copy assignment operator
    inline Vertex3d& Vertex3d::operator=(const Vertex3d &v)
    {
    this->Point::operator=(v);
    this->Point3d::operator=(v);
    this->Vertex::oeprator=(v);
    ...
    }

    编译器怎么能在Point3d和Vertex的copy assignment operators中压抑Point的copy assignment operators呢?有一种方法可以保证most-derived class会(完成)virtual base class subobject的copy行为,就是在derived class的copy assignment operator函数实例的最后,显式调用oeprator:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    inline Vertex3d& Vertex3d::operator=(const Vertex3d &v)
    {
    this->Point3d::oeprator=(v);
    this->Vertex::oeprator=(v);
    // must place this last if your compiler does
    // not suppress intermediate class invocations
    this->Point::operator=(v);
    ...
    }

    它不能够省略subobjects的多重拷贝,但可以保证语意。建议尽可能不要允许一个virtual base class的拷贝操作,甚至一个比较奇怪的建议:不要在任何virtual base class中声明数据。

5.4 对象的效能(Object Efficiency)

  • 测试。

5.5 析构语意学(Semantics of Destruction)

  • 如果class没有定义destructor,那么只有在class内含的member object(抑或是class的base class)拥有destructor的情况下,编译器才会自动合成一个来。当从父类派生子类(即使是一种虚拟派生关系)时,如果没有声明一个estructor,编译器就没有必要合成一个destructor。没有任何理由说在delete一个对象之前得先讲内容清除干净。在一个对象的生命之前,没有任何class使用层面的程序操作是必要的,因此也就不需要要给destructor。

  • 一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,但顺序相反:

    1. destructor的函数本体现在被执行,也就是说vptr会在程序员的代码执行前被重设(reset)
    2. 如果class拥有member classs objects,而后者会拥有destructors,那么它们会以生命顺序的相反顺序被调用
    3. 如果object内含一个vptr,那么首先重设(reset)相关的vtbl。指向适当的base class的vtbl
    4. 如果由任何直接的(上一层)nonvirtual base classes拥有destrucotr,它们会以其声明顺序的相反顺序被调用
    5. 如果有任何virtual base classes拥有destructor,那么也会以原来的构造顺序的相反顺序被调用
  • 跟constructor一样,对destructor的一种最佳实现策略就是维护两份destrucotr实例:

    1. 一个complete object实例,先设定好vptr(s),并调用virtual base classes destructors。
    2. 一个base class subobject实例:除非在destructor函数中调用一个virtual function,否则不会调用virtual base class destructors并设定vptr。
  • 一个object的声明结束于其destructor开始执行之时

  • 看以下代码,会发生什么:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    Point3d obj;
    POint3d *ptr = &obj;
    obj.normalize();
    ptr->normalize();

    // declaretion
    Point3d Point3d::normalie() const
    {
    register float mag = magnitude();
    Point3d normal;
    normal._x = _x/mag;
    normal._y = _y/mag;
    normal._z = _z/mag;
    return normal;
    }

    float Point3d::magnitude() const
    {
    return sqrt(_x * _x + _y * _y + _z * _z);
    }
    答案是不知道。C++支持三种类型的member function:static、nonstatic和virtual,我们蹦确定normalize()和magnitude()两函数是否为virtual或nonvirtual,但可以确定它不是static。因为
    • 它直接存取nonstatic数据。
    • 它被声明为const。是的,static member functions不可能做到这两点。

4.1 Member的各种调用方式

  • 原始的“C with Class”只支持nonstaic member functions。它收到很多质疑:

    有一种常见的观点,认为virtual functions只不过是一种蹩脚的函数指针,没有什么用……其意思主要就是,virtual functions是一种没有效能的形式。

  • Static member functions是最后被引入的一种函数类型。

Nonstatic Member Functions(非静态成员函数)
  • C++的设计准则之一就是:static member function至少必须和一般的nonmember function有相同的效率。然而,举个例子:
    1
    2
    3
    4
    5
    6
    // nonmember
    float magnitude3d(const Point3d *_this) {
    return sqrt(_this->_x * _this->_x +
    _this->_y * _this->_y +
    _this->_z * _this->_z);
    }
    乍见似乎member function比较没有效率,它经由参数取坐标,而member function却直截了当用坐标成员。然而实际上member function被内化为nonmember的形式。下面是转化步骤:
    1. 改写函数的signature(函数原型)以安插要给额外的参数到member function中,用以提供一个存取管道,使class object将此函数调用。这个额外的参数使this指针
      1
      2
      3
      4
      5
      // non-const nonstatic member
      Point3d Point3d::magnitude(Point3d *const this)
      // 如果function是const
      // const nonstatic member
      Point3d Point3d::magnitude(const Point3d *const this)
    2. 将每一个对nonstatic data member的存取操作变为经由this指针来存取
      1
      2
      3
      4
      5
      {
      return sqrt(this->_x * this->_x +
      this->_y * this->_y +
      this->_z * this->_z);
      }
    3. 将member function重新写成一个外部函数。函数名经过“mangling”处理,使它在程序中成为独一无二的词汇:
      1
      extern magnitude__7Point3dFv(register Point3d *const this);
  • 开章的normalize()函数会转化成以下形式,假设声明了Pointe3d copy constructor,而named returned value(NRV)的优化也施行了:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // C++伪码
    void normalize__7Point3dFv(register const POint3d *const this, Point3d &__result)
    {
    register float mag = this->magniude();
    // default constructor
    __result.Point3d::Point3d();
    __result._x = this->_x/mag;
    __result._y = this->_y/mag;
    __result._z = this->_z/mag;
    return;
    }

    // 有效率的方式
    Pointe3d Point3d::normalize() const
    {
    register float mag = magnitude();
    return Point3d(_x/mag, _y/mag, _z/mag);
    }
    // 转化
    void normalize__7Point3dFv(register const Point3d *const this, Point3d &__result)
    {
    register float mag = this->magnitude();
    // __result return value
    __result.Point3d::Point3d(this->_x/mag, this->_y/mag, this->_z/mag);
    return;
    }
名称的特殊处理(Name Mangling)
  • 一般而言,member的名称前面会被加上class名称,形成独一无二的命名:
    1
    2
    3
    class Bar { public: int ival; ... };
    // ival可能为
    // ival__3Bar
    为什么这么做呢?考虑这样的派生操作:
    1
    2
    3
    4
    5
    6
    7
    class Foo : public Bar { public: int ival; ... };
    // Foo对象内部结合了base class和derived class两者
    class Foo {
    public:
    int ival__3Bar;
    int ival__3Foo;
    }
    不管要处理哪个ival,通过name mangling都能指出来。由于member functions可能被重载(overloaded),所以更需要广泛的mangling手法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Point {
    public:
    void x(float newX);
    float x();
    ...
    }
    // 转化为:
    class Point {
    public:
    void X__5PointFf(float newX);
    float X__5PointFv();
    }
  • 把参数和函数名称编码在一起,编译器就在不同的编译模块之间达成了一种有限形式的类型检验。如果两个实例拥有独一无二的name mangling,任何不正确的调用操作在链接时会因无法resolved而失败。这就是确保类型安全的链接行为type-safe linkage)。它只能捕捉到函数的标记(signature,函数名称+参数个数+参数类型)错误,而返回类型错误检查不出来。而一种demangling工具,用来拦截名称并转换回去。它向使用者隐藏了内部名称,使得出错时能得到一个友好的信息返回。
Virtual Member Functions(虚拟成员函数)
  • 如果normalize()是virtual member function,那么以下调用:
    1
    ptr->normalie();
    会被转化为:
    1
    (*ptr->vptr[1])(ptr);
    其中:
    • vptr由编译器产生,一个指向vtbl的指针
    • 1是索引值
    • ptr是this指针
  • 类似道理,如果magnitude()也是virtual function,它在normalize()之中的调用操作被转换如下:
    1
    2
    // register float mag = magnitude();
    register float mag = (*this->vptr[2])(this);
    此时,由于Point3d::magnitude()是在Point3d::normalize()中被调用的,而后者已经由虚拟机之而决议(reslove)妥当,显式的调用Point3d实例会比较有效率,并因此压制由于虚拟机之而产生的不必要重复调用操作:
    1
    2
    // explicitly invocation
    register float mag = Point3d::magnitude();
    如果magnitude()声明为inline函数,会更有效率。因为使用class scope operator显式调用virtual function,resolve方式和nonstatic member function一样
    1
    register float mag = magnitude__7Point3dFv(this);
    这时对于以下调用:
    1
    2
    // Point3d obj;
    obj.normalize();
    如果编译器转化为:
    1
    (*obj.vptr[1])(&obj);
    虽然语义正确,却没有必要。所以上述经由obj调用的函数实例只可以是Point3d:normalize()。”经由class object调用virtual function”,这种操作总是被编译器像对待一般nonstatic member function一样resolved:
    1
    normalize__7Point3Fv(&obj);
    这项工程的另一个利益是,virtual function的inline函数实例可以被扩展(expanded)开来,因而提供极大的效率利益
Static Member Functions(静态成员函数)
  • 如果Point3d::normalize()是一个static member function,下面两个调用的转化:
    1
    2
    3
    4
    5
    6
    7
    obj.normalize();
    ptr->normalize();
    // 将被转化为一般的nonmember函数调用
    // obj.normalize();
    normalize__7Point3dSFv();
    // ptr->nomalize();
    normalize__7Point3dSFv();
    在引进static member functions之前,你很少看到这种怪异写法:
    1
    ((Point3d*)0)->object_count();
    在引进static member functions之前,C++语言要求member functions必须经由class的object来调用。而实际上,只有当一个或多个nonstatic data members在member function中被直接存取时,才需要class object。如果没有任何一个members被直接存取,就不需要this指针,也就不需要通过一个class object来调用member function。
    这一来存取static data members时产生了一些不规则性。如果class的static data member声明为nonpublic,那么就必须提供member functions来存取member。这时很显然,虽然补考class object来存取static member,但存取函数却得绑定一个class object上
    独立于class object之外的存取操作很重要,尤其在没有class object存在的情况。程序方法上的解决之道是把0强制转换为一个class指针:
    1
    object_count((Point3d*)0);
    至于语言层面上的解决之道,是引进的static member functions。它的主要特性是没有this指针。以下是次要特性:
    • 它不能直接存取class中的nonstatic members
    • 它不能被声明为const、volatile或virtual
    • 它不需要经由class object才被调用——虽然大部分时候它是这样被调用的
  • 如果去一个static member funciton的地址,获得的将是在内存中的位置,也就是其地址。由于static member function没有this指针,所以地址类型不是一个指向class member funciton的指针,而是一个nonmember函数指针
    1
    2
    3
    4
    5
    &Point3d::object_count();
    // 会得到数值,类型是
    unsigned int (*)();
    // 而不是
    unsigned int (Point3d::*)();
    因为static member function缺乏this指针,所以差不多等同于nonmember function。它有个意想不到的好处:成为callback函数

4.2 Virtuar Member Functions(虚拟成员函数)

  • 我们已经知道了virtual function的一般实现模型:每个class中有个vtbl,其中包含着virtual function的地址,而每个object有vptr,指向vtbl的存在。

  • 为了支持virtual function机制,对于多态对象必须有某种形式上的执行期类型判断法runtime type resolution)。也就是下面调用需要ptr在执行期有某些信息,才能找到并调用z():

    1
    ptr->z();

    这份信息不能加在ptr身上,它增加了空间负担,即使不使用多态;第二它打断了与C程序间的链接兼容性。我们需要一个更好的规范,一个以class的使用为基础,而不在乎关键词是class或struct的规范。并且必须在执行期多态runtime polymorphism)时才需要这份信息

  • C++中多态表示以一个public base class的指针(reference),寻址出一个derived class object的意思。经由指针,可以在程序中任何地方采用public derived类型,这种多态形式是消极的passive),可以在编译期完成,除了virtual base class的情况。当指出的对象被使用时,才变成积极的active)。

  • runtime type identificationRTTI)性质在1993年被引入前,C++对于积极多态(active polymorphsim)的唯一支持,就是对virtual function call的resolution操作。有了RTTI,能够在执行期查询一个多态的pointer或多态的reference了

    1
    2
    3
    4
    5
    // 积极多态的例子
    ptr->z();
    //
    if (Point3d *p3d = dynamic_cast<Point3d*>(ptr))
    return p3d->_z;

    z()是一个virtula function。是什么信息让我们在执行期调用正确呢?,我们需要知道:

    • ptr所指对象的真实类型
    • z()实例的位置,以便能够调用它

    实现上,可以在class object身上增加两个members:

    1. 一个字符串或数字,表示class的类型;
    2. 一个指针。指向表格,表格中有个virtual function的执行期地址。

    表格中的virtual functions地址是怎么建构起来的呢?virtual function(可由object被调用)在编译期获知。这个地址是固定的,执行期不增添也不替换。完全由编译器掌控,不需要执行期介入。而执行期只是备妥了函数地址,并未被找到。找到那些地址。两个步骤完成任务:

    1. 为了找到表格,class object被安插了编译器内部产生的指针,指向该表格
    2. 为了找到函数地址,每个virtual function被指派为一个表格索引值

    以上的工作都由编译器完成。执行期要做的,只是在特定的virtual table slot中激活virtual funciton

    一个class只有一个vtbl。每个table内含对应object的active virtual functions函数实例的地址,这些active virtual functions包括:

    • class所定义的函数实例。它会overriding存在的base class virtual function函数实例。
    • 继承自base class的函数实例。这是在derived class决定不改写virtual function时才出现的情况。
    • 一个pure_virtual_called()函数实例
  • 每个virtula function都被指派一个固定的索引值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Point {
    public:
    virtual ~Point();
    virtual Point& mult(float) = 0; // puree virtual function
    // ...
    float x() const { return _x; }
    virtual float y() const { return 0; }
    virtual float z() const { return 0; }
    // ...
    protected:
    Point(float x = 0.0);
    float _x;
    };

    virtual destructor被指派slot 1,而mult()被指派slot2,y()被指派slot3,z()被指派slot4。在单一继承体系中,virtual
    fucntion机制行为十分良好,有效率且容易塑造出模型。而在多重继承或虚继承中,对virtual functinos的支持就没那么好了。

多重继承下的Virtual Functions
  • 多重继承中支持virtual functions,复杂度围绕在第二个及后继base class身上,以及”必须在执行期调整this指针“这一点:

    1
    2
    3
    graph BT
    Derived -- public --- Base1
    Derived -- public --- Base2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class Base1 {
    public:
    Base1();
    virtula ~Base1();
    virtual void speakClearly();
    virtual Base1* clone() const;
    protected:
    float data_Base1;
    };

    class Base2 {
    public:
    Base2();
    virtual ~Base2();
    virtual void mumble();
    virtual Base2* clone() const;
    protected:
    float data_Base2;
    };

    class Derived : public Base1, public Base2 {
    public:
    Derived();
    virtual ~Derived();
    virtual Derived* clone() const;
    protected:
    float data_Derived;
    };
  • 它的困难度主要体现在Base2 subobject身上,有三个问题需要解决:*(1)virtual destructor*(2)被继承的Base2::mumble()(3)clone()函数实例

    1
    2
    3
    4
    5
    Base2 *pbase2 = nwe Derived;
    // 编译时期代码
    // 转移以支持第二个base class
    Derived *temp = new Derived;
    Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;

    如果没有这样的调整,任何非多态的应用都将失败:

    1
    2
    3
    4
    5
    // pbase2被指定一个Derived对象,这也应该没有问题
    pbase2->data_Base2;
    // 当程序员要删除pbase2所指的对象时
    // 需要调用正确的virtual destructor函数实例,pbase2可能需要调整
    delete pbase2;

    一般规则时,经由第二或后继base class的指针(reference)来调用derived class virtual funciton。其所连带的必要this指针调整操作,必须在执行期完成。也就是offset的大小,offset加到this指针上的程序代码,必须由编译器在某个地方插入。

    cfont编译器中的方法是将vtbl加大,容纳所需的this指针,每个virtual table slot不再是指针,而是一个集合体,内含offset以及地址:

    1
    2
    3
    (*pbase2->vptr[1])(pbase2);
    // 改变为
    (*pbase2->vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);

    其中faddr内含virtual function地址,offset内含this指针调整值。这个做法的缺点是,不管什么virtual function都得进行offset调整。
    比较有效率的解决方法是利用所谓的thunk(是一段assembly代码):(1)适当的offset调整this指针(2)跳到virtual function去,如:

    1
    2
    3
    pbase2_dtor_thunk:
    this += sizeof(base1);
    Derived::~Derived(this);

    Thunk技术允许vtbl slot继续内含简单指针,因此空间上不需要任何负担。slots的地址直接指向virtual function,也可以指向相关的thunk。

  • 调整this指针的的第二个额外负担就是,由于(1)经由derived class(或base class)调用(2)经由第二(或后继)base class调用,同一函数在vtbl可能需要多次对应的slots:

    1
    2
    3
    4
    Base1 *pbase1 = new Derived;
    Base2 *pbase2 = new Derived;
    delete pbase1;
    delete pbase2;

    两个相同的derived destructor,但需要不同的vtbl slots:

    1. pbase1不需要调整this指针。vtbl slot需放置真正destructor地址。
    2. pbase2需要调整this指针。vtbl slot需相关的thunk地址。

    多重继承下,derived class内含n-1个额外的vtbl,n表示上一层base classes的个数。对于本例的Derived而言,会有两个vtbl被编译器产生出来:

    1. 一个主要实例,与Base1共享
    2. 一个次要实例,与Base2有关

    用以支持一个clas拥有多个vtbl的传统方法是,将每个tables以外部对象的形式产生出来,并给予独一无二的名称:

    1
    2
    vtbl__Derived;          // 主
    vtbl__Base2__Derived; // 次

    于是当你将Derived对象地址指定给Base1指针或Derived指针时,被处理的vtbl时主要表格vtbl__Derived。当讲Derived对下给你地址指定给Base2指针时,被处理的vtbl时次要表格vtbl__Base2_Derived。

  • 由于执行期链接器runime linkers)的降临(可以支持动态共享函数库),符号名称的链接可能变得非常缓慢。为了调节执行期编译器的效率,Sun编译器将多个vtbl连锁为一个:指向次要表格的指针,可由主要表格表格名称加上一个offset获得,这样的策略下,每个class只有一个具名的vtbl

  • 有三种情况,第二或后继的base class会影响virtual functions的支持。

    1. 通过一个指向第二个base class的指针,调用derived class virtual function
    2. 通过一个指向derived class的指针,调用第二个base class中的一个继承而来的virtual funciton
    3. 允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type。这一点经由Derived::clone()函数实例来说明。clone函数的Derived版本传回一个Derived class指针,默默地改写了它们两个base class函数实例。当通过指向第二个base class的指针来调用clone()时,this指针的offset问题诞生了:
      1
      2
      3
      4
      Base2 *pb1 = new Derived;
      // 调用Derived* Derived::clone()
      // 返回值必须被调整,以指向Base2 subobject
      Base2 *pb2 = pb1->clone();
  • 当函数被认为足够小的时候,Sun编译器会提供一个split functions技术,以相同的算法产生两个函数。这样不论通过Base1指针还是通过Derived指针调用函数,都不需要调整返回值,而通过Base指针所调用的时另一个函数,并在返回前,为指针加上必要的offset。如果函数不小,会给函数中其中一个进入点,进入点需要三个指令。

  • thunk则是函数一开始先(1)调整this指针,然后才(2)执行程序员所写的函数码;无需调整函数调用操作。

  • Microsoft用adderss points来取代thunk策略,即overriding function期待获得的是引入该virtual function的class的地址,这就是函数的address point。

虚拟继承下的Virtual Functinos
  • 考虑下面的virtual base class派生体系:
    1
    2
    graph BT
    Point2d --- Point3d
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Point2d {
    public:
    Point2d(float = 0.0, float = 0.0);
    virtual ~Point2d();
    virtual void mumble();
    virtual float z();
    // ...
    protected:
    float _x, _y;
    };

    class Point3d : public virtula Point2d {
    public:
    Point3d(float = 0.0, float = 0.0, float = 0.0);
    ~Point3d();
    float z();
    protected:
    float _z;
    };
    当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了了迷宫一样。所以建议是,不要再一个virtual base class中声明nonstatic data members。如果这么做,你会距离复杂的深渊愈来愈近。

4.3 函数的效能

  • 下面这组测试,在编译器上计算两个3D点,其中用到一个nonmember friend function,一个member function,以及一个virtual member function。p170

4.4 指向Member Function的指针(Pointer-toMember Functions)

  • 去一个nonstatic data member 的地址,得到的结果是该member在class布局中的bytes位置(再+1)。可以想象它是一个不完整的值,它需要被绑定于某个class object的地址上,才能够被存取。
    取一个nonstatic member function的地址,如果该函数是nonvirtual,得到的结果是它在内存中真正的地址。然而也是不完全的,也需要绑定于某个class object的地址上,才能够通过它调用函数。回顾一下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    double (Point::*pmf)();
    // 定义
    double (Point::*coord)() = &Point::x;
    coord = &Point::y;
    // 调用
    (origin.*coord)();
    (ptr->*coord)();
    // 转化
    (coord)(&origin);
    (coord(ptr);
    指向member function的指针的声明语法,以及指向member selection运算符的指针,作用是作为this指针的空间保留着。这就是为什么static member function(没有this指针)的类型是”函数指针“,而不是指向member function的指针的原因。 使用一个member function指针,如果并不用于virtual functions、virtual base class或multiple base classes等情况的画,并不会比使用一个nonmember function指针的成本更高。上述三种情况对于member function指针的类型及调用都太过复杂。对于没有以上情况的class而言,编译器可以为它们提供相同的效率
支持“指向Virtual Member Functions”的指针
  • 考虑下面程序片段:
    1
    2
    float (Point::*pmf)() = &Point::z;
    Point *ptr = new Point3d;
    pmf,一个指向member function的指针,被设值为Point::z()(一个virtual function)的地址。如果由ptr调用z(),被调用的是Point3d::z(),但如果从pmf间接调用z(),正常运行吗?yes。
    对于一个virtual function取其地址,所能获得的只是一个索引值。对于一个指向member function的指针评估求值evaluated),会因为该值由两种意义而复杂化,调用也有别于常规,float (Point::*pmf)();必须允许此函数能够寻址出nonvirtual x()和virtual z()两个member functions:
    1
    2
    3
    // 两者都可以指定给pmf
    float Point::x() { return _x; }
    float Point::z() { return 0; }
    只不过其中一个代表内存地址,另一个代表vtbl中的索引值。为了使pmf能够(1)持有两种数值,(2)能区别代表内存地址还是vtbl中的索引值:
    1
    2
    3
    // true is non-virtual invocation
    // false is virtual invocation
    (((int)pmf) & ~127) ? (*pmf)(ptr) : (*ptr->vptr[(int)pmf](ptr));
在多重继承之下,指向Member Functions的指针
  • 为了让member functions的指针能够支持多重继承和虚拟继承,设计了一个结构体:
    1
    2
    3
    4
    5
    6
    7
    8
    struct __mptr {
    int delta;
    int index;
    union {
    ptrtofunc faddr;
    int v_offset;
    };
    };
    index和faddr分别持有vtbl slot和nonvirtual member function address(当index不指向vtbl时,设为-1),像以下调用:
    1
    2
    3
    4
    5
    6
    7
    (ptr->*pmf)();
    // 变成
    (pmf.index < 0)
    ? // non-virtual invocation
    (*pm.faddr)(ptr)
    : // virtual invocation
    (*ptr->vptr[pmf.index](ptr));
    这个方法受到的批评是,每个调用操作都得付出成本。Microsoft把检查拿掉,导入一个vcall thunk,它会选出并调用相关vtbl中的slot。 这个结构体的另一个副作用是,当传递一个不变值的指针给member function时,需要产生临时变量:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    extern Point3d foo(const Point3d&, Point3d (Point3d::*)());
    void bar(const Point3d &p) {
    Point3d pt = foo(p, &Point3d::normal);
    // ...
    }

    // &Point3d::normal value
    {0, -1, 10727417}
    // 将变成
    __mptr temp = {0, -1, 10727417}
    foo(p, temp);
    回到开头那个结构体。delta字段表示this指针的offset值,而v_offset字段放的是一个virtual base class的vptr位置。如果vptr被编译器放在class对象起头处,这个字段就没必要了。它只在多重继承或虚拟继承情况下才有必然性。有些编译器根据不同的class特征提供多种memer functions的指向方式。如Microsoft:
    1. 单一继承实例(有vcall thnuk地址或是函数地址);
    2. 多重继承实例(有faddr和delta两个members);
    3. 虚拟继承实例(4个members)。
”指向Member Functions之指针的效率
  • 又是测试。p180

4.5 Inline Functions

  • 一个Point class的加法运算符的可能实现内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Point {
    friend oint operator+(const Point&, const Point&);
    }

    Point operator+(const Point &lhs, const Point &rhs)
    {
    Point new_pt;
    new_pt._x = lhs._x + rhs._x;
    new_pt._y = lhs._y + rhs._y;
    return new_pt;
    }

    // void Point::x(float new_x) { _x = new_x; }
    // float Point::x() { return _x; }
    new_pt.x(lhs.x() + rhs.x());

    以上通过将存取函数声明为inline,不但可以保持直接存取的高效率,而且加法运算符不再需要被声明为Point的friend。
    然而不能够强迫任何函数都变成inline。关键词inline(或者class declaration中的member function或friend function的定义)只是一项请求。如果请求被接受,编译器就认为它可以用一个表达式(expression)合理地将这个函数扩展开来。cfront有一套复杂的测试方法,通常是用来计算assignments、function calls、virtual function calls等操作的次数。每个expression种类都有一个权值inline函数的复杂度就以这些操作的总和来决定

  • 一般处理一个inline函数有两个阶段:

    1. 分析函数定义,决定函数的intrinsic inline ability。如果函数因复杂度构建问题不可成为inline,它会被转为一个static函数,并在被编译模块内产生对应的函数定义。在一个支持模块个别编译的环境中,编译器几乎没有什么权宜之计。理想情况下,链接器会将被产生出来的重复东西清理掉,但调试信息不会。UNIX环境的strip命令可以。
    2. 真正的inline函数扩展操作是在调用的那一点上,这会带来参数的求值操作(ealuation)以及临时性对象的管理
  • 在将要扩展的点上,cfront编译器中,inline函数如果只有一个表达式,而又有后续操作,则不会扩展开来

形式参数(Formal Arguments)
  • inline扩展期间每个形式参数都被对应的实际参数取代。如果实际参数是一个常量表达式(constant expression),可以在替换之前完成求值操作(evaluations);如果是个有副作用的表达式,那么需要引入临时性对象;如果既不是常量表达式,也不是带有副作用的表达式,那么就直接代替它。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    inline int min(int i, int j)
    {
    return i < j ? i : j;
    }

    inline int bar()
    {
    int minval;
    int val1 = 1024;
    int val2 = 2048;
    minval = min(val1, val2); // 1
    minval = min(1024, 2048); // 2
    minval = min(foo(), bar() + 1); // 3
    return minval;
    }

    // 扩展
    minval = val1 < val2 ? val1 : val2; // 1
    minval = 1024; // 2
    // 3
    int t1;
    int t2;
    minval = (t1 = foo()), (t2 = bar() + 1), t1 < t2 ? t1 : t1;
局部变量(Local Variables)
  • 如果在inline定义中加入局部变量:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    inline int min(int i, int j)
    {
    int minval = i < j ? i : j;
    return minval;
    }

    {
    int local_var;
    itn minval;
    // ...
    minval = min(val1, val2);
    }
    // 扩展
    {
    int local_var;
    int minval;
    // inlnie函数的局部变量处以mangling操作
    int __min_lv-minval;
    minval =
    (__min_lv-minval =
    val1 < val2 ? val1 : val2),
    __min_lv_minval;
    }
    一般inline函数中的每个局部变量都必须放在封闭的区段中,拥有独一无二的名称。因为如果inline函数以分离的多个式子discrete statements)被扩展多次,那么只需要一组局部变量,就可以重复使用。
    inline函数中的局部变量,加上有副作用的参数,可能会导致大量临时性对象的产生。特别是如果以单一表达式(expression)被扩展多次的话:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    minval = min(avl1, val2) + min(foo(), foo()+1);
    // 扩展
    // 为局部变量产生临时变量
    int __min_lv_minval_00;
    int __min_lv_minval_01;
    // 为放置副作用产生临时变量
    int t1;
    int t2;
    minval = ((__min_lv_minval_00 =
    val1 < val2 ? val1 : val2),
    __min_lv_minval_00)
    +
    ((__min_lv_minval_01 = (t1 = foo()),
    (t1 = foo() + 1),
    t1 < t2 ? t1 : t2),
    __min_lv_minval__01);
    Inline函数对封装提供了必要的支持,可以有效存取class的nonpublic数据。它同时是C程序中大量使用#define的一个安全代替品——特别如果宏中的参数有副作用的话。然而被调用太多次的话,会产生大量的扩展码,使程序大小暴涨