編程探秘之函數(shù)

在進(jìn)入主題之前,需要提前說明:
本文中的函數(shù)是廣義上的函數(shù)迎捺,包括純面向?qū)ο笳Z言的中成員函數(shù)举畸。文中的例子都是基于C++的,運(yùn)行環(huán)境:64位MacOS Big Sur Xcode 12凳枝。為了避免干擾抄沮,代碼中省略了內(nèi)存管理的代碼。

在軟件開發(fā)過程中岖瑰,經(jīng)常會(huì)發(fā)現(xiàn)相同功能的代碼叛买,差別僅僅是幾個(gè)參數(shù)不一樣。DRY原則告訴我們蹋订,不要寫重復(fù)的代碼率挣。于是,程序員們想到了使用函數(shù)對(duì)特定功能進(jìn)行封裝露戒。幾乎所有編程語言都支持編寫函數(shù)椒功。編程中使用函數(shù)是如此地自然,人們甚至忘了它的由來智什。

一动漾、從函數(shù)早期聊起

1.誕生的故事

EDSAC計(jì)算機(jī)于1949年5月6日投入運(yùn)行,它是世界上第一臺(tái)實(shí)用的存儲(chǔ)程序計(jì)算機(jī)荠锭。此前的計(jì)算機(jī)每次執(zhí)行新的運(yùn)算旱眯,都需要插入不同的線路進(jìn)行重新裝配,而EDSAC 則通過存儲(chǔ)器中的軟件實(shí)現(xiàn)各種不同的運(yùn)算操作节沦,這就對(duì)編程提出了很高的要求键思。許多程序在運(yùn)行的過程中,都需要重復(fù)執(zhí)行某個(gè)操作甫贯,比如在某個(gè)復(fù)雜的數(shù)字運(yùn)算中吼鳞,需要多次進(jìn)行開平方操作。如果每次開平方都得把平方根代碼寫上叫搁,那么程序當(dāng)中就會(huì)出現(xiàn)許多重復(fù)代碼赔桌,占用不必要的空間(EDSAC 的內(nèi)存只有兩千字節(jié)左右),使程序變得龐大渴逻。

為了簡化編程過程疾党,威爾克斯的方法是建立子程序庫,也就是將常見的子程序單獨(dú)列出惨奕,集中起來雪位。一旦程序在運(yùn)行的過程中需要使用到某個(gè)常見的子函數(shù),計(jì)算機(jī)就會(huì)在子程序庫中“查找定義”梨撞,執(zhí)行相應(yīng)的子程序代碼雹洗,根據(jù)輸入值進(jìn)行運(yùn)算香罐,再將運(yùn)算結(jié)果返回。

威爾克斯團(tuán)隊(duì)比馮·諾依曼更早使用了匯編語言时肿,其子程序的思想也成了后來的高級(jí)編程語言中的函數(shù)庇茫。

2.為什么需要函數(shù)

“函數(shù)”這個(gè)名詞是從英文function翻譯過來的,function的原意是“功能”螃成。顧名思義旦签,一個(gè)函數(shù)就是一個(gè)功能。在很多編程語言中寸宏,main函數(shù)就是編程入口宁炫。一個(gè)較大的程序不可能把所有代碼都放到一個(gè)主函數(shù)中。

函數(shù)的出現(xiàn)解決了指令級(jí)別的重復(fù)問題氮凝。在早期淋淀,計(jì)算機(jī)存儲(chǔ)還比較小的時(shí)候,避免重復(fù)指令可以顯著的節(jié)約空間「泊迹現(xiàn)今的計(jì)算機(jī)硬件相比50年代的有了巨大優(yōu)勢(shì)朵纷,節(jié)約空間這一作用顯得沒那么重要了。相比于早期永脓,函數(shù)更重要的作用是避免編寫重復(fù)代碼袍辞,提高代碼的復(fù)用性,便于規(guī)劃常摧、組織搅吁、編程和調(diào)試

二落午、探秘普通函數(shù)

1.不同角度看編程語言

