1. 程式人生 > >對C++中建構函式、解構函式、虛擬函式及普通成員函式的理解

對C++中建構函式、解構函式、虛擬函式及普通成員函式的理解

這裡我們主要討論建構函式、解構函式、普通成員函式、虛擬函式,對這幾種函式說說自己的理解。

對建構函式的總結

對建構函式,我們先來看看如下的程式碼

#include <iostream>
using namespace std;

class Base{
public:
        Base(){
                cout<<"This is constructor from Base"<<endl;
        }
};

class Derived:public Base{
public:
        Derived(){
                cout
<<"This is constructor from Derived"<<endl; } }; int main() { Derived D; return 0; }

編譯執行的結果是:

This is constructor from Base
This is constructor from Derived

建構函式的作用的對例項化的物件進行初始化,在單繼承關係中,由於一定是先有父類才會有子類(先有爸爸再有兒子嘛),所以父類的建構函式的執行會在子類的建構函式之前
如果子類繼承來自多個父類,也就是多繼承

,那麼建構函式也是父類的建構函式先被執行完成,再執行子類的,那麼多個父類先執行哪個父類的呢?按照先繼承先執行的原則
例如程式碼:

#include <iostream>
using namespace std;

class Base{
public:
        Base(){
                cout<<"This is constructor from Base"<<endl;
        }
};

class Base2{
public:
        Base2(){
                cout
<<"This is constructor from Base2"<<endl; } }; class Derived:public Base,public Base2{ public: Derived(){ cout<<"This is constructor from Derived"<<endl; } }; int main() { Derived D; return 0; }

Derived類先繼承了Base,再繼承了Base2,所以執行結果為:

This is constructor from Base
This is constructor from Base2
This is constructor from Derived

對建構函式有的一些淺顯的認識之後我們來更加深入的理解建構函式:
建構函式除了通過例項化物件來呼叫之外,還可以怎麼呼叫?

#include <iostream>
using namespace std;

class Base{
public:
        Base(){
                cout<<"This is constructor from Base"<<endl;
        }
};

class Derived:public Base{
public:
        Derived(){
                cout<<"This is constructor from Derived"<<endl;
        }
};

int main()
{
        Derived const &D = Derived();
        cout<<"This is a test"<<endl;
        return 0;
}

這裡main函式中使用Derived()直接呼叫Derived類的建構函式,只要呼叫了建構函式,那麼就例項化了一個物件,只不過這裡直接呼叫建構函式產生的物件是一個臨時的物件,所以要訪問這個臨時物件需要使用const引用,在const引用D生命週期內,這個臨時物件就會一直存在,但由於是const引用,所以這個臨時物件不能被修改。
上面的程式碼執行的結果是:

This is constructor from Base
This is constructor from Derived
This is a test

為了說明臨時物件的生命週期,我們這裡使用解構函式,關於解構函式,後面會有講解,修改後的程式碼如下:

#include <iostream>
using namespace std;

class Base{
public:
        Base(){
                cout<<"This is constructor from Base"<<endl;
        }
        ~Base(){
                cout<<"This is destructor from Base"<<endl;
        }
};

class Derived:public Base{
public:
        Derived(){
                cout<<"This is constructor from Derived"<<endl;
        }
        ~Derived(){
                cout<<"This is constructor frome Derived"<<endl;
        }
};

int main()
{
        Derived();
        cout<<"This is a test"<<endl;
        return 0;
}

由於沒有被const引用,所以在執行完建構函式後,這個臨時物件就被執行了解構函式,因此執行的結果如下:

This is constructor from Base
This is constructor from Derived
This is constructor frome Derived
This is destructor from Base
This is a test

在列印This is a test之前,臨時物件就被析構了。

這裡還有另一個問題,我們呼叫的是子類的建構函式,基類的建構函式是什麼時候被呼叫的??其實基類的建構函式是被子類的建構函式呼叫的,編譯器在子類建構函式的最開始部分添加了呼叫基類建構函式的程式碼,這部分的編譯器幫我們乾的,呼叫的順序跟繼承的順序是一樣的。

