探索C++与Go的接口底层实现

714 阅读4分钟

1、C++ 中的“接口”

C++并没有明确的“接口”,一般约定继承某个类,已达到接口的“实现”。

首先我们来看下单继承的内存布局(依赖各厂商的实际实现,这里仅以微软实现为例进行说明····感谢宇宙最强IDE····)

其多态主要由虚函数表(vfptr)实现 : 指针或引用调用虚函数时,在运行时由对象的虚函数表+函数声明顺序决定绑定到哪个函数上

class IDuck {
public:          
    //嘎嘎地叫    
    virtual void GaGaSpeaking() = 0;      
       
    //老爷的官步    
    virtual void OfficialWalking() = 0;  
        
private:          
    unsigned int height;
};

class DonaldDuck : public IDuck { 
public:         
    void GaGaSpeaking() {       
        std::cout << "DonaldDuck Speak" << std::endl;    
    }         
    
    void OfficialWalking() {       
        std::cout << "DonaldDuck Walk" << std::endl;   
    }     
};


int main(int argc, _TCHAR* argv[])
{

    DonaldDuck * duck = new DonaldDuck();
    
    duck->GaGaSpeaking();         
    duck->OfficialWalking();         

    cout << "---------- hooking --------" << endl;

    typedef void(*DuckFunc)();
    int * addr = (int*)duck;
    DuckFunc f1 = (DuckFunc)(*((int*)(*addr)));
    f1();
    DuckFunc f2 = (DuckFunc)(*((int*)(*addr)+1));
    f2();

    return 0;
}

内存布局为:


强制调用成员函数(甚至可以是私有)


多继承

多继承下其实也是类似,按继承顺序依次排列,还是看代码示例
class IDuck {
public:          
    //嘎嘎地叫    
    virtual void GaGaSpeaking() = 0;         
    //老爷的官步    
    virtual void OfficialWalking() = 0;      
private:          
    unsigned int height;
};

class IActor {
public:
    //搞笑
    virtual void MakeFun() = 0;

private:
    std::string Name;
};

class DonaldDuck : public IDuck, public IActor { 
public:         
    void GaGaSpeaking() {       
        std::cout << "DonaldDuck Speak" << std::endl;    
    }         
    
    void OfficialWalking() {       
        std::cout << "DonaldDuck Walk" << std::endl;   
    }     

    void MakeFun() {
        std::cout << "Wa HAHAHA ~~~" << endl;
    }
};


int main(int argc, _TCHAR* argv[])
{

    DonaldDuck * duck = new DonaldDuck();
    
    duck->GaGaSpeaking();         
    duck->OfficialWalking();         
    duck->MakeFun();
    cout << "---------- hooking --------" << endl;

    typedef void(*DuckFunc)();
    int * addr = (int*)duck;
    DuckFunc f1 = (DuckFunc)(*((int*)(*addr)));
    f1();
    DuckFunc f2 = (DuckFunc)(*((int*)(*addr)+1));
    f2();

    typedef void(*ActorFunc)();
    int * addr2 = (int*)(*(int*)((IActor*)duck));
    ActorFunc f3 = (ActorFunc)(*(int*)(addr2));
    f3();

    return 0;
}

内存布局为:


菱形继承(略)

更多请参考

2、Go中的接口

Golang将interface作为一种类型,并且不依赖继承,而是以一种类似于duck-typing的实现。所谓duck-typing,是一种动态类型风格:当一个obj走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么它就可以被称为鸭子。

既然Go并没有像C++那样要求主动告诉编译器需要继承哪个父类,那么是如何实现动态类型的呢?(基于Go1.6,1.7及之后版本由于nameOff不方便gdb打印)

首先,interface由两部分组成{tab, data},其中tab保存了接口的元数据,这个很重要。

type iface struct {
        tab  *itab
        data unsafe.Pointer
}

itab中比较重要的有interfacetype及fun[],其中interfacetype保存了该接口需要实现哪些方法,fun[]则保存动态类型是如何实现这些方法的

type itab struct {
        inter  *interfacetype
        _type  *_type
        link   *itab
        bad    int32
        unused int32
        fun    [1]uintptr // variable sized
}

type interfacetype struct {
        typ  _type
        mhdr []imethod
}

type imethod struct {
        name    *string
        pkgpath *string
        _type   *_type
}

附:一篇经典论文中的图解


research!rsc: Go Data Structures: Interfaces
research!rsc: Go Data Structures: Interfaces

e.g. 唐老鸭的go版本

package main
import (
    "fmt"
)

type Duck interface {
    GaGaSpeaking()()
    OfficialWalking()()
}


type Actor interface {
    MakeFun()()
}

type DonaldDuck struct {
    height uint
    name   string
}

func (dd *DonaldDuck) GaGaSpeaking()() {
    fmt.Println("DonaldDuck gaga")
}

func (dd *DonaldDuck) OfficialWalking()() {
    fmt.Println("DonaldDuck walk")
}

func (dd *DonaldDuck) MakeFun()() {
    fmt.Println("DonaldDuck make fun")
}

func main() {

    dd := &DonaldDuck{10, "tang lao ya" }
    var duck Duck = dd
    var actor Actor = dd
    duck.GaGaSpeaking()
    actor.MakeFun()
    dd.OfficialWalking()
}

我们用gdb调试一下 首先,看下结构类型与两个接口的内存关系

(gdb) p dd
$1 = (struct main.DonaldDuck *) 0xc82000e180
(gdb) p duck
$2 = {tab = 0x7ffff7f721c0, data = 0xc82000e180}
(gdb) p actor
$3 = {tab = 0x7ffff7f721f0, data = 0xc82000e180}

可见,duck与actor的data指针都指向dd 然后是Duck接口的方法集:

(gdb) p duck.tab.inter.mhdr.len
$4 = 2
(gdb) p *(duck.tab.inter.mhdr.array[0].name)
$5 = {str = 0x4ff8c0 "GaGaSpeaking", len = 12}
(gdb) p *(duck.tab.inter.mhdr.array[1].name)
$6 = {str = 0x4ffb30 "OfficialWalking", len = 15}

以及其动态类型的具体实现(可以看到,都指向了tanglaoya的具体实现)

(gdb) info symbol duck.tab.fun[0]
main.(*DonaldDuck).GaGaSpeaking in section .text of /home/yugui/go/go
(gdb) info symbol duck.tab.fun[1]
main.(*DonaldDuck).OfficialWalking in section .text of /home/yugui/go/go

再看看Actor的方法集:

(gdb) p actor.tab.inter.mhdr.len
$12 = 1
(gdb) p *(actor.tab.inter.mhdr.array[0].name)
$13 = {str = 0x4fcc10 "MakeFun", len = 7}

及其动态类型的具体实现

(gdb) info symbol actor.tab.fun[0]
main.(*DonaldDuck).MakeFun in section .text of /home/yugui/go/go

3、总结

C++在代码编写时就明确了是否实现某个接口,并将接口信息附加在自己的内存中,但is-A的模式越来越限制模块间的解耦;Golang其宽松的接口充分降低了耦合的发生,但可能在代码书写无意中却实现了某个接口.. 此外,其实现可能会比较绕,容易发生其他错误(比如经典的interface与nil的比较等等)