C++虛函數(shù)

C++虛函數(shù)

C++虛函數(shù)是多態(tài)性實(shí)現(xiàn)的重要方式,當(dāng)某個(gè)虛函數(shù)通過(guò)指針或者引用調(diào)用時(shí),編譯器產(chǎn)生的代碼直到運(yùn)行時(shí)才能確定到底調(diào)用哪個(gè)版本的函數(shù)。被調(diào)用的函數(shù)是與綁定到指針或者引用上的對(duì)象的動(dòng)態(tài)類(lèi)型相匹配的那個(gè)。因此,借助虛函數(shù),我們可以實(shí)現(xiàn)多態(tài)性。這也是OOP的核心思想之一。

引言

考慮下面一個(gè)繼承的例子,Dog類(lèi)與Cat類(lèi)都繼承自Animal類(lèi),但是它們擁有不同的speak()方法:

class Animal
{
public:
    Animal(const string& name):
        m_name{name}
    {}

    const string& getName() const
    {
        return m_name;
    }

    string speak() const
    {
        return "???";
    }

private:
    string m_name;
};

class Cat : public Animal
{
public:
    Cat(const string& name): 
        Animal(name)
    {}

    string speak() const
    {
        return "Meow";
    }
};

class Dog : public Animal
{
public:
    Dog(const string& name):
        Animal(name)
    {}

    string speak() const
    {
        return "Woof";
    }
};

我們知道派生類(lèi)對(duì)象可以賦值給基類(lèi)的指針或者引用,但是我們希望調(diào)用這些指針或者引用時(shí),能夠調(diào)用各個(gè)派生類(lèi)自己的方法,比如下面的例子:

int main()
{
    Cat cat{ "Fred" };
    cout << "Cat is named " << cat.getName() << ", and it says " << cat.speak() << endl;
    
    Dog dog{ "Carbo" };
    cout << "Dog is named " << dog.getName() << ", and it says " << dog.speak() << endl;

    Animal* catAnimal = &cat;
    cout << "Cat is named " << catAnimal->getName() << ", and it says " << catAnimal->speak() << endl;

    Animal& dogAnimal = dog;
    cout << "Dog is named " << dogAnimal.getName() << ", and it says " << dogAnimal.speak() << endl;

        return 0;
}

但是輸出并不是預(yù)期的那樣:

cat is named Fred, and it says Meow
dog is named Garbo, and it says Woof
pAnimal is named Fred, and it says ???
pAnimal is named Garbo, and it says ???

無(wú)論是指針還是引用,它們都沒(méi)有調(diào)用其派生對(duì)象所重寫(xiě)的方法,而是基類(lèi)原有的方法。大家可能會(huì)想,為什么我非要將派生類(lèi)對(duì)象賦值給基類(lèi)的指針或者引用來(lái)調(diào)用派生類(lèi)的方法?直接利用派生類(lèi)對(duì)象調(diào)用不就可以了嗎?這樣做有很多好處,比如你想使用一個(gè)函數(shù),接收一個(gè)動(dòng)物對(duì)象類(lèi),然后打印其名字以及叫聲。但是由于這樣的動(dòng)物類(lèi)有兩個(gè),你必須利用重載的思想實(shí)現(xiàn)兩個(gè)版本:

void print(Cat& cat)
{
    cout << cat.getName() << " says " << cat.speak() << endl;
}

void print(Dog& dog)
{
    cout << dog.getName() << " says " << dog.speak() << endl;
}

兩個(gè)版本實(shí)現(xiàn)起來(lái)并沒(méi)有那么麻煩,但是如果動(dòng)物類(lèi)的種類(lèi)更多呢?這個(gè)時(shí)候你就有點(diǎn)不樂(lè)意了,僅僅是對(duì)象類(lèi)型不同,但是方法是相同的,為什么不能僅寫(xiě)一個(gè)版本:

void print(Animal& animal)
{
    cout << animal.getName() << " says " << animal.speak() << endl;
}

如果基類(lèi)能夠動(dòng)態(tài)確定其實(shí)際所指向的派生類(lèi)對(duì)象,并調(diào)用合適版本的方法,那么一個(gè)函數(shù)就可以解決上面的問(wèn)題。

看來(lái)盡管每個(gè)派生類(lèi)都有自己實(shí)現(xiàn)的speak()方法,但是它們實(shí)際上并沒(méi)有真正的重寫(xiě)基類(lèi)方法,僅僅是隱藏。因?yàn)榕缮?lèi)對(duì)象傳遞給基類(lèi)的指針或者引用并沒(méi)有調(diào)用派生類(lèi)版本的方法,依然是基類(lèi)方法。

所以,你需要虛函數(shù)!

虛函數(shù)與多態(tài)性

虛函數(shù)是類(lèi)方法中的一種特殊函數(shù),當(dāng)你調(diào)用它時(shí),它會(huì)匹配派生最遠(yuǎn)的重寫(xiě)版本。這種特性是多態(tài)性。匹配的規(guī)則是相同的函數(shù)簽名(函數(shù)名,參數(shù)個(gè)數(shù)與類(lèi)型)以及返回類(lèi)型(返回類(lèi)型可以不相同,但必須存在派生關(guān)系)。虛函數(shù)僅需要再前面加上一個(gè)virtual關(guān)鍵字即可,利用虛函數(shù)我們可以修改上面的代碼:

