C++ 對象是如何完成成員函數的調用的?

出去吃飯,路上一邊走一遍想,就明白了。

結題。

以下更新是個人理解,如有偏差還望指正。

編譯期間就解決了成員函數訪問的問題

內存對象模型裡面僅保存變數,這裡就包括指向虛表的指針,如果類定義里沒有虛函數,那就沒虛表什麼事兒,_vprt會被優化掉,如果有,那麼對象起始地址出就開始存放_vprt,如果涉及多個續表,"小夥伴,排排坐",之後的虛表指針接著第一個續表指針存放。

對於成員函數,如果調用成員函數,編譯器會負責把已經生成好的成員函數首地址給要調用的對象。比方說我前面提到的GP對象,GP.show()這個過程,會在編譯期間直接把show的地址給對象GP(不知道我這樣描述是否清楚。就是說,會直接或簡介的生成彙編代碼,讓GP去call show()函數。不會把調用的過程留到運行時去解決)。 而之前我犯糊塗,總想著得對象得有個指針來著。其實不必要,因為我們不會把問題留到運行時去確定。

最後也推薦&<&&>

如果對於這個問題還有疑問,歡迎一起交流討論。

很有趣。

謝謝各位的關注以及解答。

EOF

=================我是雙重分割線=================

在C++中,我們已知對於數據成員會被存放在對象中,但是一般的函數成員不會。如是虛函數,會通過一個virtual table的_vprt來管理好對象的虛函數。對象調用虛函數的時候是間接通過指針 _vprt來完成的。

那麼問題來了: )

對於普通的成員函數,他們並不存在在對象中,但是C++又是具體通過何種方式完成了對於這些函數的定址還有調用呢?

具體的,我們可以針對如下代碼進行討論

class grand_parent
{
public:

/*
Grand Parent Data in Public
*/
int gp_pub_data;

grand_parent():gp_prv_data(100), gp_pub_data(100){ }

virtual void f(void){ cout &<&< " GrandParent::f()" &<&< endl; } virtual void g(void){ cout &<&< " GrandParent::g()" &<&< endl; } virtual void h(void){ cout &<&< " GrandParent::h()" &<&< endl; } void show() { cout &<&< "gp_pub_data: " &<&< gp_pub_data &<&< endl; cout &<&< "gp_prv_data: " &<&< gp_prv_data &<&< endl; } private: /* Grand parent Data in Private */ int gp_prv_data; };

如果通過該類的對象,是如何完成對於成員函數show()的調用的。

如果可以的話,請從 memory model 還有 linker 兩個方面或者更多的方面進行分析

問題補充:

具體的說,假設這裡如果定義了grand_parent的對象GP(在main函數裡面),那麼這個對象GP就在main函數的stack frame里。而這個對象GP裡面有_vprt, 有數據成員 gp_prv_data 和 gp_pub_data,還有其他的數據?

正是因為我目前了解到沒別的東西了,所以我就覺得很奇怪,究竟是怎樣的方式,完成了對象GP.show()的調用。因為GP對象內部是不存在show()的信息的。沒錯,編譯期間是會把show()的地址確定下來,但是棧上GP對象並沒有額外的開銷一個指針變數或者空間去儲存這個show()函數的地址吧。

這裡成員show()並不是一個指針,而是一個具體的函數,但是對象少了指針又怎麼在運行時找到這個成員函數。

以上 : )

EOF


struct FuckTable#grand_parent
{
void (*f)(grand_parent*);
void (*g)(grand_parent*);
void (*h)(grand_parent*);
};

struct grand_parent
{
FuckTable#grand_parent* fuckTable;
int gp_pub_data;
int gp_prv_data;
};

void grand_parent#f(grand_parent* this){ ... }
void grand_parent#g(grand_parent* this){ ... }
void grand_parent#h(grand_parent* this){ ... }
void grand_parent#show(grand_parent* this){ ... }

FuckTable#grand_parent FuckTable#grand_parent#for#grand_parent =
{
grand_parent#f,
grand_parent#g,
grand_parent#h,
};

