亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

關(guān)于C++虛繼承的內(nèi)存模型問題

 更新時間:2021年07月01日 10:19:02   作者:彼 方  
C++虛繼承的內(nèi)存模型是一個老生常談的話題,實現(xiàn)方法主要依賴于編譯器,本文從多個角度通過代碼詳解C++中虛繼承的內(nèi)存模型知識,感興趣的朋友跟隨小編一起看看吧

1、前言

C++虛繼承的內(nèi)存模型是一個經(jīng)典的問題,其具體實現(xiàn)依賴于編譯器,可能會出現(xiàn)較大差異,但原理和最終的目的是大體相同的。本文將對g++中虛繼承的內(nèi)存模型進行詳細解析。

2、多繼承存在的問題

C++的多繼承是指從多個直接基類中產(chǎn)生派生類的能力,多繼承的派生類繼承了所有父類的成員。從概念上來講這是非常簡單的,但是多個基類的相互交織可能會帶來錯綜復(fù)雜的設(shè)計問題,命名沖突就是不可回避的一個,比如典型的是菱形繼承,如圖2-1所示:

在這里插入圖片描述

圖2-1 菱形繼承


在圖2-1中,類A派生出類B和類C,類D繼承自類B和類C,這個時候類A中的成員變量和成員函數(shù)繼承到類D中變成了兩份,一份來自A–>B–>D這條路徑,另一份來自A–>C–>D這條路徑。
在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數(shù)據(jù),但大多數(shù)情況下這是多余的,因為保留多份成員變量不僅占用較多的存儲空間,還容易產(chǎn)生命名沖突。假如類A有一個成員變量a,那么在類D中直接訪問a就會產(chǎn)生歧義,編譯器不知道它究竟來自A -->B–>D這條路徑,還是來自A–>C–>D這條路徑。下面是菱形繼承的代碼實現(xiàn):

#include <iostream>
#include <stdint.h>

class A
{
public:
    long a;
};

class B: public A
{
public:
    long b;
};


class C: public A
{
public:
    long c;
};

class D: public B, public C
{
public:
    void seta(long v) { a = v; } // 命名沖突
    void setb(long v) { b = v; } // 正確
    void setc(long v) { c = v; } // 正確
    void setd(long v) { d = v; } // 正確

private:
    long d;
};

int main(int argc, char* argv[])
{
    D d;
}

這段代碼就是圖2-1所示的菱形繼承的具體實現(xiàn),可以看到在類Dseta()方法中,代碼試圖直接訪問間接基類的成員變量a,結(jié)果發(fā)生了錯誤,因為類B和類C中都有成員變量a(都是從類A繼承的),編譯器不知道選用哪一個,所以產(chǎn)生了歧義。

為了消除歧義,我們可以在使用a時指明它具體來自哪個類,代碼如下:

void seta(long v) { B::a = v; }
/* 或 */
void seta(long v) { C::a = v; }

使用GDB查看變量d的內(nèi)存布局,如圖2-2所示:

在這里插入圖片描述

圖2-2 變量d的GDB調(diào)試結(jié)果


于是我們可以畫出變量d的內(nèi)存布局,如圖2-3所示:

在這里插入圖片描述

圖2-3 變量d的內(nèi)存布局

3、虛繼承簡介

為了解決多繼承時命名沖突和冗余數(shù)據(jù)的問題,C++提出了虛繼承這個概念,虛繼承可以使得在派生類中只保留一份間接基類的成員。使用方式就是在繼承方式前面加上virtual關(guān)鍵字修飾,示例代碼如下(基于前面的例子修改):

#include <iostream>
#include <stdint.h>

class A
{
public:
    long a;
};

class B: virtual public A
{
public:
    long b;
};


class C: virtual public A
{
public:
    long c;
};

class D: public B, public C
{
public:
    void seta(long v) { a = v; } // 現(xiàn)在不會沖突了
    void setb(long v) { b = v; } // 正確
    void setc(long v) { c = v; } // 正確
    void setd(long v) { d = v; } // 正確

private:
    long d;
};

int main(int argc, char* argv[])
{
    D d;
}

可以看到這段代碼使用虛繼承重新實現(xiàn)了前面提到的那個菱形繼承,這樣在派生類D中就只保留了一份間接基類A的成員變量a了,后續(xù)再直接訪問a就不會出現(xiàn)歧義了。虛繼承的目的是讓某個類做出聲明,承諾愿意共享它的基類,這個被共享的基類就稱為虛基類(Virtual Base Class),本例中的類A就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現(xiàn)了多少次,在派生類中都只包含一份虛基類的成員。本例的繼承關(guān)系如圖3-1所示:

在這里插入圖片描述

圖3-1 虛繼承下菱形繼承

從這個新的繼承體系中我們可以發(fā)現(xiàn)虛繼承的一個特征:必須在虛派生的真實需求出現(xiàn)前就已經(jīng)完成虛派生的操作。在圖3-1中,我們是當(dāng)定義類D時才出現(xiàn)了對虛派生的需求,但是如果類B和類C不是從類A虛派生得到的,那么類D還是會保留間接基類A的兩份成員,示例代碼如下:

#include <iostream>
#include <stdint.h>

class A
{
public:
    long a;
};

class B: public A
{
public:
    long b;
};


class C: public A
{
public:
    long c;
};

class D: virtual public B, virtual public C
{
public:
    void seta(long v) { a = v; } // 錯誤,不能等到定義類D時再來做虛繼承的工作
    void setb(long v) { b = v; } // 正確
    void setc(long v) { c = v; } // 正確
    void setd(long v) { d = v; } // 正確

private:
    long d;
};

int main(int argc, char* argv[])
{
    D d;
}

換個角度講,虛派生只影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。在實際開發(fā)中,位于中間層次的基類將其繼承聲明為虛繼承一般不會帶來什么問題。通常情況下,使用虛繼承的類層次是由一個人或者一個項目組一次性設(shè)計完成的。對于一個獨立開發(fā)的類來說,很少需要基類中的某一個類是虛基類,況且新類的開發(fā)者也無法改變已經(jīng)存在的類體系。

4、虛繼承在標準庫中的使用

C++標準庫中的iostream就是一個虛繼承的典型案例。iostream是從istreamostream直接繼承而來的,而istreamostream又都繼承自一個名為ios的類,這個就是一個典型的菱形繼承。此時istreamostream必須采用虛繼承,否則將導(dǎo)致iostream中保留兩份ios的成員。

iostream相關(guān)的源代碼如下(從gcc-2.95.3版本中摘錄出來的,內(nèi)容有所省略):

struct _ios_fields
{ // The data members of an ios.
    streambuf *_strbuf;
    ostream* _tie;
    int _width;
    __fmtflags _flags;
    _IO_wchar_t _fill;
    __iostate _state;
    __iostate _exceptions;
    int _precision;

    void *_arrays; /* Support for ios::iword and ios::pword. */
};

class ios : public _ios_fields
{...};

class istream : virtual public ios
{...};

class ostream : virtual public ios
{...};

class iostream : public istream, public ostream
{
public:
    iostream() { }
    iostream(streambuf* sb, ostream*tied=NULL);
};

5、虛繼承下派生類的內(nèi)存布局解析

g++中是沒有所謂的虛基類表的(據(jù)說vs是有單獨一個虛基類表的),只有一個虛表,由于平時用的比較多的是虛函數(shù),所以一般情況下都直接管它叫做虛函數(shù)表,在g++編譯環(huán)境下這種叫法其實是不嚴謹?shù)?。測試程序如下:

#include <iostream>
#include <stdint.h>

class A
{
public:
    long a;
};

class B: virtual public A
{
public:
    long b;
};

class C: virtual public A
{
public:
    long c;
};

class D: public B, public C
{
public:
    void seta(long v) { a = v; }
    void setb(long v) { b = v; }
    void setc(long v) { c = v; }
    void setd(long v) { d = v; }

private:
    long d;
};

int main(int argc, char* argv[])
{
    D d;
    d.seta(1);
    d.setb(2);
    d.setc(3);
    d.setd(4);
}

D在當(dāng)前編譯器(GCC 4.8.5)下的內(nèi)存布局如圖5-1所示:

在這里插入圖片描述

圖5-1 類D的內(nèi)存布局

從圖5-1中可以看出這個表和之前這篇文章《一文讀懂C++虛函數(shù)的內(nèi)存模型》講的虛函數(shù)表是差不多的,就多了一個vbase_offset而已。因為這里的類設(shè)計比較簡單,沒有把虛函數(shù)加進來,有虛函數(shù)的話_vptr.B或者_vptr.C下面的內(nèi)存空間存儲的就是指向?qū)?yīng)虛函數(shù)的指針了(以下只講_vptr.B的相關(guān)內(nèi)容,_vptr.C同理就不贅述了)。