class Animal
{
public:
    Animal(const string& name):
        m_name{name}
    {}

    const string& getName() const
    {
        return m_name;
    }

    virtual string speak() const
    {
        return "???";
    }

private:
    string m_name;
};

class Cat : public Animal
{
public:
    Cat(const string& name): 
        Animal(name)
    {}

    virtual string speak() const
    {
        return "Meow";
    }
};

class Dog : public Animal
{
public:
    Dog(const string& name):
        Animal(name)
    {}

    virtual string speak() const
    {
        return "Woof";
    }
};

此時(shí),再測(cè)試一下下面的代碼,可以看到輸出實(shí)現(xiàn)了預(yù)期的效果:

int main()
{
    Cat cat{ "Fred" };
    cout << "Cat is named " << cat.getName() << ", and it says " << cat.speak() << endl;
    
    Dog dog{ "Carbo" };
    cout << "Dog is named " << dog.getName() << ", and it says " << dog.speak() << endl;

    Animal* catAnimal = &cat;
    cout << "Cat is named " << catAnimal->getName() << ", and it says " << catAnimal->speak() << endl;

    Animal& dogAnimal = dog;
    cout << "Dog is named " << dogAnimal.getName() << ", and it says " << dogAnimal.speak() << endl;

        return 0;
}
output:
cat is named Fred, and it says Meow
dog is named Garbo, and it says Woof
cat is named Fred, and it says Meow
dog is named Garbo, and it says Woof

可以看到,不論是基類(lèi)版本還是派生類(lèi)版本,我們都在函數(shù)前面使用了virtual關(guān)鍵字,事實(shí)上,派生類(lèi)中的virtual關(guān)鍵字并不是必要的。一旦基類(lèi)中的方法打上了virtual標(biāo)簽,那么派生類(lèi)中匹配的函數(shù)也是虛函數(shù)。但是,還是建議在后面的派生類(lèi)中加上virtual關(guān)鍵字,作為虛函數(shù)的一種提醒,以便后面可能還會(huì)有更遠(yuǎn)的派生。

注意千萬(wàn)不要在構(gòu)造函數(shù)與析構(gòu)函數(shù)中調(diào)用虛函數(shù)。我們知道派生類(lèi)對(duì)象在創(chuàng)建時(shí),首先基類(lèi)部分先被創(chuàng)建,如果你在基類(lèi)構(gòu)造函數(shù)調(diào)用虛函數(shù)時(shí),它此時(shí)將無(wú)法調(diào)用派生類(lèi)版本的函數(shù),因?yàn)榕缮?lèi)對(duì)象還未創(chuàng)建,此時(shí)派生類(lèi)虛函數(shù)沒(méi)有作用的對(duì)象。那么,它只能調(diào)用基類(lèi)版本的虛函數(shù)。對(duì)于析構(gòu)函數(shù),派生類(lèi)對(duì)象中的派生部分先被析構(gòu),如果你在基類(lèi)析構(gòu)函數(shù)中調(diào)用了虛函數(shù),它也只能調(diào)用基類(lèi)版本的虛函數(shù),因?yàn)榕缮?lèi)對(duì)象已經(jīng)不存在了。

到底什么時(shí)候使用虛函數(shù)?大部分時(shí)候,我們希望派生類(lèi)是真正的“重寫(xiě)”基類(lèi)函數(shù),而不是“隱藏”。所以一般建議將所有方法都聲明為virtual。既然如此,為什么編譯器不默認(rèn)這樣做呢,其實(shí)對(duì)于Java語(yǔ)言來(lái)說(shuō),所有的方法默認(rèn)是虛函數(shù)。但是使用虛函數(shù)是有代價(jià)的,相對(duì)于普通函數(shù),虛函數(shù)的調(diào)用代價(jià)稍高,但是這種差別不會(huì)太大,所以還是建議所有方法都使用virtual關(guān)鍵字。

override標(biāo)識(shí)符

前面說(shuō)到,派生類(lèi)的重寫(xiě)方法必須與基類(lèi)方法要匹配,否則編譯器會(huì)認(rèn)為派生類(lèi)創(chuàng)建了一個(gè)新方法,而不是重寫(xiě)基類(lèi)的版本,看下面的例子:

class Super
{
public:
    virtual string getName1(int x)
    {
        return "Super";
    }

    virtual string getName2(int x)
    {
        return "Super";
    }
};

class Sub: public Super
{
public:
    virtual string getName1(double x)
    {
        return "Sub";
    }
    
    virtual string getName2(int x) const
    {
        return "Sub";
    }
};

int main()
{
    Sub sub;
    Super* super = ?
    
    cout << super->getName1(1) << endl;  // output: Super
    cout << super->getName2(2) << endl;  // output: Super

    cin.ignore(10);
    return 0;
}