高級(jí)編程語言屏蔽了語言特性的實(shí)現(xiàn)細(xì)節(jié)谎懦,讓使用者站在一個(gè)更高層次上去使用編程語言。在更高層次的角度使用編程語言溃斋,便于我們關(guān)注業(yè)務(wù)本身界拦,不必關(guān)注技術(shù)細(xì)節(jié),專注于使用編程語言去解決問題梗劫;當(dāng)我們從更底層的角度來思考編程語言提供的語法與特性享甸,會(huì)更透徹地理解語言運(yùn)行機(jī)制,從而認(rèn)清各種語法的本質(zhì)梳侨。

編程語言會(huì)告訴你支持哪些數(shù)據(jù)類型蛉威,不會(huì)告訴你數(shù)據(jù)類型是一種對(duì)內(nèi)存數(shù)據(jù)的解釋方式;編程語言會(huì)告訴如何定義一個(gè)類或結(jié)構(gòu)體走哺,不會(huì)告訴你類或結(jié)構(gòu)體是相關(guān)性數(shù)據(jù)的組織方式蚯嫌。編程語言會(huì)告訴你如何定義并實(shí)現(xiàn)一個(gè)函數(shù),同樣不會(huì)告訴你函數(shù)在底層是什么。下面择示,帶著好奇心妒牙,我們開啟探索之旅吧。

2.函數(shù)是如何執(zhí)行的

2.1函數(shù)的匯編表示

先來看一段簡單的代碼对妄。定義了foo函數(shù),foo函數(shù)有一個(gè)long類型的bar參數(shù)敢朱,函數(shù)體中定義long類型的兩個(gè)變量a和b剪菱,函數(shù)返回了3個(gè)變量的和。在main函數(shù)中調(diào)用foo函數(shù)拴签,返回值賦值給了result孝常。

// 代碼1
long foo(long bar) {
    long a = 1;
    long b = 2;
    return bar + a + b;
}

int main(int argc, const char * argv[]) {
    long result = foo(10);
    return 0;
}

代碼1對(duì)應(yīng)的匯編代碼如下:

// main函數(shù)
0x100003f80 <+0>:  pushq  %rbp
0x100003f81 <+1>:  movq   %rsp, %rbp
0x100003f84 <+4>:  subq   $0x20, %rsp
0x100003f88 <+8>:  movl   $0x0, -0x4(%rbp)
0x100003f8f <+15>: movl   %edi, -0x8(%rbp)
0x100003f92 <+18>: movq   %rsi, -0x10(%rbp)
0x100003f96 <+22>: movl   $0xa, %edi
0x100003f9b <+27>: callq  0x100003f50           
0x100003fa0 <+32>: xorl   %ecx, %ecx
0x100003fa2 <+34>: movq   %rax, -0x18(%rbp)
0x100003fa6 <+38>: movl   %ecx, %eax
0x100003fa8 <+40>: addq   $0x20, %rsp
0x100003fac <+44>: popq   %rbp
0x100003fad <+45>: retq   
// foo函數(shù)
0x100003f50 <+0>:  pushq  %rbp
0x100003f51 <+1>:  movq   %rsp, %rbp
0x100003f54 <+4>:  movq   %rdi, -0x8(%rbp)
0x100003f58 <+8>:  movq   $0x1, -0x10(%rbp)
0x100003f60 <+16>: movq   $0x2, -0x18(%rbp)
0x100003f68 <+24>: movq   -0x8(%rbp), %rax
0x100003f6c <+28>: addq   -0x10(%rbp), %rax
0x100003f70 <+32>: addq   -0x18(%rbp), %rax
0x100003f74 <+36>: popq   %rbp
0x100003f75 <+37>: retq   

2.2必要的匯編知識(shí)