void grand_parent#grand_parent(grand_parent* this)
{
this-&>fuckTable = FuckTable#grand_parent#for#grand_parent;
this-&>gp_prv_data = 100;
this-&>gp_pub_data = 100;
}

==========================================

// grand_parent fuck;
grand_parent fuck;
grand_parent#grand_parent(fuck);
// fuck.show();
grand_parent#show(fuck);
// fuck.f();
(fuck.fuckTable-&>f)((grand_parent*)((intptr_t)fuck + 0));

作業:請將下面的代碼還原成C語言:

#include &

using namespace std;

#define cm

class Fuck
{
public:
int x;
Fuck(int y):x(y){}
virtual ~Fuck(){}
virtual int Screw(int a) { return a + 1; }
};

class Shit : public virtual Fuck
{
public:
Shit(int y):Fuck(y + 1){}
int Screw(int a)override { return a + 2; }
};

class Bitch : public virtual Fuck
{
public:
Bitch(int y):Fuck(y + 2){}
int Screw(int a)override { return a + 3; }
};

class Dick : public Shit, public Bitch
{
public:
Dick(int y):Fuck(y + 3){}
int Screw(int a)override { return a + 3; }
};

int main()
{
auto fire = Fuck::Screw;
Dick dick = 22 cm;
Fuck* longDick = dick;
cout &<&< (longDick-&>*fire)(100) &<&< endl; return 0; }


成員函數本質上可以看做全局函數,不過第一個參數固定為this。因此對象點方法等同於方法(對象,),方法是全局的,當然能找到


「Note:定義在類內部的函數是隱式的inline函數(參見6.5.2節,第214頁)。」 —— 《C++ Primer》 中文第五版 P230

「成員函數通過一個名為this的額外隱式參數來訪問調用它的那個對象。」 —— 《C++ Primer》 中文第五版 P231

題主問的這個問題其實得怪C++……因為它默認是靜態綁定的。

《C++ Primer》是本好書……


普通成員函數調用的的時候會額外加一個指針進去。也就是

GP.show() == grand_parent::show(GP)


為什麼地址是確定的還需要一個指針去記錄呢,比如你寫了一個全局的test()函數,在main里調用他,反彙編以後發現這個調用會被直接翻譯成call xxxx,對類的成員函數的調用也是一樣的啊,對普通成員函數的調用都是static binding的,在編譯階段就能把所有調用確定下來,而編譯階段這個函數的地址是已知常數,所以沒必要再用一個指針去記錄他,不過類的成員函數和普通的全局函數有一點不一樣就是調用類的成員函數的時候會首先傳入調用他的對象的地址作為第一個參數


除了虛函數,決定調用哪一個函數的過程在編譯期就已經完成了。

每一個成員函數的第一個參數都應為this,這是編譯器生成的。

有些題外話

在使用std::bind來包裝成員函數時,第二個參數必須要設定為一個相應對象的地址(第一個對象是需要被bind的成員函數的地址),否則是不能通過編譯的。


糾結了好久這個問題,看到帖子恍然大悟,編譯時候類是知道自己有哪些no-virtual成員函數(編譯器通過編碼規則判斷),並且下發所有被調用的成員函數首地址給了每一個對象,編譯結束後,類不在關心自己有哪些成員函數,因為每一個對象都得到了分配,所以類中也就不用存貯函數的地址了,所以GP.show()不需要通過類去找show,而是在編譯的時候就已經將show定好了,GP只是提供一個this來供show()調用決定當前的操作對象。


class CTest
{
public:
void func(){cout &<&< "Hello world!" &<&< endl;} }; int main() { CTest* pCTest = NULL; pCTest-&>func();
}

---------
output:
---------
Hello world!


一個C++成員函數只是類範圍內的又一個成員。X類每一個非靜態的成員函數都會接受一個特殊的隱藏參數——this指針,類型為X* const。

該指針在後台初始化為指向成員函數工作於其上的對象。同樣,在成員函數體內,成員變數的訪問是通過在後台計算與this指針的偏移來進行。


推薦閱讀:

vs2013 有必要 使用 visual assist或resharper嗎?
如何閱讀protobuf源碼?
為什麼說 Scala 是 JVM 上的 C++?

TAG:編程語言 | 編程 | C |