可以看到,派生類(lèi)的兩個(gè)虛方法并沒(méi)有重寫(xiě)基類(lèi)版本,這是由于兩個(gè)方法的函數(shù)簽名并不一樣。所以將派生類(lèi)對(duì)象賦值給基類(lèi)的指針只能是調(diào)用基類(lèi)方法。但是,實(shí)際上我們希望派生類(lèi)的兩個(gè)方法是重寫(xiě)版本。有時(shí)候,我們很容易犯一些小錯(cuò)誤導(dǎo)致重寫(xiě)失敗,比如上面的例子。還有時(shí)候,我們修改了基類(lèi)虛函數(shù),但是沒(méi)有更新派生類(lèi)的對(duì)應(yīng)重載版本,也將有可能使重寫(xiě)失效。為了避免這樣的錯(cuò)誤,C++引入了override標(biāo)識(shí)符,使用這個(gè)標(biāo)識(shí)符告訴編譯器這是重寫(xiě)的方法,如果方法不匹配,那么將無(wú)法通過(guò)編譯。用override修改代碼如下:

class Super
{
public:
    virtual string getName1(int x)
    {
        return "Super";
    }

    virtual string getName2(int x)
    {
        return "Super";
    }
};

class Sub: public Super
{
public:
    virtual string getName1(double x) override
    {
        return "Sub";
    }
    
    virtual string getName2(int x) const override
    {
        return "Sub";
    }
    // 此時(shí)無(wú)法編譯
};

所以,只要重寫(xiě)基類(lèi)方法,建議使用override標(biāo)識(shí)符,避免無(wú)意的錯(cuò)誤。

final標(biāo)識(shí)符

有時(shí)候,你不想派生類(lèi)重寫(xiě)基類(lèi)的虛方法,此時(shí)可以使用final標(biāo)識(shí)符,這個(gè)時(shí)候如果派生類(lèi)重寫(xiě)了基類(lèi)虛方法,那么將無(wú)法編譯:

class A
{
public:
    virtual void someMethod() { cout << "A" << endl; }
}

class B: public A
{
public:
    // 基類(lèi)A的someMethod方法沒(méi)有final標(biāo)識(shí)符,那么B可以重寫(xiě)該方法
    // 但是此虛方法使用了final標(biāo)識(shí)符,后面的派生類(lèi)無(wú)法重寫(xiě)
    virtual void someMethod() override final { cout << "B" << endl; }
}

class C: public B
{
public:
    // 無(wú)法編譯,因?yàn)椴辉试S重寫(xiě)
    virtual void someMethod() override { cout << "C" << endl; }
}

而且final標(biāo)識(shí)符還可以直接用于類(lèi),此時(shí)該類(lèi)將不能被繼承:

class A
{
public:
    virtual void someMethod() { cout << "A" << endl; }
};

// B可以繼承A
class B final: public A
{
public:
    virtual void someMethod() override { cout << "B" << endl; }
};

// B無(wú)法被繼承,此時(shí)無(wú)法編譯
class C: public B
{
public:
    virtual void someMethod() override { cout << "C" << endl; }
};

協(xié)變返回類(lèi)型

前面說(shuō)過(guò),要想成功重寫(xiě)方法,基類(lèi)虛方法與派生類(lèi)虛方法必須匹配,其中返回類(lèi)型也必須一致。但是有時(shí)候返回類(lèi)型不相同,也能實(shí)現(xiàn)重寫(xiě),此時(shí)返回類(lèi)型存在繼承關(guān)系:基類(lèi)方法返回類(lèi)型是一個(gè)指向某一類(lèi)的指針或者引用,而派生類(lèi)重寫(xiě)版本的返回類(lèi)型是指向派生類(lèi)的指針或者引用。這種情況稱為協(xié)變返回類(lèi)型。下面是一個(gè)例子:

class Super
{
public:
    virtual Super* getThis() { return this; }
};

class Sub : public Super
{
    virtual Sub* getThis() override { return this; }
};

析構(gòu)函數(shù)要聲明為虛函數(shù)

對(duì)于析構(gòu)函數(shù),大部分時(shí)間我們只需要使用編譯器提供的默認(rèn)版本就好,除非涉及到釋放動(dòng)態(tài)分配的內(nèi)存。但是如果存在繼承,虛函數(shù)最好聲明為虛函數(shù)。否則刪除一個(gè)實(shí)際指向派生類(lèi)的基類(lèi)指針,只會(huì)調(diào)用基類(lèi)的析構(gòu)函數(shù),而不會(huì)調(diào)用派生類(lèi)的析構(gòu)函數(shù)以及派生類(lèi)數(shù)據(jù)成員的析構(gòu)函數(shù)。這樣就可能造成內(nèi)存泄露,看下面的例子:

class Resource
{
public:
    Resource() { cout << "Resource created!" << endl; }
    ~Resource() { cout << "Resource destoryed!" << endl; }
};

class Super
{
public:
    Super() { cout << "Super constructor called!" << endl; }
    ~Super() { cout << "Super destructor called!" << endl; }
};

class Sub : public Super
{
public:
    Sub() { cout << "Sub constructor called!" << endl;}

    ~Sub() { cout << "Sub destructor called!" << endl;}
private:
    Resource res;
};

如果執(zhí)行下面的代碼:

int main()
{
    Sub* sub = new Sub;
    Super* super = sub;
    delete super;

    cin.ignore(10);
        return 0;
}

其輸出為:

Super constructor called!
Resource created!
Sub constructor called!
Super destructor called!