在正式分析foo函數(shù)的執(zhí)行過程之前,需要先補(bǔ)充一下必要的匯編知識(shí)蚓哩。
本文中的匯編分析是在Xcode中進(jìn)行的构灸,匯編是x86-64匯編,匯編風(fēng)格是AT&T岸梨。
AT&T匯編在指令后面加上q等字母表示操作數(shù)據(jù)的字節(jié)數(shù)喜颁,匯編指令后面的b表示操作1個(gè)字節(jié)的數(shù)據(jù),w表示操作2個(gè)字節(jié)數(shù)據(jù)曹阔, l 表示操作4個(gè)字節(jié)數(shù)據(jù)半开,q表示操作8個(gè)字節(jié)數(shù)據(jù)。
%后面跟著的是寄存器赃份,$后跟著的是操作數(shù)寂拆,()表示間接尋址。
參數(shù)傳遞按從左至右的順序依次是:rdi, rsi, rdx, rcx, r8, r9,如果多余6個(gè)才有壓棧的方式進(jìn)行參數(shù)傳遞抓韩。
常用的匯編指令如下表:


2.3匯編分析

為了了解foo函數(shù)的執(zhí)行過程纠永,我們先分析下foo函數(shù)的匯編代碼。

// foo函數(shù)匯編分析
0x100003f50 <+0>:  pushq  %rbp //將rbp寄存器的值壓棧谒拴,目的是函數(shù)結(jié)束可以恢復(fù)rbp的值
0x100003f51 <+1>:  movq   %rsp, %rbp //將rsp寄存器的值賦值給rbp尝江,也即rbp指向棧頂
0x100003f54 <+4>:  movq   %rdi, -0x8(%rbp) //將rdi的值存儲(chǔ)到棧頂-8地址的空間,rdi中存儲(chǔ)的函數(shù)main調(diào)用時(shí)傳過來的10英上,稍后分析main函數(shù)會(huì)再次提到
0x100003f58 <+8>:  movq   $0x1, -0x10(%rbp) //將1存儲(chǔ)到棧頂-16地址的空間茂装,0x是16進(jìn)制表示法,其中0x10即是10進(jìn)制的16
0x100003f60 <+16>: movq   $0x2, -0x18(%rbp) //將2存儲(chǔ)到棧頂-24地址的空間
0x100003f68 <+24>: movq   -0x8(%rbp), %rax //將10賦值給寄存器rax善延,此時(shí)rax中存儲(chǔ)值是10
0x100003f6c <+28>: addq   -0x10(%rbp), %rax //將1加rax值的和賦值給rax少态,此時(shí)rax中存儲(chǔ)值是11
0x100003f70 <+32>: addq   -0x18(%rbp), %rax //將2加rax值的和賦值給rax,此時(shí)rax中存儲(chǔ)值是13
0x100003f74 <+36>: popq   %rbp //恢復(fù)rbp的值
0x100003f75 <+37>: retq   //函數(shù)調(diào)用結(jié)束結(jié)束易遣,返回main函數(shù)

從上面的分析中彼妻,我們知道在foo返回前rax寄存器中存儲(chǔ)的值13,在執(zhí)行retq指令時(shí)并沒有返回任何值,那main函數(shù)是如何拿到foo函數(shù)的返回值的呢侨歉?

我們直接分析和foo調(diào)用相關(guān)的代碼屋摇,無關(guān)代碼暫不分析。

// mian函數(shù)匯編分析
0x100003f96 <+22>: movl   $0xa, %edi // 將10存入寄存器edi中幽邓,在foo的匯編分析中使用到了edi的值
0x100003f9b <+27>: callq  0x100003f50 // 調(diào)用foo函數(shù)        
0x100003fa0 <+32>: xorl   %ecx, %ecx // 清理寄存器ecx炮温,此處和foo調(diào)用無關(guān)
0x100003fa2 <+34>: movq   %rax, -0x18(%rbp) // 將寄存器rax存儲(chǔ)的值存儲(chǔ)到-0x18(%rbp)的內(nèi)存中,也即賦值給result

至此牵舵,我們已經(jīng)分析明白整個(gè)函數(shù)的執(zhí)行過程柒啤。下圖描述了foo函數(shù)調(diào)用過程棧的變化。

三畸颅、探秘成員函數(shù)

1.對(duì)象內(nèi)存模型

