1. 程式人生 > >C++解析(24):抽象類和介面、多重繼承

C++解析(24):抽象類和介面、多重繼承

0.目錄

1.抽象類和介面

2.被遺棄的多重繼承

3.小結

1.抽象類和介面

1.1 抽象類

面向物件中的抽象類

  • 可用於表示現實世界中的抽象概念
  • 是一種只能定義型別,而不能產生物件的類
  • 只能被繼承並重寫相關函式
  • 直接特徵是相關函式沒有完整的實現

Shape是現實世界中各種圖形的抽象概念,因此:

  • 程式中必須能夠反映抽象的圖形
  • 程式中通過抽象類表示圖形的概念
  • 抽象類不能建立物件只能用於繼承

1.2 純虛擬函式

抽象類與純虛擬函式:

  • C++語言中沒有抽象類的概念
  • C++中通過純虛擬函式實現抽象類
  • 純虛擬函式是指只定義原型的成員函式
  • 一個C++類中存在純虛擬函式就成為了抽象類

純虛擬函式的語法規則:

示例——抽象類:

#include <iostream>

using namespace std;

class Shape
{
public:
    virtual double area() = 0;
};

class Rect : public Shape
{
    int ma;
    int mb;
public:
    Rect(int a, int b)
    {
        ma = a;
        mb = b;
    }
    double area()
    {
        return ma * mb;
    }
};

class Circle : public Shape
{
    int mr;
public:
    Circle(int r) { mr = r; }
    double area()
    {
        return 3.14 * mr * mr;
    }
};

void area(Shape* p)
{
    double r = p->area();
    
    cout << "r = " << r << endl;
}

