多态

什么是多态?为什么要使用多态?什么时候用多态?多态是如何实现的?使用多态有什么好处?   在学习《大话设计模式》的时候经常会遇到多态,但什么是多态?为什么要使用多态?什么时候用多塔?多态是如何实现的?使用多态有什么好处?我们将根据以上五个问题展开激烈的讨论。 什么是多态?   多态,顾名思义指“多种形态”。多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。 例:猪、狗、猫都属于动物类,我们将动物类作为父类,猪“哼哼叫”,狗“汪汪叫”,猫“喵喵叫”,猪、狗、猫三... 阅读详情

多态


向上类型转换

对象可以作为它自己的类或它的基类的对象来使用。另外,还能通过基类的地址操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来使用,这被称为向上类型转换(upcasting)。甚至可以直接把一个派生类的对象赋值给基类的对象。

#include <iostream>
using namespace std;
enum note{ middleC, Csharp, Eflat };

class Instrument{
public:
    void play(note) const{
        cout << "Instrument::play" << endl;
    }
};

class Wind: public Instrument{
public:
    void play(note) const{
        cout << "Wind::play" << endl;
    }
};

void tune(Instrument& i){
    i.play(middleC);
}
int main(){
    Wind flute;
    tune(flute);    //Upcasting
}

函数tune()通过引用接受一个Instrument,同样能够接受一个从Instrument继承而来的类的对象。这里无需类型转换,就能将wind对象传给tune()。在instrument中的接口必定存在于wind中。wind到instrument的向上类型转换会使wind的接口变窄,但是不会窄过instrument的整个接口。
上面程序的输出是:

Instrument::play

我们传递给tune()的参数是Wind&,他也是Instrument&,所以它调用了Instrument::play()。然而实际中,我们可能并不想这么做,我们想要派生类对象使用派生类的方法,即调用Wind::play(),虽然tune()函数的形参类型是基类指针。

仔细思考一下tune()函数的执行过程。它接受一个Instrument地址,由于继承是一种is-a的关系,它也可以接受一个instrument派生类对象的地址,这种向上类型转换是自然的。在tune()函数内部,它调用这个对象的play()方法。这个函数不知道Instrument有多少种派生类,但是它需要对每一个派生类做出相应的play()调用,那么它怎么由一个对象地址得到该对象所需要的play函数地址呢?程序编译期间是无法确定的,因为只有在程序执行的过程中才知道传递给tune()的形参的类型(我们就是要这种功能)。这就需要这样一个地址能够自己说明自己的类型信息,即它在继承树的位置(它是Instrument还是Wind)。

虚函数

上面讲到的是动态绑定技术,一个函数调用只有在实际运行期间才能知道函数入口地址。它与普通的函数调用有很大差别,前面的例子程序就是普通函数。为了引起动态绑定,C++要求在基类中声明这个函数的时候使用virtual关键字(定义的时候并不需要)。动态绑定只对virtual函数起作用。如果一个基类的函数是virtual的,那么他的派生类中的此函数也是virtual的(可以不用virtual关键字声明)。在派生类中virtual的函数的定义叫重写(overriding)(一般的函数的定义叫重定义)。

前面的例子中在Instrument类中play()的声明前加virtual,程序的输出就变成了Wind::play.

#include <iostream>

using namespace std;

enum note{ middleC, Csharp, Cflat};

class Instrument{
public:
    virtual void play(note) const {
        cout << "Instrument::play\n";
    }
    virtual char* what() const{
        return "Instrument";
    }
    virtual void adjust(int){}
};

class Wind: public Instrument{
public:
    void play(note)const {
        cout << "Wind::play\n";
    }
    char* what() const{
        return "Wind";
    }
    void adjust(int){}
};

class Percussion: public Instrument{
public:
    void play(note) const {
        cout << "Percussion::play\n";
    }
    char* what() const { return "Percussion";}
    void adjust(int){}
};

class Stringed: public Instrument{
public:
    void play(note) const {
        cout << "Stringer::play\n";
    }
    char* what() const {return "Stringed";}
    void adjust(int){}
};

class Brass: public Wind{
public:
    void play(note) const{
        cout << "Brass::play" << endl;
    }
    char* what() const { return "Brass";}
};

class WoodWind: public Wind{
public:
    void play(note) const {
        cout << "WoodWind::play" << endl;
    }
    char* what() const { return "WoodWind";}
};

void tune(Instrument& i){
    i.play(middleC);
}

void f(Instrument& i){ i.adjust(1);}

Instrument* a[] = {
        new Wind,
        new Percussion,
        new Stringed,
        new Brass
};

int main(){
    Wind flute;
    Percussion drum;
    Stringed violin;
    Brass flugelhorn;
    WoodWind recorder;
    tune(flute);
    tune(drum);
    tune(violin);
    tune(flugelhorn);
    tune(recorder);
    f(flugelhorn);
    for(int i = 0; i < 4; i++)
        tune(*a[i]);
}

运行结果:

Wind::play
Percussion::play
Stringer::play
Brass::play
WoodWind::play
Wind::play
Percussion::play
Stringer::play
Brass::play