可以看到,派生類(lèi)的析構(gòu)函數(shù)沒(méi)有執(zhí)行,其數(shù)據(jù)成員Resource也沒(méi)有被析構(gòu)。但是如果你將析構(gòu)函數(shù)都聲明為虛函數(shù),上面的代碼將得到如下的結(jié)果:

Super constructor called!
Resource created!
Sub constructor called!
Resource destoryed!
Sub destructor called!
Super destructor called!

此時(shí),程序按照預(yù)期輸出,所以,對(duì)于繼承問(wèn)題,沒(méi)有理由不將析構(gòu)函數(shù)聲明為虛函數(shù)!

函數(shù)調(diào)用捆綁

要想深刻理解虛函數(shù)機(jī)理,首先要了解函數(shù)調(diào)用捆綁機(jī)制。捆綁指的是將標(biāo)識(shí)符(如變量名與函數(shù)名)轉(zhuǎn)化為地址。這里我們僅僅關(guān)注有關(guān)函數(shù)調(diào)用的捆綁。我們知道每個(gè)函數(shù)在編譯的過(guò)程中是存在一個(gè)唯一的地址的。如果我們?cè)诔绦蚨卫锩嬷苯诱{(diào)用某個(gè)函數(shù),那么編譯器或者鏈接器會(huì)直接將函數(shù)標(biāo)識(shí)符替換為一個(gè)機(jī)器地址。這種方式是早捆綁,或者說(shuō)是靜態(tài)捆綁。因?yàn)槔壥窃诔绦蜻\(yùn)行之前完成的。看下面的簡(jiǎn)單例子:

int add(int x, int y)
{
    return x + y;
}

int subtract(int x, int y)
{
    return x - y;
}

int multiply(int x, int y)
{
    return x * y;
}

int main()
{
    int x;
    cout << "Enter a number: ";
    cin >> x;

    int y;
    cout << "Enter another number: ";
    cin >> y;

    int op;
    cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
    cin >> op;

    int result;
    switch (op)
    {
        // 使用早綁定來(lái)直接調(diào)用函數(shù)
        case 0: result = add(x, y); break;
        case 1: result = subtract(x, y); break;
        case 2: result = multiply(x, y); break;
    }

    cout << "The answer is: " << result << endl;

        return 0;
}

由于上面三個(gè)函數(shù)的調(diào)用都是直接使用函數(shù)名,采用早捆綁的方式。編譯器會(huì)將每個(gè)函數(shù)調(diào)用替換為一個(gè)跳轉(zhuǎn)指令,這個(gè)指令告訴CPU跳轉(zhuǎn)到函數(shù)的地址來(lái)執(zhí)行。

但是有時(shí)候,我們?cè)诔绦蜻\(yùn)行前并不知道調(diào)用哪個(gè)函數(shù),此時(shí)必須使用晚捆綁或者動(dòng)態(tài)捆綁。晚綁定的一個(gè)例子就是使用函數(shù)指針,修改上面的例子:

int main()
{
    int x;
    cout << "Enter a number: ";
    cin >> x;

    int y;
    cout << "Enter another number: ";
    cin >> y;

    int op;
    cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
    cin >> op;

    // 定義一個(gè)函數(shù)指針
    int(*opFun)(int, int) = nullptr;
    switch (op)
    {
        // 使用早捆綁來(lái)直接調(diào)用函數(shù)
        case 0: opFun = add; break;
        case 1: opFun = subtract; break;
        case 2: opFun = multiply; break;
    }

    // 通過(guò)函數(shù)指針來(lái)調(diào)用,只能是晚捆綁
    cout << "The answer is: " << opFun(x, y) << endl;

        return 0;
}

使用函數(shù)指針來(lái)間接調(diào)用函數(shù),編譯器在編譯階段并不知道函數(shù)指針到底指向哪個(gè)函數(shù),所以必須使用動(dòng)態(tài)捆綁的方式。

動(dòng)態(tài)綁定看起來(lái)更靈活,但是其是有代價(jià)的。靜態(tài)捆綁時(shí),CUP可以直接跳轉(zhuǎn)到函數(shù)地址。但是動(dòng)態(tài)捆綁,CPU必須先提取指針的地址,然后再跳轉(zhuǎn)到指向的函數(shù)地址。這多了一個(gè)步驟!

虛函數(shù)表(Vtable)

C++使用了一種稱為“虛表”的晚捆綁技術(shù)來(lái)實(shí)現(xiàn)虛函數(shù)。虛表是一個(gè)函數(shù)查詢表,以動(dòng)態(tài)捆綁的方式解析函數(shù)調(diào)用。每個(gè)具有一個(gè)或者多個(gè)虛函數(shù)的類(lèi)都有一張?zhí)摫?,這個(gè)表是在編譯階段建立的靜態(tài)數(shù)組,其中包含了每個(gè)虛方法的函數(shù)指針,這些指針指向的是該類(lèi)可見(jiàn)的派生最遠(yuǎn)的函數(shù)實(shí)現(xiàn)。其次,編譯器會(huì)在基類(lèi)對(duì)象都會(huì)添加一個(gè)隱含指針,這里我們稱為*__vptr。這個(gè)指針當(dāng)然能夠被派生類(lèi)所繼承,這相當(dāng)重要。當(dāng)類(lèi)的實(shí)例被創(chuàng)建時(shí),這個(gè)指針指向該類(lèi)所對(duì)應(yīng)的虛表。這樣,當(dāng)使用某個(gè)對(duì)象調(diào)用虛方法時(shí),通過(guò)該指針查找虛表,然后根據(jù)實(shí)際的對(duì)象類(lèi)型執(zhí)行正確版本的方法調(diào)用??聪旅娴暮?jiǎn)單例子:

class Base
{
public:
    virtual void function1() { }
    virtual void function2() { }
}

class D1: public Base
{
public:
    virtual void function1() override { }
}

class D2: public Base
{
public:
    virtual void function2() override { }
}

上面包含3個(gè)類(lèi),其中派生類(lèi)D1與D2分別重寫(xiě)了基類(lèi)的function1()和function2()虛方法。編譯器會(huì)相應(yīng)地創(chuàng)建3個(gè)不同的虛表,分別對(duì)應(yīng)每個(gè)類(lèi)。而且編譯器也會(huì)自動(dòng)地為基類(lèi)添加一個(gè)函數(shù)指針,如下所示:

class Base
{
public:
    FunctionPointer *__vptr;
    virtual void function1() { }
    virtual void function2() { }
}

class D1: public Base
{
public:
    virtual void function1() override { }
}

class D2: public Base
{
public:
    virtual void function2() override { }
}

這樣,每個(gè)類(lèi)實(shí)例創(chuàng)建時(shí),*__vptr將指向該類(lèi)所對(duì)應(yīng)的虛表,比如基類(lèi)的一個(gè)實(shí)例創(chuàng)建時(shí),這個(gè)指鎮(zhèn)就指向基類(lèi)的虛表。

下面我們看看每個(gè)類(lèi)的虛表是怎么建立的。因?yàn)閮H有兩個(gè)虛方法,所以每個(gè)虛表僅包含兩個(gè)函數(shù)指針,分別對(duì)應(yīng)function1()和function2()。但是每個(gè)函數(shù)指針實(shí)際指向的是那個(gè)類(lèi)所可見(jiàn)的派生最遠(yuǎn)的函數(shù)實(shí)現(xiàn):

  • Base的虛表:因?yàn)锽ase的實(shí)例僅可見(jiàn)自己的成員,所以它的虛表中的指針?lè)謩e指向Base::function1()和Base::function2();
  • D1的虛表:D1的實(shí)例可見(jiàn)Base的成員與自身的成員,但是D1僅重寫(xiě)了function1(),所以虛表中的指針?lè)謩e指向D1::function1()和Base::function2();
  • D2的虛表:與D1類(lèi)似,分別指向Base::function1()和D2::function2()。

下面是具體的示意圖(來(lái)源:learncpp):

虛函數(shù)表

所以,下面的代碼就有了很好的解釋?zhuān)?/p>

int main()
{
    D1 d1;   // d1中的*__vptr指向類(lèi)D1的虛表
    Base *dPtr = &d1;  // dPtr對(duì)*__vptr是可見(jiàn)的,但是實(shí)際上其指向的是D1的虛表;
    dPtr->function1();  // 此時(shí)dPtr通過(guò)虛表查找,調(diào)用的是D1::function1()
}

使用虛表技術(shù),虛函數(shù)得以正確實(shí)現(xiàn)!從而實(shí)現(xiàn)多態(tài)性!

純虛函數(shù)與抽象基類(lèi)

有時(shí)候,基類(lèi)的某個(gè)虛方法并不需要實(shí)現(xiàn),但是希望派生類(lèi)能夠提供重寫(xiě)的版本。這個(gè)時(shí)候,你需要定義純虛函數(shù)。純虛函數(shù)在類(lèi)的定義中顯示說(shuō)明該方法不需要實(shí)現(xiàn),其作用在于指明派生類(lèi)必須要重寫(xiě)它。純虛函數(shù)的定義很簡(jiǎn)單:方法聲明后緊跟著=0。如果一個(gè)類(lèi)中至少含有一個(gè)純虛函數(shù),那么這個(gè)類(lèi)是抽象基類(lèi),因?yàn)檫@個(gè)類(lèi)無(wú)法實(shí)例化。當(dāng)繼承一個(gè)抽象類(lèi)時(shí),必須重寫(xiě)所有純虛函數(shù),否則繼承出來(lái)的類(lèi)也是一個(gè)抽象類(lèi)。下面演示例子:

class Animal
{
public:
    Animal(const string& name):
        m_name{name}
    {}

    const string& getName() const
    {
        return m_name;
    }

    virtual string speak() const = 0;  // 純虛函數(shù)
    // 因?yàn)榘粋€(gè)純虛方法,所以是抽象基類(lèi)

private:
    string m_name;
};

class Cat : public Animal
{
public:
    Cat(const string& name): 
        Animal(name)
    {}

    // 重寫(xiě)了純虛方法,所以Cat不是抽象類(lèi),可以實(shí)例化
    virtual string speak() const
    {
        return "Meow";
    }
};

// Dog沒(méi)有重寫(xiě)基類(lèi)的純虛方法,所以仍然無(wú)法實(shí)例化
class Dog : public Animal
{
public:
    Dog(const string& name):
        Animal(name)
    {}
};

