the semantics of data

来源:https://www.cnblogs.com/chenglixue/archive/2022/10/19/16807005.html
-Advertisement-
Play Games

class object layout //64位系統 class A{ }; //sizeof(A)為1 class B : virtual public A{ }; //sizeof(B)為8 class C : virtual public A{ }; //sizeof(C)為8 class ...


class object layout

//64位系統
class A{ };		//sizeof(A)為1
class B : virtual public A{ };	//sizeof(B)為8
class C : virtual public A{ };	//sizeof(C)為8
class D : public B, public C{ };	//sizeof(D)為16

//sizeof(A)為1是因為編譯器會安插一個char,使得多個object會有不同的地址

  • 記憶體佈局:

    image-20221016191124550

  • 造成B和C大小為8的原因如下:

    • 語言本身造成的額外負擔。若derived class派生自virtual base class,則derived class中含有一個vbptr指針,此指針指向virtual base class subobject或一個相關表格vbtable,而vbtable存放virtual base class subobject地址或編譯位置(offset)
      • 註:derived class中包含本身和base class組成了對象,而屬於某個基類的對象就是base class subobject
    • 編譯器對特殊情況的優化處理。virtual base class A subobject的1 bytes一般放於derived class的固定部分的末端,某些編譯器會對empty virtual base class提供特殊支持
      • empty virtual base class不定義任何數據,提供一個virtual interface。某些編譯器處理下,一個empty virtual base class被視為derived class object最開始的那一部分,並沒有使用任何的額外空間。因為含有member,所以也沒有必要安插char
    • Alignment padding的限制。聚合的結構體大小收alignment限制,使其在記憶體更有效率地被存取
  • nonstatic data members和virtual nonstatic data members都存與class object中,且沒有強制定義其排列順序;static data members存於global data segment,不影響class object大小

  • nonstatic data members在class object中同一個access level的記憶體排列順序應和被聲明的順序相同,不受static data members影響

  • class object的同一個access section中members不一定非得連續排列,member的alignment和內部使用的data members可能會介於聲明的members間;且多個access section中data members可以自由排序,不用考慮聲明順序

  • access sections的多少並不影響記憶體大小

    class A
    {
        public:
        ...
        private:
        	float x;
        	static int y;
        private:
        	float z;
        	static int i;
        private:
        	float j;
    }
    

the binding of a data member

  • 現有以下代碼:
extern float x;

class A
{
    public:
    	A(float, float, float);
    	float X() const { return x; };
    
    private:
    	float x, y, z;
}
  • 放在現在,X()的返回值肯定是class內部那個,但在以前的編譯器,此操作會返回extern那個。因此,這也就產生了兩種防禦性程式風格:

    • 將所有data member放於class聲明最開始處

      class A
      {
          private:
          	float x, y, z;
          
          public:
          	//這樣將保證class內部
          	float X() const { return x; };
      }
      
    • 將所有inline member functions,放於class外。inline函數實體,在整個class聲明完全看見後,綁定操作才會進行

      class A
      {
          public:
          	A();
          
          private:
          	float x,y,z;
      };
      
      inline float A::X() const
      {
          return x;
      }
      
  • 請思考如下代碼:

    typedef int length;
    
    class A
    {
        public:
        	//length被判定為int類型
        	//_val 判定為A::_val
        	void do1( length val ) { _val = val; };
        	length do1() { return _val; };
        
        private:
        	//這裡length必須在"本class對它的第一個操作前"被看見.否則先前的判定操作不合法
        	typedef float length;
        	length _val;
    }
    
  • 對於member function的argument list來說,argument list中的名稱會在它們第一次遭遇時被適當判斷完成。因此,需要將nested type聲明放於判斷前

data member 的存取

​ 現有如下代碼:

A a;
//x的存取成本?
a.x = 0.0;
A* ot = &A;
//通過指針的x的存取成本?
pt->x = 0.0
  • 用指針進行存取:若A為derived class且繼承體系中含有virtual base class,且存取的member從virtual base class繼承而來,和單一繼承、多重繼承這樣的就有很大差距,因為這個存取操作需要延遲至執行器,經由一個額外的間接導引解決

