1. 程式人生 > >為什麽通過空指針(NULL)能夠正確調用類的部分成員函數

為什麽通過空指針(NULL)能夠正確調用類的部分成員函數

函數的調用 wid 分析 使用 coo win data- func 標準

#include <iostream>

using namespace std;

class B {
public:
    void foo() { cout << "B foo " << endl; }
    void pp() { cout << "B pp" << endl; }
    void FunctionB() { cout << "funB" << endl; }
};

int main()
{
    B *somenull = NULL;
    somenull->foo();
    somenull->pp();
    somenull->FunctionB();

    return 0;
}

為什麽 somenull 為空指針,還能執行通過呢?

能夠闡明“靜態綁定”和“動態綁定”的差別。


真正的原因是:由於對於非虛成員函數,C++這門語言是靜態綁定的。

這也是C++語言和其他語言Java, Python的一個顯著差別。

以此以下的語句為例:

somenull->foo();
這語句的意圖是:調用對象somenull的foo成員函數。

假設這句話在Java或Python等動態綁定的語言之中,編譯器生成的代碼大概是:
找到somenull的foo成員函數。調用它。

(註意,這裏的找到是程序執行的時候才找的,這也是所謂動態綁定的含義:執行時才綁定這個函數名與其相應的實際代碼。

有些地方也稱這樣的機制為遲綁定。晚綁定。)
可是對於C++。為了保證程序的執行時效率,C++的設計者覺得凡是編譯時能確定的事情,就不要拖到執行時再查找了。所以C++的編譯器看到這句話會這麽幹:
1:查找somenull的類型,發現它有一個非虛的成員函數叫foo。(編譯器幹的)
2:找到了。在這裏生成一個函數調用,直接調B::foo(somenull)。
所以到了執行時,因為foo()函數裏面並沒有不論什麽須要解引用somenull指針的代碼,所以真實情況下也不會引發segment fault。這裏對成員函數的解析,和查找其相應的代碼的工作都是在編譯階段完畢而非執行時完畢的,這就是所謂的靜態綁定。也叫早綁定。


正確理解C++的靜態綁定能夠理解一些特殊情況下C++的行為。

this 指針是空指針 不去騷擾他 他就不搞死你
你敢動他試試

假設還沒有看煩,能夠參考以下的這些東西。

有以下的一個簡單的類:

class CNullPointCall
{
public:
static void Test1();
void Test2();
void Test3(int iTest);
void Test4();

private:
static int m_iStatic;
int m_iTest;
};

int CNullPointCall::m_iStatic
= 0;

void CNullPointCall::Test1()
{
cout
<< m_iStatic << endl;
}

void CNullPointCall::Test2()
{
cout
<< "Very Cool!" << endl;
}

void CNullPointCall::Test3(int iTest)
{
cout
<< iTest << endl;
}

void CNullPointCall::Test4()
{
cout
<< m_iTest << endl;
}

那麽以下的代碼都正確嗎?都會輸出什麽?

CNullPointCall *pNull = NULL; // 沒錯,就是給指針賦值為空
pNull->Test1(); // call 1
pNull->Test2(); // call 2
pNull->Test3(13); // call 3
pNull->Test4(); // call 4

你肯定會非常奇怪我為什麽這麽問。

一個值為NULL的指針怎麽能夠用來調用類的成員函數呢?。但是實事卻非常讓人驚訝:除了call 4那行代碼以外,其余3個類成員函數的調用都是成功的。都能正確的輸出結果。並且包括這3行代碼的程序能非常好的執行。
經過細心的比較就能夠發現,call 4那行代碼跟其它3行代碼的本質差別:類CNullPointCall的成員函數中用到了this指針。


對於類成員函數而言,並非一個對象相應一個單獨的成員函數體,而是此類的全部對象共用這個成員函數體。 當程序被編譯之後。此成員函數地址即已確定。而成員函數之所以能把屬於此類的各個對象的數據差別開, 就是靠這個this指針。函數體內全部對類數據成員的訪問, 都會被轉化為this->數據成員的方式。


而一個對象的this指針並非對象本身的一部分。不會影響sizeof(“對象”)的結果。this作用域是在類內部,當在類的非靜態成員函數中訪問類的非靜態成員的時候。編譯器會自己主動將對象本身的地址作為一個隱含參數傳遞給函數。也就是說,即使你沒有寫上this指針。編譯器在編譯的時候也是加上this的。它作為非靜態成員函數的隱含形參。對各成員的訪問均通過this進行。


對於上面的樣例來說,this的值也就是pNull的值。也就是說this的值為NULL。

而Test1()是靜態函數,編譯器不會給它傳遞this指針,所以call 1那行代碼能夠正確調用(這裏相當於CNullPointCall::Test1())。對於Test2()和Test3()兩個成員函數,盡管編譯器會給這兩個函數傳遞this指針,可是它們並沒有通過this指針來訪問類的成員變量,因此call 2和call 3兩行代碼能夠正確調用;而對於成員函數Test4()要訪問類的成員變量,因此要使用this指針,這個時候發現this指針的值為NULL。就會造成程序的崩潰。
事實上,我們能夠想象編譯器把Test4()轉換成例如以下的形式:

void CNullPointCall::Test4(CNullPointCall *this)
{
cout
<< this->m_iTest << endl;
}

而把call 4那行代碼轉換成了以下的形式:

CNullPointCall::Test4(pNull);

所以會在通過this指針訪問m_iTest的時候造成程序的崩潰。
以下通過查看上面代碼用VC 2005編譯後的匯編代碼來詳解一下奇妙的this指針。
上面的C++代碼編譯生成的匯編代碼是以下的形式:

CNullPointCall *pNull = NULL;
0041171E mov dword ptr [pNull],
0
pNull
->Test1();
00411725 call CNullPointCall::Test1 (411069h)
pNull
->Test2();
0041172A mov ecx,dword ptr [pNull]
0041172D call CNullPointCall::Test2 (4111E0h)
pNull
->Test3(13);
00411732 push 0Dh
00411734 mov ecx,dword ptr [pNull]
00411737 call CNullPointCall::Test3 (41105Ah)
pNull
->Test4();
0041173C mov ecx,dword ptr [pNull]
0041173F call CNullPointCall::Test4 (411032h)

通過比較靜態函數Test1()和其它3個非靜態函數調用所生成的的匯編代碼能夠看出:非靜態函數調用之前都會把指向對象的指針pNull(也就是this指針)放到ecx寄存器中(mov ecx,dword ptr [pNull])。這就是this指針的特殊之處。看call 3那行C++代碼的匯編代碼就能夠看到this指針跟一般的函數參數的差別:一般的函數參數是直接壓入棧中(push 0Dh)。而this指針卻被放到了ecx寄存器中。

在類的非成員函數中假設要用到類的成員變量,就能夠通過訪問ecx寄存器來得到指向對象的this指針。然後再通過this指針加上成員變量的偏移量來找到對應的成員變量。


以下再通過另外一個樣例來說明this指針是如何被傳遞到成員函數中和如何使用this來訪問成員變量的。
依舊是一個非常easy的類:

class CTest
{
public:
void SetValue();

private:
int m_iValue1;
int m_iValue2;
};

void CTest::SetValue()
{
m_iValue1
= 13;
m_iValue2
= 13;
}

用例如以下的代碼調用成員函數:

CTest test;
test.SetValue();

上面的C++代碼的匯編代碼為:

CTest test;
test.SetValue();
004117DC lea ecx,[test]
004117DF call CTest::SetValue (4111CCh)

相同的,首先把指向對象的指針放到ecx寄存器中;然後調用類CTest的成員函數SetValue()。

地址4111CCh那裏存放的事實上就是一個轉跳指令,轉跳到成員函數SetValue()內部。

004111CC jmp CTest::SetValue (411750h)

而411750h才是類CTest的成員函數SetValue()的地址。

void CTest::SetValue()
{
00411750 push ebp
00411751 mov ebp,esp
00411753 sub esp,0CCh
00411759 push ebx
0041175A push esi
0041175B push edi
0041175C push ecx
// 1
0041175D lea edi,[ebp-0CCh]
00411763 mov ecx,33h
00411768 mov eax,0CCCCCCCCh
0041176D rep stos dword ptr es:[edi]
0041176F pop ecx
// 2
00411770 mov dword ptr [ebp-8],ecx // 3
m_iValue1 = 13;
00411773 mov eax,dword ptr [this] // 4
00411776 mov dword ptr [eax],0Dh // 5
m_iValue2 = 13;
0041177C mov eax,dword ptr [
this] // 6
0041177F mov dword ptr [eax+4],0Dh // 7
}
00411786 pop edi
00411787 pop esi
00411788 pop ebx
00411789 mov esp,ebp
0041178B pop ebp
0041178C ret

以下對上面的匯編代碼中的重點行進行分析:
1、將ecx寄存器中的值壓棧,也就是把this指針壓棧。
2、ecx寄存器出棧,也就是this指針出棧。


3、將ecx的值放到指定的地方,也就是this指針放到[ebp-8]內。


4、取this指針的值放入eax寄存器內。

此時,this指針指向test對象,test對象僅僅有兩個int型的成員變量,在test對象內存中連續存放。也就是說this指針眼下指向m_iValue1。
5、給寄存器eax指向的地址賦值0Dh(十六進制的13)。事實上就是給成員變量m_iValue1賦值13。
6、同4。
7、給寄存器eax指向的地址加4的地址賦值。在4中已經說明,eax寄存器內存放的是this指針,而this指針指向連續存放的int型的成員變量m_iValue1。

this指針加4(sizeof(int))也就是成員變量m_iValue2的地址。

因此這一行就是給成員變量m_iValue2賦值。
通過上面的分析。我們能夠從底層了解了C++中this指針的實現方法。

盡管不同的編譯器會使用不同的處理方法。可是C++編譯器必須遵守C++標準,因此對於this指針的實現應該都是幾乎相同的。


為什麽通過空指針(NULL)能夠正確調用類的部分成員函數