int main()
{
    // Animal animal{"luly"}; // 無(wú)法編譯,因?yàn)槌橄蠡?lèi)無(wú)法實(shí)例化
    Cat cat{ "Sally" };      // 合法
    // Dog dog{ "Betsy" };      // 非法,抽象類(lèi)無(wú)法實(shí)例化

    // 下面的代碼可以運(yùn)行,因?yàn)榭梢灾赶蚩梢詫?shí)例化的派生類(lèi)對(duì)象
    Animal* aPtr = new Cat{ "Sally" }; 
    cin.ignore(10);
        return 0;
}

抽象類(lèi)至少包含一個(gè)純虛方法,抽象類(lèi)提供了一種禁止其他代碼直接實(shí)例化對(duì)象的方法,但是重寫(xiě)純虛方法的派生類(lèi)可以實(shí)例化。

接口類(lèi)

接口是一個(gè)抽象的概念,使用者只關(guān)注功能而不要求了解實(shí)現(xiàn)。一個(gè)接口類(lèi)可以看成一些純虛方法的集合,這意味著接口類(lèi)僅有定義功能,而沒(méi)有具體的實(shí)現(xiàn)。C++ 其實(shí)沒(méi)有單獨(dú)的接口概念,而在Java和C#等語(yǔ)言中接口是與類(lèi)相區(qū)別的。但是 C++ 仍然可以使用接口類(lèi)實(shí)現(xiàn)類(lèi)似的效果。有時(shí)候,我們也稱接口類(lèi)為純抽象類(lèi),因?yàn)檫@個(gè)類(lèi)中全是虛方法。下面是一個(gè)純抽象類(lèi)的例子:

// 樂(lè)器純抽象類(lèi)
class Instrument
{
public:
    virtual void play() const = 0;
    virtual string what() const = 0;
    virtual void adjust(int) = 0;
};

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

    virtual string what() const override
    {
        return "Wind";
    }

    virtual void adjust(int i) override {}
};

class Brass : public Instrument
{
public:
    virtual void play() const override
    {
        cout << "Brass: paly" << endl;
    }

    virtual string what() const override
    {
        return "Brass";
    }

    virtual void adjust(int i) override {}
};

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

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

int main()
{
    
    Wind wind;
    Brass brass;
    tune(wind);
    tune(brass);
    f(wind);
    f(brass);
        return 0;
}

可以看到Instrument是一個(gè)純抽象類(lèi),其只提供方法的聲明,具體卻沒(méi)有實(shí)現(xiàn)。但是它的兩個(gè)派生類(lèi)分別重寫(xiě)了這些純虛方法,因此可以實(shí)例化。并且兩個(gè)函數(shù)可以接收任意繼承了Instrument的類(lèi)實(shí)例對(duì)象。進(jìn)一步說(shuō),這兩個(gè)函數(shù)僅關(guān)注接收的對(duì)象是否提供了Instrument所要求的接口,但是不關(guān)注具體是怎么實(shí)現(xiàn)的。純抽象類(lèi)提供了更高級(jí)的抽象!這符合OOP的思想。

虛基類(lèi)

虛基類(lèi)主要是用來(lái)解決菱形層次結(jié)構(gòu)中的歧義基類(lèi)問(wèn)題。菱形層次結(jié)構(gòu)是多重繼承中的一個(gè)典例,還是例子說(shuō)話:

class PoweredDevice
{
public:
    PoweredDevice(int power)
    {
        cout << "PoweredDevice: " << power << endl;
    }

    virtual void reportError() { cout << "Error" << endl; }
};

class Scanner : public PoweredDevice
{
public:
    Scanner(int scanner, int power) :
        PoweredDevice(power)
    {
        cout << "Scanner: " << scanner << endl;
    }
};

class Printer : public PoweredDevice
{
public:
    Printer(int printer, int power) :
        PoweredDevice(power)
    {
        cout << "Printer: " << printer << endl;
    }
};

class Copier : public Scanner, public Printer
{
public:
    Copier(int scanner, int printer, int power):
        Scanner(scanner, power), Printer(printer, power)
    {}
};
int main()
{
    Copier copier(1, 2, 3);
    // output:
    // PoweredDevice: 3
    // Scanner : 1
    // PoweredDevice : 3
    // Printer : 2
    // 可以看到PoweredDevice被繼承了兩次

    // 無(wú)法編譯,有歧義,因?yàn)槔^承了兩個(gè)版本的PoweredDevice
    copier.reportError();   
        return 0;
}

上面的繼承關(guān)系有點(diǎn)復(fù)雜,但是畫(huà)出繼承圖譜(來(lái)源:learncpp)就很清晰了:

多重繼承圖譜

Scanner和Printer分別繼承了PoweredDevice類(lèi),然后Copier又同時(shí)繼承了Scanner和Printer類(lèi),我們實(shí)際希望Copier僅繼承一次PoweredDevice類(lèi),但是實(shí)際上Copier包含了兩個(gè)版本的PoweredDevice。所以可以看到,次PoweredDevice被構(gòu)造了兩次。而且更嚴(yán)重的是,PoweredDevice中沒(méi)有被重寫(xiě)的方法是無(wú)法調(diào)用的,因?yàn)榫幾g器會(huì)給出一個(gè)有歧義的錯(cuò)誤!解決這個(gè)錯(cuò)誤的方法很多,比如你可以在Copier類(lèi)中明確聲明繼承的版本:using Scanner::PoweredDevice::reportError;。但是這本質(zhì)上沒(méi)有解決多版本的繼承問(wèn)題。

