探秘C 機制的實現

2021-06-20 03:23:23 字數 3944 閱讀 5426

我曾經自學過c++,現在回想起來,當時是什麼都不懂。說不上能使用c++,倒是被c++牽著鼻子走了。高中搞noip並不允許使用stl庫,比賽中c++物件導向的機制基本沒有什麼用武之地,所以高中搞noip名為用c++,其實就是c加上了cout和cin。

前幾天看韓老師的《老碼識途》,裡面記錄了一些c++物件導向機制的探索,又勾起了我的興趣。而這個學期自學了彙編,又給了我自己動手探索提供了能力基礎,自己上手以後,從乙個更加底層的視角看c++機制的實現,讓我在黑暗中摸到了馴服c++的韁繩。

本質上是指標,這一點即使大家沒有看反彙編應該也是猜到了。

class father

2: ;
9:
10:

class child : father

11: ;

乙個father物件裡只包含 (低位址 –> 高位址) : ia_,ib_。也就是乙個father物件的大小是8個位元組,函式並不會占用記憶體空間。

為什麼不會?

其實類的成員函式可以看做本質上與普通函式相同。

編譯器在編譯的時候就知道函式的位置,所以呼叫普通函式的時候會直接 call 函式位址(偏移)。也就是被硬編碼了,函式的位址是固定的( 不考慮重定位之類的情況 )。

而成員函式的呼叫也是如此,只是編譯器還多做了一件事情,就是判斷這個物件有沒有呼叫這個函式的「許可權」(函式不是你宣告的,當然無權呼叫),「許可權」不夠就會報錯,告訴那個物件型別沒有這個方法。

所以,類物件的大小與這個類的方法數多少是沒關係的。成員函式和普通函式本質上一樣,實現這個機制,要靠編譯器來做工作。

成員函式與普通函式不同之處之一就是訪問物件的資料。

要訪問乙個物件的元素,說白了就是要找到這個元素所在的記憶體位置,也就是要有指標。

我們沒有看到傳遞this指標,因為這件事又是編譯器幫我們做了。

反彙編會看到物件呼叫乙個方法的時候,會將這個物件的首部位址賦值給ecx暫存器,通過暫存器來傳遞this指標。

我們在成員函式裡可以不需明寫this指標地呼叫物件元素,還是因為編譯器幫我們多做了一步「翻譯」。

不多說,就是編譯器在編譯階段通過原始碼來判斷某個元素是不是能夠被訪問,某個方法是不是能夠被呼叫,執行的時候並不會有訪問限制。看**:

#include

2:
3:

class exp

4:
13:

void out()

14:
17: };
18:
19:

int main()

20:

結果是: 0    0

1    2

雖然 ia_,ib_是私有的,但是還是被外界修改了。因為編譯器無法知道我幹了這事(顯式的 oa.ia_ = 1 就被發現了哈)

說道底還是編譯器幫我們在多做了一些工作,生成了一些額外**。

需要注意的是:

void test( father op )

2:
4:
5:

int main()

6:

會呼叫拷貝建構函式。

一樣還是編譯器的功勞,c++最後生成的函式名是與引數有關的,所以又不同引數的函式最後生成的函式名不同,看似同名,實則不同。在函式呼叫的時候,編譯器會判斷引數的型別,相應的可以生成乙個函式名進行「匹配」。( 當然不止這麼簡單,還會考慮發生型別轉換的情況 )

從記憶體布局的角度上看

struct child : father

和 struct child

2: ;

相同(虛函式情況後面討論)。子類的前面部分和父類是一樣的。

所以乙個接受 father * 引數的函式可以接受 child *引數,而且轉換是安全的。

有 father & 型別引數的函式可以接受 child &,但是繼承方式要public。but , why ?

protected和private繼承模式,子類繼承的父類的介面對外都是隱藏的,所以以乙個father &傳入的引數所有的方法元素原則上是不可用的,用了肯定是違反規則的,編譯器判定這一點,所以報錯。

比較特別的是這個。

question:為什麼需要虛函式?

網上看到的答案:基類可以通過虛函式對子類的相識功能進行管理。(我的c++primer被借走以後就此失蹤,所以只能網上找了)。