static data members

  • class object里的static data member,對於class objects和其本身,都不會產生額外負擔

  • 無論是複雜的繼承關係還是單一的class object,static data member永遠只有一個實例

  • static data member每次被取用時,編譯器都會對其進行轉化

    //a.i = 0;
    A::i = 0; 
    
    //pt->i = 0;
    A::i = 0;
    
  • 多個相同的classs都聲明相同的static member,在data segment中這肯定會導致名稱衝突,但編譯器對其進行name-mangling,也就是暗中對每一個衝突的static data member編碼,如此即可獲得獨一無二的識別代碼

    • 不同的編譯器有不同的name-mangling,但都包含兩點:
      • 運用一個演算法推導識別代碼
      • 若編譯系統必須和使用者交談,這是識別代碼可以被輕易地推導回原來的名稱
  • 對於以上代碼,雖然使用的member selection operators對static data member進行存取操作,但這隻是圖方便,實際上static data member並不在class object中,因此也並沒有通過class object

若由A中的一函數調用static data member,會發生如下轉化:

//do為A中的函數
do().i = 0;

//轉化求值
(void) do();
A.i = 0;

若取static data member地址,也只會得到指向其類型的指針,並不會指向其class member

&A::i;

//轉化
const int*

nonstatic data members

  • nonstatic data members存放在class object中,需經過explict或implicit class object進行存取,且進行存取操作時,編譯器還需要把class object的起始地址加上data member的offset

    A A::do1( const A& pt )
    {
        x += pt.x;
        y += pt.y;
        z += pt.z;
    }
    
    //轉化
    A A::do1( A* const this, const A& pt )
    {
        this->x += pt.x;
        this->y += pt.y;
        this->z += pt.z;
    }
    
    --------------------------------------------分割線--------------------------------------------------------
    
    a.y = 0.0;
    
    //起始地址+offset
    &a + (A::y - 1);
    
    • 這裡的"-1"操作是因為指向data member的指針的offset總是被加上1,如此編譯系統即可區分"指向data member的指針,用以指出class的第一個member"和"指向data member的指針,沒有指向任何member"兩種情況

      • 取一個nonstatic dat member的地址,會得到它在class中的offset;而取一個綁定在class object上的data member的地址,會得到他在記憶體中的真實地址
      class B
      {
          public:
          	virtual ~B();
          protected:
          	static B origin;
          	float x,y,z;
      }
      
      //&origin: 當前地址減去offset並加一
      float B::* p1 = &origin.y;
      
      //最終得到val:offset + 1
      float B::* p2 = &B::x;	//B::* 是指向B data member的指針
      
    • 因為offset的值於編譯期即可得出,因此存取一個nonstatic data member其實效率和c struct member一樣,派不派生也是如此

data member的繼承

單一繼承

  • 對於derived class object,編譯器可以自由其derived class member 和 base class member的排列順序,但大部分編譯器中,base class members會先出現(以上virtual base class除外)

​ 現有以下代碼:

class Point2d
{
    public:
    	float x() { return _x; }
    	float y() { return _y; }
    	
    	void operation+=( const Point2d& rhs )
        {
            _x += rhs.x();
            _y += rhs.y();
        }
    	...	//constructor
            
    private:
    	float _x, _y;
}

class Point3d
{
    public:
    	float z() { return _z; }
    
    	void operation+=( const Point3d& rhs )
        {
            Point2d::operator+=( rhs );
            _z += rhs.z();
        }
    	...//constructor
    
    private:
    	float _z;
}
  • 以上這種繼承被稱為具體繼承(concrete inheritance),derived class繼承base class的data member和 member function,將之局部化,但這種行為並不會增加空間和時間上的額外負擔。沒有virtual function時,佈局其實和c struct一樣

  • 對於具體繼承,需要註意因alignment padding膨脹的空間

    //32位
    
    //A大小 4 + 1 + alignment 3
    class A
    {
        private:
        	int val;
        	char c1;
    }
    
    //B大小由8 + 1 + aligment3
    class B : public A
    {
    	private:
        	char c2;
    }
    
    //根據B的意思,C也就是16
    class C : public B
    {
        private:
        	char c3;
    }
    

    也許你會認為,這不是浪費很多空間嗎,為什麼不讓derived class member直接填上base class aligment那一部分?

    image-20221018185652966

    如果是以上佈局,又會產生一個問題:繼承而得的members會被覆蓋

    B* pb;
    A* pa1, pa2;	//可指向ABC
    
    pa1 = pb;
    
    //這將導致c2的值被覆蓋掉
    *pa2 = *pa1;
    