先來看一段代碼担巩。在main函數(shù)中,將一個(gè)數(shù)組的地址通過強(qiáng)制轉(zhuǎn)換賦值給了Person類型的指針變量person1没炒,person2指向的是通過new關(guān)鍵字初始化的對(duì)象涛癌。通過person1和person2調(diào)用introduceOneself函數(shù),輸出的結(jié)果分別是什么送火?

// 代碼2
#include <iostream>
using namespace std;

class Person {
    long m_age;
    long m_height;
    long m_weight;
public:
    Person(long age, long height, long weight) {
        m_age = age;
        m_height = height;
        m_weight = weight;
    }
    void introduceOneself() {
        cout << m_age << endl;
        cout << m_height << endl;
        cout << m_weight << endl;
    }
    long foo(long bar) {
        long a = 1;
        long b = 2;
        return bar + a + b;
    }
};

int main(int argc, const char * argv[]) {
    long array[3] = {20, 180, 75};
    Person *person1 = (Person *)array;
    person1->introduceOneself();
    
    Person *person2 = new Person(20, 180, 75);
    person2->introduceOneself();
    
    return 0;
}

無論是通過person1還是person2調(diào)用introduceOneself函數(shù)拳话,最終輸出的結(jié)果都是20,180种吸,75假颇。


輸出結(jié)果告訴我們,Preson對(duì)象的內(nèi)存布局和數(shù)組并沒什么差異骨稿。數(shù)組中的元素按照順序依次從低地址到高地址排列笨鸡,同樣,對(duì)象中的成員變量也是(多繼承和虛函數(shù)除外)坦冠。



person1內(nèi)存數(shù)據(jù)如下圖:



person2的內(nèi)存數(shù)據(jù)如下圖:

2.對(duì)象與成員函數(shù)

在上一小節(jié)中形耗,我們分析了對(duì)象的內(nèi)存模型,可以看出對(duì)象的內(nèi)存模型中并沒有存儲(chǔ)和函數(shù)相關(guān)的信息辙浑,對(duì)象是怎么調(diào)用函數(shù)的呢激涤?接下來,我們研究一下對(duì)象是如何調(diào)用自己的函數(shù)判呕。introduceOneself函數(shù)中使用cout函數(shù)倦踢,分析起來干擾項(xiàng)太多,我們直接分析Person中的foo函數(shù)的調(diào)用過程侠草。

// main函數(shù)的匯編分析
0x100003bfa <+106>: movq   -0x18(%rbp), %rdi // -0x18(%rbp)存儲(chǔ)的是person2的對(duì)象地址辱挥,即寄存器rdi存儲(chǔ)著preson2的地址
0x100003bfe <+110>: movl   $0xa, %esi // 將10存儲(chǔ)到esi寄存器
0x100003c03 <+115>: callq  0x100003d10 // 調(diào)用foo函數(shù)

通過main函數(shù)的匯編,可以看到在調(diào)用foo函數(shù)時(shí)边涕,傳了2個(gè)參數(shù)晤碘,rdi中存儲(chǔ)著對(duì)象的地址褂微,esi中存儲(chǔ)著函數(shù)的實(shí)參10。

// foo函數(shù)的匯編匯編分析
0x100003d10 <+0>:  pushq  %rbp
0x100003d11 <+1>:  movq   %rsp, %rbp
0x100003d14 <+4>:  movq   %rdi, -0x8(%rbp) // 將對(duì)象地址存(this)儲(chǔ)到-0x8(%rbp)的內(nèi)存單元
0x100003d18 <+8>:  movq   %rsi, -0x10(%rbp) // 將10儲(chǔ)到-0x8(%rbp)的內(nèi)存單元
0x100003d1c <+12>: movq   -0x8(%rbp), %rax // this存儲(chǔ)rax中
0x100003d20 <+16>: movq   -0x10(%rbp), %rcx // 將10存儲(chǔ)到rcx中
0x100003d24 <+20>: addq   (%rax), %rcx // 將20+10=30存儲(chǔ)到rcx中
0x100003d27 <+23>: addq   0x8(%rax), %rcx // 將30+180=210存儲(chǔ)到rcx中
0x100003d2b <+27>: addq   0x10(%rax), %rcx // 75+210=285存儲(chǔ)到rcx中
0x100003d2f <+31>: movq   %rcx, %rax // 將285存儲(chǔ)到rax中
0x100003d32 <+34>: popq   %rbp
0x100003d33 <+35>: retq