这个例子只是多了几个继承类和层次,virtual机制同样工作。注意f(flugelhorn这个调用,flugelhorn是Brass类,它继承自Wind(Wind继承自Instrument),它Wind中有虚函数adjust,而Brass类中没有定义相应的虚函数,这时候他就使用继承层次中最近的那个虚函数。即Wind中的adjust。
虚函数机制

如何实现动态捆绑

虚函数机制也就是动态捆绑,它的实现需要编译器添加额外的代码或数据。关键字virtual告诉编译器它不应该执行静态捆绑,相反,它应该自动安装实现动态捆绑所需要的机制。这意味着,如果对Brass对象通过基类Instrument地址调用play(),将得到恰当的函数。

为了达到这个目的,典型的编译器(当然还有其他的实现方法)对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数的地址。在每个带有虚函数的类中,编译器秘密地放置一个指针,称为vpointer(VPTR),指向这个对象的VTABLE。当通过基类指针做虚函数调用时,编译器静态地插入能取得这个VPTR并在VTABLE中查找函数地址的代码,这样就能调用正确的函数并引起动态捆绑的发生。

为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的,不用程序员关心。利用虚函数,即使在编译器还不知道这个对象的特定类型的情况下,也能调用这个对象中正确的函数。

具体细节

存放类型信息

在任何类中不存在显式的类型信息。而先前的例子告诉我们,必须有一些类型信息放在对象中;否则,类型将不能再运行时建立。

下面例子展示含有虚函数的类和普通的类的不同:

#include <iostream>
using namespace std;

class NoVirtual{
    int a;
public:
    void x() const{}
    int i() const{ return 1;}
};

class OneVirtual{
    int a;
public:
    virtual void x() const{}
    int i() const { return 1;}
};

class TwoVirtual{
    int a;
public:
    virtual void x() const{}
    virtual int i() const{ return 1;}
};

int main(){
    cout << "int: " << sizeof(int) << endl;
    cout << "NoVirtual: " << sizeof(NoVirtual) << endl;
    cout << "OneVirtual: "<< sizeof(OneVirtual) << endl;
    cout << "TwoVirtual: " << sizeof(TwoVirtual) << endl;
}

运行结果:

int: 4
NoVirtual: 4
OneVirtual: 8
TwoVirtual: 8

不带虚函数的类的长度就是数据成员的总长度,而带有单个虚函数的类OneVirtual的对象的长度是NoVirtual的长度加上一个指针的长度。它反映出,如果有一个或多个虚函数,编译器都只加入一个VPTR。这是因为VPTR指向一个存放函数地址的表,我们只需要一个表,所有的虚函数地址都在表中。
这个例子需要一个数据成员。没有数据成员的类编译器也强制他有非零的长度,否则无法存放。

对于前面的instrument类的例子,这个Instrument的指针数组没有特殊的类型信息,它的每一个元素都指向一个类型为Instrument的对象。Wind,Percussion,Stringed和Brass都可以归入这个类别之中,所以他们的地址自然的放入这个数组。然而,编译器并不知道他们是比Instrument具有更多内容的东西,所以就将他们留给自己的设备处理。
每当创建一个包含虚函数的类或从包含虚函数的类派生出一个类时,编译器就为这个类创建一个唯一的VTABLE。在这个表中,编译器放置了在这个类中或在它的基类中所有已经声明为virtual的函数的地址。如果在这个派生类中没有对基类中的虚函数进行重写,编译器就是用基类的虚函数地址(在Brass类中的adjust函数就是这种情况)。

如果能看到由虚函数调用而产生的汇编代码,这将是很有帮助的。下面是在函数f(Instrument& i)内部调用
i.adjust(1);
某个编译器所产生的输出:

push 1
push si
mov bx, word ptr [si]
call word ptr [bx + 4]
add sp ,4

C++函数调用的参数与C函数调用一样,是从右向左进栈的,所以参数1首先压栈。对于这个函数,寄存器si存放i的地址。因为他是被选中对象的首地址,它也被压栈。这个首地址对应于this的值,所以成员函数知道他工作在那个特殊的对象上。
现在,必须实现实际的虚函数调用。首先,必须产生VPTR,使得能够找到VTABLE。对于这个编译器,VPTR在对象的开头,所以this的内容对应于VPTR。下面这一行
mov bx, word ptr [si]
取出si(即this)所指的字,他就是VPTR。将这个VPTR放到寄存器bx中。
放在bx中的内容是VTABLE的首地址,但被调用的函数不是在VTABLE中第0个位置,而是在第二个位置(因为他是这个表中的第三个函数)。这种内存下,地址占用两个字节。所以对bx + 4再间接寻址。然后执行call。

因为VPTR决定了对象的虚函数的行为,所以我们可以看到VPTR指向相应的VTABLE是多么重要。在VPTR适当初始化之前绝对不能调用虚函数。当然,可以保证初始化的地点是在构造函数中。幸运的是,编译器已经自己帮我们做好了。

C++是注重效率的。如果编译器有一个它知道确切类型的对象,那么尽管这个类包含虚函数,编译器也可以不使用动态绑定,这样直接使用Call的函数调用会更高效而且不会引起问题。比如对于对象(注意,是对象,若是地址,信息就不够确定他的类型)进行成员函数调用,编译器可以直接确定待函数的函数是哪个,就把从VPTR找VTABLE,从VTABLE找虚函数入口地址省掉,直接变成一条call语句。

抽象基类和纯虚函数

在程序设计时,常常希望基类仅仅作为其派生类的接口,而不想用户实际地创建一个积累对象。要做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function)。来使基类称为抽象类(abstract)。纯虚函数使用关键字virtual,并且在其后面加上=0。如果试图用抽象类定义一个对象,编译器会报错。