這里可以看到_vptr.B指向的是虛函數(shù)的起始地址(因為這里沒有虛函數(shù),所以下面緊接著就是_vptr.C的內(nèi)容),而不是與它相關(guān)聯(lián)的全部信息的起始地址,事實上從圖5-1中可以看出_vptr.B - 3 ~ _vptr.B這個范圍內(nèi)的數(shù)據(jù)都是類B虛表的內(nèi)容(不知道編譯器為什么這么設(shè)計,這里也進行揣測了),這三個特殊的內(nèi)存地址存儲的內(nèi)容解析如下:

_vptr.B - 1:這里存儲的是typeinfo for D,里面的內(nèi)容其實也是一個指針,指向的是類D的運行時信息,這些玩意都是為了支持RTTI的。RTTI的相關(guān)內(nèi)容以后會講,這里就先不多分析了。_vptr.B - 2:這里存儲的是offset_to_top,這個表示的是當(dāng)前的虛表指針距離類開頭的距離,可以看到對于_vptr.B來說這個值就是0,因為_vptr.B就存在于類D的起始位置,而對于_vptr.C來說這個值是-16,大家可以算一下_vptr.C與類D的起始位置確實是差兩個地址也就是16個字節(jié)(64位系統(tǒng)),至于為什么是負數(shù),這是因為堆內(nèi)存是向下增長的,越往下地址數(shù)值越大。

offset_to_top深度解析:在多繼承中,由于不同基類的起點可能處于不同的位置,因此當(dāng)需要將它們轉(zhuǎn)化為實際類型時,this指針的偏移量也不相同。由于實際類型在編譯時是未知的,這要求偏移量必須能夠在運行時獲取。實體offset_to_top表示的就是實際類型起始地址到當(dāng)前這個形式類型起始地址的偏移量。在向上動態(tài)轉(zhuǎn)換到實際類型時(即基類轉(zhuǎn)派生類),讓this指針加上這個偏移量即可得到實際類型的地址。需要注意的是,由于一個類型即可以被單繼承,也可以被多繼承,因此即使只有單繼承,實體offset_to_top也會存在于每一個多態(tài)類型之中。
(這里要注意一點就是offset_to_top只存在于多態(tài)類型中,所以我們可以看到在第二小節(jié)那個例子中,根本就沒有什么所謂的虛表之類的東西,它也就不支持RTTI,最簡單的大家可以使用dynamic_cast去試試,會報錯說該類型不具備多態(tài)性質(zhì)的。那么問題來了,怎樣才能以最簡短的方式讓它具備多態(tài)的性質(zhì)呢?很簡單,定義一個析構(gòu)函數(shù),用virtual修飾即可)

_vptr.B - 3:這里存儲的是vbase_offset,這個表示的是當(dāng)前虛表指針與其對應(yīng)的虛基類的距離。從圖中可以看出對于_vptr.B來說這個值是40,算一下剛好是_vptr.Ba的差距,_vptr.C同理。

vbase_offset深度解析:以測試程序為例,對于類型為B的引用,在編譯時,無法確定它的虛基類A它在內(nèi)存中的偏移量。因此,需要在虛表中額外再提供一個實體,表明運行時它的基類所在的位置,這個實體稱為vbase_offset,位于offset_to_top上方。

接下來我們通過GDB來驗證一下前面講的內(nèi)容,先打印出變量d的內(nèi)存信息,如圖5-2所示:

在這里插入圖片描述

圖5-2 變量d的內(nèi)存信息

從圖5-2中可以看到變量d的內(nèi)容與前面分析的差不多,接下來我們來看一下這兩個虛表的內(nèi)容,如圖5-3所示:

在這里插入圖片描述

圖5-3 虛表內(nèi)存信息


從圖5-3中可以看出前面的內(nèi)存圖是正確的,接下來就再看一下變量d自身的內(nèi)存布局,如圖5-4所示:

在這里插入圖片描述

圖5-4 變量d的內(nèi)存布局

圖5-4顯示出的結(jié)果和前面圖5-1的完全一致,到這里調(diào)試就結(jié)束了,由調(diào)試結(jié)果可以知道圖5-1的內(nèi)存模型是正確的。

