• 一是C风格的类型转换过于粗鲁,能允许你在任何类型之间进行转换。不过如果要进行更精确的类型转换,这会是一个优点。在这些类型转换中存在着巨大的不同,例如把一个指向const对象的指针(pointer-to-const-object)转换成指向非const对象的指针(pointer-to-non-const-object)(即一个仅仅去除const的类型转换),把一个指向基类的指针转换成指向子类的指针(即完全改变对象类型)。传统的C风格的类型转换不对上述两种转换进行区分。(这一点也不令人惊讶,因为C风格的类型转换是为C语言设计的,而不是为C++语言设计的)。二来C风格的类型转换在程序语句中难以识别。在语法上,类型转换由圆括号和标识符组成,而这些可以用在C++中的任何地方。人工阅读很可能忽略了类型转换的语句,而利用象grep的工具程序也不能从语句构成上区分出它们来。

    C++通过引进四个新的类型转换操作符克服了C风格类型转换的缺点,这四个操作符是, static_cast, const_cast, dynamic_cast, 和reinterpret_cast。在大多数情况下,对于这些操作符你只需要知道原来你习惯于这样写,(type) expression 而现在你总应该这样写:static_cast<type>(expression) 例如,假设你想把一个int转换成double,以便让包含int类型变量的表达式产生出浮点数值的结果。如果用C风格的类型转换,你能这样写:int firstNumber, secondNumber;
    ...
    double result = ((double)firstNumber)/secondNumber;
    如果用上述新的类型转换方法,你应该这样写:
    double result = static_cast<double>(firstNumber)/secondNumber;
    这样的类型转换不论是对人工还是对程序都很容易识别。

    static_cast在功能上基本上与C风格的类型转换一样强大,含义也一样。它也有功能上限制。例如,你不能用static_cast象用C风格的类型转换一样把struct转换成int类型或者把double类型转换成指针类型,另外,static_cast不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。

    其它新的C++类型转换操作符被用在需要更多限制的地方。const_cast用于类型转换掉表达式的const或volatileness属性。通过使用const_cast,你向人们和编译器强调你通过类型转换想做的只是改变一些东西的constness或者 volatileness属性。这个含义被编译器所约束。如果你试图使用const_cast来完成修改constness 或者volatileness属性之外的事情,你的类型转换将被拒绝。下面是一些例子:
    class Widget { ... };
    class SpecialWidget: public Widget { ... };
    void update(SpecialWidget *psw);
    SpecialWidget sw; // sw 是一个非const 对象。
    const SpecialWidget& csw = sw; // csw 是sw的一个引用
    // 它是一个const 对象
    update(&csw); // 错误!不能传递一个const SpecialWidget* 变量给一个处理SpecialWidget*类型变量的函数
    update(const_cast<SpecialWidget*>(&csw));// 正确,csw的const被显示地转换掉(csw和sw两个变量值在update函数中能被更新)
    update((SpecialWidget*)&csw);// 同上,但用了一个更难识别的C风格的类型转换
    Widget *pw = new SpecialWidget;
    update(pw); // 错误!pw的类型是Widget*,但是update函数处理的是SpecialWidget*类型
    update(const_cast<SpecialWidget*>(pw));//错误!const_cast仅能被用在影响constness or volatileness的地方上。不能用在向继承子类进行类型转换。

    到目前为止,const_cast最普通的用途就是转换掉对象的const属性。

    第二种特殊的类型转换符是dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时):
    Widget *pw;
    ...
    update(dynamic_cast<SpecialWidget*>(pw));
    // 正确,传递给update函数一个指针是指向变量类型为SpecialWidget的pw的指针
    // 如果pw确实指向一个对象,否则传递过去的将使空指针。
    void updateViaRef(SpecialWidget& rsw);
    updateViaRef(dynamic_cast<SpecialWidget&>(*pw));
    //正确。 传递给updateViaRef函数
    // SpecialWidget pw 指针,如果pw
    // 确实指向了某个对象
    // 否则将抛出异常
    dynamic_casts在帮助你浏览继承层次上是有限制的。它不能被用于缺乏虚函数的类型上(参见条款M24),也不能用它来转换掉constness
    int firstNumber, secondNumber;
    ...
    double result = dynamic_cast<double>(firstNumber)/secondNumber;
    // 错误!没有继承关系
    const SpecialWidget sw;
    ...
    update(dynamic_cast<SpecialWidget*>(&sw));
    // 错误! dynamic_cast不能转换
    // 掉const。
    如你想在没有继承关系的类型中进行转换,你可能想到static_cast。如果是为了去除const,你总得用const_cast。

    这四个类型转换符中的最后一个是reinterpret_cast。使用这个操作符的类型转换,其的转换结果几乎都是执行期定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。reinterpret_casts的最普通的用途就是在函数指针类型之间进行转换。例如,假设你有一个函数指针数组:
    typedef void (*FuncPtr)(); // FuncPtr is 一个指向函数的指针,该函数没有参数返回值类型为void
    FuncPtr funcPtrArray[10]; // funcPtrArray 是一个能容纳10个FuncPtrs指针的数组
    让我们假设你希望(因为某些莫名其妙的原因)把一个指向下面函数的指针存入funcPtrArray数组:
    int doSomething();
    你不能不经过类型转换而直接去做,因为doSomething函数对于funcPtrArray数组来说有一个错误的类型。在FuncPtrArray数组里的函数返回值是void类型,而doSomething函数返回值是int类型。
    funcPtrArray[0] = &doSomething; // 错误!类型不匹配
    reinterpret_cast可以让你迫使编译器以你的方法去看待它们:
    funcPtrArray[0] = // this compiles
    reinterpret_cast<FuncPtr>(&doSomething);
    转换函数指针的代码是不可移植的(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果(参见条款M31),所以你应该避免转换函数指针类型,除非你处于着背水一战和尖刀架喉的危急时刻。一把锋利的刀。一把非常锋利的刀。
    如果你使用的编译器缺乏对新的类型转换方式的支持,你可以用传统的类型转换方法代替static_cast, const_cast, 以及reinterpret_cast。也可以用下面的宏替换来模拟新的类型转换语法:
    #define static_cast(TYPE,EXPR) ((TYPE)(EXPR))
    #define const_cast(TYPE,EXPR) ((TYPE)(EXPR))
    #define reinterpret_cast(TYPE,EXPR) ((TYPE)(EXPR))
    你可以象这样使用使用:
    double result = static_cast(double, firstNumber)/secondNumber;
    update(const_cast(SpecialWidget*, &sw));
    funcPtrArray[0] = reinterpret_cast(FuncPtr, &doSomething);
    这些模拟不会象真实的操作符一样安全,但是当你的编译器可以支持新的的类型转换时,它们可以简化你把代码升级的过程。没有一个容易的方法来模拟dynamic_cast的操作,但是很多函数库提供了函数,安全地在派生类与基类之间进行类型转换。如果你没有这些函数而你有必须进行这样的类型转换,你也可以回到C风格的类型转换方法上,但是这样的话你将不能获知类型转换是否失败。当然,你也可以定义一个宏来模拟dynamic_cast的功能,就象模拟其它的类型转换一样:
    #define dynamic_cast(TYPE,EXPR) (TYPE)(EXPR)
    请记住,这个模拟并不能完全实现dynamic_cast的功能,它没有办法知道转换是否失败。
    我知道,是的,我知道,新的类型转换操作符不是很美观而且用键盘键入也很麻烦。如果你发现它们看上去实在令人讨厌,C风格的类型转换还可以继续使用并且合法。然而,正是因为新的类型转换符缺乏美感才能使它弥补了在含义精确性和可辨认性上的缺点。并且,使用新类型转换符的程序更容易被解析(不论是对人工还是对于工具程序),它们允许编译器检测出原来不能发现的错误。这些都是放弃C风格类型转换方法的强有力的理由。还有第三个理由:也许让类型转换符不美观和键入麻烦是一件好事。

  • 指针与引用看上去完全不同(指针用操作符“*”和“->”,引用使用操作符“. ”),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?

    首先,要认识到在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。“但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?”
    char *pc = 0; // 设置指针为空值
    char& rc = *pc; // 让引用指向空值
    这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生)。应该躲开写出这样代码的人,除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。
    因为引用肯定会指向一个对象,在C++里,引用应被初始化
    string& rs; // 错误,引用必须被初始化
    string s("xyzzy");
    string& rs = s; // 正确,rs指向s
    指针没有这样的限制。
    string *ps; // 未初始化的指针
    // 合法但危险
    不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。
    void printDouble(const double& rd)
    {
    cout << rd; // 不需要测试rd,它
    } // 肯定指向一个double值
    相反,指针则应该总是被测试,防止其为空:
    void printDouble(const double *pd)
    {
    if (pd) { // 检查是否为NULL
    cout << *pd;
    }
    }

    指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变
    string s1("Nancy");
    string s2("Clancy");
    string& rs = s1; // rs 引用 s1
    string *ps = &s1; // ps 指向 s1
    rs = s2; // rs 仍旧引用s1,
    // 但是 s1的值现在是
    // "Clancy"
    ps = &s2; // ps 现在指向 s2;
    // s1 没有改变

    总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。还有一种情况,就是当你重载某个操作符时,你应该使用引用。最普通的例子是操作符[]。这个操作符典型的用法是返回一个目标对象,其能被赋值。
    vector<int> v(10); // 建立整形向量(vector),大小为10;
    // 向量是一个在标准C库中的一个模板(见条款M35)
    v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值
    如果操作符[]返回一个指针,那么后一个语句就得这样写:
    *v[5] = 10;
    但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。(这有一个有趣的例外,参见条款M30),当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针。

  • 公有继承体现“是一个”(即子是一个父)的概念

    当写下类D("Derived" )从类B("Base")公有继承时,你实际上是在告诉编译器(以及读这段代码的人):类型D 的每一个对象也是类型B 的一个对象,但反之不成立;你是在说:B 表示一个比D 更广泛的概念,D 表示一个比
    B 更特定概念;你是在声明:任何可以使用类型B 的对象的地方,类型D 的对象也可以使用,因为每个类型D 的对象是一个类型B 的对象。相反,如果需要一个类型D 的对象,类型B 的对象就不行:每个D "是一个" B, 但反之不成
    立。C++采用了公有继承的上述解释。看这个例子:
    class Person { ... };
    class Student: public Person { ... };
    从日常经验中我们知道,每个学生是人,但并非每个人是学生。这正是上面的层次结构所声明的。我们希望,任何对 "人" 成立的事实 ---- 如都有生日----也对 "学生" 成立;但我们不希望,任何对 "学生" 成立的事实 ---- 如都在
    某一学校上学 ----也对 "人" 成立。人的概念比学生的概念更广泛;学生是一种特定类型的人。
    在C++世界中,任何一个其参数为Person 类型的函数(或Person 的指针或Person 的引用)可以实际取一个Student 对象(或Student 的指针或Student的引用):
    void dance(const Person& p); // 任何人可以跳舞
    void study(const Student& s); // 只有学生才学习
    Person p; // p 是一个人
    Student s; // s 是一个学生
    dance(p); // 正确,p 是一个人
    dance(s); // 正确,s 是一个学生,
    // 一个学生"是一个"人
    study(s); // 正确
    study(p); // 错误! p 不是学生
    只是公有继承才会这样。也就是说,只是Student 公有继承于Person 时,C++的行为才会象我所描述的那样。私有继承则是完全另外一回事(见条款42),至于保护继承,好象没有人知道它是什么含义。另外,Student "是一个"
    Person 的事实并不说明Student 的数组 "是一个" Person 数组。关于这一话题的讨论参见条款M3。
    公有继承和 "是一个" 的等价关系听起来简单,但在实际应用中,可能不会总是那么直观有时直觉会误导你。例如,有这样一个事实:企鹅是鸟;还有这样一个事实:鸟会飞。如果想简单地在C++中表达这些事实,我们会这样
    做:
    class Bird {
    public:
    virtual void fly(); // 鸟会飞
    ...
    };
    class Penguin:public Bird { // 企鹅是鸟
    ...
    };
    突然间我们陷入困惑,因为这种层次关系意味着企鹅会飞,而我们知道这不是事实。发生什么了?造成这种情况,是因为使用的语言(汉语)不严密。说鸟会飞,并不是说所有的鸟会飞,通常,只有那些有飞行能力的鸟才会飞。如果更精确一点,我们都知道,实际上有很多种不会飞的鸟,所以我们会提供下面这样的层次结构,它更好地反映了现实:
    class Bird {
    ... // 没有声明fly 函数
    };
    class FlyingBird: public Bird {
    public:
    virtual void fly();
    ...
    };
    class NonFlyingBird: public Bird {
    ... // 没有声明fly 函数
    };
    class Penguin: public NonFlyingBird {
    ... // 没有声明fly 函数
    };
    这种层次就比最初的设计更忠于我们所知道的现实。但关于鸟类问题的讨论,现在还不能完全结束。因为在有的软件系统中,说企鹅是鸟是完全合适的。比如说,如果程序只和鸟的嘴、翅膀有关系而不涉及到飞,最初的设计就很合适。这看起来可能很令人恼火,但它反映了一个简单的事实:没有任何一种设计可以理想到适用于任何软件。好的设计是和软件系统现在和将来所要完成的功能密不可分的(参见条款M32)。如果程序不涉及到飞,并且将来也不会,那么让Penguin 派生于Bird 就会是非常合理的设计。实际上,它会比那个区分会飞和不会飞的设计还要好,因为你的设计中不会用到这种区分。在设计层次中增加多余的类是一种很糟糕的设计,就象在类之间制定了错误的继承关系一样。
    对于 "所有鸟都会飞,企鹅是鸟,企鹅不会飞" 这一问题,还可以考虑用另外一种方法来处理。也就是对penguin 重新定义fly 函数,使之产生一个运行时错误:
    void error(const string& msg); // 在别处定义
    class Penguin: public Bird {
    public:
    virtual void fly() { error("Penguins can't fly!"); }
    ...
    };
    解释型语言如Smalltalk 喜欢采用这种方法,但这里要认识到的重要一点是,上面的代码所说的可能和你所想的是完全不同的两回事。它不是说,"企鹅不会飞",而是说,"企鹅会飞,但让它们飞是一种错误"。怎么区分二者的不同?这可以从检测到错误发生的时间来区分。"企鹅不会飞" 的指令是由编译器发出的,"让企鹅飞是一种错误" 只能在运行时检测到。为了表示 "企鹅不会飞" 这一事实,就不要在Penguin 对象中定义fly 函数:
    class Bird {
    ... // 没有声明fly 函数
    };
    class NonFlyingBird: public Bird {
    ... // 没有声明fly 函数
    };
    class Penguin: public NonFlyingBird {
    ... // 没有声明fly 函数
    };
    如果想使企鹅飞,编译器就会谴责你的违规行为:
    Penguin p;
    p.fly(); // 错误!
    用Smalltalk 的方法得到的行为和这完全不同。用那种方法,编译器连半句话都不会说。C++的处理方法和Smalltalk 的处理方法有着根本的不同,所以只要是在用C++编程,就要采用C++的方法做事。另外,在编译时检测错误比在运行时检测错误有某些技术上的优点,详见条款46。
    也许你会说,你在鸟类方面的知识很贫乏。但你可以借助于你的初等几何知识,对不对?我是说,矩形和正方形总该不复杂吧?
    那好,回答这个简单问题:类Square(正方形)可以从类Rectangle(矩
    形)公有继承吗?
    Rectangle
    ^
    | ?
    Square
    "当然可以!" 你会不屑地说,"每个人都知道一个正方形是一个矩形,但反过来通常不成立。" 确实如此,至少在高中时可以这样认为。但我不认为我们还是高中生。
    看看下面的代码:
    class Rectangle {
    public:
    virtual void setHeight(int newHeight);
    virtual void setWidth(int newWidth);
    virtual int height() const; // 返回当前值
    virtual int width() const; // 返回当前值
    ...
    };
    void makeBigger(Rectangle& r) // 增加r 面积的函数
    {
    int oldHeight = r.height();
    r.setWidth(r.width() + 10); // 对r 的宽度增加10
    assert(r.height() == oldHeight); // 断言r 的高度未变
    }
    很明显,断言永远不会失败。makeBigger 只是改变了r 的宽度,高度从没被修改过。现在看下面的代码,它采用了公有继承,使得正方形可以被当作矩形来处理:
    class Square: public Rectangle { ... };
    Square s;
    ...
    assert(s.width() == s.height()); // 这对所有正方形都成立
    makeBigger(s); // 通过继承,s "是一个" 矩形
    // 所以可以增加它的面积
    assert(s.width() == s.height()); // 这还是对所有正方形成立
    很明显,和前面的断言一样,后面的这个断言也永远不会失败。因为根据定义,正方形的宽和高应该相等。
    那么现在有一个问题。我们怎么协调下面的断言呢?调用makeBigger 前,s 的宽和高相等;makeBigger 内部,s 的宽度被改变,高度未变;· 从makeBigger 返回后,s 的高度又和宽度相等。(注意s 是通过引用传给makeBigger 的,所以makeBigger 修改了s 本身,而不是s 的拷贝)
    怎么样?
    欢迎加入公有继承的精彩世界,在这里,你在其它研究领域养成的直觉 ---- 包括数学 ---- 可能不象你所期望的那样为你效劳。对于上面例子中的情况来说,最根本的问题在于:对矩形适用的规则(宽度的改变和高度没关系)不适
    用于正方形(宽度和高度必须相同)。但公有继承声称:对基类对象适用的任何东西 ---- 任何!---- 也适用于派生类对象。在矩形和正方形的例子(以及条款40 中涉及到set 的一个类似的例子)中,所声称的原则不适用,所以用公有继承来表示它们的关系只会是错误。当然,编译器不会阻拦你这样做,但正如我们所看到的,它不能保证程序可以工作正常。正如每个程序员都知道的,代码通过编译并不说明它能正常工作。但也不要太担心你多年积累的软件开发直觉在步入到面向对象设计时会没有用武之地。那些知识还是很有价值,但既然你在自己的设计宝库中又增加了
    继承这一利器,你就要用新的眼光来扩展你的专业直觉,从而指导你开发出正确无误的面向对象程序。很快,你会觉得让Penguin 从Bird 继承或让Square从Rectangle 继承的想法很可笑,就象现在某个人向你展示一个长达数页的函
    数你会觉得可笑一样。也许它是解决问题的正确方法,只是不太合适。
    当然,"是一个" 的关系不是存在于类之间的唯一关系。类之间常见的另两个关系是 "有一个" 和 "用...来实现"。这些关系在条款40 和42 进行讨论。这两个关系中的某一个被不正确地表示成 "是一个" 的情况并不少见,这将导致
    错误的设计。所以,一定要确保自己理解这些关系的区别,以及怎么最好地用C++来表示它们。

    明智地使用私有继承, 私有继承体现的是“用父来实现子”关系,虽然“分层”同样可以表达这种关系,而且大多数情况下建议用“分层”,但是有些情况下可以利用继承的优点来做到一些“分层”做不到的事情
    条款35 说明,C++将公有继承视为 "是一个" 的关系。它是通过这个例子来证实的:假如某个类层次结构中,Student 类从Person 类公有继承,为了使某个函数成功调用,编译器可以在必要时隐式地将Student 转换为Person。这个例子很值得再看一遍,只是现在,公有继承换成了私有继承:
    class Person { ... };
    class Student: // 这一次我们
    private Person { ... }; // 使用私有继承
    void dance(const Person& p); // 每个人会跳舞
    void study(const Student& s); // 只有学生才学习
    Person p; // p 是一个人
    Student s; // s 是一个学生
    dance(p); // 正确, p 是一个人
    dance(s); // 错误!一个学生不是一个人
    很显然,私有继承的含义不是 "是一个",那它的含义是什么呢?"别忙!" 你说。"在弄清含义之前,让我们先看看行为。私有继承有那些行为特征呢?" 那好吧。关于私有继承的第一个规则正如你现在所看到的:和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象(如Student)转换成基类对象(如Person)。这就是上面的代码中为对象s调用dance 会失败的原因。第二个规则是,从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员。行为特征就这些。这为我们引出了私有继承的含义:私有继承意味着 "用...来实现"。如果使类D 私有继承于类B,这样做是因为你想利用类B 中已经存在的某些代码,而不是因为类型B 的对象和类型D 的对象之间有什么概念上的关系。因而,私有继承纯粹是一种实现技术。用条款36 引入的术语来说,私有继承意味着只是继承实现,接口会被忽略。如果D 私有继承于B,就是说D 对象在实现中用到了B 对象,仅此而已。私有继承在软件 "设计" 过程中毫无意义,只是在软件 "实现" 时才有用
    私有继承意味着 "用...来实现" 这一事实会给程序员带来一点混淆,因为条款40 指出,"分层" 也具有相同的含义。怎么在二者之间进行选择呢?答案很简单:尽可能地使用分层,必须时才使用私有继承。什么时候必须呢?这往往是指有保护成员和/或虚函数介入的时候 ---- 但这个问题过一会儿再深入讨论。

    条款41 提供了一种方法来写一个Stack 模板,此模板生成的类保存不同类型的对象。你应该熟悉一下那个条款。模板是C++最有用的组成部分之一,但一旦开始经常性地使用它,你会发现,如果实例化一个模板一百次,你就可
    能实例化了那个模板的代码一百次。例如Stack 模板,构成Stack<int>成员函数的代码和构成Stack<double>成员函数的代码是完全分开的。有时这是不可避免的,但即使模板函数实际上可以共享代码,这种代码重复还是可能存在。这种目标代码体积的增加有一个名字:模板导致的 "代码膨胀"。这不是件好事。对于某些类,可以采用通用指针来避免它。采用这种方法的类存储的是指针,而不是对象,实现起来就是:创建一个类,它存储的是对象的void*指针。创建另外一组类,其唯一目的是用来保证类型安全。这些类都借助第一步中的通用类来完成实际工作。下面的例子使用了条款41 中的非模板Stack 类,不同的是这里存储的是通用指针,而不是对象:
    class GenericStack {
    public:
    GenericStack();
    ~GenericStack();
    void push(void *object);
    void * pop();
    bool empty() const;
    private:
    struct StackNode {
    void *data; // 节点数据
    StackNode *next; // 下一节点
    StackNode(void *newData, StackNode *nextNode)
    : data(newData), next(nextNode) {}
    };
    StackNode *top; // 栈顶
    GenericStack(const GenericStack& rhs); // 防止拷贝和
    GenericStack& // 赋值(参见
    operator=(const GenericStack& rhs); // 条款27)
    };
    因为这个类存储的是指针而不是对象,就有可能出现一个对象被多个堆栈指向的情况(即,被压入到多个堆栈)。所以极其重要的一点是,pop 和类的析构函数销毁任何StackNode 对象时,都不能删除data 指针 ---- 虽然还是得要
    删除StackNode 对象本身。毕竟,StackNode 对象是在GenericStack 类内部分配的,所以还是得在类的内部释放。所以,条款41 中Stack 类的实现几乎完全满足the GenericStack 的要求。仅有的改变只是用void*来替换T。
    仅仅有GenericStack 这一个类是没有什么用处的,但很多人会很容易误用它。例如,对于一个用来保存int 的堆栈,一个用户会错误地将一个指向Cat对象的指针压入到这个堆栈中,但编译却会通过,因为对void*参数来说,指针
    就是指针。为了重新获得你所习惯的类型安全,就要为GenericStack 创建接口类(interface class),象这样:
    class IntStack { // int 接口类
    public:
    void push(int *intPtr) { s.push(intPtr); }
    int * pop() { return static_cast<int*>(s.pop()); }
    bool empty() const { return s.empty(); }
    private:
    GenericStack s; // 实现
    };
    class CatStack { // cat 接口类
    public:
    void push(Cat *catPtr) { s.push(catPtr); }
    Cat * pop() { return static_cast<Cat*>(s.pop()); }
    bool empty() const { return s.empty(); }
    private:
    GenericStack s; // 实现
    };
    正如所看到的,IntStack 和CatStack 只是适用于特定类型。只有int 指针可以被压入或弹出IntStack,只有Cat 指针可以被压入或弹出CatStack。IntStack和CatStack 都通过GenericStack 类来实现,这种关系是通过分层(参见条款40)来体现的,IntStack 和CatStack 将共享GenericStack 中真正实现它们行为的函数代码。另外,IntStack 和CatStack 所有成员函数是(隐式)内联函数,这意味着使用这些接口类所带来的开销几乎是零。但如果有些用户没认识到这一点怎么办?如果他们错误地认为使用GenericStack 更高效,或者,如果他们鲁莽而轻率地认为类型安全不重要,那该怎么办? 怎么才能阻止他们绕过IntStack 和CatStack 而直接使用GenericStack(这会让他们很容易地犯类型错误,而这正是设计C++所要特别避免的)呢?没办法!没办法防止。但,也许应该有什么办法。在本条款的开始我就提到,要表示类之间 "用...来实现" 的关系,有一个选择是通过私有继承。现在这种情况下,这一技术就比分层更有优势,因为通过它可以让你告诉别人:GenericStack 使用起来不安全,它只能用来实现其它的类。具体做法是将GenericStack 的成员函数声明为保护类型:
    class GenericStack {
    protected:
    GenericStack();
    ~GenericStack();
    void push(void *object);
    void * pop();
    bool empty() const;
    private:
    ... // 同上
    };
    GenericStack s; // 错误! 构造函数被保护
    class IntStack: private GenericStack {
    public:
    void push(int *intPtr) { GenericStack::push(intPtr); }
    int * pop() { return static_cast<int*>(GenericStack::pop()); }
    bool empty() const { return GenericStack::empty(); }
    };
    class CatStack: private GenericStack {
    public:
    void push(Cat *catPtr) { GenericStack::push(catPtr); }
    Cat * pop() { return static_cast<Cat*>(GenericStack::pop()); }
    bool empty() const { return GenericStack::empty(); }
    };
    IntStack is; // 正确
    CatStack cs; // 也正确
    和分层的方法一样,基于私有继承的实现避免了代码重复,因为这个类型安全的接口类只包含有对GenericStack 函数的内联调用。在GenericStack 类之上构筑类型安全的接口是个很花俏的技巧,但需要手工去写所有那些接口类是件很烦的事。幸运的是,你不必这样。你可以让模板来自动生成它们。下面是一个模板,它通过私有继承来生成类型安全的堆栈接口
    template<class T>
    class Stack: private GenericStack {
    public:
    void push(T *objectPtr) { GenericStack::push(objectPtr); }
    T * pop() { return static_cast<T*>(GenericStack::pop()); }
    bool empty() const { return GenericStack::empty(); }
    };
    这是一段令人惊叹的代码,虽然你可能一时还没意识到。因为这是一个模板,编译器将根据你的需要自动生成所有的接口类。因为这些类是类型安全的,用户类型错误在编译期间就能发现。因为GenericStack 的成员函数是保护类
    型,并且接口类把GenericStack 作为私有基类来使用,用户将不可能绕过接口类。因为每个接口类成员函数被(隐式)声明为inline,使用这些类型安全的类时不会带来运行开销;生成的代码就象用户直接使用GenericStack 来编写的一样(假设编译器满足了inline 请求 ---- 参见条款33)。因为GenericStack 使用了void*指针,操作堆栈的代码就只需要一份,而不管程序中使用了多少不同类型的堆栈。简而言之,这个设计使代码达到了最高的效率和最高的类型安全。很难做得比这更好。
    本书的基本认识之一是,C++的各种特性是以非凡的方式相互作用的。这个例子,我希望你能同意,确实是非凡的。
    从这个例子中可以发现,如果使用分层,就达不到这样的效果。只有继承才能访问保护成员,只有继承才使得虚函数可以重新被定义。(虚函数的存在会引发私有继承的使用,例子参见条款43)因为存在虚函数和保护成员,有时私
    有继承是表达类之间 "用...来实现" 关系的唯一有效途径
    。所以,当私有继承是你可以使用的最合适的实现方法时,就要大胆地使用它。同时,广泛意义上来说,分层是应该优先采用的技术,所以只要有可能,就要尽量使用它。

    通过分层来体现 "有一个" 或 "用...来实现"
    使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。例如:
    class Address { ... }; // 某人居住之处
    class PhoneNumber { ... };
    class Person {
    public:
    ...
    private:
    string name; // 下层对象
    Address address; // 同上
    PhoneNumber voiceNumber; // 同上
    PhoneNumber faxNumber; // 同上
    };
    本例中,Person 类被认为是置于string,Address 和PhoneNumber 类的上层,因为它包含那些类型的数据成员。"分层" 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入embedding)。条款35 解释了公有继承的含义是 "是一个"。对应地,分层的含义是 "有一个" 或 "用...来实现"。上面的Person 类展示了 "有一个" 的关系。一个Person 对象 "有一个" 名字,地址,电话号码和传真号码。你不能说,一个人 "是一个" 名字或一个人 "是一个" 地址;你得说,一个人 "有一个" 名字, "有一个" 地址,等等。大多数人对区分这些没什么困难,所以混淆 "是一个" 和 "有一个" 的情况相对来说比较少见。
    稍微有点麻烦的是区分 "是一个" 和 "用...来实现"。例如,假设需要一个类模板,用来表示任意对象的集合,并且集合中没有重复元素。程序设计中,重用(Reuse)是再好不过的一件事了,而且你也许已经读过条款49 中关于C++
    标准库的总体介绍,那么,你的第一反应一定是想采用标准库中的set 模板。是啊,既然可以使用别人所写的东西,为什么还要再去写一个新的模板呢?但是,深入研究set 的帮助文档后,你会发现,set 的下述限制将不能满足
    你的程序要求:set 要求包含在它内部的元素必须是完全有序的,即,对set 中的任两个元素a 和b 来说,一定可以确定:要么a<b,要么b<a。对许多类型来说,这个要求很容易满足,而且,对象间完全有序使得set 可以在性能方面提供某些保证,这一点很吸引人。(参见条款49 了解标准库在性能上更多的保证)然而,你所需要的是更广泛的东西:一个类似set 的类,但对象不必完全有序;用C++标准所包装的术语来说,它们只需要所谓的 "相等可比较性":对于同种类型的a 和b 对象来说,要能确定是否a==b。这种要求更简单,它更适合于那些表示颜色这类东西的类型。总不能说红色比绿色更少或绿色比红色更少吧?看来,对你的程序来说,还是得需要自己来写个模板。当然,重用还是件好事。作为数据结构专家,你知道,在实现集合的众多选择中,一个最简单的办法是采用链表。你一定猜到了什么。对,标准库中正有这么一个list 模板(用来产生链表类)!所以可以重用它。具体来说,你决定让自己的Set 模板从list 继承。即,Set<T>将从list<T>继承。因为,在你的实现中,Set 对象实际上将是list 对象。于是你这样声明Set模板:
    // Set 中错误地使用了list
    template<class T>
    class Set: public list<T> { ... };

    至此,一切好象都很正确,但实际上错误不小。正如条款35 所说明的,如果D "是一个" B,对B 成立的所有事实对D 也成立。但是,list 对象可以包含重复元素,所以如果3051 这个值被增加到list<int>中两次,list 中将包含3051
    的两个拷贝。相反,Set 不可以包含重复元素,所以如果3051 被增加到Set<int>中两次,Set 中将只包含这个值的一个拷贝。于是,说一个Set "是一个" list 就是弥天大谎,因为如上所述,有一些在list 对象中成立的事实在Set 对象中不成立。
    因为这两个类的关系并非 "是一个",所以用公有继承来表示它们的关系就是一个错误。正确的方法是让Set 对象 "用list 对象来实现":
    // Set 中使用list 的正确方法
    template<class T>
    class Set {
    public:
    bool member(const T& item) const;
    void insert(const T& item);
    void remove(const T& item);
    int cardinality() const;
    private:
    list<T> rep; // 表示一个Set
    };
    Set 的成员函数可以利用list 以及标准库其它部分所提供的大量功能,所以,实现代码既不难写也很易读:
    template<class T>
    bool Set<T>::member(const T& item) const
    { return find(rep.begin(), rep.end(), item) != rep.end(); }
    template<class T>
    void Set<T>::insert(const T& item)
    { if (!member(item)) rep.push_back(item); }
    template<class T>
    void Set<T>::remove(const T& item)
    {
    list<T>::iterator it =
    find(rep.begin(), rep.end(), item);
    if (it != rep.end()) rep.erase(it);
    }
    template<class T>
    int Set<T>::cardinality() const
    { return rep.size(); }
    这些函数很简单,所以很自然地想到将它们作为内联函数;但在做最后决定前,还是回顾一下条款33 所做的讨论。(上面的代码中,find, begin, end,push_back 等函数是标准库基本框架的一部分,它们可用来对list 这样的容器
    模板进行操作。标准库框架的总体介绍参见条款49 和M35。)值得指出的是,Set 类的接口没有做到完整并且最小(参见条款18)。从完整性上来说,它最大的遗漏在于不能对Set 中的内容进行循环,而这一功能对很多程序来说是必需的(标准库中的所有成员都提供了这一功能,包括set)。Set 的另一个缺陷是没有遵循标准库所采用的容器类常规(见条款49 和M35),从而造成使用Set 时更难以利用库中其它的部分。Set 的接口尽管有这些瑕疵,但下面这一点不能被掩盖:Set 在理解它和list的关系上,具有无可辩驳的正确性。这种关系并非 "是一个"(虽然初看会以为是),而是 "用...来实现",通过分层来实现这种关系是类的设计者应该感到自豪的。
    顺便说一句,当通过分层使两个类产生联系时,实际上在两个类之间建立了编译时的依赖关系。关于为什么要考虑到这一点以及如何减少这方面的麻烦,参见条款34。

  • C++在幕后为你所写、所调用的函数  Effective C++ 45 和 27

    一个空类什么时候不是空类? ---- 当C++编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,赋值运算符,一个析构函数,一对取址运算符另外,如果你
    没有声明任何构造函数,它也将为你声明一个缺省构造函数
    。所有这些函数都是公有的。换句话说,如果你这么写:
    class Empty{};和你这么写是一样的:
    class Empty {
    public:
    Empty(); // 缺省构造函数
    Empty(const Empty& rhs); // 拷贝构造函数
    ~Empty(); // 析构函数 ---- 是否为虚函数看下文说明
    Empty& operator=(const Empty& rhs); // 赋值运算符
    Empty* operator&(); // 取址运算符
    const Empty* operator&() const;
    };
    现在,如果需要,这些函数就会被生成,但你会很容易就需要它们。下面的代码将使得每个函数被生成:
    const Empty e1; // 缺省构造函数
    // 析构函数
    Empty e2(e1); // 拷贝构造函数
    e2 = e1; // 赋值运算符
    Empty *pe2 = &e2; // 取址运算符
    // (非const)
    const Empty *pe1 = &e1; // 取址运算符
    // (const)
    假设编译器为你写了函数,这些函数又做些什么呢?是这样的,缺省构造函数和析构函数实际上什么也不做,它们只是让你能够创建和销毁类的对象(对编译器来说,将一些 "幕后" 行为的代码放在此处也很方便 ---- 参见条款33
    和M24。)。注意,生成的析构函数一般是非虚拟的(参见条款14),除非它所在的类是从一个声明了虚析构函数的基类继承而来。缺省取址运算符只是返回对象的地址。这些函数实际上就如同下面所定义的那样:
    inline Empty::Empty() {}
    inline Empty::~Empty() {}
    inline Empty * Empty::operator&() { return this; }
    inline const Empty * Empty::operator&() const
    { return this; }
    至于拷贝构造函数和赋值运算符,官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行 "以成员为单位的" 逐一拷贝构造(赋值)。即,如果m 是类C 中类型为T 的非静态数据成员,并且C 没有声明拷贝
    构造函数(赋值运算符),m 将会通过类型T 的拷贝构造函数(赋值运算符)被拷贝构造(赋值)---- 如果T 有拷贝构造函数(赋值运算符)的话。如果没有,规则递归应用到m 的数据成员,直至找到一个拷贝构造函数(赋值运算符)或固定类型(例如,int,double,指针,等)为止。默认情况下,固定类型的对象拷贝构造(赋值)时是从源对象到目标对象的 "逐位" 拷贝。对于从别的类继承而来的类来说,这条规则适用于继承层次结构中的每一层,所以,用户自定义的构造函数和赋值运算符无论在哪一层被声明,都会被调用。
    我希望这已经说得很清楚了。但怕万一没说清楚,还是给个例子。看这样一个NamedObject 模板的定
    义,它的实例是可以将名字和对象联系起来的类:
    template<class T>
    class NamedObject {
    public:
    NamedObject(const char *name, const T& value);
    NamedObject(const string& name, const T& value);
    ...
    private:
    string nameValue;
    T objectValue;
    };
    因为NamedObject 类声明了至少一个构造函数,编译器将不会生成缺省构造函数;但因为没有声明拷贝构造函数和赋值运算符,编译器将生成这些函数(如果需要的话)。
    看下面对拷贝构造函数的调用:
    NamedObject<int> no1("Smallest Prime Number", 2);
    NamedObject<int> no2(no1); // 调用拷贝构造函数
    编译器生成的拷贝构造函数必须分别用no1.nameValue 和no1.objectValue来初始化no2.nameValue 和no2.objectValue。nameValue 的类型是string,string有一个拷贝构造函数(你可以在标准库中查看string 来证实 ---- 参见条款49),所以no2.nameValue 初始化时将调用string 的拷贝构造函数,参数为no1.nameValue。另一方面,NamedObject<int>::objectValue 的类型是int(因为这个模板实例中,T 是int),int 没有定义拷贝构造函数,所以no2.objectValue是通过从no1.objectValue 拷贝每一个比特(bit)而被初始化的。编译器为NamedObject<int>生成的赋值运算符也以同样的方式工作,但通常,编译器生成的赋值运算符要想如上面所描述的那样工作,与此相关的所有代码必须合法且行为上要合理。如果这两个条件中有一个不成立,编译器将拒绝为你的类生成operator=,你就会在编译时收到一些诊断信息。例如,假设NamedObject 象这样定义,nameValue 是一个string 的引用,objectValue 是一个const T:
    template<class T>
    class NamedObject {
    public:
    // 这个构造函数不再有一个const 名字参数,因为nameValue
    // 现在是一个非const string 的引用。char*构造函数
    // 也不见了,因为引用要指向的是string
    NamedObject(string& name, const T& value);
    ... // 同上,假设没有
    // 声明operator=
    private:
    string& nameValue; // 现在是一个引用
    const T objectValue; // 现在为const
    };
    现在看看下面将会发生什么:
    string newDog("Persephone");
    string oldDog("Satch");
    NamedObject<int> p(newDog, 2); // 正在我写本书时,我们的 爱犬Persephone 即将过她的第二个生日
    NamedObject<int> s(oldDog, 29); // 家犬Satch 如果还活着,会有29 岁了(从我童年时算起)
    p = s; // p 中的数据成员将会发生些什么呢?
    赋值之前,p.nameValue 指向某个string 对象,s.nameValue 也指向一个string,但并非同一个。赋值会给p.nameValue 带来怎样的影响呢?赋值之后,p.nameValue 应该指向 "被s.nameValue 所指向的string" 吗,即,引用本身应该被修改吗?如果是这样,那太阳从西边出来了,因为C++没有办法让一个引用指向另一个不同的对象(参见条款M1)。或者,p.nameValue 所指的string对象应该被修改吗? 这样的话,含有 "指向那个string 的指针或引用" 的其它对象也会受影响,也就是说,和赋值没有直接关系的其它对象也会受影响。这是编译器生成的赋值运算符应该做的吗?

    面对这样的难题,C++拒绝编译这段代码。如果想让一个包含引用成员的类支持赋值,你就得自己定义赋值运算符。对于包含const 成员的类(例如上面被修改的类中的objectValue)来说,编译器的处理也相似;因为修改const
    成员是不合法的,所以编译器在隐式生成赋值函数时也会不知道怎么办。还有,如果派生类的基类将标准赋值运算符声明为private, 编译器也将拒绝为这个派生类生成赋值运算符。因为,编译器为派生类生成的赋值运算符也应该处理基类部分(见条款16 和M33),但这样做的话,就得调用对派生类来说无权访问的基类成员函数,这当然是不可能的。
    以上关于编译器生成函数的讨论引发了这样的问题:如果想禁止使用这些函数,那该怎么办呢?也就是说,假如你永远不想让类的对象进行赋值,所以有意不声明operator=,那该怎么做呢?这个小难题的解决方案正是条款27 讨
    论的主题。指针成员和编译器生成的拷贝构造函数及赋值运算符之间的相互影响经常被人忽视,关于这个话题的讨论请查看条款11。

    如果不想使用隐式生成的函数就要显式地禁止它
    假设想写一个类模板Array,它所生成的类除了可以进行上下限检查外,其它行为和C++标准数组一样。设计中面临的一个问题是怎么禁止掉Array 对象之间的赋值操作,因为对标准C++数组来说赋值是不合法的:
    double values1[10];
    double values2[10];
    values1 = values2; // 错误!
    对很多函数来说,这不是个问题。如果你不想使用某个函数,只用简单地不把它放进类中。然而,赋值运算符属于那种与众不同的成员函数,当你没有去写这个函数时,C++会帮你写一个(见条款45)。那么,该怎么办呢?方法是声明这个函数(operator=),并使之为private。显式地声明一个成员函数,就防止了编译器去自动生成它的版本;使函数为private,就防止了别人去调用它。
    但是,这个方法还不是很安全,成员函数和友元函数还是可以调用私有函数,除非——如果你够聪明的话——不去定义(实现)这个函数。这样,当无意间调用了这个函数时,程序在链接时就会报错
    对于Array 来说,模板的定义可以象这样开始:
    template<class T>
    class Array {
    private:
    // 不要定义这个函数!
    Array& operator=(const Array& rhs);
    ...
    };
    现在,当用户试图对Array 对象执行赋值操作时,编译器会不答应;当你自己无意间在成员或友元函数中调用它时,链接器会嗷嗷大叫。不要因为这个例子就认为本条款只适用于赋值运算符。不是这样的。它适用于条款45 所介绍的每一个编译器自动生成的函数。实际应用中,你会发现赋值和拷贝构造函数具有行为上的相似性(见条款11 和16),这意味着几乎任何时候当你想禁止它们其中的一个时,就也要禁止另外一个。

  •  Effective c++ 22条 -- 尽量用“传引用”而不用“传值”

    C 语言中,什么都是通过传值来实现的,C++继承了这一传统并将它作为默认方式。除非明确指定,函数的形参总是通过“实参的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝。
    正如我在本书的导言中所指出的,“通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作。
    例如,看下面这个(只是假想的)类的结构:
    class Person {
    public:
    Person(); // 为简化,省略参数
    //
    ~Person();
    ...
    private:
    string name, address;
    };
    class Student: public Person {
    public:
    Student(); // 为简化,省略参数
    //
    ~Student();
    ...
    private:
    string schoolName, schoolAddress;
    };
    现在定义一个简单的函数returnStudent,它取一个Student 参数(通过值)然后立即返回它(也通过值)。定义完后,调用这个函数:
    Student returnStudent(Student s) { return s; }
    Student plato; // Plato(柏拉图)在
    // Socrates(苏格拉底)门下学习
    returnStudent(plato); // 调用returnStudent
    这个看起来无关痛痒的函数调用过程,其内部究竟发生了些什么呢?简单地说就是:首先,调用了Student 的拷贝构造函数用以将s 初始化为plato;然后再次调用Student 的拷贝构造函数用以将函数返回值对象初始化为s;接着,s 的析构函数被调用;最后,returnStudent 返回值对象的析构函数被调用。所以,这个什么也没做的函数的成本是两个Student 的拷贝构造函数加上两个Student 析构函数。但没完,还有!Student 对象中有两个string 对象,所以每次构造一个Student对象时必须也要构造两个string 对象。Student 对象还是从Person 对象继承而来的,所以每次构造一个Student 对象时也必须构造一个Person 对象。一个Person 对象内部有另外两个string 对象,所以每个Person 的构造也必然伴随另两个string 的构造。所以,通过值来传递一个Student 对象最终导致调用了一个Student 拷贝构造函数,一个Person 拷贝构造函数,四个string 拷贝构造函数。当Student 对象被摧毁时,每个构造函数对应一个析构函数的调用。所以,通过值来传递一个Student 对象的最终开销是六个构造函数和六个析构函
    数。因为returnStudent 函数使用了两次传值(一次对参数,一次对返回值),这个函数总共调用了十二个构造函数和十二个析构函数!
    在C++编译器的设计者眼里,这是最糟糕的情况。编译器可以用来消除一些对拷贝构造函数的调用(C++标准——见条款50——描述了具体在哪些条件下编译器可以执行这类的优化工作,条款M20 给出了例子)。一些编译器也这
    样做了。但在不是所有编译器都普遍这么做的情况下,一定要对通过值来传递对象所造成的开销有所警惕。
    为避免这种潜在的昂贵的开销,就不要通过值来传递对象,而要通过引用:
    const Student& returnStudent(const Student& s)
    { return s; }
    这会非常高效:没有构造函数或析构函数被调用,因为没有新的对象被创建。通过引用来传递参数还有另外一个优点:它避免了所谓的“切割问题(slicing problem)”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。这往往不是你所想要的。例如,假设设计这么一套实现图形窗口系统的类:
    class Window {
    public:
    string name() const; // 返回窗口名
    virtual void display() const; // 绘制窗口内容
    };
    class WindowWithScrollBars: public Window {
    public:
    virtual void display() const;
    };
    每个Window 对象都有一个名字,可以通过name 函数得到;每个窗口都可以被显示,着可以通过调用display 函数实现。display 声明为virtual 意味着一个简单的Window 基类对象被显示的方式往往和价格昂贵的WindowWithScrollBars 对象被显示的方式不同(见条款36,37,M33)。
    现在假设写一个函数来打印窗口的名字然后显示这个窗口。下面是一个用错误的方法写出来的函数:
    // 一个受“切割问题”困扰的函数
    void printNameAndDisplay(Window w)
    {
    cout << w.name();
    w.display();
    }
    想象当用一个WindowWithScrollBars 对象来调用这个函数时将发生什么:
    WindowWithScrollBars wwsb;
    printNameAndDisplay(wwsb);
    参数w 将会作为一个Windows 对象而被创建(它是通过值来传递的,记得吗?),所有wwsb 所具有的作为WindowWithScrollBars 对象的行为特性都被“切割”掉了。printNameAndDisplay 内部,w 的行为就象是一个类Window的对象(因为它本身就是一个Window 的对象),而不管当初传到函数的对象类型是什么。尤其是, printNameAndDisplay 内部对display 的调用总是Window::display,而不是WindowWithScrollBars::display。
    解决切割问题的方法是通过引用来传递w:
    // 一个不受“切割问题”困扰的函数
    void printNameAndDisplay(const Window& w)
    {
    cout << w.name();
    w.display();
    }
    现在w 的行为就和传到函数的真实类型一致了。为了强调w 虽然通过引用传递但在函数内部不能修改,就要采纳条款21 的建议将它声明为const。传递引用是个很好的做法,但它会导致自身的复杂性,最大的一个问题就是别名问题,这在条款17 进行了讨论。另外,更重要的是,有时不能用引用来传递对象,参见条款23。最后要说的是,引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——
    例如int— — 传值实际上会比传引用更高效。

    Effective c++ 23条 -- 不能传引用时也只能传值 

    据说爱因斯坦曾提出过这样的建议:尽可能地让事情简单,但不要过于简单。在C++语言中相似的说法应该是:尽可能地使程序高效,但不要过于高效。一旦程序员抓住了“传值”在效率上的把柄(参见条款22),他们会变得
    十分极端,恨不得挖出每一个隐藏在程序中的传值操作。岂不知,在他们不懈地追求纯粹的“传引用”的过程中,他们会不可避免地犯另一个严重的错误:传递一个并不存在的对象的引用。这就不是好事了。
    看一个表示有理数的类,其中包含一个友元函数,用于两个有理数相乘:
    class Rational {
    public:
    Rational(int numerator = 0, int denominator = 1);
    ...
    private:
    int n, d; // 分子和分母
    friend
    const Rational // 参见条款21:为什么
    operator*(const Rational& lhs, // 返回值是const
    const Rational& rhs)
    };
    inline const Rational operator*(const Rational& lhs,
    const Rational& rhs)
    {
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    }
    很明显,这个版本的operator*是通过传值返回对象结果,如果不去考虑对象构造和析构时的开销,你就是在逃避作为一个程序员的责任。另外一件很明显的事实是,除非确实有必要,否则谁都不愿意承担这样一个临时对象的开销。
    那么,问题就归结于:确实有必要吗?答案是,如果能返回一个引用,当然就没有必要。但请记住,引用只是一
    个名字,一个其它某个已经存在的对象的名字。无论何时看到一个引用的声明,就要立即问自己:它的另一个名字是什么呢?因为它必然还有另外一个什么名字(见条款M1)。拿operator*来说,如果函数要返回一个引用,那它返回的必须是其它某个已经存在的Rational 对象的引用,这个对象包含了两个对象相乘的结果。但,期望在调用operator*之前有这样一个对象存在是没道理的。也就是说,
    如果有下面的代码:
    Rational a(1, 2); // a = 1/2
    Rational b(3, 5); // b = 3/5
    Rational c = a * b; // c 为 3/10
    期望已经存在一个值为3/10 的有理数是不现实的。如果operator* 一定要返回这样一个数的引用,就必须自己创建这个数的对象。一个函数只能有两种方法创建一个新对象:在堆栈里或在堆上。在堆栈里创建对象时伴随着一个局部变量的定义,采用这种方法,就要这样写operator*:
    // 写此函数的第一个错误方法  返回了局部对象的引用.
    inline const Rational& operator*(const Rational& lhs,
    const Rational& rhs)
    {
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
    }
    这个方法应该被否决,因为我们的目标是避免构造函数被调用,但result必须要象其它对象一样被构造。另外,这个函数还有另外一个更严重的问题,它返回的是一个局部对象的引用,关于这个错误,条款31 进行了深入的讨论。
    那么,在堆上创建一个对象然后返回它的引用呢?基于堆的对象是通过使用new 产生的,所以应该这样写operator*:
    // 写此函数的第二个错误方法  容易造成内存泄露
    inline const Rational& operator*(const Rational& lhs,
    const Rational& rhs)
    {
    Rational *result =
    new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
    }
    首先,你还是得负担构造函数调用的开销,因为new 分配的内存是通过调用一个适当的构造函数来初始化的(见条款5 和M8)。另外,还有一个问题:谁将负责用delete 来删除掉new 生成的对象呢?实际上,这绝对是一个内存泄漏。也许,你的眼前出现了这样一段代码:operator*返回一个“在函数内部定义的静态Rational 对象”的引用:
    // 写此函数的第三个错误方法   逻辑错误
    inline const Rational& operator*(const Rational& lhs,
    const Rational& rhs)
    {
    static Rational result; // 将要作为引用返回的
    // 静态对象
    lhs 和rhs 相乘,结果放进result;
    return result;
    }
    这个方法看起来好象有戏,虽然在实际实现上面的伪代码时你会发现,不调用一个Rational 构造函数是不可能给出result 的正确值的,而避免这样的调用正是我们要谈论的主题。就算你实现了上面的伪代码,但,你再聪明也不能
    最终挽救这个不幸的设计。想知道为什么,看看下面这段写得很合理的用户代码:
    bool operator==(const Rational& lhs, // Rationals 的operator==
    const Rational& rhs); //
    Rational a, b, c, d;
    ...
    if ((a * b) == (c * d)) {
    处理相等的情况;
    } else {
    处理不相等的情况;
    }
    看出来了吗?((a*b) == (c*d)) 会永远为true,不管a,b,c 和d 是什么值!用等价的函数形式重写上面的相等判断语句就很容易明白发生这一可恶行为的原因了:if (operator==(operator*(a, b), operator*(c, d)))
    注意当operator==被调用时,总有两个operator*刚被调用,每个调用返回operator*内部的静态Rational 对象的引用。于是,上面的语句实际上是请求operator==对“operator*内部的静态Rational 对象的值”和“operator*内部的静态Rational 对象的值”进行比较,这样的比较不相等才怪呢!
    所以,写一个必须返回一个新对象的函数的正确方法就是让这个函数返回一个新对象。对于Rational 的operator*来说,这意味着要不就是下面的代码(就是最初看到的那段代码),要不就是本质上和它等价的代码:
    inline const Rational operator*(const Rational& lhs,
    const Rational& rhs)
    {
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    }
    的确,这会导致“operator*的返回值构造和析构时带来的开销”,但归根结底它只是用小的代价换来正确的程序运行行为而已。况且,你所担心的开销还有可能永远不会出现:和所有程序设计语言一样,C++允许编译器的设计者
    采用一些优化措施来提高所生成的代码的性能,所以,在有些场合,operator*的返回值会被安全地除去(见条款M20)。当编译器采用了这种优化时(当前大部分编译器这么做),程序和以前一样继续工作,只不过是运行速度比你预计的要快而已。
    以上讨论可以归结为:当需要在返回引用和返回对象间做决定时,你的职责是选择可以完成正确功能的那个。至于怎么让这个选择所产生的代价尽可能的小,那是编译器的生产商去想的事。

    Effective c++ 31条 -- 千万不要返回局部对象的引用,也不要返回函数内部用new 初始化的
    指针的引用

    本条款听起来很复杂,其实不然。它只是一个很简单的道理,真的,相信我。
    先看第一种情况:返回一个局部对象的引用。它的问题在于,局部对象 ----- 顾名思义 ---- 仅仅是局部的。也就是说,局部对象是在被定义时创建,在离开生命空间时被销毁的。所谓生命空间,是指它们所在的函数体。当函数返回
    时,程序的控制离开了这个空间,所以函数内部所有的局部对象被自动销毁。
    因此,如果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前被销毁了。当想提高程序的效率而使函数的结果通过引用而不是值返回时,这个问题就会出现。下面的例子和条款23 中的一样,其目的在于详细说明什么时候该返回引用,什么时候不该:
    class Rational { // 一个有理数类
    public:
    Rational(int numerator = 0, int denominator = 1);
    ~Rational();
    ...
    private:
    int n, d; // 分子和分母
    // 注意operator* (不正确地)返回了一个引用
    friend const Rational& operator*(const Rational& lhs,
    const Rational& rhs);
    };
    // operator*不正确的实现
    inline const Rational& operator*(const Rational& lhs,
    const Rational& rhs)
    {
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
    }
    这里,局部对象result 在刚进入operator*函数体时就被创建。但是,所有的局部对象在离开它们所在的空间时都要被自动销毁。具体到这个例子来说,result 是在执行return 语句后离开它所在的空间的。所以,如果这样写:
    Rational two = 2;
    Rational four = two * two; // 同operator*(two, two)
    函数调用时将发生如下事件:
    1. 局部对象result 被创建。
    2. 初始化一个引用,使之成为result 的另一个名字;这个引用先放在另一边,留做operator*的返回值。
    3. 局部对象result 被销毁,它在堆栈所占的空间可被本程序其它部分或其他程序使用。
    4. 用步骤2 中的引用初始化对象four。
    一切都很正常,直到第4 步才产生了错误,借用高科技界的话来说,产生了"一个巨大的错误"。因为,第2 步被初始化的引用在第3 步结束时指向的不再是一个有效的对象,所以对象four 的初始化结果完全是不可确定的。教训很明显:别返回一个局部对象的引用。
    "那好,"你可能会说,"问题不就在于要使用的对象离开它所在的空间太早吗?我能解决。不要使用局部对象,可以用new 来解决这个问题。"象下面这样:

    // operator*的另一个不正确的实现
    inline const Rational& operator*(const Rational& lhs,const Rational& rhs)
    {
    // create a new object on the heap
    Rational *result =new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    // return it
    return *result;
    }
    这个方法的确避免了上面例子中的问题,但却引发了新的难题。大家都知道,为了在程序中避免内存泄漏,就必须确保对每个用new 产生的指针调用delete,但是,这里的问题是,对于这个函数中使用的new,谁来进行对应的
    delete 调用呢?所以要记住你的教训:写一个返回废弃指针的函数无异于坐等内存泄漏的来临。
    另外,假如你认为自己想出了什么办法可以避免"返回局部对象的引用"所带来的不确定行为,以及"返回堆(heap)上分配的对象的引用"所带来的内存泄漏,那么,请转到条款23,看看为什么返回局部静态(static)对象的引用也会工
    作不正常。看了之后,也许会帮助你避免头痛医脚所带来的麻烦。

    高质量编程指南 林跃 -- 7.4 指针参数是如何传递内存的?

    如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例7-4-1 中,Test 函数的语句GetMemory(str, 200)并没有使str 获得期望的内存,str 依旧是NULL,为什么?
    void GetMemory(char *p, int num)
    {
    p = (char *)malloc(sizeof(char) * num);
    }
    void Test(void)
    {
    char *str = NULL;
    GetMemory(str, 100); // str 仍然为 NULL
    strcpy(str, "hello"); // 运行错误
    }
    毛病出在函数GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针参数p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致参数p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请了新的内存,只是把_p 所指的内存地址改变了,但是p 丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory 就会泄露一块内存,因为没有用free 释放内存。如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例7-4-2。
    void GetMemory2(char **p, int num)
    {
    *p = (char *)malloc(sizeof(char) * num);
    }
    void Test2(void)
    {
    char *str = NULL;
    GetMemory2(&str, 100); // 注意参数是 &str,而不是str
    strcpy(str, "hello");
    cout<< str << endl;
    free(str);
    }

    这里强调不要用return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例7-4-4。
    char *GetString(void)
    {
    char p[] = "hello world";
    return p; // 编译器将提出警告
    }
    void Test4(void)
    {
    char *str = NULL;
    str = GetString(); // str 的内容是垃圾
    cout<< str << endl;
    }

    另外注意两个参数设计的原则

    如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。
    例如:void StringCopy(char *strDestination,const char *strSource);
    如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。

    引用与指针的比较
    引用是C++中的概念,初学者容易把引用和指针混淆一起。以下程序中,n 是m 的一个引用(reference),m 是被引用物(referent)。
    int m;
    int &n = m;
    n 相当于m 的别名(绰号),对n 的任何操作就是对m 的操作。例如有人名叫王小毛,他的绰号是“三毛”。说“三毛”怎么怎么的,其实就是对王小毛说三道四。所以n 既不是m 的拷贝,也不是指向m 的指针,其实n 就是m 它自己。
    引用的一些规则如下:
    (1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
    (2)不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
    (3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象),所以不能把类成员声明成引用类型。
    以下示例程序中,k 被初始化为i 的引用。语句k = j 并不能将k 修改成为j 的引用,只是把k 的值改变成为6。由于k 是i 的引用,所以i 的值也变成了6。
    int i = 5;
    int j = 6;
    int &k = i;
    k = j; // k 和i 的值都变成了6;
    上面的程序看起来象在玩文字游戏,没有体现出引用的价值。引用的主要功能是传递函数的参数和返回值。C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
    以下是“值传递”的示例程序。由于Func1 函数体内的x 是外部变量n 的一份拷贝,改变x 的值不会影响n, 所以n 的值仍然是0。
    void Func1(int x)
    {
    x = x + 10;
    }

    int n = 0;
    Func1(n);
    cout << “n = ” << n << endl; // n = 0
    以下是“指针传递”的示例程序。由于Func2 函数体内的x 是指向外部变量n 的指针,改变该指针的内容将导致n 的值改变,所以n 的值成为10。
    void Func2(int *x)
    {
    (* x) = (* x) + 10;
    }

    int n = 0;
    Func2(&n);
    cout << “n = ” << n << endl; // n = 10
    以下是“引用传递”的示例程序。由于Func3 函数体内的x 是外部变量n 的引用,x 和n 是同一个东西,改变x 等于改变n,所以n 的值成为10。
    void Func3(int &x)
    {
    x = x + 10;
    }

    int n = 0;
    Func3(n);
    cout << “n = ” << n << endl; // n = 10
    对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?答案是“用适当的工具做恰如其分的工作”。
    指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。

    避免返回内部数据的句柄

    假设B 是一个const String 对象:
    class String {
    public:
    String(const char *value); // 具体实现参见条款11
    ~String(); // 构造函数的注解参见条款M5
    operator char *() const; // 转换String -> char*;// 参见条款M5
    ...
    private:
    char *data;
    };
    const String B("Hello World"); // B 是一个const 对象
    既然B 为const,最好的情况当然就是无论现在还是以后,B 的值总是"Hello World"。这就寄希望于别的程序员能以合理的方式使用B 了。特别是,千万别有什么人象下面这样残忍地将B 强制转换掉const(参见条款21):
    String& alsoB =const_cast<String&>(B);  // 使得alsoB 成为B 的另一个名字,但不具有const 属性
    然而,即使没有人做这种残忍的事,就能保证B 永远不会改变吗?看看下面的情形:
    char *str = B; // 调用B.operator char*()
    strcpy(str, "Hi Mom"); // 修改str 指向的值
    B 的值现在还是"Hello World"吗?或者,它是否已经变成了对母亲的问候语?答案完全取决于String::operator char*的实现。下面是一个有欠考虑的实现,它导致了错误的结果。但是,它工作起来确实很高效,所以很多程序员才掉进它的错误陷阱之中:
    // 一个执行很快但不正确的实现
    inline String::operator char*() const
    { return data; }
    这个函数的缺陷在于它返回了一个"句柄"(在本例中,是个指针),而这个句柄所指向的信息本来是应该隐藏在被调用函数所在的String 对象的内部。这样,这个句柄就给了调用者自由访问data 所指的私有数据的机会。换句话说,
    有了下面的语句:char *str = B;
    情况就会变成这样:
    str------------------------->"Hello World\0"
    /
    /
    B.data
    显然,任何对str 所指向的内存的修改都使得B 的有效值发生变化。所以,即使B 声明为const,而且即使只是调用了B 的某个const 成员函数,B 也会在程序运行过程中得到不同的值。特别是,如果str 修改了它所指的值,B 也会
    改变。String::operator char*本身写的没有一点错,麻烦的是它可以用于const 对象。如果这个函数不声明为const,就不会有问题,因为这样它就不能用于象B这样的const 对象了。
    但是,将一个String 对象转换成它相应的char*形式是很合理的一件事,无论这个对象是否为const。所以,还是应该使函数保持为const。这样的话,就得重写这个函数,使得它不返回指向对象内部数据的句柄:
    // 一个执行慢但很安全的实现
    inline String::operator char*() const
    {
    char *copy = new char[strlen(data) + 1];
    strcpy(copy, data);
    return copy;
    }
    这个实现很安全,因为它返回的指针所指向的数据只是String 对象所指向数据的拷贝;通过函数返回的指针无法修改String 对象的值。当然,安全是要有代价的:这个版本的String::operator char* 运行起来比前面那个简单版本要
    慢;此外,函数的调用者还要记得delete 掉返回的指针。如果不能忍受这个版本的速度,或者担心内存泄露,可以来一点小小的改动:使函数返回一个指向const char 的指针:
    class String {
    public:
    operator const char *() const;
    ...
    };
    inline String::operator const char*() const
    { return data; }
    这个函数既快又安全。虽然它和最初给出的那个函数不一样,但它可以满足大多数程序的需要。这个做法还和C++标准组织处理string/char*难题的方案一致:标准string 类型中包含一个成员函数c_str,它的返回值是string 的const char*版本。关于标准string 类型的更多信息参见条款49。
    指针并不是返回内部数据句柄的唯一途径。引用也很容易被滥用。下面是一种常见的用法,还是拿String 类做例子:
    class String {
    public:
    ...
    char& operator[](int index) const
    { return data[index]; }
    private:
    char *data;
    };
    String s = "I'm not constant";
    s[0] = 'x'; // 正确, s 不是const
    const String cs = "I'm constant";
    cs[0] = 'x'; // 修改了const string,
    // 但编译器不会通知
    注意String::operator[]是通过引用返回结果的。这意味着函数的调用者得到的是内部数据data[index]的另一个名字,而这个名字可以用来修改const 对象的内部数据。这个问题和前面看到的相同,只不过这次的罪魁祸首是引用,
    而不是指针。这类问题的通用解决方案和前面关于指针的讨论一样:或者使函数为非const,或者重写函数,使之不返回句柄。如果想让String::operator[]既适用于const 对象又适用于非const 对象,可以参见条款21。
    并不是只有const 成员函数需要担心返回句柄的问题,即使是非const 成员函数也得承认:句柄的合法性失效的时间和它所对应的对象是完全相同的。这个时间可能比用户期望的要早很多,特别是当涉及的对象是由编译器产生的
    临时对象时。
    例如,看看这个函数,它返回了一个String 对象:
    String someFamousAuthor() // 随机选择一个作家名
    { // 并返回之
    switch (rand() % 3) { // rand()在<stdlib.h>中 (还有<cstdlib>。参见条款49)
    case 0:
    return "Margaret Mitchell"; // 此作家曾写了 "飘",
    // 一部绝对经典的作品
    case 1:
    return "Stephen King"; // 他的小说使得许多人
    // 彻夜不眠
    case 2:
    return "Scott Meyers"; // 嗯...滥竽充数的一个
    }
    return ""; // 程序不会执行到这儿,但对于一个有返回值的函数来说,任何执行途径上都要有返回值
    }
    someFamousAuthor 的返回值是一个String 对象,一个临时String 对象(参见条款M19)。这样的对象是暂时性的,它们的生命周期通常在函数调用表达式结束时终止。例如上面的情况中,包含someFamousAuthor 函数调用的表达式结束时,返回值对象的生命周期也就随之结束。
    具体看看下面这个使用someFamousAuthor 的例子,假设String 声明了一个上面的operator const char*成员函数:
    const char *pc = someFamousAuthor();
    cout << pc;
    不论你是否相信,谁也不能预测这段代码将会做些什么,至少不能确定它会做些什么。因为当你想打印pc 所指的字符串时,字符串的值是不确定的。造成这一结果的原因在于pc 初始化时发生了下面这些事件:
    1. 产生一个临时String 对象用以保存someFamousAuthor 的返回值。
    2. 通过String 的operator const char*成员函数将临时String 对象转换为const char*指针,并用这个指针初始化pc。
    3. 临时String 对象被销毁,其析构函数被调用。

    析构函数中,data 指针被删除(代码详见条款11)。然而,data 和pc 所指的是同一块内存,所以现在pc 指向的是被删除的内存--------其内容是不可确定的。因为pc 是被一个指向临时对象的句柄初始化的,而临时对象在被创建后又立即被销毁,所以在pc 被使用前句柄已经是非法的了。也就是说,无论想做什么,当要使用pc 时,pc 其实已经名存实亡。这就是指向临时对象的句柄所带来的危害。
    所以,对于const 成员函数来说,返回句柄是不明智的,因为它会破坏数据抽象。对于非const 成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮(dangle)的。所以一定要象避免
    悬浮的指针那样,尽量避免悬浮的句柄。
    同样不能对本条款绝对化。在一个大的程序中想消灭所有可能的悬浮指针是不现实的,想消灭所有可能的悬浮句柄也是不现实的。但是,只要不是万不得已,就要避免返回句柄,这样,不但程序会受益,用户也会更信赖你。

  • 2008-12-12

    c++中的虚函数 - [C/C++]

    1.简介 
    虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
    class A
    {
    public:
        virtual void foo() { cout << "A::foo() is called" << endl;}
    };

    class B: public A
    {
    public:
        virtual void foo() { cout << "B::foo() is called" << endl;}
    };

    那么,在使用的时候,我们可以:

    A * a = new B();
    a->foo();       // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!

        这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

    虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:

    class A
    {
    public:
        virtual void foo();
    };

    class B: public A
    {
        virtual void foo();
    };

    void bar()
    {
        A a;
        a.foo();   // A::foo()被调用
    }

    1.1 多态
        在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:
    void bar(A * a)
    {
        a->foo();  // 被调用的是A::foo() 还是B::foo()?
    }

    因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。

    这种同一代码可以产生不同效果的特点,被称为“多态”。

    1.2 多态有什么用?
        多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。

        在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者“知道”(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。

        多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要“知道”(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。

    1.3 如何“动态联编”
        编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把“标准的”方式简单介绍一下。

        我所说的“标准”方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对1.1中的例子:

    void bar(A * a)
    {
        a->foo();
    }

    会被改写为:

    void bar(A * a)
    {
        (a->vptr[1])();
    }

        因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。

        虽然实际情况远非这么简单,但是基本原理大致如此。

    1.4 overload和override
        虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”和“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:

    override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,这个我会在“语法”部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为“覆盖”,还贴切一些。
    overload约定成俗的被翻译为“重载”。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
    2. 虚函数的语法
        虚函数的标志是“virtual”关键字。

    2.1 使用virtual关键字
        考虑下面的类层次:

    class A
    {
    public:
        virtual void foo();
    };

    class B: public A
    {
    public:
        void foo();    // 没有virtual关键字!
    };

    class C: public B  // 从B继承,不是从A继承!
    {
    public:
        void foo();    // 也没有virtual关键字!
    };

        这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。但是如果基类中没有声明某函数为virtual, 而只是在派生类中声明了某函数为virtual,则并不能实现这两个类的多态.

    2.2 纯虚函数
        如下声明表示一个函数为纯虚函数:

    class A
    {
    public:
        virtual void foo()=0;   // =0标志一个虚函数为纯虚函数
    };

        一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。

    2.3 虚析构函数
        析构函数也可以是虚的,甚至是纯虚的。例如:

    class A
    {
    public:
        virtual ~A()=0;   // 纯虚析构函数
    };
    当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:

    class A
    {
    public:
        A() { ptra_ = new char[10];}
        ~A() { delete[] ptra_;}        // 非虚析构函数
    private:
        char * ptra_;
    };

    class B: public A
    {
    public:
        B() { ptrb_ = new char[20];}
        ~B() { delete[] ptrb_;}
    private:
        char * ptrb_;
    };

    void foo()
    {
        A * a = new B;
        delete a;
    }

        在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?

        如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。

        纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。

    2.4 虚构造函数?
        构造函数不能是虚的

    3. 虚函数使用技巧 3.1 private的虚函数
        考虑下面的例子:

    class A
    {
    public:
        void foo() { bar();}
    private:
        virtual void bar() { ...}
    };

    class B: public A
    {
    private:
        virtual void bar() { ...}
    };

        在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。

        这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。

    3.2 构造函数和析构函数中的虚函数调用
        一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:

    class A
    {
    public:
        A() { foo();}        // 在这里,无论如何都是A::foo()被调用!
        ~A() { foo();}       // 同上
        virtual void foo();
    };

    class B: public A
    {
    public:
        virtual void foo();
    };

    void bar()
    {
        A * a = new B;
        delete a;
    }

        如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()。

    但是如果基类中有两个虚函数A,B. A中调用B. 当向下塑型即"parent* b = new child()"时, 在派生类中如果没有实现A, 而实现了B, 此时b->A()时, 调用的也是派生类中的B. 此点可以考虑虚函数的实现(VTBL)来考虑. 如

    class a
    {

    public:
     a(){
      cout << "a"<<endl;
     };

     virtual ~a(){
      cout << "~a<<endl; }

     virtual void test(){
      show();
      cout << "af"<<endl;
     }
     virtual show(){cout << "showa"<< endl;};

    };


    class b :public a
    {

    public:
     b(){
      cout << "b"<<endl;
     };
     
     virtual ~b(){
      cout << "~b<<endl;
     }
     virtual show(){cout << "showb"<< endl;};
     };

    此时 a* px = new b(); px->test(); 虽然b中没有实现test函数,要调用a中的test, 但是show却调用的是b中的实现. 编译器为每一个具有虚拟函数的类(无论是继承来的,还是本身的)都准备一个VTBL,并按顺序列出各个虚函数的入口地址,如果父类和子类中都有某一个函数,则在子类的VTBL中只列出子类的实现,而不会列出父类的实现, 对于没有在子类中override的虚函数,则列出父类中的实现.  所以如果类本身是子,虽然赋予了父类的类型,但是VTBL却是父的,所以会表现多态性. 虚拟表的实现参见"More Effective C++"的M24.

    3.3 多继承中的虚函数 3.4 什么时候使用虚函数
        在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。

        以设计模式[2]中Factory Method模式为例,Creator的factoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然Product类一定也有多态(虚函数)。

        另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。

        现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。

    虚基类继承

    在缺省情况下C++中的继承是“按值组合”的一种特殊情况。当我们写
    class Bear : public ZooAnimal { ... };
    每个Bear 类对象都含有其ZooAnimal 基类子对象的所有非静态数据成员以及在Bear中声明的非静态数据成员, 类似地当派生类自己也作为一个基类对象时如:
    class PolarBear : public Bear { ... };
    则PolarBear 类对象含有在PolarBear 中声明的所有非静态数据成员以及其Bear 子对象的所有非静态数据成员和ZooAnimal 子对象的所有非静态数据成员。在单继承下这种由继承支持的特殊形式的按值组合提供了最有效的最紧凑的对象表示。在多继承下当一个基类在派生层次中出现多次时就会有问题. 最主要的实际例子是iostream 类层次结构。ostream 和istream 类都从抽象ios 基类派生而来,而iostream 类又是从ostream 和istream 派生
    class iostream :public istream, public ostream { ... };
    缺省情况下,每个iostream 类对象含有两个ios 子对象:在istream 子对象中的实例以及在ostream 子对象中的实例。这为什么不好?从效率上而言,存储ios 子对象的两个复本,浪费了存储区,因为iostream 只需要一个实例。而且,ios 构造函数被调用了两次每个子对象一次。更严重的问题是由于两个实例引起的二义性。例如,任何未限定修饰地访问ios 的成员都将导致编译时刻错误:到底访问哪个实例?如果ostream 和istream 对其ios 子对象的初始化稍稍不同,会怎样呢?怎样通过iostream 类保证这一对ios 值的一致性?在缺省的按值组合机制下,真的没有好办法可以保证这一点. C++语言的解决方案是,提供另一种可替代按“引用组合”的继承机制虚拟继承(virtual inheritance) 在虚拟继承下只有一个共享的基类子对象被继承而无论该基类在派生层次中出现多少次共享的基类子对象被称为虚拟基类。通过用关键字virtual 修政一个基类的声明可以将它指定为被虚拟派生。例如,下列声明使得ZooAnimal 成为Bear 和Raccoon 的虚拟基类:
    // 关键字 public 和 virtual
    // 的顺序不重要
    class Bear : public virtual ZooAnimal { ... };
    class Raccoon : virtual public ZooAnimal { ... };
    虚拟派生不是基类本身的一个显式特性,而是它与派生类的关系如前面所说明的,虚拟继承提供了“按引用组合”。也就是说,对于子对象及其非静态成员的访问是间接进行的。这使得在多继承情况下,把多个虚拟基类子对象组合成派生类中的一个共享实例,从而提供了必要的灵活性。同时,即使一个基类是虚拟的,我们仍然可以通过该基类类型的指针或引用,来操纵派生类的对象。

    用图形表示概念:

    virtual                               virtual继承时:    
      A               A                                     A  
        \           /                                     /   \  
          B       C                                     B       C  
            \   /                                         \   /  
              D                                             D

    决不要重新定义继承而来的非虚函数
    class B {
    public:
    void mf();
    ...
    };

    class D: public B {
    public:
    void mf(); // 隐藏了B::mf; 参见条款50
    ...
    };
    pB->mf(); // 调用B::mf
    pD->mf(); // 调用D::mf
    行为的两面性产生的原因在于,象B::mf 和D::mf 这样的非虚函数是静态绑定的(参见条款38)。这意味着,因为pB 被声明为指向B 的指针类型,通过pB 调用非虚函数时将总是调用那些定义在类B 中的函数 ---- 即使pB 指向的是从B 派生的类的对象,如上例所示。相反,虚函数是动态绑定的(再次参见条款38),因而不会产生这类问题。如果mf 是虚函数,通过pB 或pD 调用mf 时都将导致调用D::mf,因为pB 和pD 实际上指向的都是类型D 的对象。所以,结论是,如果写类D 时重新定义了从类B 继承而来的非虚函数mf,D 的对象就可能表现出精神分裂症般的异常行为。也就是说,D 的对象在mf 被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。
    实践方面的论据就说这么多。我知道你现在想知道的是,不能重新定义继承而来的非虚函数的理论依据是什么。我很高兴解答。条款35 解释了公有继承的含义是 "是一个",条款36 说明了为什么 "在一个类中声明一个非虚函数实际上为这个类建立了一种特殊性上的不变性"。如果将这些分析套用到类B、类D 和非虚成员函数B::mf,那么,
    适用于B 对象的一切也适用于D 对象,因为每个D 的对象 "是一个" B 的对象。
    B 的子类必须同时继承mf 的接口和实现,因为mf 在B 中是非虚函数。那么,如果D 重新定义了mf,设计中就会产生矛盾。如果D 真的需要实现和B 不同的mf,而且每个B 的对象 ---- 无论怎么特殊 ---- 也真的要使用B实现的mf,那么,每个D 将不 "是一个" B。这种情况下,D 不能从B 公有继承。相反,如果D 真的必须从B 公有继承,而且D 真的需要和B 不同的mf 的实现,那么,mf 就没有为B 反映出特殊性上的不变性。这种情况下,mf 应该是虚函数。最后,如果每个D 真的 "是一个" B,并且如果mf 真的为B 建立了特殊性上的不变性,那么,D 实际上就不需要重新定义mf,也就决不能这样做。不管采用上面的哪一种论据都可以得出这样的结论:任何条件下都要禁止重新
    定义继承而来的非虚函数。

  • http://blog.csdn.net/wang_junjie/archive/2008/07/03/2608997.aspx

    本文所有代码均在VC2008下编译、调试。如果您使用的编译器不同,结果可能会有差别,但本文讲述的原理对于大部分编译器应该是相似的。对于本文的标题,实在不知道用什么表示更恰当,因为本文不仅淡了内存泄露检测机制,也谈到了指针越界的检测机制。到底应该说是MFC的机制,还是C++的机制?Anyway,相信你看了一定会有所收获。并欢迎常来本博客http://lionel.bokee.com留言讨论。在我们开发MFC应用程序的时候,不知大家是否注意到Debug版本输出窗口经常会有下面这样的信息:

    Detected memory leaks!
    Dumping objects ->
    c:\my.data\my.codes\memleak\memleak\memleak.cpp(34) : {126} normal block at 0x00A321A0, 4 bytes long.
     Data: <    > 01 00 00 00 
    Object dump complete.

      编译器是怎么知道我们写的代码有内存泄露并能精确到文件、行号的呢?事实上也并不是所有情况都能精确到文件、行号,看看下面这种情况:

    Detected memory leaks!
    Dumping objects ->
    First-chance exception at 0x75c739e5 (kernel32.dll) in MemLeak.exe:
    0xC0000005: Access violation reading location 0x711af9f4.
    #File Error#(62) : {137} normal block at 0x00A721A0, 4 bytes long.
     Data: <    > CD CD CD CD 
    Object dump complete.

      虽然检测出了内存泄露,但我们只能知道内存地址、行号,文件名是#File Error#,而且还伴随着内存非法访问的异常。这个异常看似是MFC在检测内存泄露的时候产生的。  下面我们从C++内存分配与回收的两个操作符new, delete一步步分析C++内存管理以及MFC内存泄露检测机制。所有这些都是针对Debug版本的,最后我们再看看Release版本的情况。

    一、内存分配操作符new   新建一个MFC应用程序,无论是Win32 Console Application + MFC Support,还是MFC Application或者是MFC DLL。编译器为我们生成的代码最前面,在#include下面都会有下面这三行代码:

    #ifdef _DEBUG
    #define new DEBUG_NEW
    #endif

      这三句话的意思是,如果是Debug版本,那么将new操作符定义为DEBUG_NEW。在afx.h中有对DEBUG_NEW的定义:

    // Memory tracking allocation
    void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
    #define DEBUG_NEW new(THIS_FILE, __LINE__)

      看来MFC是重新定义了一个new操作符,并把文件名、行号调试信息传给了new。下面是这个new操作符调用的其它函数。可见是按照MFC -> C++ -> C -> Win32 API的流程分配的内存:

    DEBUG_NEW
    -> void* __cdecl operator new(size_t nSize, LPCSTR lpszFileName, int nLine)             afxmem.cpp
    -> void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)  afxmem.cpp
    -> extern "C" _CRTIMP void* __cdecl _malloc_dbg(…)                                     dbgheap.c
    -> extern "C" void* __cdecl _nh_malloc_dbg(…)                                          dbgheap.c
    -> extern "C" static void * __cdecl _nh_malloc_dbg_impl(…)                             dbgheap.c
    -> extern "C" static void * __cdecl _heap_alloc_dbg_impl(…)                            dbgheap.c
    -> __forceinline void * __cdecl _heap_alloc (size_t size)                               malloc.c
    -> LPVOID WINAPI HeapAlloc(…);                                                         winbase.h

    二、内存回收操作符delete   MFC并没有重新定义delete操作符,因为所有调试信息已经传给了new操作符。delete操作符只要依然按照MFC -> C++ -> C -> Win32 API的流程将之前分配的内存释放掉就可以了:

    operator delete
    -> class CCRTAllocator::static void Free(void* p) throw()                              atlalloc.h
    -> extern "C" _CRTIMP void __cdecl _free_dbg(void * pUserData, int nBlockUse)          dbgheap.c
    -> extern "C" void __cdecl _free_dbg_nolock(void * pUserData, int nBlockUse)           dbgheap.c
    -> void __cdecl _free_base (void * pBlock)                                             free.c
    -> BOOL WINAPI HeapFree(…);                                                           winbase.h

    三、C++内存链   内存链是MFC检测内存泄露的基础,当我们每new一块内存,_heap_alloc_dbg_impl就会把这块内存加入内存链,当我们delete一块内存,_free_dbg_nolock就会把这块内存从内存链中删除。VC的实现是使用了一个双向链表。每一个节点的结构定义如下:

    typedef struct _CrtMemBlockHeader
    {
            struct _CrtMemBlockHeader * pBlockHeaderNext;            // 下一个节点指针
            struct _CrtMemBlockHeader * pBlockHeaderPrev;            // 前一个节点指针
            char *                      szFileName;                  // 调用new的文件名
            int                         nLine;                       // 调用new的行
            size_t                      nDataSize;                   // 调用new分配内存大小
            int                         nBlockUse;                   // 本块内存使用目的
            long                        lRequest;                    // 请求编号
            unsigned char               gap[nNoMansLandSize];        // 内存前面的空白
            /* followed by:
             *  unsigned char           data[nDataSize];             // 真正的内存
             *  unsigned char           anotherGap[nNoMansLandSize]; // 内存后面的空白
             */
    } _CrtMemBlockHeader;

      结构体中有几个成员可能需要解释一下。nBlockUse表示本块内存的用途,一般取值为_NORMAL_BLOCK。lRequest表示请求内存的编号,初始值为1,每请求一次,该值加1。我们在输出窗口看到的normal block就表示nBlockUse=_NORMAL_BLOCK, {137} 就是lRequest的值。data是真正返回给我们的指针,编译器在data前后用gap, anotherGap将数据保护起来并赋予特殊的值,以检测我们对指针操作是否越界。这些空白区域内存大小为#define nNoMansLandSize 4。data同样被赋予特殊的值,特殊值总共有四种:

    static unsigned char _bNoMansLandFill = 0xFD;   /* fill no-man's land with this */
    static unsigned char _bAlignLandFill  = 0xED;   /* fill no-man's land for aligned routines */
    static unsigned char _bDeadLandFill   = 0xDD;   /* fill free objects with this */
    static unsigned char _bCleanLandFill  = 0xCD;   /* fill new objects with this */

    比如说我们new了一个int对象,int* p = new int;那么上面这个结构体内容如下:

    +------------------------------------------------------------------------------+
    | pBlockHeaderNext | …… | gap: FDFDFDFD | p: CDCDCDCD | anotherGap: FDFDFDFD |
    +------------------------------------------------------------------------------+

      比如说我们内存访问越界了:*(p+1) = 0,那么在delete这个指针的时候,_free_dbg_nolock会对gap, anotherGap的值进行检查,发现不等于_bNoMansLandFill,就报错。如果我们写*(p+1) = 0xFDFDFDFD,那么就把编译器骗了,编译器认为内存访问并没有越界。当我们delete一块内存的时候,这块内存会被用_bDeadLandFill填充。如果我们new了多个对象,那么这些对象就链接再了一起,例如:

    int* pB = new int;
    int* pA = new int;

    内存布局如下:

    +--------------------------------------------------------------------------+
    |   +--------------------------+              +--------------------------+ |
    +-> | pHead = pBlockHeaderNext | -----------> | pBlockHeaderNext = NULL  | |
        |--------------------------|              |--------------------------| |
        | pBlockHeaderPrev = NULL  |              | pBlockHeaderPrev      ->-|-+
        |--------------------------|              |--------------------------|
        |          ......          |              |          ......          |
        |--------------------------|              |--------------------------|
        |gap: FDFDFDFD             |              |gap: FDFDFDFD             |
        |--------------------------|              |--------------------------|
        |pA: CDCDCDCD              |              |pB: CDCDCDCD              |
        |--------------------------|              |--------------------------|
        |anotherGap: FDFDFDFD      |              |anotherGap: FDFDFDFD      |
        +--------------------------+              +--------------------------+

      知道了内存块的布局,我们甚至可以通过一个指针,打印出当前new过的所有对象内存地址及大小。为了验证上述内容的正确性,我们不妨写一个简单的验证程序:

    int* pB = new int(2);
    int* pA = new int(1);
    cout << "*pA = " << *pA << ", *pB = " << *pB << endl;   // *pA = 1, *pB = 2
    *((int*)(*(pA - 8)) + 8) = 1;
    *((int*)(*(pB - 7)) + 8) = 2;
    cout << "*pA = " << *pA << ", *pB = " << *pB << endl;   // *pA = 2, *pB = 1
    delete pA;
    delete pB;

    四、内存泄露检测机制   MFC正是因为有了内存链,才可以检测出哪些内存还没有被释放。在程序退出的时候,dbgheap.c中的extern "C" _CRTIMP int __cdecl _CrtDumpMemoryLeaks(void)函数会被调用,然后遍历当前的内存链,看看还有哪些内存没有被释放,然后打印出内存泄露的信息。原理很简单,这里不再赘述。那么为什么有的情况下我们无法通过输出的信息定位到具体泄露的文件呢?为什么有的时候会显示#File Error#?看看上面提到的结构体中文件名的保存char * szFileName,仅仅保存了一个指向文件名的指针而已。这个文件名是作为一个字符串,保存在.exe或.dll的.rdata中的。如果在.exe文件退出的时候,我们显式加载的.dll文件已经被我们卸载了,并且在该.dll文件内存存在内存泄露的话,虽然_CrtDumpMemoryLeaks会尝试读取并显示文件名,但szFileName指针指向的内存空间已经是无效的了。_CrtDumpMemoryLeaks在读取文件名之前会先调用API函数IsBadReadPtr判断该指针是否有效。如果已经无效则显示#File Error#。本文最开始所提到的异常,正是由IsBadReadPtr导致的。

    五、Release版本   对于Release版本,就没有上面提到的内存链了。对于newdelete的调用将会被直接转到malloc.c和free.c。因为没有内存链,没有多余的保护数据填充,没有内存越界检测机制,所以有些时候Debug版本会崩溃,但是Release版本却没有。这并不代表代码没有问题,而是内存非法访问更难发现了,当Release版本崩溃的时候,问题也更难定位了。

       上述内存泄露检测、内存越界访问检测的原理很简单,但并不能查出所有内存非法访问。所以永远不要乱用指针,然后把所有对指针的判断都用try{}catch{}规避。因为并不是所有指针非法访问都能catch到,即使catch到了,内存也可能已经被写坏了。

  • 我推荐使用PageHeap.Exe和Gflags.Exe,主要的原因还是因为当有人问内存越界的错误如何查出来的时候,国外的朋友经常会推荐这两个工具(highly recommend)。我用过之后,也觉得有些时候用用还是有好处的。

    PageHeap.Exe将针对某个指定的应用程序启用Page Heap标志,从而自动监视所有的malloc、new和heapAlloc的内存分配,找出内存错误。

    PageHeap.Exe的下载地点:

    http://download.microsoft.com/download/vc60pro/utility/6.0/win98/en-us/pageheap1.exe

    下面我们简单地给出PageHeap使用步骤:

    第一步:

    在命令行中运行PageHeap.Exe。如果你以前设置过启用Global Page Heap标志,那么你将看到一个列表,给出所有已经启用了的应用程序的名字,不含路径。

    如下所示:

    C:\>pageheap

    pgh.exe                                  enabled

    testSplit.exe                            enabled


    第二步:

    编译一个小程序,其中有如下代码:

    void main()
    {
      int m_len = 5;
      char *m_p = (char *)HeapAlloc (GetProcessHeap (),    HEAP_ZERO_MEMORY, m_len);
      m_p[m_len] = 0;
      HeapFree (GetProcessHeap (),0, m_p);
    }

    Build出一个Debug版本。运行之,你看不到有任何异常的报告。

    但其实m_p[m_len]=0这句话就是越界写了,因为只分配到了m_p[m_len-1]!这种情况就叫Dynamic memory overrun。用BoundsChecker是可以查到的。

    这时,表面上看不出任何问题,但是一颗定时炸弹已经埋下了。


    第三步:

    在命令行中运行PageHeap /enable YourApplicationName.exe 0x01。

    再运行一次不带参数的PageHeap,察看上面的命令是否生效。你的应用程序应该在启用的列表中。

    注意:千万不要在YourApplication.Exe前面加上路径!!

    0x01的含义在后面说明。


    第四步:

    再次运行你的程序。

    你将会注意到在Output窗口的加载各种DLL之前,多了几句话:

    Loaded exports for 'C:\WINNT\System32\ntdll.dll'
    Page heap: process 0x57C created heap @ 00130000 (00230000, flags 0x1)
    Loaded 'C:\WINNT\system32\MFC42D.DLL', no matching symbolic information found.
    ..
    Loaded 'C:\WINNT\system32\MSVCP60D.DLL', no matching symbolic information found.
    Page heap: process 0x57C created heap @ 00470000 (00570000, flags 0x1)
    Loaded exports for 'C:\WINNT\system32\imm32.dll'

    这就是Page Heap的监视机制在发挥作用!他告诉你你的堆00470000被创建出来了。

    然后程序退出后,Output窗口有这么几句话表明一定有什么错误发生了:

    Page heap: block @ 0015AFF8 is corrupted (reason 10)
    Page heap: reason: corrupted suffix pattern
    Page heap: process 0x57C destroyed heap @ 00471000 (00570000)
    The thread 0x8A8 has exited with code 0 (0x0).

    这说明在销毁堆00470000时遇到了麻烦,就是数据块0015AFF8被误用了,原因是误用了下标语法。看,说得多么清楚!也节省了许多翻来覆去查代码的工作!


    PageHeap的使用中有几点值得注意:

    1:启用PageHeap不能够影响正在运行中的应用程序。如果你需要启用一些正在运行且不能重启的程序的PageHeap,那请运行PageHeap启用后,重新启动机器。

    2:要想查看PageHeap把信息放到哪里了,请打开你的注册表,来到HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options

    你将会看到你的应用程序也在这个项下面。你的应用程序的GlobalFlag被设置为了0x02000000,PageHeapFlags被设置为了0x01。

    3:PageHeap的原理是这样,它在已分配的内存的后面放上几个守护字节(Guard Bytes),再跟上一个标记为PAGE_NOACCESS的内存页。这样,已分配内存的后面如果被重写了,那么守护字节就会被改变,于是当内存被释放时,PageHeap就会引发一个AV(Access Violation)。大体上就是这样。所以只有最后释放这块问题内存时,才会有PageHeap的报告!这就是PageHeap的局限性吧。

    参数0x01的含义:

    FLAGS hex value (0x...) has the following structure:

        B7-B0   Bit flags    1 - enable page heap

       01 - enable page heap. If zero normal heap is used. In 99% of the cases you will want this to be set.
       02 - collect stack traces (default on checked builds)
       04 - minimize memory impact
       08 - minimize randomly(1)/based on size range(0)
       10 - catch backward overruns

    看到了吗?你还可以设置参数为0x10,从而可以检查内存向前的越界写!

    Gflags.Exe是微软的Debugging Tools里面的工具。在Windows 2000的Resource Kit中也可以找得到。我们也可以用它来完成和PageHeap相同的任务。当然,Gflags.EXE还能做许许多多其他的事情。这里我们就不介绍了,总之物超所值。

    具体的使用办法是:

    1)     运行Gflags.Exe;

    2)  你将看到一个对话框。在”Image File”的编辑框中写下你的应用程序的名字,如YourApp.Exe。注意不要路径!

    3)  选择”Image File Options”的单选钮;

    4)  这时,你会看到对话框的内容突然一变。选中“Place heap
    allocations at ends of pages”前的复选框。

    5)  点击Apply按钮。

    这样,就达到了PageHeap的效果。现在运行你的程序,overwrite你的堆,就应该生成一个AV了!


    (请结合查看微软KB:SAMPLE: PageHeap1.exe Finds Heap Corruption and Memory Errors (Q264471))

  • 在STL中基本容器有: string、vector、list、deque、set、map

    set 和map都是无序的保存元素,只能通过它提供的接口对里面的元素进行访问
    set:集合, 用来判断某一个元素是不是在一个组里面,使用的比较少
    map:映射,相当于字典,把一个值映射成另一个值,如果想创建字典的话使用它好了
    string、vector、list、deque、set 是有序容器
    1.string
    string 是basic_string<char> 的实现,在内存中是连续存放的.为了提高效率,都会有保留内存,如string s= "abcd",这时s使用的空间可能就是255, 当string再次往s里面添加内容时不会再次分配内存.直到内容>255时才会再次申请内存,因此提高了它的性能. 当内容>255时,string会先分配一个新内存,然后再把内容复制过去,再复制先前的内容.

    对string的操作,如果是添加到最后时,一般不需要分配内存,所以性能最快; 如果是对中间或是开始部分操作,如往那里添加元素或是删除元素,或是代替元素,这时需要进行内存复制,性能会降低.

    如果删除元素,string一般不会释放它已经分配的内存,为了是下次使用时可以更高效.

    由于string会有预保留内存,所以如果大量使用的话,会有内存浪费,这点需要考虑.还有就是删除元素时不释放过多的内存,这也要考虑.

    string中内存是在堆中分配的,所以串的长度可以很大,而char[]是在栈中分配的,长度受到可使用的最大栈长度限制.

    如果对知道要使用的字符串的最大长度,那么可以使用普通的char[],实现而不必使用string.
    string用在串长度不可知的情况或是变化很大的情况.

    如果string已经经历了多次添加删除,现在的尺寸比最大的尺寸要小很多,想减少string使用的大小,可以使用:
    string s = "abcdefg";
    string y(s); // 因为再次分配内存时,y只会分配与s中内容大一点的内存,所以浪费不会很大
    s.swap(y); // 减少s使用的内存

    如果内存够多的话就不用考虑这个了
    capacity是查看现在使用内存的函数
    大家可以试试看string分配一个一串后的capacity返回值,还有其它操作后的返回值

    2.vector
    vector就是动态数组.它也是在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放.如果新值>当前大小时才会再分配内存
    对最后元素操作最快(在后面添加删除最快 ), 此时一般不需要移动内存,只有保留内存不够时才需要
    对中间和开始处进行添加删除元素操作需要移动内存,如果你的元素是结构或是类,那么移动的同时还会进行构造和析构操作,所以性能不高.
    访问方面,对任何元素的访问都是O(1),也就是是常数的,所以vector常用来保存需要经常进行随机访问的内容,并且不需要经常对中间元素进行添加删除操作.

    相比较可以看到vector的属性与string差不多,同样可以使用capacity看当前保留的内存,使用swap来减少它使用的内存.
    总结. 需要经常随机访问请用vector
    3.list

    list就是链表,元素也是在堆中存放,每个元素都是放在一块内存中
    list没有空间预留习惯,所以每分配一个元素都会从内存中分配,每删除一个元素都会释放它占用的内存,这与上面不同,可要看好了

    list在哪里添加删除元素性能都很高,不需要移动内存,当然也不需要对每个元素都进行构造与析构了,所以常用来做随机操作容器.
    但是访问list里面的元素时就开始和最后访问最快
    访问其它元素都是O(n) ,所以如果需要经常随机访问的话,还是使用其它的好

    总结
    如果你喜欢经常添加删除大对象的话,那么请使用list
    要保存的对象不大,构造与析构操作不复杂,那么可以使用vector代替
    list<指针>完全是性能最低的做法,这种情况下还是使用vector<指针>好,因为指针没有构造与析构,也不占用很大内存
    4.deque

    双端队列,也是在堆中保存内容的.它的保存形式如下:

    [堆1]
    ...
    [堆2]
    ...
    [堆3]

    每个堆保存好几个元素,然后堆和堆之间有指针指向,看起来像是list和vector的结合品,不过确实也是如此
    deque可以让你在前面快速地添加删除元素,或是在后面快速地添加删除元素,然后还可以有比较高的随机访问速度

    vector是可以快速地在最后添加删除元素,并可以快速地访问任意元素
    list是可以快速地在所有地方添加删除元素,但是只能快速地访问最开始与最后的元素
    deque在开始和最后添加元素都一样快,并提供了随机访问方法,像vector一样使用[]访问任意元素,但是随机访问速度比不上vector快,因为它要内部处理堆跳转
    deque也有保留空间.另外,由于deque不要求连续空间,所以可以保存的元素比vector更大,这点也要注意一下.还有就是在前面和后面添加元素时都不需要移动其它块的元素,所以性能也很高
  • This "vi" tutorial is intended for those who wish to master and advance their skills beyond the basic features of the basic editor. It covers buffers, "vi" command line instructions, interfacing with UNIX commands, and ctags. The vim editor is an enhanced version of vi. The improvements are clearly noticed in the handling of tags.

    The advantage of learning vi and learning it well is that one will find vi on all Unix based systems and it does not consume an inordinate amount of system resources. Vi works great over slow network ppp modem connections and on systems of limited resources. One can completely utilize vi without departing a single finger from the keyboard. (No hand to mouse and return to keyboard latency)

    NOTE: Microsoft PC Notepad users who do not wish to use "vi" should use "gedit" (GNOME edit) or "gnp" (GNOME Note Pad) on Linux. This is very similar in operation to the Microsoft Windows editor, "Notepad". (Other Unix systems GUI editors: "dtpad", which can be found in /usr/dt/bin/dtpad for AIX, vuepad on HP/UX, or xedit on all Unix systems.)


    Related YoLinux Tutorials:

    °Software development tools

    °Advanced VI

    °Emacs and C/C++

    °C++ Info, links

    °MS/Visual C++ Practices

    °C++ Memory corruption and leaks

    °C++ String Class

    °C++ STL vector, list

    °Posix Threads

    °Fork and Exec

    °GDK Threads

    °Clearcase Commands

    °YoLinux Tutorials Index


     


    Free Information Technology Magazine Subscriptions and Document Downloads


    Free Information Technology Software and Development Magazine Subscriptions and Document Downloads


    Vim Installation:

    Red Hat / CentOS / Fedora:

    • rpm -ivh vim-common-...rpm vim-minimal-...rpm vim-enhanced-...rpm vim-X11-...rpm
    • yum install vim-common vim-minimal vim-enhanced vim-X11
    Ubuntu / Debian:
    • apt-get install vim vim-common vim-gnome vim-gui-common vim-runtime
    Compiling Vim from source:
    • Download vim source from http://vim.org
    • tar xzf vim-7.0.tar.gz
    • cd vim70
    • ./configure --prefix=/opt --enable-cscope
    • make
    • make install
    Basic "vi" features

    One edits a file in vi by issuing the command: vi file-to-edit.txt

    The vi editor has three modes, command mode, insert mode and command line mode.

    1. Command mode: letters or sequence of letters interactively command vi. Commands are case sensitive. The ESC key can end a command.
    2. Insert mode: Text is inserted. The ESC key ends insert mode and returns you to command mode. One can enter insert mode with the "i" (insert), "a" (insert after), "A" (insert at end of line), "o" (open new line after current line) or "O" (Open line above current line) commands.
    3. Command line mode: One enters this mode by typing ":" which puts the command line entry at the foot of the screen.

    Partial list of interactive commands:

    Cursor movement:
    KeystrokesAction
    h/j/k/lMove cursor left/down/up/right
    spacebarMove cursor right one space
    -/+Move cursor down/up in first column
    ctrl-dScroll down one half of a page
    ctrl-uScroll up one half of a page
    ctrl-fScroll forward one page
    ctrl-bScroll back one page
    M (shift-h)Move cursor to middle of page
    HMove cursor to top of page
    LMove cursor to bottom of page
    W
    w
    5w
    Move cursor a word at a time
    Move cursor ahead 5 words
    B
    b
    5b
    Move cursor back a word at a time
    Move cursor back a word at a time
    Move cursor back 5 words
    e
    5e
    Move cursor to end of word
    Move cursor ahead to the end of the 5th word
    0 (zero)Move cursor to beginning of line
    $Move cursor to end of line
    )Move cursor to beginning of next sentence
    (Move cursor to beginning of current sentence
    GMove cursor to end of file
    %Move cursor to the matching bracket.
    Place cursor on {}[]() and type "%".
    '.Move cursor to previously modified line.
    'aMove cursor to line mark "a" generated by marking with keystroke "ma"
    'AMove cursor to line mark "a" (global between buffers) generated by marking with keystroke "mA"
    ]'Move cursor to next lower case mark.
    ['Move cursor to previous lower case mark.

    Editing commands:

    KeystrokesAction
    iInsert at cursor
    aAppend after cursor
    AAppend at end of line
    ESCTerminate insert mode
    uUndo last change
    UUndo all changes to entire line
    oOpen a new line
    dd
    3dd
    Delete line
    Delete 3 lines.
    DDelete contents of line after cursor
    CDelete contents of line after cursor and insert new text. Press esc key to end insertion.
    dw
    4dw
    Delete word
    Delete 4 words
    cwChange word
    xDelete character at cursor
    rReplace character
    ROverwrite characters from cursor onward
    sSubstitute one character under cursor continue to insert
    SSubstitute entire line and begin to insert at beginning of line
    ~Change case of individual character
    ctrl-a
    ctrl-x
    Increment number under the cursor.
    Decrement number under the cursor.
    /search_string{CR}Search for search_string
    ?search_string{CR}Search backwards (up in file) for search_string
    /\<search_string\>{CR}Search for search_word
    Ex: /\<s\>
    Search for variable "s" but ignore declaration "string" or words containing "s". This will find "string s;", "s = fn(x);", "x = fn(s);", etc
    nFind next occurrence of search_word
    NFind previous occurrence of search_word
    .repeat last command action.

    Terminate session:

    • Use command: ZZ
      Save changes and quit.
    • Use command line: ":wq"
      Save (write) changes and quit.
    • Use command line: ":w"
      Save (write) changes without quitting.
    • Use command line: ":q!"
      Ignore changes and quit. No changes from last write will be saved.
    • Use command line: ":qa"
      Quit all files opened.

    Advanced "vi" features

    Interactive Commands:

    • Marking a line:
      Any line can be "Book Marked" for a quick cursor return.
      • Type the letter "m" and any other letter to identify the line.
      • This "marked" line can be referenced by the keystroke sequence "'" and the identifying letter.
        Example: "mt" will mark a line by the identifier "t".
        "'t" will return the cursor to this line at any time.
        A block of text may be referred to by its marked lines. i.e.'t,'b
    • vi line buffers:
      To capture lines into the buffer:
      • Single line: "yy" - yanks a single line (defined by current cursor position) into the buffer
      • Multiple lines: "y't" - yanks from current cursor position to the line marked "t"
      • Multiple lines: "3yy" - yank 3 lines. Current line and two lines below it.
      Copy from buffer to editing session:
      • "p" - place contents of buffer after current line defined by current cursor position.
    • vim: Shift a block of code left or right:
      • Enter into visual mode by typing the letter "v" at the top (or bottom) of the block of text to be shifted.
      • Move the cursor to the bottom (or top) of the block of text using "j", "k" or the arrow keys.
        Tip: Select from the first collumn of the top line and the last character of the line on the bottom line.
        Zero ("0") will move the cursor to the first character of a line and "$" will move the cursor to the last character of the line.
      • Type >> to shift the block to the right.
        Type << to shift the block to the left.
      Note: The number of characters shifted is controlled by the "shift width" setting. i.e. 4: ":set sw=4"
      This can be placed in your $HOME/.vimrc file.

    Command Line:

    • command options:
      The vi command line interface is available by typing ":". Terminate with a carriage return.
      Example commands:
      • :help topic
        If the exact name is unknown, TAB completion will cycle through the various options given the first few letters. Ctrl-d will print the complete list of possibilites.
      • :set all - display all settings of your session.
      • :set ic - Change default to ignore case for text searches
        Default is changed from noignorecase to ignorecase. (ic is a short form otherwise type set ignorecase)
      • Common options to set:
        Full "set" CommandShort formDescription
        autoindent/noautoindentai/noai{CR} returns to indent of previous line
        autowrite/noautowriteaw/noawSee tags
        errorbells/noerrorbellseb/noebSilence error beep
        flash/noflashfl/noflScreen flashes upon error (for deaf people or when noerrorbells is set)
        tabstop=8tsTab key displays 8 spaces
        ignorecase/noignorecaseic/noicCase sensitive searches
        number/nonumbernu/nonuDisplay line numbers
        showmatch/noshowmatchno abbreviationsCursor shows matching ")" and "}"
        showmode/noshowmodeno abbreviationsEditor mode is displayed on bottom of screen
        taglengthtlDefault=0. Set significant characters
        closepunct='".,;)]} % key shows matching symbol.
        Also see showmatch
        linelimit=1048560 Maximum file size to edit
        wrapscan/nowrapscanws/nowsBreaks line if too long
        wrapmargin=0/nowrapmarginwm/nowmDefine right margin for line wrapping.
        list/nolist Display all Tabs/Ends of lines.
        bg=dark
        bg=light

        VIM: choose color scheme for "dark" or "light" console background.

    • Executing Unix commands in vi:
      Any UNIX command can be executed from the vi command line by typing an "!" before the UNIX command.
      Examples:
      • ":!pwd" - shows your current working directory.
      • ":r !date" - reads the results from the date command into a new line following the cursor.
      • ":r !ls -1" - Place after the cursor, the current directory listing displayed as a single column.
    • Line numbers:
      Lines may be referenced by their line numbers. The last line in the file can be referenced by the "$" sign.
      The entire file may be referenced by the block "1,$" or "%"
      The current line is referred to as "."
      A block of text may be referred to by its marked lines. i.e. 5,38 or 't,'b
    • Find/Replace:
      Example:
      • :%s/fff/rrrrr/ - For all lines in a file, find string "fff" and replace with string "rrrrr" for the first instance on a line.
      • :%s/fff/rrrrr/g - For all lines in a file, find string "fff" and replace with string "rrrrr" for each instance on a line.
      • :%s/fff/rrrrr/gc - For all lines in a file, find string "fff" and replace with string "rrrrr" for each instance on a line. Ask for confirmation
      • :%s/fff/rrrrr/gi - For all lines in a file, find string "fff" and replace with string "rrrrr" for each instance on a line. Case insensitive.
      • :'a,'bs/fff/rrrrr/gi - For all lines between line marked "a" (ma) and line marked "b" (mb), find string "fff" and replace with string "rrrrr" for each instance on a line. Case insensitive.
      • :%s/*$/ - For all lines in a file, delete blank spaces at end of line.
      • :%s/\(.*\):\(.*\)/\2:\1/g - For all lines in a file, move last field delimited by ":" to the first field. Swap fields if only two.
      For more info type:
      • :help substitute
      • :help pattern
      • :help gdefault
      • :help cmdline-ranges
    • Sorting:
      Example:
      • Mark a block of text at the top line and bottom line of the block of text. i.e. "mt" and "mb" on two separate lines. This text block is then referenced as "'t,'b.
      • :'t,'b !sort

    • Moving columns, manipulating fields and awk:
      :'t,. !awk '{print $3 " " $2 " " $1}' - This will reverse the order of the columns in the block of text. The block of text is defined here as from the line marked with the keystroke "bt" and the current line ("."). This text block is referenced as "'t,."
                    aaa bbb ccc              ccc bbb aaa
                    xxx yyy zzz   becomes->  zzz yyy xxx
                    111 222 333              333 222 111
      
    • Source Code Formatting: C++/Java
      • Use vim visual text selection to mark the lines to format (beautify):
        • eg. Whole file:
          • Go to first line in file: shift-v
          • Go to last line in file: shift-g
          • Select the key equals: =
        This will align all braces and indentations. For the equivalent in emacs see the YoLinux emacs tutorial.
    • Text Formatting:
      • Mark a block of text at the top line and bottom line of the block. i.e. "mt" and "mb" on two separate lines.
      • Example: ":'t,'b !nroff"
      • You can insert nroff commands i.e.:
        .ce 3Center the next three lines
        .fiFill text - left and right justify (default)
        .nfNo Fill
        .ls 2Double line spacing
        .spSingle line space
        .sv 1.0iVertical space at top of page space
        .nsTurn off spacing mode
        .rsRestore spacing mode
        .ll 6.0iLine length = 6 inches
        .in 1.0iIndent one inch
        .ti 1.0iTemporarily one time only indent one inch
        .pl 8.0iPage length = 8 inches
        .bpPage break
        Example:
        .fi
        .pl 2i
        .in 1.0i
        .ll 6.0i
        .ce
        Title to be centered
        .sp
        The following text bla bla bla bla bla bla bla bla bla bla 
        bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla 
        bla bla bla bla bla bla bla bla bla bla bla bla bla bla 
        bla bla bla bla bla bla bla bla bla bla bla bla bla bla bla 
        bla bla bla bla bla
        

        Becomes:

                                 Title to be centered
        
                  The following text bla bla bla bla bla bla bla bla
                  bla  bla  bla  bla bla bla bla bla bla bla bla bla
                  bla bla bla bla bla bla bla bla bla  bla  bla  bla
                  bla  bla  bla  bla bla bla bla bla bla bla bla bla
                  bla bla bla bla bla bla bla bla bla  bla  bla  bla
                  bla bla bla bla
        
        
        
    • Spell Checking:
      • Mark a block of text by marking the top line and bottom line of the block. i.e. "mt" and "mb" on two separate lines.
      • :'t,'b !spell will cause the block to be replaced with misspelled words.
      • Press "u" to undo.
      • Proceed to correct words misspelled.
    • Macros:
      :map letter commands_strung_together
      :map - lists current key mappings
      Example - :map g n cwNEW_WORD{ctrl-v}{esc}i{ctrl-v}{CR}
      This example would find the next search occurrence, change the word and insert a line feed after the word. The macro is invoked by typing the letter "g".
      • Control/Escape/Carriage control characters must be prefixed with ctrl-V.
      • Choose a letter which is not used or important. (i.e. a poor choice would be "i" as this is used for insert)
    • Double spacing:
      • :%s/$/{ctrl-V}{CR}/g
        This command applies an extra carriage return at the end of all lines
    • Strip blanks at end of line:
      • :%s/{TAB}*$//
    • Strip DOS ctrl-M's:
      • :1,$ s/{ctrl-V}{ctrl-M}//

      Note: In order to enter a control character, one muust first enter ctrl-v. This is true throughout vi. For example, if searching for a control character (i.e. ctrl-m): /ctrl-v ctrl-M If generating a macro and you need to enter esc without exiting the vi command line the esc must be prefixed with a ctrl-v: ctrl-v esc.
    • Editing multiple files:
      • vi file1 file2 file3
      • :n Edit next file (file2)
      • :n Edit next file (file3)
      • :rew Rewind to the first file (file1)
    • Line folding:

      Many times one may encounter a file with folded lines or may wish to fold lines. The following image is of a file with folded lines where each "+" represents a set of lines not viewed but a marker line prefixed with a "+" is shown stating how many lines have been folded and out of view. Folding helps manage large files which are more easily managed when text lines are grouped into "folds".

      Example: vim /usr/share/vim/vim63/plugin/netrw.vim

      VIM folded lines

      Keystrokes:

      KeystrokeDescription
      zRUnfold all folded lines in file.
      zaOpen/close (toggle) a folded group of lines.
      zAOpen a closed fold or close an open fold recursively.
      zcClose a folded group of lines.
      zCClose all folded lines recursively.
      zdDelete a folded line.
      zDDelete all folded lines recursively.
      zEEliminate all folded lines in file.
      zFCreate "N" folded lines.
    • Hyper-Linking to include files:
      • Place cursor over the file name (i.e. #include "fileABC.h")
      • Enter the letter combination: gf
        (go to file)
      This will load file fileABC.h into vim. Use the following entry in your ~/.vimrc file to define file paths. Change path to something appropriate if necessary.
      "Recursively set the path of the project.
      set path=$PWD/**
      
    • Batch execution of vi from a command file:
      Command file to change HTML file to lower case and XHTML compiance:
      :1,$ s/<HTML>/<html>/g
      :1,$ s/<\/HTML>/<\/html>/g
      :1,$ s/<HEAD>/<head>/g
      :1,$ s/<\/HEAD>/<\/head>/g
      :1,$ s/<TITLE>/<title>/g
      :1,$ s/<\/TITLE>/<\/title>/g
      :1,$ s/<BODY/<body/g
      :1,$ s/<\/BODY/<\/body/g
      :1,$ s/<UL>/<ul>/g
      :1,$ s/<\/UL>/<\/ul>/g
      ...
      ..
      .
      :1,$ s/<A HREF/<a href/g
      :1,$ s/<A NAME/<a name/g
      :1,$ s/<\/A>/<\/a>/g
      :1,$ s/<P>/<p>/g
      :1,$ s/<B>/<b>/g
      :1,$ s/<\/B>/<\/b>/g
      :1,$ s/<I>/<i>/g
      :1,$ s/<\/I>/<\/i>/g
      :wq
             
      
      Execute: vi -e file-name.html < ViCommands-HtmlUpdate.txt

      [Potential Pitfall]: This must be performed while vim has none of the files open which are to be affected. If it does, vim will error due to conflicts with the vim swap file.


    Tagging:

    This functionality allows one to jump between files to locate subroutines.

    • ctags *.h *.c This creates a file names "tags".

    Edit the file using vi.

    • Unix command line: vi -t   subroutine_name This will find the correct file to edit.
      OR
    • Vi command line: :tag subroutine_name This will jump from your current file to the file containing the subroutine. (short form :ta subroutine_name )
      OR
    • By cursor position: ctrl-] Place cursor on the first character of the subroutine name and press ctrl-] This will jump to the file containing the subroutine.
      Note: The key combination ctrl-] is also the default telnet connection interrupt. To avoid this problem when using telnet block this telnet escape key by specifying NULL or a new escape key:
      • telnet -E file-name
      • telnet -e "" file-name

    In all cases you will be entered into the correct file and the cursor will be positioned at the subroutine desired.
    If it is not working properly look at the "tags" file created by ctags. Also the tag name (first column) may be abbreviated for convenience. One may shorten the significant characters using :set taglength=number

    Tag Notes:

    • A project may have a tags file which can be added and referred to by: :set tags=tags\ /ad/src/project1.tags
      A "\" must separate the file names.
    • :set autowrite will automatically save changes when jumping from file to file, otherwise you need to use the :w command.

    vim tagging notes: (These specific tag features not available in vi)

    Tag CommandDescription
    :tag start-of-tag-name_TABVim supports tag name completion. Start the typing the tag name and then type the TAB key and name completion will complete the tag name for you.
    :tag /search-stringJump to a tag name found by a search.
    ctrl-]The vim editor will jump into the tag to follow it to a new position in the file or to a new file.
    ctrl-tThe vim editor will allow the user to jump back a level.
    (or :pop)
    :tselect <function-name>When multiple entries exist in the tags file, such as a function declaration in a header file and a function definition (the function itself), the operator can choose by issuing this command. The user will be presented with all the references to the function and the user will be prompted to enter the number associated with the appropriate one.
    :tnextWhen multiple answers are available you can go to the next answer.
    :set ignorecase
    (or :set ic)
    The ignore case directive affects tagging.
    :tagsShow tag stack (history)
    :4popJump to a particular position in the tag stack (history).
    (jump to the 4th from bottom of tag stack (history).
    The command ":pop" will move by default "1" backwards in the stack (history).)
    or
    :4tag
    (jump to the 4th from top of tag stack)
    :tnextJump to next matching tag.
    (Also short form :tn and jump two :2tnext)
    :tpreviousJump to previous matching tag.
    (Also short form :tp and jump two :2tp)
    :tfirstJump to first matching tag.
    (Also short form :tf, :trewind, :tr)
    :tlastJump to last matching tag.
    (Also short form :tl)
    :set tags=./tags,./subdir/tags
    
    Using multiple tag files (one in each directory).
    Allows one to specify all tags files in directory tree: set tags=src/**/tags
    Use Makefile to generate tags files as well as compile in each directory.

    Links:


    The ctags utility:

    There are more than one version of ctags out there. The original Unix version, the GNU version and the version that comes with vim. This discussion is about the one that comes with vim. (default with Red Hat)

    For use with C++:

    • ctags version 5.5.4:
         ctags *.cpp ../inc/*.h
    • ctags version 5.0.1:
         ctags --lang=c++ --c-types=+Ccdefgmnpstuvx *.cpp ../inc/*.h

    To generate a tags file for all files in all subdirectories: ctags -R .

    The ctags program which is written by the VIM team is called " Exuberant Ctags" and supports the most features in VIM.

    Man page: ctags - Generate tag files for source code


    Defaults file:

    VIM: $HOME/.exrc

    • ~/.vimrc
    • ~/.gvimrc
    • ~/.vim/ (directory of vim config files.)

    VI: $HOME/.exrc

    Example:
             set autoindent
             set wrapmargin=0
             map g hjlhjlhjlhlhjl
             "
             " S = save current vi buffer contents and run spell on it,
             "     putting list of misspelled words at the end of the vi buffer.
             map S G:w!^M:r!spell %^M
             colorscheme desert
             "Specify that a dark terminal background is being used.
             set bg=dark
            
    

    Notes:

    • Look in /usr/share/vim/vim61/colors/ for available colorschemes.
      (I also like "colorscheme desert")
    • Alternate use of autoindent: set ai sw=3


    Using vim and cscope:

    Cscope was developed to cross reference C source code. It now can be used with C++ and Java and can interface with vim.

    Using Cscope to cross reference souce code will create a database and allow you to traverse the source to find calls to a function, occurances of a function, variable, macros, class or object and their respective declarations. Cscope offers more complete navigation than ctags as it has more complete cross referencing.

    Vim must be compiled with Cscope support. Red Hat Enterprise Linux 5 (or CentOS 5), includes vim 7.0 with cscope support. Earlier versions of Red Hat or Fedora RPM does not support Cscope and thus must be compiled.

    Compiling Vim from source:

    • Download vim source from http://www.vim.org/
    • tar xzf vim-7.0.tar.gz
    • cd vim70
    • ./configure --prefix=/opt --enable-cscope
    • make
    • make install

    Using Cscope with vim:

    The Cscope database (cscope.out) is generated the first time it is invoked. Subsequent use will update the database based on file changes.
    The database can be generated manually using the command i.e.: cscope -b *.cpp *.h or cscope -b -R .

    Invoke Cscope from within vim from the vim command line. Type the following: :cscope find search-type search-string The short form of the command is ":cs f" where the "search-type" is:

    Search TypeType short formDescription
    symbolsFind all references to a symbol
    globalgFind global definition
    callscFind calls of this function
    calleddFind functions that the specified function calls
    texttFind specified text string
    filefOpen file
    includeiFind files that "#include" the specified file

    Results of the Cscope query will be displayed at the bottom of the vim screen.

    • To jump to a result type the results number (+ enter)
    • Use tags commands to return after a jump to a result: ctrl-t
      To return to same spot as departure, use ctrl-o
    • To use "tags" navigation to search for words under the cursor (ctrl-\) instead of using the vim command line ":cscope" (and "ctrl-spaceBar" instead of ":scscope"), use the vim plugin, cscope_maps.vim [cache]
      When using this plugin, overlapping ctags navigation will not be available. This should not be a problem since cscope plugin navigation is the same but with superior indexing and cross referenceing.
      Place this plugin in your directory "$HOME/.vim/plugin"
      Plugin required for vim 5 and 6. This feature is compiled in with vim 7.0 on Red Hat Enterprise Linux 5 and CentOS 5 and newer Linux OS's. Attempts to use the plugin when not required will result in the following error:
      E568: duplicate cscope database not added
    • Cycle through results:
      • Next result: :tnext
      • Previous result: :tprevious
    • Create a split screen for Cscope results: :scscope find search-type search-string
      (Short form: :scs f search-type search-string)
    • Use command line argument ":cscope -R": Scan subdirectories recursively
    • Use Cscope ncurses based GUI without vim: cscope
      • ctrl-d: Exit Cscope GUI

    Cscope command line arguments:

    ArgumentDescription
    -RScan subdirectories recursively
    -bBuild the cross-reference only.
    -CIgnore letter case when searching.
    -fFileNameSpecify Cscope database file name instead of default "cscope.out".
    -Iinclude-directoriesLook in "include-directories" for any #include files whose names do not begin with "/".
    -iFilesScan specified files listed in "Files". File names are separated by linefeed. Cscope uses the default file name "cscope.files".
    -kKernel mode ignores /usr/include.
    Typical: cscope -b -q -k
    -qcreate inverted index database for quick search for large projects.
    -sDirectoryNameUse specified directory for source code. Ignored if specified by "-i".
    -uUnconditionally build a new cross-reference file..
    -vVerbose mode.
    file1 file2 ...List files to cross reference on the command line.

    Cscope environment variable:

    Environment VariableDescription
    CSCOPE_EDITOREditor to use: /usr/bin/vim
    EDITORDefault: /usr/bin/vim
    INCLUDEDIRSColon-separated list of directories to search for #include files.
    SOURCEDIRSColon-separated list of directories to search for additional source files.
    VPATHColon-separated list of directories to search. If not set, cscope searches only in the current directory.

    Manually Generating file cscope.files

    File: $HOME/bin/gen_cscope or /opt/bin/gen_cscope
    #!/bin/bash
    find ./ -name "*.[ch]pp" -print > cscope.files
    cscope -b -q -k
    
    Generates cscope.files of ".cpp" and ".hpp" source files for a C++ project.

    Note that this generates CScope files in the current working directory. The CScope files are only usefull if you begin the vim session in the same directory. Thus if you have a heirarchy of directories, perform this in the top directory and reference the files to be edited on the command line with the relative path from the same directory in which the CScope files were generated.


    Also see:


    Vim plugins:

    Vim default plugins:

    Vim comes with some default plugins which can be found in:

    • Red Hat / CentOS / Fedora:
      • RHEL4: /usr/share/vim/vim70/autoload/
      • Fedora 3:/usr/share/vim/vim63/plugin/
    • Ubuntu / Debian:
      • Ubuntu 6.06: /usr/share/vim/vim64/plugin/

    Additional custom plugins:

    User added plugins are added to the user's local directory: ~/.vim/plugin/ or ~/.vimrc/plugin/


    Default vim plugins:

    File Explorer / List Files: explorer.vim

    Help is available with the following command: :help file-explorer

    CommandDescription
    :ExploreList files in your current directory
    :Explore directory-nameList files in specified directory
    :VexploreSplit with a new vertical window and then list files in your current directory
    :SexploreSplit with a new horizontal window and then list files in your current directory

    The new window buffer created by ":Vexplore" and ":Sexplore" can be closed with ":bd" (buffer delete).


    Additional custom plugins:

    CScope: cscope_maps.vim

    See cscope and vim description and use in this tutorial above.

    Tabbed pages: minibufexpl.vim

    This plugin allows you to open multiple text files and accessed by their tabs displayed at the top of the frame.
    KeystrokeDescription
    :bnNew buffer
    :bdBuffer delete
    :b3Go to buffer number 3
    ctrl-w followed by "k"New buffer. Puts curson in upper tabbed portion of window. Navigate with arrow keys or "h"/"l".
    :qaQuit vim out of all buffers
    tabThe "tab" key jumps between tabbed buffers.

    Recommended ~/.vimrc file entry:

    "Hide abandon buffers in order to not lose undo history.
    set hid
    
    This vim directive will allow undo history to remain when switching buffers.

    The new window buffer tab created can be closed with ":bd" (buffer delete).

    Links:


    Alternate between the commensurate include and source file: a.vim

    Most usefull when used with the vim plugin "minibufexpl.vim"

    Usefull for C/C++ programmers to switch between the source ".cpp" and commensurate ".hpp" or ".h" file and vice versa.

    CommandDescription
    :Aswitches to the header file corresponding to the current file being edited (or vise versa)
    :ASsplits and switches
    :AVvertical splits and switches
    :ATnew tab and switches
    :ANcycles through matches
    :IHswitches to file under cursor
    :IHSsplits and switches
    :IHVvertical splits and switches
    :IHTnew tab and switches
    :IHNcycles through matches
    If you are editing fileX.c and you enter ":A" in vim, you will be switched to the file fileX.h

    Links:


    Vim tip:

    Using a mousewheel with vim in an xterm. Place in file $HOME/.Xdefaults

    XTerm*VT100.Translations: #override \n\ 
    : string("0x9b") string("[64~") \n\ 
    : string("0x9b") string("[65~")
    

    Links:

    vim booksBooks:
    ultimate guide to vi"The Ultimate Guide to VI and EX Text Editors"
    Hewlet Packard Corporation
    ISBN #0-8053-4460-8, Addison-Wesley Pub Co., Benjamin/Cummings Publishing Company
    Amazon.com
    Learn vi"Learning the vi Editor (6th edition)"
    by Linda Lamb, Arnold Robbins
    ISBN #1565924266, O'Reilly
    Amazon.com
    vi improved"Vi iMproved (VIM)
    by Steve Oualline
    ISBN #0735710015, Sams (1st edition)
    Amazon.com