当继承一个抽象类时,派生类需要实现所有的纯虚函数,否则它也会称为抽象类。如果一个类中全是抽象函数,那么也称作纯抽象类。

建立公共接口的唯一原因是它能够对于每个不同的子类有不同的表示。它建立一个基本的格式,用来确定什么是对于所有的派生类是公有的。

从VTABLE的角度来说。定义一个纯虚函数,等于是告诉编译器在VTABLE中为函数保留一个位置,但这个位置中不放任何地址。只要有一个函数在类中被声明为纯虚函数,则VTABLE就是不完全的。那么,当试图创建这个类的对象的时候,编译器就不能安全的创建一个抽象类的对象。

纯虚函数禁止对抽象类的函数以传值方式调用。通过抽象类,可以保证在向上类型转换期间总是使用指针或引用。

纯虚定义

在基类中,对纯虚函数提供定义是可能的。我们仍然告诉编译器不允许产生抽象基类的对象,而且如果要创建派生类的对象,该派生类必须定义了对应的虚函数。然而,我们可能希望一段公共代码,使一些或所有的派生类定义都能调用,而不必在每个函数中重复这段代码。

例如:

#include <iostream>

using namespace std;

class Pet{
public:
    virtual void speak() const = 0;
    virtual void eat() const = 0;
    //inline pure virtual function illegal;
    //! virtual void sleep() const = 0 {}
};

//Ok,not defined inline
void Pet::eat() const{
    cout << "Pet::eat\n";
}

void Pet::speak() const {
    cout << "Pet::speak\n";
}

class Dog: public Pet{
public:
    //Use the common Pet code:
    void speak() const { Pet::speak(); }
    void eat() const { Pet::eat(); }
};

int main(){
    Dog simba;
    simba.speak();
    simba.eat();
}

运行结果:

Pet::speak
Pet::eat

Pet的VTABLE仍然空着,但这个派生类中刚好有一个函数,可以通过名字调用它。


可以想象,当实现继承和重新定义一些虚函数时,编译器对新类创建一个新的VTALBE,并且创建新函数的地址,对于没有重新定义的虚函数使用基类函数的地址。无论如何,对于每个可创建的额对象(即它的类不含有纯虚函数),在VTABLE中总有一个函数地址的全集,所以绝对不能对不在其中的地址进行调用。
但是在派生类中继承或增加新的虚函数时会发生什么呢?

#include <iostream>
#include <string>

using namespace std;

class Pet{
    string pname;
public:
    Pet(const string& petName): pname(petName){}
    virtual string name() const {return pname;}
    virtual string speak() const {return "";}
};

class Dog: public Pet{
    string name;
public:
    Dog(const string& petName): Pet(petName){}
    //New virtual function in Dog class
    virtual string sit() const {
        return Pet::name() + " sits ";
    }
    string speak() const {
        return Pet::name() + " says bark! ";
    }
};

int main(){
    Pet* p[] = {new Pet("generic"),new Dog("bob") };
    cout << "p[0]->speak() = " << p[0]->speak() << endl;
    cout << "P[1]->speak() = " << p[1]->speak() << endl;
    //! cout << "p[1]->sit() = " << p[1]->sit() << endl;
}

类Pet中含有两个虚函数:speak()和name(),而在类Dog中又增加了第三个称为sit()的虚函数,并且重新定义了speak()的含义。编译器在Dog的VTABLE中把speak()的偏移与基类VTABLE中speak()的偏移相同。因为编译器产生的代码在VTABLE中使用一个简单的偏移来选择虚函数。不论对象属于哪个特殊的类,它的VTABLE都是以同样的方法设置,所以对虚函数的调用总是使用同样的方法。

然而这里,编译器只对指向基类的指针进行操作。而这个基类只有speak和name函数,所以它就是编译器唯一允许调用的函数。那么,如果只有基类的指针,那么编译器怎么可能知道自己正在对Dog对象进行工作呢?这个指针可能指向其他一些没有sit函数的类。在VTALBE中,可能有,也可能没有一些其他函数的地址,但无论如何,对这个VTALBE地址做虚函数调用都不是我们想做的。所以编译器通过防止我们对只存在与派生类中的函数做虚函数调用来完成工作。
有时,我们可能知道指针实际上指向的是哪一种特殊子类的对象。这时如果想调用只存在于这个子类中的函数,则必须类型转换这个指针。

((Dog*)p[1])->sit();

这里发生了向下类型转换,从基类指针到派生类指针。向上类型转换是自动进行的,无须强制。向下类型转换是不安全的,因为这里没有关于实际类型的编译时信息,所以必须准确地知道这个类实际上是什么类型。如果把它转换成错误的类型,就会出错。这实际是运行时类型确认问题(RTTI)。

对象切片

当多态地处理对象时,传地址与传值有明显的不同。地址都有相同的长度,传递派生类(它通常稍大一点)对象的地址和传递基类(通常小一点)对象的地址大小是相同的。这是使用多态的目的,即让对基类对象操作的代码也能透明地操作派生类对象。

