C++很「虛」

0引言:在學習C++時,碰到過如下四個以「虛」命名的概念,在系統理解這些高大上的術語後,才發現它們果然「名不虛傳」。html

  爲了方便捋清楚這些概念和之間的相互關係,本人對其進行了系統的總結,歡迎討論。ios

1.虛基類數組

(1)做用:間接派生類只保存共同基類的一份成員(數據成員/函數成員),優化存儲空間。函數

 

(2)虛基類初始化方法:性能

  在基類的直接派生類中聲明爲虛函數(virtual public B / virtual public C),在最後的派生類中初始化直接基類和虛基類(這一點要特別注意,虛基類也是由最後的派生類執行,會屏蔽直接派生類對虛基類的初始化【避免虛基類屢次初始化】,即D必須對A、B、C進行初始化,B和C對A的初始化不起做用。)學習

(3)栗子測試

 1 #include <iostream>
 2 using namespace std;
 3 
 4 //虛基類
 5 class A
 6 {
 7 public:
 8     A(int da)
 9     {
10         data_a = da;
11         cout << "A" << endl;
12     }
13     ~A()
14     {
15         cout << "~A" << endl;
16     }
17 protected:
18     int data_a;
19 };
20 //直接派生類
21 class B :virtual public A
22 {
23 public:
24    B(int da, int db) :A(da)
25     {
26         data_b = db;
27         cout << "B" << endl;
28     }
29     ~B()
30     {
31         cout << "~B" << endl;
32     }
33 protected:
34     int data_b;
35 };
36 
37 //直接派生類
38 class C :virtual public A
39 {
40 public:
41     C(int da, int dc) :A(da)
42     {
43         data_c = dc;
44         cout << "C" << endl;
45     }
46     ~C()
47     {
48         cout << "~C" << endl;
49     }
50 protected:
51     int data_c;
52 };
53 //間接派生類
54 class D : public B, public C
55 {
56 public:
57     D(int da, int db, int dc, int dd) :A(da), B(da, db), C(da, dc)
58     {
59         data_d = dd;
60         cout << "D" << endl;
61     }
62     ~D()
63     {
64         cout << "~D" << endl;
65     }
66     void display()
67     {
68        cout << "data_a=" << data_a << "\t" << "data_b=" << data_b << "\t" << "data_c=" << data_c << "\t" << "data_d=" << data_d << endl;
69     }
70 protected:
71     int data_d;
72 };
73 
74 int main()
75 {
76     D test_d(10, 20, 30, 40);
77     test_d.display();
78 
79     return 0;
80 }

輸出:優化

 

2.虛函數ui

(1)思考:在基類和派生類中存在兩個函數不只名字相同,參數個數也相同,可是功能不一樣即函數體不一樣【不是函數重載!】,如何實現兩個函數的調用?this

通常思路是採起同名覆蓋原則,即派生類中同名函數覆蓋掉基類同名函數,若是想調用基類的同名函數,必須用類做用域符::來進行區分。這種作法在派生結構複雜時使用不太方便,可否採用一種調用形式,既能夠調用派生類也能夠調用基類同名函數?

這就是虛函數大展手腳的時候了,虛函數容許在派生類中從新定義與基類同名的函數,而且容許經過基類指針或引用來訪問基類和派生類的同名函數。

(2)栗子

//main.cpp
#include <iostream>
#include "virtual.h"
using namespace std;
int main()
{
    Shape *pShape = new Shape();//定義基類指針,指向基類對象所在內存空間
    pShape->PrintArea();
    Retangle ret(10, 20);
    pShape = &ret;//將基類指針指向派生類類型對象內存
    pShape->PrintArea();
    Circle cir(10);
    pShape = &cir;//將基類指針指向派生類類型對象內存
    pShape->PrintArea();

    return 0;
}

//virtual.h
#ifndef virtual_h
#define virtual_h
class Shape
{
public:
    virtual void PrintArea();
    virtual double CalculateArea();
protected:
    double area;
};

class Retangle: public Shape
{
public:
    Retangle(double len, double wid);
    virtual void PrintArea();
    virtual double CalculateArea();
private:
    double length;
    double width;
};

class Circle : public Shape
{
public:
    Circle(double r);
    virtual void PrintArea();
    virtual double CalculateArea();
private:
    double radius;
};
#endif