int main()
{
    Rect rect(1, 2);
    Circle circle(10);
    
    area(&rect);
    area(&circle);
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
r = 2
r = 314
  • 抽象類只能用作父類被繼承
  • 子類必須實現純虛擬函式的具體功能
  • 純虛擬函式被實現後成為虛擬函式
  • 如果子類沒有實現純虛擬函式,則子類成為抽象類

1.3 介面

滿足下面條件的C++類則稱為介面

  • 類中沒有定義任何的成員變數
  • 所有的成員函式都是公有的
  • 所有的成員函式都是純虛擬函式
  • 介面是一種特殊的抽象類

示例——介面:

#include <iostream>

using namespace std;

class Channel
{
public:
    virtual bool open() = 0;
    virtual void close() = 0;
    virtual bool send(char* buf, int len) = 0;
    virtual int receive(char* buf, int len) = 0;
};

int main()
{
    return 0;
}

C++中沒有真正的介面,但是C++的後續語言Java、C#直接支援介面的概念!

2.被遺棄的多重繼承

2.1 C++中的多重繼承

C++支援編寫多重繼承的程式碼:

  • 一個子類可以擁有多個父類
  • 子類擁有所有父類的成員變數
  • 子類繼承所有父類的成員函式
  • 子類物件可以當作任意父類物件使用

多重繼承的語法規則:

2.2 多重繼承的問題一

示例——多重繼承的問題一:

#include <iostream>

using namespace std;

class BaseA
{
    int ma;
public:
    BaseA(int a) { ma = a; }
    int getA() { return ma; }
};

class BaseB
{
    int mb;
public:
    BaseB(int b) { mb = b; }
    int getB() { return mb; }
};

class Derived : public BaseA, public BaseB
{
    int mc;
public:
    Derived(int a, int b, int c) : BaseA(a), BaseB(b)
    {
        mc = c;
    }
    int getC() { return mc; }
    void print()
    {
        cout << "ma = " << getA() << ", "
             << "mb = " << getB() << ", "
             << "mc = " << mc << endl;
    }
};

int main()
{
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl; // 12
    
    Derived d(1, 2, 3);
    
    d.print();
    
    cout << "d.getA() = " << d.getA() << endl;
    cout << "d.getB() = " << d.getB() << endl;
    cout << "d.getC() = " << d.getC() << endl;
    
    cout << endl;
    
    BaseA* pa = &d;
    BaseB* pb = &d;
    
    cout << "pa->getA() = " << pa->getA() << endl;
    cout << "pb->getB() = " << pb->getB() << endl;
    
    cout << endl;
    
    void* paa = pa;
    void* pbb = pb;
    
    if( paa == pbb )
    {
        cout << "Pointer to the same object!" << endl; 
    }
    else
    {
        cout << "Error" << endl;
    }
    
    cout << "pa = " << pa << endl;
    cout << "pb = " << pb << endl;
    cout << "paa = " << paa << endl;
    cout << "pbb = " << pbb << endl; 
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
sizeof(Derived) = 12
ma = 1, mb = 2, mc = 3
d.getA() = 1
d.getB() = 2
d.getC() = 3

pa->getA() = 1
pb->getB() = 2

Error
pa = 0x7ffc9f641dc0
pb = 0x7ffc9f641dc4
paa = 0x7ffc9f641dc0
pbb = 0x7ffc9f641dc4

通過多重繼承得到的物件可能擁有“不同的地址”!!
解決方案:

(其實pa和pb還是指向了同一個物件,但是指向的是同一個物件的不同位置,打個比方就是pa指向了這個物件的腦袋,pb指向了這個物件的胸口。。。)

2.3 多重繼承的問題二

多重繼承可能產生冗餘的成員:

示例——多重繼承的問題二:

#include <iostream>

using namespace std;

class People
{
    string m_name;
    int m_age;
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout << "Name = " << m_name << ", "
             << "Age = " << m_age << endl;
    }
};

class Teacher : public People
{
public:
    Teacher(string name, int age) : People(name, age) { }
};

class Student : public People
{
public:
    Student(string name, int age) : People(name, age) { }
};

class Doctor : public Teacher, public Student
{
public:
    Doctor(string name, int age) : Teacher(name + "1", age + 10), Student(name + "2", age + 1) { }
};

int main()
{
    Doctor d("Bob", 33);
    
    //d.print();
    d.Teacher::print();
    d.Student::print();
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
Name = Bob1, Age = 43
Name = Bob2, Age = 34

當多重繼承關係出現閉合時將產生資料冗餘的問題!!!!
解決方案:虛繼承

  • 虛繼承能夠解決資料冗餘問題
  • 中間層父類不再關心頂層父類的初始化
  • 最終子類必須直接呼叫頂層父類的建構函式

示例——使用虛繼承解決資料冗餘:

#include <iostream>

using namespace std;

class People
{
    string m_name;
    int m_age;
public:
    People(string name, int age)
    {
        m_name = name;
        m_age = age;
    }
    void print()
    {
        cout << "Name = " << m_name << ", "
             << "Age = " << m_age << endl;
    }
};

class Teacher : virtual public People
{
public:
    Teacher(string name, int age) : People(name, age) { }
};

class Student : virtual public People
{
public:
    Student(string name, int age) : People(name, age) { }
};

class Doctor : public Teacher, public Student
{
public:
    Doctor(string name, int age) : Teacher(name+"1", age), Student(name+"2", age), People(name+"3", age) { }
};

int main()
{
    Doctor d("Delphi", 33);
    
    d.print();
    d.Teacher::print();
    d.Student::print();
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
Name = Delphi3, Age = 33
Name = Delphi3, Age = 33
Name = Delphi3, Age = 33

問題:
當架構設計中需要繼承時,無法確定使用直接繼承還是虛繼承!!

2.4 多重繼承的問題三

多重繼承可能產生多個虛擬函式表

示例——多重繼承的問題三:

#include <iostream>

using namespace std;

class BaseA
{
public:
    virtual void funcA()
    {
        cout << "BaseA::funcA()" << endl;
    }
};

class BaseB
{
public:
    virtual void funcB()
    {
        cout << "BaseB::funcB()" << endl;
    }
};

class Derived : public BaseA, public BaseB
{
};

int main()
{
    Derived d;
    BaseA* pa = &d;
    BaseB* pb = &d;
    BaseB* pbb = (BaseB*)pa;
    
    cout << "sizeof(d) = " << sizeof(d) << endl;
    
    cout << "Using pa to call funcA()..." << endl;
    pa->funcA();
    
    cout << "Using pb to call funcB()..." << endl;
    pb->funcB();
    
    cout << "Using pbb to call funcB()..." << endl;
    pbb->funcB();
    
    cout << endl;
    
    cout << "pa = " << pa << endl;
    cout << "pb = " << pb << endl;
    cout << "pbb = " << pbb << endl;
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
sizeof(d) = 16
Using pa to call funcA()...
BaseA::funcA()
Using pb to call funcB()...
BaseB::funcB()
Using pbb to call funcB()...
BaseA::funcA()

pa = 0x7fffd5157c20
pb = 0x7fffd5157c28
pbb = 0x7fffd5157c20

需要進行強制型別轉換時,C++中推薦使用新式型別轉換關鍵字!!
解決方案:dynamic_cast

示例——使用新式型別轉換dynamic_cast關鍵字:

#include <iostream>

using namespace std;

class BaseA
{
public:
    virtual void funcA()
    {
        cout << "BaseA::funcA()" << endl;
    }
};

class BaseB
{
public:
    virtual void funcB()
    {
        cout << "BaseB::funcB()" << endl;
    }
};

class Derived : public BaseA, public BaseB
{
};

int main()
{
    Derived d;
    BaseA* pa = &d;
    BaseB* pb = &d;
    BaseB* pbb = (BaseB*)pa; // oops!!
    BaseB* pbc = dynamic_cast<BaseB*>(pa);
    
    cout << "sizeof(d) = " << sizeof(d) << endl;
    
    cout << "Using pa to call funcA()..." << endl;
    pa->funcA();
    
    cout << "Using pb to call funcB()..." << endl;
    pb->funcB();
    
    cout << "Using pbb to call funcB()..." << endl;
    pbb->funcB();
    
    cout << "Using pbc to call funcB()..." << endl;
    pbc->funcB();
    
    cout << endl;
    
    cout << "pa = " << pa << endl;
    cout << "pb = " << pb << endl;
    cout << "pbb = " << pbb << endl;
    cout << "pbc = " << pbc << endl;
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
sizeof(d) = 16
Using pa to call funcA()...
BaseA::funcA()
Using pb to call funcB()...
BaseB::funcB()
Using pbb to call funcB()...
BaseA::funcA()
Using pbc to call funcB()...
BaseB::funcB()

pa = 0x7ffcc2c27ff0
pb = 0x7ffcc2c27ff8
pbb = 0x7ffcc2c27ff0
pbc = 0x7ffcc2c27ff8

2.5 正確的使用多重繼承

工程開發中的“多重繼承”方式:

  • 單繼承某個類 + 實現(多個)介面

示例——正確的多繼承方式:

#include <iostream>

using namespace std;

class Base
{
protected:
    int mi;
public:
    Base(int i) { mi = i; }
    int getI() { return mi; }
    bool equal(Base* obj)
    {
        return (this == obj);
    }
};

class Interface1
{
public:
    virtual void add(int i) = 0;
    virtual void minus(int i) = 0;
};

class Interface2
{
public:
    virtual void multiply(int i) = 0;
    virtual void divide(int i) = 0;
};

class Derived : public Base, public Interface1, public Interface2
{
public:
    Derived(int i) : Base(i) { }
    void add(int i) { mi += i; }
    void minus(int i) { mi -= i; }
    void multiply(int i) { mi *= i; }
    void divide(int i)
    {
        if( i != 0 ) { mi /= i; }
    }
};

int main()
{
    Derived d(100);
    Derived* p = &d;
    Interface1* pInt1 = &d;
    Interface2* pInt2 = &d;
    
    cout << "p->getI() = " << p->getI() << endl;    // 100
    
    pInt1->add(10);
    pInt2->divide(11);
    pInt1->minus(5);
    pInt2->multiply(8);
    
    cout << "p->getI() = " << p->getI() << endl;    // 40
    
    cout << endl;
    
    cout << "pInt1 == p : " << p->equal(dynamic_cast<Base*>(pInt1)) << endl;
    cout << "pInt2 == p : " << p->equal(dynamic_cast<Base*>(pInt2)) << endl;
    
    return 0;
}

執行結果為:

[[email protected] Desktop]# g++ test.cpp
[[email protected] Desktop]# ./a.out 
p->getI() = 100
p->getI() = 40

pInt1 == p : 1
pInt2 == p : 1

一些有用的工程建議:

  • 先繼承自一個父類,然後實現多個介面
  • 父類中提供equal()成員函式
  • equal()成員函式用於判斷指標是否指向當前物件
  • 多重繼承相關的強制型別轉換用dynamic_cast完成

3.小結

  • 抽象類用於描述現實世界中的抽象概念
  • 抽象類只能被繼承不能建立物件
  • C++中沒有抽象類的概念
  • C++中通過純虛擬函式實現抽象類
  • 類中只存在純虛擬函式的時成為介面
  • 介面是一種特殊的抽象類
  • C++支援多重繼承的程式設計方式
  • 多重繼承容易帶來問題
    1. 可能出現“同一個物件的地址不同”的情況
    2. 虛繼承可以解決資料冗餘的問題
    3. 虛繼承的使得架構設計可能出現問題
  • 多繼承中可能出現多個虛擬函式表指標
  • 多重繼承相關的強制型別轉換用dynamic_cast完成
  • 工程開發中採用“單繼承多介面”的方式使用多繼承
  • 父類提供成員函式用於判斷指標是否指向當前物件