如果对一个类型进行向上类型转换,而且不使用地址或引用,这个对象被‘切片’,直到剩下的是适合于目的的子对象。

#include <iostream>
#include <string>

using namespace std;

class Pet{
    string pname;
public:
    Pet(const string& name): pname(name){}
    virtual string name()const  {return pname;}
    virtual string description() const{ return "this is a" + pname;}
};

class Dog: public Pet{
    string favoriteActivity;
public:
    Dog(const string& name,const string& Acitivity)
        :Pet(name), favoriteActivity(Acitivity){}
    string description() const {
        return Pet::name() + "likes to " + favoriteActivity;
    }

};

void describe(Pet p){   //slice the object
    cout << p.description() << endl;
}

int main(){
    Pet p("Alice");
    Dog d("Bob","sleep");
    describe(p);
    describe(d);
}

运行结果:

this is aAlice
this is aBob

函数describe通过传值方式传递一个类型为pet的对象。然后对这个对象调用虚函数description。我们可能希望第一次调用产生”This is a Alice”,而第二次产生”Bob likes to sleep”。实际上,两次调用都是调用了基类版本的description。
在这个程序中发生了两件事。第一,describe接受的是Pet对象(而不是指针或引用),所以describe中的任何调用都将引起一个与Pet大小相同的对象压栈并在调用后清除。这意味着,如果一个由Pet派生而来的类的对象被传给describe,编译器会接受他,但是只拷贝这个对象的对应于Pet的部分,切除这个对象的派生部分。如图所示:
对象切片
这个切片过程发生在describe的实参拷贝初始化形参的过程中。形参毫无疑问是Pet类型,所以它的VPTR指向Pet类型的VTALBE,然后从实参的成员中拷贝所需要的成员。在调用description函数的时候,编译器根据对象的类型决定调用那个description,显然此时形参的类型是Pet。
对象切片发生在拷贝构造过程中。

重载和重新定义

重新定义一个基类中的重载函数将会隐藏所有该函数的其他基类版本。而当对虚函数进行这些操作时,情况会有所不同。

#include <iostream>
#include <string>

using namespace std;

class Base{
public:
    virtual int f() const {
        cout << "Base::f()\n";
        return 1;
    }
    virtual void f(string) const{}
    virtual void g() const{}
};

class Derived1: public Base{
public:
    void g() const{}
};

class Derived2: public Base{
public:
    int f() const {
        cout << "Derived2::f()\n";
        return 2;
    }
};

class Derived3: public Base{
public:
    //can not change return type
    //! void f() const {}
};

class Derived4:public Base{
public:
    //change argument
    int f(int) const{
        cout << "Derived4:f()\n";
        return 4;
    }
};

int main(){
    string s("hello");
    Derived1 d1;
    int x = d1.f();
    d1.f(s);
    Derived2 d2;
    x = d2.f();
    //! d2.f(s);    //string version hidden
    Derived4 d4;
    x = d4.f(1);
    //! d4.f();     //hidden
    //! d4.f(s);    //hidden
    Base& br = d4;
    //! br.f(1);    //couldn't find this member function in Base;
    br.f();     //ok
    br.f(s);    //ok
}

运行结果:

Base::f()
Derived2::f()
Derived4:f()
Base::f()

在Derived3中,编译器不允许我们改变重新定义过的函数的返回值(如果f()不是虚函数,这是允许的)。这很好理解,派生类可以使用基类的虚函数,而如果再定义一个同名同参而不同返回值的函数当然不行。
对于普通函数的规则仍然适用。如果重新定义了基类的一个重载成员函数,则在派生类中其他的重载函数版本将会被隐藏。

变量返回值类型

上例的类Derived3显示了我们不能在重新定义的过程中改变返回值类型。通常是这样的,但也有特例。如果返回一个指向基类的指针或引用,则该函数的重新定义版本可以返回一个指向派生类的指针或引用。

#include <iostream>
#include <string>

using namespace std;

class PetFood{
public:
    virtual string foodType() const = 0;
};

class Pet{
public:
    virtual string type() const = 0;
    virtual PetFood* eats() = 0;
};

class Bird: public Pet{
public:
    string type() const {return "Bird";}
    class BirdFood: public PetFood{
    public:
        string foodType() const {return "BirdFood";}
    };
    PetFood* eats() { return &bf;}
private:
    BirdFood bf;
};

class Cat: public Pet{
public:
    string type() const {return "Cat";}
    class CatFood: public PetFood{
    public:
        string foodType() const { return "Birds";}
    };
    CatFood* eats() { return &cf;}
private:
    CatFood cf;
};
int main(){
    Bird b;
    Cat c;
    Pet* p[] = {&b, &c};
    for(int i = 0; i < sizeof( p ) / sizeof(*p); i ++){
        cout << p[i]->type() << " eats " << p[i]->eats()->foodType()<< endl;
    }
    Cat::CatFood* cf = c.eats();
    Bird::BirdFood* bf;
    //! bf = b.eats();
    bf = dynamic_cast<Bird::BirdFood*>(b.eats());
}

虚函数和构造函数

当创建一个包含有虚函数的对象时,必须初始化它的VPTR以指向相应的VTABLE。这必须在对虚函数进行任何调用之前完成。生成一个对象是构造函数的工作,所以设置VPTR也是构造函数的任务。编译器在构造函数的开头秘密地插入能初始化VPTR的代码。如果我们没有为一个类显示定义构造函数,编译器会自动合成一个默认构造函数。如果该类有虚函数,则生成的构造函数也会设置VPTR。