//virtual.cpp
#include "virtual.h"
#include <iostream>
using namespace std;
void Shape::PrintArea()
{
    cout << "當前沒有形狀設置!" << endl;
}
double Shape::CalculateArea()
{
    return area;
}
Retangle::Retangle(double len, double wid)
{
    length = len;
    width = wid;
}
double Retangle::CalculateArea()
{
    area = length * width;
    return Shape::CalculateArea();
}
void Retangle::PrintArea()
{
    CalculateArea();
    cout << "矩陣面積爲:" << area << endl;
}
Circle::Circle(double r)
{
    this->radius = r;
}
double Circle::CalculateArea()
{
    area = 3.14*radius*radius;
    return Shape::CalculateArea();
}
void Circle::PrintArea()
{
    CalculateArea();
    cout << "圓的面積爲:" << area << endl;
}

輸出:

 

(3)關於使用虛函數的好處:

1) 基類裏聲明爲虛函數函數體能夠爲空,它的做用就是爲了能讓這個函數在它的子類裏面能夠被同名使用,這樣編譯器就可使用後期綁定來達到多態了;

2) 一般咱們把不少函數加上virtual,是一個好的習慣,雖然犧牲了一些性能,可是增長了面向對象的多態性,由於你很難預料到基類裏面的這個函數不在派生類裏面不去修改它的實現。

 

3.純虛函數

  純虛函數就很好理解了,有些函數雖然並非基類所須要的,可是派生類可能會用到,因此會在基類中爲派生類保留一個函數名,以便之後派生類定義使用。純虛函數不具有函數功能,是不能被調用的。

以上述代碼爲例,能夠將Shape類中的 PrintArea()定義爲純虛函數, virtual void PrintArea() = 0;

 

4.虛析構函數

  在學習派生類的析構函數時,留了一個坑,就是析構函數的調用次序問題,在不使用虛函數的前提下,析構函數的調用次序是:先調用派生類構造函數清理新增的成員,再調用子對象析構函數(基類構造函數)清理子對象,最後再調用基類構造函數清理基類成員,過程正好與構造函數的調用過程相反。

在使用虛函數時,析構函數的調用會出現哪些狀況?

一樣是虛函數例子中的代碼(加上構造和析構語句),進行如下測試:

1 int main()
2 {
3     Shape *pShape = new Circle(10);//基類指針指向派生類對象內存空間
4     delete pShape;
5     pShape = NULL;
6 
7     return 0;
8 }

 輸出:

測試結果代表:程序調用了兩次構造函數,可是隻調用了一次析構函數,形成了內存泄漏。

這是由於派生類析構函數沒法從基類繼承,在沒有聲明基類析構函數爲虛函數時,基類指針釋放時沒法找到派生類析構函數地址,也就不能釋放派生類對象所在內存空間。而將基類析構函數也聲明爲虛函數時,該基類全部派生類也將自動成爲虛函數,全部虛析構函數的入口地址都會存放在一個虛函數表(指針數組)中,查找方便,這樣就避免了沒法調用派生類析構函數所形成的內存泄漏問題了。

 

5.總結:

(1).虛析構函數是創建在虛函數的基礎之上的,即在想使用基類指針訪問派生類對象時必需要聲明基類虛析構函數,無論基類是否須要析構函數;

(2).由於虛函數表會佔據必定的空間開銷,在不存在上述1中狀況時沒有必要使用虛函數;

(3).多態性:由於編譯器只作靜態的語法檢查,沒法肯定調用對象,運行時才肯定關聯關係,因此多態性又分爲靜態多態性和動態多態性。

  靜態多態性(編譯時的多態性,靜態關聯)是指在程序編譯時就可以肯定調用的是哪一個函數,函數重載/運算符重載/經過對象名調用的虛函數都屬於靜態關聯。

  動態多態性(運行時多態性,動態關聯,滯後關聯)是指只有在程序運行時纔可以肯定操做的對象,經過虛函數實現。

 

6.參考:

http://blog.chinaunix.net/uid-26851094-id-3327323.html

http://blog.sina.com.cn/s/blog_7c773cc50100y9hz.html

相關文章
相關標籤/搜索