在foo函數(shù)中通過寄存器rdi取到對(duì)象地址园爷,將對(duì)象地址存儲(chǔ)到寄存器rax中宠蚂,然后分別通過(%rax)、0x8(%rax)童社、0x10(%rax)獲取到m_age求厕、m_height、m_weight扰楼。

分析了foo函數(shù)的調(diào)用過程呀癣,也不難理解為什么通過person1和person2調(diào)用introduceOneself函數(shù)輸出的結(jié)果是一樣的了。

3.繼承與多態(tài)

3.1繼承與函數(shù)

同樣灭抑,先來看段代碼。Student類繼承自Person類抵代,在main函數(shù)中person變量前后指向了Person對(duì)象和Student對(duì)象腾节,并調(diào)用了introduceOneself函數(shù)。

// 代碼3
class Person {
public:
    void introduceOneself() {
        cout << "Person" << endl;
    }
};

class Student : public Person {
public:
    void introduceOneself() {
        cout << "Student" << endl;
    }
};

int main(int argc, const char * argv[]) {
    Person *person = new Person();
    person->introduceOneself();
    
    person = new Student();
    person->introduceOneself();
    
    return 0;
}

運(yùn)行上面的代碼荤牍,輸出結(jié)果都會(huì)是Person案腺。

看一下main函數(shù)的匯編,可以看到兩次調(diào)用了introduceOneself 康吵,調(diào)用地址都是0x1000031f0劈榨,說明兩次調(diào)用同一個(gè)函數(shù)。第一次調(diào)用introduceOneself時(shí)晦嵌,寄存器rdi存儲(chǔ)的是Person對(duì)象的地址同辣,第二次調(diào)用時(shí),寄存器rdi存儲(chǔ)的是Student對(duì)象的地址惭载。

調(diào)用普通函數(shù)旱函,會(huì)直接根據(jù)指針的類型調(diào)用對(duì)應(yīng)的函數(shù),不會(huì)考慮指針實(shí)際的指向描滔,這是在編譯期做的事情棒妨。

// main函數(shù)的匯編
0x1000031a0 <+0>:  pushq  %rbp
0x1000031a1 <+1>:  movq   %rsp, %rbp
0x1000031a4 <+4>:  subq   $0x20, %rsp
0x1000031a8 <+8>:  movl   $0x0, -0x4(%rbp)
0x1000031af <+15>: movl   %edi, -0x8(%rbp)
0x1000031b2 <+18>: movq   %rsi, -0x10(%rbp)
0x1000031b6 <+22>: movl   $0x1, %edi
0x1000031bb <+27>: callq  0x100003e24               
0x1000031c0 <+32>: movq   %rax, -0x18(%rbp)
0x1000031c4 <+36>: movq   -0x18(%rbp), %rdi
0x1000031c8 <+40>: callq  0x1000031f0   // 第一次調(diào)用introduceOneself           
0x1000031cd <+45>: movl   $0x1, %edi
0x1000031d2 <+50>: callq  0x100003e24              
0x1000031d7 <+55>: movq   %rax, -0x18(%rbp)
0x1000031db <+59>: movq   -0x18(%rbp), %rdi
0x1000031df <+63>: callq  0x1000031f0  // 第一次調(diào)用introduceOneself            
0x1000031e4 <+68>: xorl   %eax, %eax
0x1000031e6 <+70>: addq   $0x20, %rsp
0x1000031ea <+74>: popq   %rbp
0x1000031eb <+75>: retq   

3.2多態(tài)與虛函數(shù)

簡單修改一下代碼3,修改后的代碼如下含长。代碼4和代碼3的區(qū)別在于在introduceOneself函數(shù)前面多個(gè)virtual關(guān)鍵字券腔,表明introduceOneself是一個(gè)虛函數(shù)。

// 代碼4
class Person {
    long m_age = 10;
public:
    virtual void introduceOneself() {
        cout << "Person" << endl;
    }
};