构造函数调用顺序

所有的基类构造函数总是在继承类构造函数之前被调用。因为构造函数的工作就是建立对象。派生类只能访问它自己的成员,而不能访问基类的私有成员。只有基类的构造函数才能正确初始化它自己的成员。如果不在构造函数初始化列表中显式调用基类的构造函数,他就调用基类默认构造函数,如果没有默认构造函数,编译器报错。

虚函数在构造函数中的行为

构造函数的调用层次会导致一个有趣的问题。如果我们在构造函数中调用了虚函数,那会怎么样呢?虚函数的调用是在运行时决定的,因为编译时这个对象并不能知道它是属于这个成员函数的那个类,还是属于由它派生出来的类。
然而,对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。也就是说,虚机制在构造函数中不工作。

这种行为有两个理由。在概念上,构造函数的工作是生成一个对象。在任何构造函数中,可能只是部分形成对象——我们只能知道基类已经被初始化,但不知道哪个类是从这个基类派生而来的。然而,虚函数在继承层次上是向前和向外进行调用。它可以调用在派生类中的函数。如果我们在构造函数中也这么做,那么我们所调用的函数可能操作还没有被初始化的成员,所以不行。

第二,当一个构造函数被调用时,它首先要初始化它的VPTR。然而,它只能知道它属于“当前”类——即构造函数所在的类。于是它完全忽视这个对象是否是基于其他类的。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为它不知道谁继承它)。所以他使用的VPTR必须是对于这个类的VTABLE。只要它是最后一个构造函数调用,那么这个VPTR就是对象最后的VPTR。如果接着还有一个更晚派生的类的构造函数被调用,那么这个派生类又将自己对应的VPTR覆盖旧的VPTR。所以,VPTR是由最后调用的构造函数确定的,这也是构造顺序为什么是从基类逐层到派生类底层的原因。

当这一系列构造函数调用正在发生时,每个构造函数都已经设置VPTR指向自己类的VTABLE。如果此时发生了虚函数调用(即构造函数内调用了虚函数),它将只能通过自己的VTABLE调用,而不是最后派生的VTABLE。另外,许多编译器认识到,如果在构造函数内进行虚函数调用(其实不虚),应该使用早捆绑,可以优化速度。

析构函数和虚拟析构函数

构造函数时不能为虚函数的,但是析构函数可以而且常常必须是虚的。

构造函数的工作就是一块一地组合成一个对象。它首先调用基类构造函数,然后再调用继承顺序中更晚的类的构造函数(同样,它也必须按照此方法调用成员对象的构造函数)。类似地,析构函数必须拆卸某层次的对象。为了完成这项工作,编译器生成代码来调用所有的析构函数,但它必须按照与构造函数调用相反的顺序。这就是,析构函数从最晚派生的类开始,并向上到基类。这是安全且合理的:当前析构函数一直知道基类成员仍是有效的。如果需要在析构函数中调用某一基类的成员函数,进行这样的操作是安全的。因此,析构函数能够对自身进行清除,然后调用下一个析构函数。每个析构函数知道它所在类从哪一个类派生而来,但不知道它派生出哪些类。

通常,析构函数的执行是相当充分的。但是,如果想通过指向某个对象基类的指针操作这个对象(也就是,通过一般的接口操作这个对象),会怎么样呢?

#include <iostream>
#include <string>

using namespace std;

class Base1{
public:
    ~Base1(){ cout << "Base1::~Base1()\n";}
};
class Derived1:public Base1{
public:
    ~Derived1(){ cout << "Derived1::Derived1()\n";}
};
class Base2{
public:
    virtual ~Base2() { cout << "Base2::~Base2()\n";}
};
class Derived2: public Base2{
public:
    ~Derived2() { cout << "Derived2:~Derived2()\n";}
};

int main(){
    Base1 *bp = new Derived1;   //Upcast
    delete bp;
    Base2* b2p = new Derived2;  //Upcast
    delete b2p;
}

运行结果:

Base1::~Base1()
Derived2:~Derived2()
Base2::~Base2()

从结果可以看出,delete bp只调用基类的析构函数。而当delete b2p调用时,在基类的析构函数执行后,派生类的析构函数将会执行,这正是我们所期望的。不把析构函数设为虚函数是一个隐匿的错误,因为它常常不会对程序有直接的影响,但要注意它不知不觉地引起了内存泄漏。

纯虚析构函数

尽管纯虚析构函数是合法的,但是使用时有一个限制:必须为纯虚析构函数提供一个函数体。这看起来有点违反常规;如果它需要一个函数体,那它又如何成为“纯”?类似于前面说到过的纯虚定义,一个纯虚函数可以提供定义,目的是为了代码重用。而纯虚析构函数必须有函数体,是因为对象的析构过程是逐层进行的,如果一个析构函数没有函数体,怎么体现这个析构函数被调用了呢。因此,编译器和连接器强迫纯虚析构函数一定有一个函数体。。

然而,当从某个含有纯虚析构函数的类中继承一个类,不要求在派生类中提供纯虚函数的定义。

#include <iostream>
using namespace std;