多態(單一)繼承

​ 現有如下代碼:

class Point2d
{
    public:
    	float x() { return _x; }
    	float y() { return _y; }
    	
    	virtual void operation+=( const Point2d& rhs )
        {
            _x += rhs.x();
            _y += rhs.y();
        }
    	
    	virtual float z() { return 0.0; }
    	virtual void z(float) { }
    	...	//constructor
            
    private:
    	float _x, _y;
}

class Point3d
{
    public:
    	float z() { return _z; }
    
    	void operation+=( const Point2d& rhs )
        {
            Point2d::operator+=( rhs );
            _z += rhs.z();
        }
    	...//constructor
    
    private:
    	float _z;
}

//p1和p2可能為Point2d類型,也可能為Point3d類型
void do( Point2d& p1, Point2d& p2 )
{
    p1 += p2;
}
  • 支持多態繼承會造成空間和時間上的負擔:
    • virtual table,存放virtual functions地址和slots(支持runtime type identification)
    • 每個class object導入一個vptr
    • 優化constructor,在其中設定vptr的初值,使其指向class應對應的virtual table
    • 優化destructor,在其中抹去vptr
  • 對於編譯器來說,vptr一般放於class object尾端,如此可以保留base class C對象佈局,放在c中亦可使用

多重繼承

  • 對於單一繼承這種形式,base class object 和 derived class object都是從相同地址開始(例如先前實例中),因此將derived class object指定給base class的指針或引用,編譯器不需要針對其修改地址,執行效率很高

​ 現有如下代碼:

class Point2d
{
    public:
    ...	//含有virtual函數
        
    protected:
    float _x, _y;
}

class Point3d : public Point2d
{
    ...
    
    protected:
    	float _z;
}

class Vertex
{
    public:
    ... //含有virtual函數
        
    protected:
    	Vertex* next;
}

class Vertex3d : public point3d, public Vertex
{
    ...
    
    protected:
    	float mumble; 
}

Vertex3d v3d;
Vertex* pv;
Point2d* p2d;
Point3d* p3d;

pv = &v3d;
//內部轉換 pv = (Vertex*)( ( (char*)&v3d ) + sizeof(Point3d) );

//無需轉換
p2d = &v3d;
p3d = &v3d
    
Vertex3d* pv3d;
Vertex* pv;

//若想進行指針的指定操作,還需加個判斷
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof( Point3d );		//pv3d可能為野指針

記憶體佈局:

image-20221018201105749

  • c++並未要求多重derived class object中,base class objects有特定的排列順序
  • 對於多重派生對象,例如Vertex3d,將地址指定給最左端base class(point3d)時,無需修改地址,因為兩者起始地址相同;但往後的base class,需要修改地址,加上或減去介於其中的base class subobjects大小。若存取往後的base class data members,也並不需要付出額外成本,members的位置在編譯期已固定,通過offset運算即可得出

虛擬繼承

​ iostram library:

//對應如下左圖
class ios {...};
class istream : public ios {...};
class ostream : public ios {...};
class iostream : public istream, public ostream {...};

//對應如下右圖
class ios {...};
class istream : virtual public ios {...};
class ostream : virtual public ios {...};
class iostream : public istream, public ostream {...};

image-20221018203932844

​ 根據如上可知,虛擬繼承可以解決存儲多個同一base class的問題(ios),那麼這是如何實現的呢?

  • class內若內含virtual base class subobjects,會被分割為兩部分:一個不變區域和一個共用區域
    • 不變區域:含有固定的offset,不受影響,可以直接存取
    • 共用區域:也就是virtual base class subobjects,這一區域會受每次派生操作影響而變化,只可以被簡介存取

​ 編譯期實現策略:

class Point2d
{
    ...
    protected:
    	float _x, _y;
}

class Point3d : virtual public Point2d
{
    ...
    protected:
    	float _z;
}

class Vertex : virtual public Point2d
{
    ...
    protected:
    	Vertex* next;
}