class Student : public Person {
    long m_no = 20201234;
public:
    void introduceOneself() {
        cout << "Student" << endl;
    }
};

int main(int argc, const char * argv[]) {
    Person *person = new Person();
    person->introduceOneself();
    
    person = new Student();
    person->introduceOneself();
    
    return 0;
}

運(yùn)行上面代碼拘泞,輸出結(jié)果將分別是Person和Student纷纫。

person指向的對(duì)象是運(yùn)行時(shí)決定的,在編譯時(shí)是無法決定的陪腌。那么涛酗,上面的代碼是怎么調(diào)用到對(duì)應(yīng)對(duì)應(yīng)的函數(shù)呢铡原?

在OC中,調(diào)用對(duì)象的函數(shù)是通過對(duì)象中的isa指針找到對(duì)應(yīng)的類對(duì)象商叹,類對(duì)象中保存著函數(shù)列表燕刻,然后就可以間接找到函數(shù)地址進(jìn)行調(diào)用了。其實(shí)剖笙,在C++中卵洗,也有類似的機(jī)制,當(dāng)有虛函數(shù)存在時(shí)弥咪,在對(duì)象的內(nèi)存空間前8個(gè)字節(jié)中存儲(chǔ)著虛函數(shù)列表的地址过蹂,也即虛表(這里指向的并不是虛表的表頭,是表頭+16的位置聚至,從這里開始存儲(chǔ)著各個(gè)虛函數(shù)的地址酷勺,這里可以不關(guān)心這個(gè)細(xì)節(jié))。通過虛表扳躬,就可以找到對(duì)應(yīng)的函數(shù)的地址脆诉。

下圖顯示的是Student對(duì)象的內(nèi)存數(shù)據(jù),前8個(gè)字節(jié)(紅色框中)的數(shù)據(jù)是0x0100004068贷币,也即Student對(duì)象的虛函數(shù)地址是0x0100004068击胜。


下圖顯示的是0x0100004068內(nèi)存的數(shù)據(jù),可以看到黃色框中的數(shù)據(jù)是0x0100003D60 役纹,這個(gè)地址其實(shí)就是introduceOneself的地址偶摔。


下圖顯示的是斷點(diǎn)introduceOneself函數(shù)調(diào)用的截圖,可以看到introduceOneself函數(shù)調(diào)用地址和上圖黃色框中的數(shù)據(jù)是0x0100003D60是一樣的促脉。



通過上面的分析辰斋,我們已經(jīng)明白了虛函數(shù)的機(jī)制。下面換個(gè)角度瘸味,從匯編的代碼看下虛函數(shù)是怎么調(diào)用的亡呵。由于匯編比較長,省略了部分無關(guān)代碼硫戈,只需關(guān)注和虛函數(shù)相關(guān)的代碼锰什。