這里要補充一點,就是對于虛繼承下的類D,和第二節(jié)那個沒有虛繼承的相比,基類A的位置被移動到了類D的最末尾,不過不用擔(dān)心,運行時可以靠vbase_offset找到它。

6、總結(jié)

本文先是對虛繼承的概念以及使用場景進行了說明,然后通過一個內(nèi)存模型圖向大家展示了g++下虛繼承的內(nèi)存形態(tài),最后使用GDB查看實際的內(nèi)存情況來驗證內(nèi)存模型圖的正確性。本文為了更直觀地展示虛繼承的內(nèi)存模型,示例設(shè)計得很簡單,類的設(shè)計中只有一個成員變量而沒有成員函數(shù)、虛函數(shù)等其它內(nèi)容。本文與前文《一文讀懂C++虛函數(shù)的內(nèi)存模型》相當(dāng)于拋磚引玉,為下文作鋪墊,在下一篇文章中我將對一些稍微復(fù)雜一點的情景進行分析,看看完整形態(tài)的虛表究竟是什么樣的。

到此這篇關(guān)于關(guān)于C++虛繼承的內(nèi)存模型問題的文章就介紹到這了,更多相關(guān)C++虛繼承的內(nèi)存模型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Qt簡單實現(xiàn)密碼器控件

    Qt簡單實現(xiàn)密碼器控件

    這篇文章主要為大家詳細介紹了Qt簡單實現(xiàn)密碼器控件,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • C++引用的詳細解釋

    C++引用的詳細解釋

    以下是對C++中引用的使用進行了詳細的總結(jié)介紹,需要的朋友可以過來參考下,希望對大家有所幫助,希望能夠給你帶來幫助
    2021-11-11
  • C語言中關(guān)于計算字符串長度的幾種方式

    C語言中關(guān)于計算字符串長度的幾種方式

    這篇文章主要介紹了C語言中關(guān)于計算字符串長度的幾種方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • C++中const用于函數(shù)重載的示例代碼

    C++中const用于函數(shù)重載的示例代碼

    這篇文章主要介紹了C++中const用于函數(shù)重載的相關(guān)資料,需要的朋友可以參考下
    2017-09-09
  • C語言中的字符(char)詳細講解

    C語言中的字符(char)詳細講解

    本篇文章主要介紹C語言中char的知識,并附有代碼實例,以便大家在學(xué)習(xí)的時候更好的理解,有需要的可以看一下
    2016-07-07
  • C++實現(xiàn)猜牌小游戲

    C++實現(xiàn)猜牌小游戲

    這篇文章主要為大家詳細介紹了C++實現(xiàn)猜牌小游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-07-07
  • C++ 17轉(zhuǎn)發(fā)一個函數(shù)調(diào)用的完美實現(xiàn)

    C++ 17轉(zhuǎn)發(fā)一個函數(shù)調(diào)用的完美實現(xiàn)

    這篇文章主要給大家介紹了關(guān)于C++ 17如何轉(zhuǎn)發(fā)一個函數(shù)調(diào)用的完美實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家學(xué)習(xí)或者使用C++17具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-08-08
  • C++ 中 vector 的常用操作方法匯總

    C++ 中 vector 的常用操作方法匯總

    在C++的STL中,vector是一個動態(tài)數(shù)組,可以在運行時調(diào)整大小,本文介紹了vector的初始化、元素訪問、修改、迭代器操作、容量管理以及性能優(yōu)化技巧,通過這些操作,可以有效地使用vector管理數(shù)據(jù),本文介紹C++  vector 操作,感興趣的朋友一起看看吧
    2024-10-10
  • 詳解VS2010實現(xiàn)創(chuàng)建并生成動態(tài)鏈接庫dll的方法

    詳解VS2010實現(xiàn)創(chuàng)建并生成動態(tài)鏈接庫dll的方法

    在某些應(yīng)用程序場景下,需要將一些類或者方法編譯成動態(tài)鏈接庫dll,以便別的.exe或者.dll文件可以通過第三方庫的方式進行調(diào)用,下面就簡單介紹一下如何通過VS2010來創(chuàng)建動態(tài)鏈接庫
    2022-12-12
  • 基于linux下獲取時間函數(shù)的詳解

    基于linux下獲取時間函數(shù)的詳解

    本篇文章是對linux下獲取時間的函數(shù)進行了詳細的分析介紹,需要的朋友參考下
    2013-05-05

最新評論