class AbstractBase{
public:
    virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {}

class Derived: public AbstractBase{

};
int main(){
    Derived d;
}

一般来说,如果在派生类中基类的纯虚函数(和其他纯虚函数)没有重新定义,则派生类将会成为抽象类。但是,如果不对析构函数定义,编译器将会自动地为每个类生成一个析构函数定义并且派生类不会成为抽象类。
那么问题来了,纯虚析构函数的目的是什么?
当我们的类仅含有一个纯虚函数时,就会发现这个唯一的差别:析构函数。析构函数的纯虚性的唯一效果就是阻止基类的实例化。如果有其他的纯虚函数,则它们也会阻止类的实例化,但如果没有那些纯虚函数,可以方便的定义一个纯虚析构函数来完成这个任务而没有副作用。

析构函数中的虚机制

如果在一个普通的成员函数中调用一个虚函数,则会使用动态绑定机制来调用这个函数。而对于析构函数,只有成员函数的“本地”版本被调用,虚机制被忽略。

#include <iostream>
using namespace std;

class Base{
public:
    virtual ~Base(){
        cout << "~Base()\n";
        f();
    }
    virtual void f() { cout << "Base::f()\n";}
};

class Derived: public Base{
public:
    ~Derived() { cout << "~Derived()\n";}
    void f() {cout << "Derived::f()\n";}
};
int main(){
    Base* bp = new Derived;
    delete bp;
}

运行结果:

~Derived()
~Base()
Base::f()

在析构函数的调用中,Derived::f()没有被调用,即使f()是一个虚函数。
这是因为:析构函数总是“外层”(最晚派生的析构函数想基类的析构函数),如果在某个层次的析构函数中使用虚机制,实际上被调用的函数可能属于低于当前派生层次的类,而这个函数就可能操作已经被消除的数据。所以,编译器决定在编译的时候只调用这个函数的本层次版本。注意,在构造函数中也是如此,那是因为在构造时,后续派生的类的信息还不可用,在析构函数中,信息(VPTR)虽然存在,但不可靠。

向下类型转换

不同于向上类型转换总是清除地知道基类(典型情况下是一个,也可以是多继承),当进行想下类型转换时,通常会有多种选择进行。C++提供了一个特殊的称为dynamic_cast的显式类型转换,它是一种安全类型向下类型转换的操作。当使用dynamic_cast向下转换成一个特殊的类型时,只有类型转换是正确并且成功时,返回值是一个所需类型的指针,否则为0。

#include <iostream>
using namespace std;

class Pet{
public:
    virtual ~Pet(){}
};

class Dog:public Pet{};

class Cat:public Pet{};

int main(){
    Pet *p  = new Cat;  //Upcast
    //Try to cast it to Dog
    Dog *d1 = dynamic_cast<Dog*>(p);
    //Try to cast it back to Cat
    Cat *c1 = dynamic_cast<Cat*>(p);
    cout << "p = "<< (long)p << endl;
    cout << "d1 = "<< (long)d1 << endl;
    cout << "c1 = "<< (long)c1 << endl;
    return 0;
}

运行结果:

p = 7019896
d1 = 0
c1 = 7019896

当使用dynamic_cast操作一个指针,他必须属于一个多态的类层次———它必须含有虚函数,因为dynamic_cast使用了存放在对象头部的VPTR即VTABLE信息来判断实际的类型。不正确的向下类型转换返回0,所以,无论何时进行向下类型转换都要检查返回值为非0。但我们不用确保指针的值完全一样,因为通常在向上类型转换和向下类型转换的时候指针会进行调整。

YOLOv11改进策略【Head/分割头】| 结合CVPR-2024 中的DynamicConv 动态卷积 改进分割头, 优化模型(独家改进) 在大规模视觉预训练中,通常模型的性能受到数据、参数和FLOP三个关键因素的影响。一般来说,模型的参数数量越多,FLOP也越高,但在移动设备等对计算资源有限制的场景下,需要低FLOP的模型同时又希望模型能从大规模预训练中受益。传统的方法很难在增加参数的同时保持低FLOP,因此需要一种新的设计来解决这个问题,模块应运而生。 阅读详情

相关推荐

Crossbar RRAM技术解析:从1TnR架构到存储革命

非易失性存储器(NVM)是数据存储的核心技术,其原理基于物理状态的稳定改变以实现数据持久化。阻变存储器(RRAM)作为一种新兴NVM,利用导电细丝在介质层中的形成与断裂实现数据存储,具有高速、低功耗和结构简单的优势。在工程实践中,RRAM面临潜行路径电流等挑战,而Crossbar提出的1TnR架构通过引入共享晶体管有效解决了这一问题,在保证性能的同时提升了集成密度。该技术与CMOS工艺兼容,支持后端集成与多层堆叠,为高密度存储和嵌入式应用提供了新的解决方案。结合当前行业对高能效、高密度存储的需求,RRAM在

weixin_42523104的博客 319

C++编程思想 第1卷 第15章 多态性和虚函数 虚函数

为了特定的函数,为了引起晚捆绑,C++要求在基类中声明这个函数时使用 virtual关键字。 为了创建一个像virtual这样的成员函数,可以简单地在这个函数声明的前面 加上关键字virtual 注意,仅需要在基类中声明一个函数为virtual。调用所有匹配基类声明行为 的派生类函数都将使用虚机制 在基类中的play()之前增加virtual关键字 //: C15:Instrument3...

eyetired的博客 208

Cadence AMS数模混合仿真:从零搭建高效验证环境的实战指南

本文是Cadence AMS数模混合仿真的实战指南,详细介绍了如何从零搭建高效的芯片验证环境。内容涵盖环境准备、Testbench创建、核心Config视图与ADE设置、仿真运行调试及性能加速技巧,旨在帮助工程师解决SoC设计中数字与模拟模块的集成验证难题,确保芯片设计一次成功。

weixin_29164091的博客 320

C++虚函数调用的汇编过程

下面的程序改编自《Thinking in C++》的Instrument4.cpp#include using namespace std;enum note { middleC, Csharp, Cflat }; // Etc.class Instrument { public: virtual void play(note)

kenby的专栏 1247

手把手教你复现KDD 2024时空预测顶会论文:以UniST和ImputeFormer为例的代码实战与避坑指南

本文详细指导如何复现KDD 2024顶会论文中的UniST和ImputeFormer模型,涵盖环境配置、数据处理、模型训练及评估等关键步骤。通过实战案例和避坑指南,帮助研究者掌握时空预测与插补技术,提升在智慧城市、交通管理等领域的应用能力。

weixin_29204205的博客 279

TB6600驱动器共阴共阳接法傻傻分不清?一张图搞定STM32与42步进电机的接线避坑指南

本文详细解析了TB6600驱动器共阴共阳接法的本质区别,提供了STM32与42步进电机的高效连接方案。通过实际案例和接线图示,帮助读者避免常见错误,确保驱动器与MCU的安全连接,提升项目稳定性。

weixin_28710999的博客 244

2019-11-25

#include<iostream> using namespace std; enum note {middleC,Csharp,Eflat}; class instrument{ int a; //int b; /*int c; int d; int e; int f;*/ public: virtual void p...

u012248443的博客 235

封神台靶场,为了小芳,冲之——cookie注入

第二个靶场 打开网站 找一下注入点 一般在新闻这种里面点开看看 先判断是否存在注入 有waf 尝试了很多方法绕过都没成功,/and/,aandnd,AnD等一系列都没绕过去,乌鸡鲅鱼 删掉id=169 发现 然后判断cookie注入 在搜索框输入 Javascript:alert(document.cookie=”id=”+escape(‘169’)); 然后弹窗了(其中 “id=” 取决URL中的id= escape中的数值也是URL中id=的值)返回刚刚的页面,把ID删除后一样能正常显示。说明服务器能

Wubuqing的博客 652

C++虚函数

C++纯虚函数&&抽象类

755

银河麒麟操作系统最佳实践专栏介绍

本专栏聚焦国产银河麒麟操作系统(桌面版/服务器版)的实战应用,提供系统安装、配置优化、应用部署、故障排查等实用指南。内容基于真实操作场景,涵盖Linux通用技术(如RAID配置、环境变量管理)、服务器系统专项(如数据库迁移、驱动安装、安全加固)及桌面系统技巧(如PXE安装、镜像封装)。面向IT运维、开发者和国产化转型人员,突出实战性和差异化,帮助用户规避常见问题,提升系统管理效率。所有教程均配有详细步骤和截图,确保可操作性,并持续更新适配新版系统。

信创运维工程师 · 银河麒麟 & 容器实战 | 金融生产环境迁移·等保加固 | 142篇原创·17万+阅读 532

Linux内存管理(17):percpu 分配器——基本原理

随着 SMP(对称多处理器架构)的发展,在享受处理器更快的执行效率的同时,也为数据同步带来了更大的挑战,因为在 SMP 架构中,程序确实是在并发执行,从单 CPU 只需要考虑调度风险,到SMP 中还需要考虑多 CPU 中同时执行的风险,而且在单 CPU 下乱序执行也会在 SMP 架构中带来,从而也衍生了一些新的同步机制,例如:内存屏障、percpu 机制等等。

私房菜 1249

【ThinkingInC++】67、多态性和虚函数

第十五章 多态性和虚函数 /** * 书本:【ThinkingInC++】 * 功能:纯抽象类 * 时间:2014年10月6日13:10:28 * 作者:cutter_point */ #include using namespace std; enum note {middleC, Csharp, Cflat}; //创建一个抽象类 //基类 class Instrument { p

cutter_point的专栏 897

Apache DolphinScheduler 实现了对 OceanBase 的元数据库支持

Apache DolphinScheduler 是一款开源的分布式工作流任务调度系统,它提供了可视化的工作流设计和调度功能,支持大数据任务的自动化调度和监控。最近的更新中,Apache DolphinScheduler 引入了对 OceanBase 数据库的元数据库支持,这为用户在大数据环境中管理和调度任务提供了更多选择。用户可以通过 DolphinScheduler 提供的可视化界面进行工作流的设计和调度,也可以通过 DolphinScheduler 提供的 API 进行任务的编程调度。

VavzNvml的博客 495

什么是多态

目录 什么是多态 多态的特点 多态存在的三个必要条件 多态时成员变量、静态方法和非静态方法的特点 什么是多态 多态就是一个对象变量可以指向多种实际类型的现象,同一个事件发生在不同的对象上会产生不同的结果。 多态的特点 好处:消除类型之间的耦合关系 弊端:不能用子类方法,可用转型的方法解决 转型: ·向上转型:【自动】父类 父类对象 = 子类实例; Fu f...

weixin_43827227的博客 1万+

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Java中编译时多态与运行时多态

文章目录1. 什么是多态2. 编译时多态3. 运行时多态总结Reference 口诀:成员变量,静态方法看左边;非静态方法:编译看左边,运行看右边 当父类变量引用子类对象时(Father f = new Son();),在这个引用变量f指向的对象中,它的成员变量和静态方法与父类是一致的,他的非静态方法,在编译时是与父类一致的,运行时却与子类一致(发生了复写) 1. 什么是多态 多态是指允许不同子类型的对象对同一行为作出不同的响应。例如在生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞

xylitolz的博客 9380

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

C++多态——静态多态与动态多态

多态:顾名思义,多态就是多种形态,也就是对不同对象发送同一个消息,不同对象会做出不同的响应。并且多态分为静态多态和动态多态。静态多态就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数,例如:函数的重载,对象名加点操作符执行成员函数等,都是静态多态,其中,重载是在形成符号表的时候,对函数名做了区分,从而确定了程序执行到这里将要执行哪个函数,对象名加点操作符执行成员函数是通过this指针来调用的

stay_the_course的博客 1万+

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

java 多态的应用——多态数组和多态参数详解

java 多态的应用——多态参数和多态数组内容分享,本篇博文为java 面向对象三大特性——多态篇的补充。

优快云 / 稀土掘金/ B站 / GitHub: Cyan_RA9 3837

多态 静态多态 动态多态

抽象类提供一个框架,强制子类实现特定的功能。

weixin_43490759的博客 2936

C++ 多态全解析:静态多态与动态多态详解

本文将系统梳理多态的定义、官方权威解释、静态与动态多态的区别、典型代码与注意事项,帮助你彻底搞懂C++多态。

Darker的博客 2325

多态性

多态性 多态性的概念 派生一个类的原因并非总是为了继承或添加新成员,有时是为了重新定义基类的成员,使基类成员“获得新生” 面向对象程序设计的真正力量不仅仅是继承,而是允许派生类对象像基类对象一样处理,其核心机制就是多态和动态联编 多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数 从广义上说,多态性是指一段程序能够处理多种类型对象的能力 在C++中,这种多态性可以通过重载多态(函数和运算符重载),强制多态(类型强制转换),

weixin_51958878的博客 5264

Java多态:多态多态,多种形态

Java多态:灵活处理对象的行为展现机制

子夜的个人博客 1742

面向对象设计领域中的参数多态,包含多态,过载多态和强制多态

在面向对象设计中,多态性是一个重要的概念,它允许我们以一种统一的方式处理不同类型的对象,并根据对象的实际类型来执行相应的行为。通过将具体的对象看作是其父类或接口的实例,并在方法调用时根据实际的对象类型来确定具体的执行逻辑,我们可以以一种统一的方式处理不同类型的对象。- 当方法需要操作的对象类型不确定时,可以使用多态来接受不同类型的对象,减少代码的重复。使用场景: - 当需要执行类似的操作,但操作的参数类型或个数可能不同时,可以使用过载多态来定义多个方法,使得代码更清晰、简洁,并提高代码的可读性。

辣香牛肉面的博客 1468

参数多态、包含多态、过载多态和强制多态

多态分为两种:通用的多态和特定的多态。两者的区别是前者对工作的类型不加限制,允许对不同类型的值执行相同的代码;后者只对有限数量的类型有效,而且对不同类型的值可能要执行不同的代码。     通用多态又分为参数多态(parametric)和包含多态(Inclusion Polymorphism);特定多态分为过载多态(overloading)和强制多态(coercion)。     强制多态(

飞老虎 1万+

静态多态和动态多态的区别

#include <iostream> using namespace std; class A { public: virtual void function() { cout << "I am A" << endl; } }; class B :public A { public: void function() { cout << "I used to be A but now I am B" << endl; } void fun

weixin_45027171的博客 6918

多态以及多态底层的实现原理

1.必须是基类的指针或者引用去调用虚函数2.虚函数必须完成了重写或者覆盖因为我们前面所将的切片的类型兼容转换,只有基类的指针或者引用才能即指向基类对象又指向派生类对象.虚函数的重写或者覆盖所指的是它的实现重写,这样基类和派生类才能有不同的函数.才能实现多态.class Apublic:public:int main()ptr1->a();ptr2->a();return 0;以上就是多态的实现.

20 • backend dev • building things that just work • learning systems & low level stuff • love video games 943

编译时多态和运行时多态

编译时多态和运行时多态 多态分为两种情况:编译时多态和运行时多态。如果在编译时能够确定执行多态方法称为编译时多态,否则称为运行时多态。 一、编译时多态 方法重载都是编译时多态。根据实际参数的数据类型、个数和次序,Java在编译时能够确定执行重载方法。 方法覆盖表现出两种多态性,当对象引用本类实例时,为编译时多态,否则为运行时多态。例如,以下声明p、m引用本类实例,调用toString()方法是编译时多态。 public class Test { public static void

twx31420的博客 4997
上一篇: 句柄类
下一篇: GPIO
InvokerZhang
博客等级 码龄11年 9粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值