建構函式除了不可以被定義為虛擬函式之外,建構函式中C++的虛擬機制是無效的,具體參考我的博文《建構函式中,虛擬機制不會被執行》。

對解構函式的總結

理解了建構函式之後,我們很容易理解解構函式,解構函式就是當物件被銷燬、刪除時呼叫的函式。
在演示直接使用建構函式建立臨時物件的時候已經藉助了解構函式來理解臨時物件的建立,解構函式的執行順序與建構函式的執行順序相反,是先析構子類,再析構父類,這個應該不難理解。
同構造函式被呼叫的道理一樣,基類的解構函式是在子類的解構函式中被呼叫的,編譯器會在子類解構函式的最後新增呼叫基類的析構的程式碼,這也是編譯器幫我們做的。
關於上面的描述,我就不以程式碼的形式加以說明了,其實跟建構函式的理解的相似的,但是解構函式與建構函式不同,解構函式是可以被定義為虛擬函式的,而且當一個類為基類時,那麼這個類的解構函式一定要被定義為虛擬函式,所以我上面演示的程式碼沒有把Base的解構函式定義為虛擬函式其實是很有問題的,具體什麼問題,請參考我的博文《C++中虛解構函式的作用》。

當基類定義了一個虛的解構函式時,被子類繼承時,基類的解構函式將被覆蓋,那麼基類的解構函式是如何被正確執行的呢??因為子類的解構函式最後呼叫了基類的解構函式啊。既然被覆蓋了,那還怎麼呼叫?看完文章就知道啦。

虛擬函式的總結

C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。

虛擬函式表

對C++ 瞭解的人都應該知道虛擬函式(Virtual Function)是通過一張虛擬函式表(Virtual Table)來實現的。簡稱為V-Table。 在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函式。這樣,在有虛擬函式的類的例項中這個表被分配在了 這個例項的記憶體中,所以,當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得由為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。

這裡我們著重看一下這張虛擬函式表。在C++的標準規格說明書中說到,編譯器必需要保證虛擬函式表的指標存在於物件例項中最前面的位置(這是為了保證正確取到虛擬函式的偏移量)。 這意味著我們通過物件例項的地址得到這張虛擬函式表,然後就可以遍歷其中函式指標,並呼叫相應的函式。

聽我扯了那麼多,我可以感覺出來你現在可能比以前更加暈頭轉向了。 沒關係,下面就是實際的例子,相信聰明的你一看就明白了。

假設我們有這樣的一個類:

class Base {

public:

virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }

};

按照上面的說法,我們可以通過Base的例項來得到虛擬函式表。 下面是實際例程:

typedef void(*Fun)(void);

Base b;

Fun pFun = NULL;

cout << "虛擬函式表地址:" << (int*)(*(int*)(&b)) << endl;

cout << "虛擬函式表 — 第一個函式地址:" << (int*)*((int*)(*(int*)(&b))) << endl;

pFun = (Fun)*((int*)*(int*)(&b));/*獲得虛擬函式表第一個函式*/

pFun();/*執行虛擬函式*/

實際執行經果如下:

虛擬函式表地址:0012FED4
虛擬函式表 — 第一個函式地址:0044F148
Base::f

通過這個示例,我們可以看到,我們可以通過強行把&b轉成int ,取得虛擬函式表的地址,然後,再次取址就可以得到第一個虛擬函式的地址了,也就是Base::f(),這在上面的程式中得到了驗證(把int 強制轉成了函式指標)。通過這個示例,我們就可以知道如果要呼叫Base::g()和Base::h(),其程式碼如下:

(Fun)*((int*)*(int*)(&b)+0); // Base::f()

(Fun)*((int*)*(int*)(&b)+1); // Base::g()

(Fun)*((int*)*(int*)(&b)+2); // Base::h()

這個時候你應該懂了吧。什麼?還是有點暈。也是,這樣的程式碼看著太亂了。沒問題,讓我畫個圖解釋一下。如下所示:

注意:在上面這個圖中,我在虛擬函式表的最後多加了一個結點,這是虛擬函式表的結束結點,就像字串的結束符“\0”一樣,其標誌了虛擬函式表的結束。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛擬函式表,如果值是0,表示是最後一個虛擬函式表。

下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛擬函式表的樣子。沒有覆蓋父類的虛擬函式是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

一般繼承(無虛擬函式覆蓋)

下面,再讓我們來看看繼承時的虛擬函式表是什麼樣的。假設有如下所示的一個繼承關係:

請注意,在這個繼承關係中,子類沒有過載任何父類的函式。那麼,在派生類的例項中,其虛擬函式表如下所示:

對於例項:Derive d; 的虛擬函式表如下:

我們可以看到下面幾點:

1)虛擬函式按照其宣告順序放於表中。

2)父類的虛擬函式在子類的虛擬函式前面。

我相信聰明的你一定可以參考前面的那個程式,來編寫一段程式來驗證。

一般繼承(有虛擬函式覆蓋)

覆蓋父類的虛擬函式是很顯然的事情,不然,虛擬函式就變得毫無意義。下面,我們來看一下,如果子類中有虛擬函式過載了父類的虛擬函式,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。

為了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函式:f()。那麼,對於派生類的例項,其虛擬函式表會是下面的一個樣子:

我們從表中可以看到下面幾點,

1)覆蓋的f()函式被放到了虛表中原來父類虛擬函式的位置。

2)沒有被覆蓋的函式依舊。

這樣,我們就可以看到對於下面這樣的程式,

Base *b = new Derive();

b->f();

由b所指的記憶體中的虛擬函式表的f()的位置已經被Derive::f()函式地址所取代,於是在實際呼叫發生時,是Derive::f()被呼叫了。這就實現了多型。

多重繼承(無虛擬函式覆蓋)

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函式。

對於子類例項中的虛擬函式表,是下面這個樣子:

我們可以看到:

1) 每個父類都有自己的虛表。

2) 子類的成員函式被放到了第一個父類的表中。(所謂的第一個父類是按照宣告順序來判斷的)

這樣做就是為了解決不同的父類型別的指標指向同一個子類例項,而能夠呼叫到實際的函式。

多重繼承(有虛擬函式覆蓋)

下面我們再來看看,如果發生虛擬函式覆蓋的情況。

下圖中,我們在子類中覆蓋了父類的f()函式。

下面是對於子類例項中的虛擬函式表的圖:

我們可以看見,三個父類虛擬函式表中的f()的位置被替換成了子類的函式指標。這樣,我們就可以任一靜態型別的父類來指向子類,並呼叫子類的f()了。如:

Derive d;

Base1 *b1 = &d;

Base2 *b2 = &d;

Base3 *b3 = &d;

b1->f(); //Derive::f()

b2->f(); //Derive::f()

b3->f(); //Derive::f()

b1->g(); //Base1::g()

b2->g(); //Base2::g()

b3->g(); //Base3::g()

通過父型別的指標訪問子類自己的虛擬函式

我們知道,子類沒有過載父類的虛擬函式是一件毫無意義的事情。因為多型也是要基於函式過載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛擬函式,但我們根本不可能使用下面的語句來呼叫子類的自有虛擬函式:

Base1 *b1 = new Derive();

b1->f1(); //編譯出錯

任何妄圖使用父類指標想呼叫子類中的未覆蓋父類的成員函式的行為都會被編譯器視為非法,所以,這樣的程式根本無法編譯通過。但在執行時,我們可以通過指標的方式訪問虛擬函式表來達到違反C++語義的行為。(關於這方面的嘗試,通過閱讀後面附錄的程式碼,相信你可以做到這一點)

再談虛解構函式

其實虛解構函式和一般的虛擬函式沒有太大的區別,虛解構函式也是儲存在虛擬函式表中的,區別在於基類和子類虛解構函式雖然名字不同,但是子類的解構函式會替換掉父類虛解構函式在虛擬函式表中的位置(覆蓋),還記得嗎?子類的解構函式的開頭自動幫我們呼叫了基類的解構函式,所以編譯器實際上為了實現呼叫基類的解構函式,在子類的解構函式開頭就添加了基類的解構函式的函式呼叫。