class Vertex3d : public Vertex, public Point3d
{
    ...
    protected:
    	float mumble;
}
  • 一般的佈局策略是先安排derived class不變部分,隨後建立共用部分

  • 存取class的共用部分:在每一個derived class object中安插一些指針,每個指針指向一個virtual base class

image-20221018205728613

void Point3d::operator+=( const Point3d& rhs )
{
    _x += rhs._x;
    _y += rhs._y;
    _z += rhs._z;
}

//進行如下轉換

__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
_z += rhs._z;
----------------------------------分割線-------------------------
    
Point2d* p2d = pv3d;
//進行如下轉換
Point2d* p2d = pv3d ? pv3d->__vbcPoint2d : 0;

​ 然而,這種實現模型卻存在兩個缺點:

  • 每個對象針對每一個virtual base class含有一個指向其class的指針
  • 隨著虛擬繼承串鏈的變長,間接存取層次也會增加。(如三層虛擬派生,則有三次間接存取,也就是三個virtual base class指針)

​ 解決:

  • 對於第一個,引入virtual base class table,virtual base class指針放在table中,編譯期會安插一個指針指向virtual base class table

  • 對於第二個,拷貝取得所有的nested virtual base class指針

  • virtual base class最有效的形式:一個抽象virtual base class,不含data member

對象成員和指向data member的指針效率

  • 對於對象成員,在編譯期未優化時聚合、封裝、繼承方式在存取方面都有效率上的差異;優化後都是相同的,且封裝並不會帶來執行器的效率成本。其中聚合和封裝、單一繼承效率高,因為單一繼承中members被連續存儲在derived class中,且offset於編譯期就計算出了;但虛擬繼承的效率很低
  • 對於指向data member的指針,在編譯期未優化時,通過指針間接存取效率相對於直接存取會更低,但優化後都是一樣的;單一繼承並不會降低效率,但虛擬繼承中,因每一層都導入一個額外層次的間接性,因此效率較差

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 策略模式是一種非常容易理解的設計模式,其最常見的應用場景是,利用它來避免冗長的 if-else 或 switch 分支判斷。 ...
  • 實際工程中,很多時候需要做到工程的分散,尤其是涉及到保密的源碼模塊。這裡以Qt Quick為例基於cmake演示一遍工程的多項目化。 ...
  • 滿漢樓01-4 4.功能實現03 4.5訂座功能 4.5.1功能說明 如果該餐桌處於已經預定或者就餐狀態時,不能進行預定,並給出相應提示 4.5.2思路分析 根據顯示界面,要考慮以下兩種狀態 檢測餐桌是否存在 檢測餐桌的狀態 如果餐桌存在且狀態為空(即可以預定),在預定過後要修改餐桌狀態 4.5.3 ...
  • 1.通過key獲取value dict = {key1: value1, key2:value2} dict['key1'] 可獲取到key1對應的value1 person = {'name': 'tt', 'age': 13} print(person['age']) # 13 test_dic ...
  • 滿漢樓01-2 4.功能實現01 4.1導入驅動和工具類 4.1.1導入驅動 首先將連接mysql的相關jar包引入項目中,分別右鍵,點擊add as library 4.1.2導入工具類Utility 準備工具類Utility,提高開發效率,並搭建整個項目的整體架構 在實際開發中,公司都會提供相應 ...
  • @(文章目錄) 前言 說明下如何在VSCode下麵搭建C/C++環境以及運行 下載 點擊該鏈接,進行ming64安裝包下載:ming64安裝包 VSCode安裝請自行百度,這裡不在贅述。 安裝 將下載完成後的安裝包,解壓放到C盤下麵即可,如下圖所示: 添加環境變數 在我的電腦上點擊右鍵-->屬性:進 ...
  • 滿漢樓01 1.需求分析 滿漢樓項目說明 因為javaGUI不是學習的重點,這裡將繼續使用控制台界面來代替界面和事件處理 完成的功能: 登錄 訂座 點餐 結賬 查看賬單等功能 在實際項目中,獨立完成項目新功能非常重要,這是鍛煉編程能力和思想的重要途徑 2.界面設計 用戶登錄 顯示餐桌狀態 預定 顯示 ...
  • 聲明 本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關! 本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...