此時(shí),你可以用虛基類(lèi),使用虛基類(lèi),只需要在繼承列表中加上virtual關(guān)鍵字:

class PoweredDevice
{
public:
    PoweredDevice(int power)
    {
        cout << "PoweredDevice: " << power << endl;
    }

    virtual void reportError() { cout << "Error" << endl; }
};

class Scanner : virtual public PoweredDevice
{
public:
    Scanner(int scanner, int power) :
        PoweredDevice(power)
    {
        cout << "Scanner: " << scanner << endl;
    }
};

class Printer : virtual public PoweredDevice
{
public:
    Printer(int printer, int power) :
        PoweredDevice(power)
    {
        cout << "Printer: " << printer << endl;
    }
};

class Copier : public Scanner, public Printer
{
public:
    // Note: 虛基類(lèi)是由派生最遠(yuǎn)的類(lèi)負(fù)責(zé)創(chuàng)建,所以,
    //       構(gòu)造函數(shù)初始化列表中需要增加虛基類(lèi)的構(gòu)造函數(shù)調(diào)用
    Copier(int scanner, int printer, int power):
        Scanner(scanner, power), Printer(printer, power),
        PoweredDevice(power)
    {}
    
};
int main()
{
    Copier copier(1, 2, 3);

    // 合法
    copier.reportError(); 

    // output:
    // PoweredDevice: 3
    // Scanner : 1
    // Printer : 2
    // 可以看到PoweredDevice繼承了一次
    
    return 0;
}

利用虛基類(lèi),可以解決上面多重繼承中歧義基類(lèi)問(wèn)題,基類(lèi)僅被繼承一次。但是要注意的是此時(shí)的虛基類(lèi)由派生最遠(yuǎn)的類(lèi)負(fù)責(zé)創(chuàng)建(可以看成該類(lèi)的直接基類(lèi)),因?yàn)镻oweredDevice并沒(méi)有無(wú)參構(gòu)造函數(shù),所以在Copier構(gòu)造函數(shù)初始化列表中必須加上PoweredDevice的有參構(gòu)造函數(shù)調(diào)用!

說(shuō)點(diǎn)題外話,盡管虛基類(lèi)可以解決多重繼承中的菱形層次結(jié)構(gòu),但是看起來(lái)還是很抽象與復(fù)雜。實(shí)際上,多重繼承本來(lái)就是一個(gè)很有爭(zhēng)議的話題,因?yàn)槭褂枚嘀乩^承會(huì)使得繼承體系變得復(fù)雜,而且產(chǎn)生一系列問(wèn)題,像Java和C#這類(lèi)語(yǔ)言,是不允許多重繼承的,但是其單獨(dú)提供了接口,類(lèi)可以繼承多個(gè)接口,這也相當(dāng)于多重繼承了。而且好處是接口的繼承相當(dāng)于組合,這也是比較推崇的!

對(duì)象切片

前面講過(guò),實(shí)現(xiàn)虛函數(shù)及多態(tài)性必須要用傳地址的方式(引用或者指針)。一般,地址具有相同的長(zhǎng)度,這意味著派生類(lèi)對(duì)象的地址與基類(lèi)對(duì)象的地址也是相同,盡管派生類(lèi)對(duì)象所占的內(nèi)存一般要高過(guò)基類(lèi)對(duì)象。所以,傳地址的方式不會(huì)導(dǎo)致類(lèi)型信息損失,進(jìn)而可以實(shí)現(xiàn)多態(tài)性。看下面的例子:

class Base
{
public:
    Base(int value):
        m_value{value}
    {}

    virtual string getName() const { return "Base"; }
    int getValue() const { return m_value; }
protected:
    int m_value;
};

class Derived: public Base
{
public:
    Derived(int value):
        Base(value)
    {}

    virtual string getName() const override { return "Derived"; }
}

int main()
{
    Derived derived{ 5 };
    cout << "derived is a " << derived.getName() << " with value " << derived.getValue() << endl;
    // output: derived is a Derived with value 5
    Base& ref = derived;
    cout << "ref is a " << ref.getName() << " with value " << ref.getValue() << endl;
    // output: ref is a Derived with value 5
    Base* ptr = &derived;
    cout << "ptr is a " << ptr->getName() << " with value" << ptr->getValue() << endl;
    // output: ptr is a Derived with value 5
    Base base = derived;
    cout << "base is a " << base.getName() << " with value " << base.getValue() << endl;
    // output: base is a Base with value 5
    return 0;
}

可以看到使用引用或者指針的方式,多態(tài)性都能夠?qū)崿F(xiàn),但是傳值的方式就存在問(wèn)題。當(dāng)我們將一個(gè)派生類(lèi)對(duì)象直接賦值給基類(lèi)對(duì)象時(shí),僅僅基類(lèi)的部分被復(fù)制,派生類(lèi)的那部分信息將丟失。我們稱這種現(xiàn)象為“對(duì)象切片”:對(duì)象丟失了自己原有的部分信息。使用對(duì)象本身并沒(méi)有問(wèn)題,但是處理不當(dāng),會(huì)造成很多問(wèn)題,看下面的例子:

int main()
{
    Derived d1{5};
    Derived d2{2};
    Base& b = d2;
    b = d1;   // 有隱患
    return 0;
}

上面的例子很簡(jiǎn)單,但是會(huì)有問(wèn)題:首先d2引用給b時(shí),b將指向d2,這沒(méi)有問(wèn)題。但是將d1的值直接賦值給b時(shí),會(huì)發(fā)生對(duì)象切片,只有d1的基類(lèi)部分復(fù)制給b。此時(shí),問(wèn)題來(lái)了,你會(huì)發(fā)現(xiàn)現(xiàn)在d2擁有d1的基類(lèi)部分與d2的派生部分,這顯得很混亂!所以,盡可能地別使用對(duì)象切片,否則你會(huì)麻煩不斷!

動(dòng)態(tài)轉(zhuǎn)型

前面的例子,我們都是將派生類(lèi)對(duì)象復(fù)制給基類(lèi)對(duì)象,不管是通過(guò)傳地址的方式還是對(duì)象切片方式。這些都是向上轉(zhuǎn)型——在類(lèi)層次中向上移動(dòng)。我們不禁會(huì)想,肯定會(huì)存在可以向下移動(dòng)的向下轉(zhuǎn)型。一般來(lái)說(shuō),派生類(lèi)包含基類(lèi)信息,所以向上轉(zhuǎn)型是容易的。但是,反過(guò)來(lái)可能會(huì)失??!因?yàn)闊o(wú)法保證基類(lèi)對(duì)象實(shí)際上存儲(chǔ)的是派生類(lèi)對(duì)象。看下面的例子:

void process(Base* ptr)
{
    Derived* derived = static_cast<Derived*>(ptr);
    // 后序處理
    // ...
}

process函數(shù)接收一個(gè)基類(lèi)指針,但是在內(nèi)部使用static_cast向下轉(zhuǎn)型為派生類(lèi)指針,然后進(jìn)行后序處理。如果送入process函數(shù)的指針實(shí)際上就是指向派生類(lèi)對(duì)象,那么上面的代碼是沒(méi)有問(wèn)題的。但是,如果僅僅傳入就是指向基類(lèi)對(duì)象的指針,或者指向其他派生類(lèi)的指針,那么函數(shù)內(nèi)部的轉(zhuǎn)型將存在問(wèn)題:由于static_cast在運(yùn)行時(shí)是不檢查對(duì)象實(shí)際類(lèi)型的,這將導(dǎo)致不可控行為!

為了解決這樣的隱患,C++引入了運(yùn)行時(shí)的動(dòng)態(tài)類(lèi)型轉(zhuǎn)化操作符dynamic_cast。dynamic_cast在運(yùn)行時(shí)檢測(cè)底層對(duì)象的類(lèi)型信息。如果類(lèi)型轉(zhuǎn)換沒(méi)有意義,那么它將返回一個(gè)空指針(對(duì)于指針類(lèi)型)或者拋出一個(gè)std::bad_cast異常(對(duì)于引用類(lèi)型)。所以,可以修改上面的代碼如下:

void process(Base* ptr)
{
    Derived* derived = dynamic_cast<Derived*>(ptr);
    if (derived == nullptr)
    {
        // 后序處理
        // ...
    }
}

盡管如此,向下轉(zhuǎn)型還是不推薦的,除非必要!

Reference

[1] cpp leraning online(本文按照該教程書(shū)寫(xiě),作者人很nice,可以直接留言).
[2] Marc Gregoire. Professional C++, Third Edition, 2016.
[3] cppreference
[4] Bruce Eckel, Chuck Allison. Thinking in C++, Second Edition, 2011.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 參考來(lái)源:知乎 定義一個(gè)函數(shù)為虛函數(shù),不代表函數(shù)為不被實(shí)現(xiàn)的函數(shù)。定義他為虛函數(shù)是為了允許用基類(lèi)的指針來(lái)調(diào)用子類(lèi)的...
    夜幕青雨閱讀 898評(píng)論 0 6
  • 什么是繼承?什么是多重繼承?多重繼承存在變量和函數(shù)名沖突怎么辦?子類(lèi)對(duì)象和父類(lèi)對(duì)象的內(nèi)存模型是什么樣的?虛繼承如何...
    金戈大王閱讀 2,722評(píng)論 3 12
  • 說(shuō)明:本系列文章翻譯自Android官方文檔。分為四篇:android monkeyrunner 官方文檔andr...
    lovexiaov閱讀 967評(píng)論 1 1
  • 一般的感謝,都是讓人感到舒服的,可是你有沒(méi)有遇到過(guò)那種會(huì)讓你感到心疼的感謝。我曾經(jīng)就有,每每想起,心口就隱隱作痛,...
    慢跑的小暖閱讀 751評(píng)論 1 4
  • 拜讀得到各位老師的文章一段時(shí)間了,剛剛開(kāi)始的時(shí)候比較興奮,似乎在平平淡淡的生活中聽(tīng)到了悠揚(yáng)的音樂(lè),發(fā)現(xiàn)了新的趣味,...
    LvJack閱讀 204評(píng)論 0 0

友情鏈接更多精彩內(nèi)容