虛函式具體怎麼回事就不細說了,討論一下背後的機制。

為了能夠實現虛函式,每個有虛函式的類有一張對應的虛表。這個虛表儲存在唯讀記憶體區,記錄了對應函式的位址。(ps:乙個類就只有乙個虛表)

每個類物件都要儲存乙個虛表指標,儲存本類的虛表位址。所以你使用 father *指標指向乙個child物件,呼叫的虛函式是child的。

虛表指標儲存在每個物件的首部。

class child : father

2: ;

好。問題來了,child繼承了father,但是father的函式並沒有為child再量身定做一次,也就是說無論是father物件還是child物件,他們呼叫funca()都是同乙個函式。但是father並沒有__vfptr,child物件在頭部多了這個,funca()中用this指標定位ia_和ib_不是都不正確嗎?

現象告訴我們funca()是可以正確訪問ia_和ib_,所以推測child物件在呼叫funca的時候,傳的不是真正的首部位址,而是往後偏移了四個位元組。

反彙編,確實如此。這麼說father類裡不能呼叫虛函式了?當然,father都還不知道虛函式這回事,怎麼在funca中呼叫。

還有乙個有趣的現象:

#include

2:
3:

class base

4:
10: };
11:
12:

class cb : public base

13:
19: };
20:
21:

class cc : public base

22:
28: };
29:
30:

void test( cb& ob )

31:
34:
35:

int main()

36:

猜猜結果啊,買定離手。

結果是:cb   cb   cc    cc

在43行的地方,修改了ob的虛表指標,讓其指向cc類的虛表。

但是ob.showid()沒理會我們的修改,還是呼叫cb類的showid。反彙編,發現他沒走「獲取虛表指標,在虛表中得到相應的函式位址」這一套,直接呼叫了。因為一般人不會閒著蛋疼去改物件的虛表指標的,物件的型別是明確的,編譯器可以通過這些資訊確定呼叫的函式位址,所以沒必要走他一套,這樣效率還更高。

而pcb->showid()就不同了,他很乖地地走了流程,因為乙個父類指標可以指向乙個子類物件,編譯器無法找資訊,所以走流程。

那現在糾結了,為神馬 ((cb*)(&ob))->showid() 輸出cb。

反彙編看,發現編譯器又擅自做主,沒有走指標的流程。

那你猜猜((base*)(&ob))->showid();輸出的是什麼?cc。

比較二者的差異,可以大概發現一些端倪,什麼時候走流程,什麼時候不走。

最後是test(ob)了,前面說過引用的本質是指標,所以這個結果很好理解。

還有,想過

void test2( base op )

2:

拷貝的時候有沒有拷貝虛表指標嗎?試試就知道,厄…發現沒有。

前面說過這樣會呼叫拷貝建構函式,但是你在這個函式你沒有寫虛表指標的賦值。但是**的編譯器已經幫你悄悄加上去了哈哈哈哈~。(唉?節操呢)

每個類有特定的虛表位址,每個物件會儲存這個虛表位址,應該想到了吧,偷懶,不寫了。

綜上。可以看到,物件導向機制在底層並不特別,機制的實現主要靠的是編譯器。

Spark Streaming反壓機制探秘

spark streaming中的反壓機制是spark 1.5.0推出的新特性,可以根據處理效率動態調整攝入速率。當批處理時間 batch processing time 大於批次間隔 batch interval,即 batchduration 時,說明處理資料的速度小於資料攝入的速度,持續時間過...

探秘AOP實現原理

可以這麼說,aop是基於動態 實現的。那麼,這個過程是怎樣的?首先,我們有這樣的乙個service類,它是被作為切面的乙個類 public class service implements user new handler new service 這裡需要實現乙個handler public cla...

探秘C 仿函式

最近喵哥遇到乙個問題 如何在不借助額外空間 新建vector等 來實現map自己的想法 不只是表面的公升序 降序 排序 sort只適用於順序容器,map並不可以使用 如果忽略 不借助額外空間這個要求 完全可以用乙個vector來實現 include include include include i...