// main函數(shù)的匯編
// 省略部分代碼              
0x100003056 <+54>:  movq   -0x20(%rbp), %rdi // Person對(duì)象地址
0x10000305a <+58>:  callq  0x1000030c0 // 調(diào)用Person構(gòu)造函數(shù)
0x10000305f <+63>:  movq   -0x20(%rbp), %rax // Person對(duì)象地址存儲(chǔ)到rax
0x100003063 <+67>:  movq   %rax, -0x18(%rbp) // -0x18(%rbp)就是person變量的地址,Person對(duì)象地址賦值給person變量
0x100003067 <+71>:  movq   -0x18(%rbp), %rcx // person變量的值存儲(chǔ)到rcx丁逝,rcx此時(shí)存儲(chǔ)的是Person對(duì)象的地址
0x10000306b <+75>:  movq   (%rcx), %rdx // 虛表地址存在Person對(duì)象的開頭位置汁胆,通過Person對(duì)象地址取到虛表地址并存儲(chǔ)到rdx
0x10000306e <+78>:  movq   %rcx, %rdi // Person對(duì)象地址存儲(chǔ)到rdi
0x100003071 <+81>:  callq  *(%rdx) // rdx存儲(chǔ)的是虛表地址,*(%rdx)表示取到虛表前8個(gè)字節(jié)的內(nèi)容(也即虛函數(shù)的地址),然后調(diào)用虛函數(shù)
// 省略部分代碼               
0x100003093 <+115>: movq   -0x28(%rbp), %rdi // Student對(duì)象地址
0x100003097 <+119>: callq  0x1000030e0 // 調(diào)用Person構(gòu)造函數(shù)             
0x10000309c <+124>: movq   -0x28(%rbp), %rax // Student對(duì)象地址存儲(chǔ)到rax
0x1000030a0 <+128>: movq   %rax, -0x18(%rbp) // Student對(duì)象地址賦值給person變量
0x1000030a4 <+132>: movq   -0x18(%rbp), %rax // -0x18(%rbp)就是person變量的地址霜幼,Student對(duì)象地址賦值給person變量
0x1000030a8 <+136>: movq   (%rax), %rcx // 虛表地址存在Student對(duì)象的開頭位置嫩码,通過Student對(duì)象地址取到虛表地址并存儲(chǔ)到rdx
0x1000030ab <+139>: movq   %rax, %rdi // Student對(duì)象地址存儲(chǔ)到rax,也即this指針
0x1000030ae <+142>: callq  *(%rcx) // rcx存儲(chǔ)的是虛表地址罪既,*(%rcx)表示取到虛表前8個(gè)字節(jié)的內(nèi)容(也即虛函數(shù)的地址),然后調(diào)用虛函數(shù)
0x1000030b0 <+144>: xorl   %eax, %eax
0x1000030b2 <+146>: addq   $0x30, %rsp
0x1000030b6 <+150>: popq   %rbp
0x1000030b7 <+151>: retq  

3.4思考與提升

  1. 上面的內(nèi)容并沒分析多繼承下的虛表情況铸题,讀者可以自行思考這種情況下的虛表和對(duì)象的關(guān)系铡恕,可以使用上面的分析方法進(jìn)行驗(yàn)證。
  2. 子類如果沒有重寫父類的虛函數(shù)丢间,情況會(huì)怎么樣探熔?
  3. OC是怎么實(shí)現(xiàn)繼承與多態(tài)的,與C++的實(shí)現(xiàn)方法有什么異同烘挫?

參考資料:

【1】《計(jì)算機(jī):一部歷史》皮得·本特利 著
【2】《匯編語言》第三版 王爽 著
【3】《深度探索C++對(duì)象模型》Stanley B.Lippman斯坦利·B.李普曼) 著诀艰,侯捷

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市饮六,隨后出現(xiàn)的幾起案子其垄,更是在濱河造成了極大的恐慌,老刑警劉巖卤橄,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绿满,死亡現(xiàn)場離奇詭異,居然都是意外死亡窟扑,警方通過查閱死者的電腦和手機(jī)喇颁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辜膝,“玉大人无牵,你說我怎么就攤上這事漾肮〕Ф叮” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵克懊,是天一觀的道長忱辅。 經(jīng)常有香客問我,道長谭溉,這世上最難降的妖魔是什么墙懂? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮扮念,結(jié)果婚禮上损搬,老公的妹妹穿的比我還像新娘。我一直安慰自己柜与,他們只是感情好巧勤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著弄匕,像睡著了一般颅悉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上迁匠,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天剩瓶,我揣著相機(jī)與錄音驹溃,去河邊找鬼。 笑死延曙,一個(gè)胖子當(dāng)著我的面吹牛豌鹤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播搂鲫,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼傍药,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了魂仍?” 一聲冷哼從身側(cè)響起拐辽,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎擦酌,沒想到半個(gè)月后俱诸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赊舶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年睁搭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笼平。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡园骆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寓调,到底是詐尸還是另有隱情锌唾,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布夺英,位于F島的核電站晌涕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏痛悯。R本人自食惡果不足惜余黎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望载萌。 院中可真熱鬧惧财,春花似錦、人聲如沸扭仁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斋枢。三九已至帘靡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓤帚,已是汗流浹背描姚。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工涩赢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人轩勘。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓筒扒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绊寻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子花墩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容