訪問non-public的虛擬函式

另外,如果父類的虛擬函式是private或是protected的,但這些非public的虛擬函式同樣會存在於虛擬函式表中,所以,我們同樣可以使用訪問虛擬函式表的方式來訪問這些non-public的虛擬函式,這是很容易做到的。

一般成員函式

關於C++中的成員函式,我之前只停留在會使用,但並不理解物件是如何呼叫成員函式的,又是怎麼訪問到成員變數的,現在就來分析分析C++是如何實現類的成員函式的。

首先,我們知道函式的指令也是儲存在記憶體中等待被執行的,這個儲存程式碼的區域被稱為程式碼段(本文不探究這個),既然是在記憶體中,那麼就可以通過地址來訪問到,C++中類的成員函式也是這樣的,類的成員函式實際上就和一般的全域性函式儲存的地方是一樣的,只是加了個類名作為名字空間(namespace),所以我們可以像前文一樣通過地址訪問虛擬函式,來通過地址訪問一般的成員函式。

class test  
{  
public:  
    void print (){  
       printf ("function print");  
    }  
};  

typedef void (test::*fun)();  
fun f = &test::print;  
test t;  
(t.*f)();

其實我們知道類成員函式只是函式第一個引數是this指標,但是this指標不是通過普通函式引數壓棧方式,而是放進ecx中。

總結

這篇文章寫了好久,參考了很到大神的文章,加上自己的一些理解,終於整理完了,還是很開心的。

由於最近自己搭建一個個人部落格網站,所以以後的博文應該都第一時間放再那裡,這邊我也會更新,不過可能就不是第一時間,歡迎來我的個人部落格網站:Veaxen’s

相關推薦

C++建構函式函式虛擬函式普通成員函式理解

這裡我們主要討論建構函式、解構函式、普通成員函式、虛擬函式,對這幾種函式說說自己的理解。 對建構函式的總結 對建構函式,我們先來看看如下的程式碼 #include <iostream> using namespace std; cla

C++基類的函式為什麼要用virtual虛函式【轉】

(轉自:https://blog.csdn.net/iicy266/article/details/11906457) 知識背景          要弄明白這個問題,首先要了解下C++中的動態繫結。&n

C++為什麼要將函式定義成虛擬函式

      派生類的成員由兩部分組成,一部分是從基類那裡繼承而來,一部分是自己定義的。那麼在例項化物件的時候,首先利用基類建構函式去初始化從基類繼承而來的成員,再用派生類建構函式初始化自己定義的部分。 同時,不止建構函式派生類只負責自己的那部分,解構函式也是,所以派生

C++基類的函式為什麼要用virtual虛函式

知識背景          要弄明白這個問題,首先要了解下C++中的動態繫結。  正題          直接的講,C++中基類採用virtual虛解構函式是為了防止記憶體洩漏。具體地說,如果派生類中申請了記憶體空間,並在其解構函式中對這些記憶體空間進行釋放

學習kotlin第13天_具體化的型別引數內聯屬性宣告集合

繼續之前的坑,我原本不打算繼續看文件了,直接上手個小專案,但是專案中遇見個小問題,list似乎和java中的有區別。。。一查文件發現在後面。。。所以繼續踩坑。 坑1、有時候我們需要型別作為引數傳給

C++類的一些細節(過載重寫覆蓋隱藏,建構函式函式拷貝建構函式賦值函式在繼承時的一些問題)

1 函式的過載、重寫(重定義)、函式覆蓋及隱藏 其實函式過載與函式重寫、函式覆蓋和函式隱藏不是一個層面上的概念。前者是同一個類內,或者同一個函式作用域內,同名不同引數列表的函式之間的關係。而後三者是基類和派生類函式不同情況下的關係。 1.1 函式過載

C++class類 的 建構函式函式

說明: demo.cpp:main.cpp所在之處 Line.h:線段類的.h檔案 Line.cpp:線段類的.cpp檔案 Coordinate.h:座標類的.h檔案 Coordinate.cpp:

[c/c++]建構函式函式可不可以丟擲異常

usingnamespace std;class A...{public:    A()    ...{        cout <<"construction fun"<< endl;        throw1;    }    ~A()    

C++抽象類以及虛/純虛函式的區別與介紹

一、虛擬函式 在某基類中宣告為 virtual 並在一個或多個派生類中被重新定義的成員函式,用法格式為:virtual+函式返回型別+ 函式名(引數表) {函式體};實現多型性,通過指向派生類的基類指標或引用,訪問派生類中同名覆蓋成員函式。 二、純虛擬函式 純虛擬函式是一種

C++筆記】編寫類string的建構函式函式和賦值函式

#include<iostream> using namespace std; class String { public: String(const char *str=NULL); //普通建構函式 String(const Stri

c++單鏈表【建構函式運算子過載函式增刪查改等】

c++中的單向連結串列寫法:實現增刪查改、建構函式、運算子過載、解構函式等。建立標頭檔案SList.h#pragma once typedef int DataType; //SList要訪問SListNode,可以通過友元函式實現,友元函式在被訪問的類中 class SL

C++在單繼承多繼承虛繼承時,建構函式複製建構函式賦值操作符函式的執行順序和執行內容

一、本文目的與說明     1. 本文目的:理清在各種繼承時,建構函式、複製建構函式、賦值操作符、解構函式的執行順序和執行內容。     2. 說明:雖然複製建構函式屬於建構函式的一種,有共同的地方,但是也具有一定的特殊性,所以在總結它的性質時將它單獨列出來了。  

C++ (建構函式函式動態申請空間)

#include<iostream> #include<string.h> using namespace std; //整型陣列: class IntArray//動態陣列 {

C++建構函式,拷貝建構函式函式

C++中預設建構函式就是沒有形參的建構函式。準確的說法,按照《C++ Primer》中定義:只要定義一個物件時沒有提供初始化式,就是用預設建構函式。為所有 的形參提供預設實參的建構函式也定義了預設建構函式。 合成的預設建構函式,即編譯器自動生成的預設建構函式。《C++ Pr

C/C++面試題:編寫類String的建構函式函式和賦值函式

考點:建構函式、解構函式和賦值函式的編寫方法出現頻率:☆☆☆☆☆已知類String的原型為:        class String        {        public:                String(const char *str = NULL);

C++第十週【任務2】定義一個名為CPerson的類,有以下私有成員:姓名身份證號性別和年齡,成員函式建構函式函式輸出資訊的函式

/* (程式頭部註釋開始) * 程式的版權和版本宣告部分 * Copyright (c) 2011, 煙臺大學計算機學院學生 * All rights reserved. * 檔名稱: C++第十週【任務2】 * 作

C++ 建構函式函式能否呼叫虛擬函式

牛客網 ------------------- ------------------- ------------------- 設計模式 ------------------- -------------------

C++ 建構函式預設建構函式函式和物件初始化

#include <iostream> using namespace std; class Student{     private:         int m_age;         int m_grade;         string m_sex

C++建構函式函式虛擬函式

建構函式 1.建立物件時會依次呼叫基類和子類的建構函式,各個建構函式負責對自己類中定義的成員的初始化工作。 2.如果使用者不宣告任何建構函式,編譯器將提供一個預設建構函式(default constructor),只要使用者定義了自己的建構函式,不論包不包括預設建構函式,編

C++建構函式函式的呼叫順序

1.參考文獻 2.建構函式、解構函式與拷貝建構函式介紹 2.1建構函式 建構函式不能有返回值預設建構函式時,系統將自動呼叫該預設建構函式初始化物件,預設建構函式會將所有資料成員都初始化為零或空 建立一個物件時,系統自動呼叫建構函式2.2解構函式 解構函式沒有引數,也