1、C++ 中的“接口”
C++并没有明确的“接口”,一般约定继承某个类,已达到接口的“实现”。
首先我们来看下单继承的内存布局(<font color= Crimson size=4>依赖各厂商的实际实现,这里仅以微软实现为例进行说明····感谢宇宙最强IDE····</font>)
其多态主要由虚函数表(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
}
附:一篇经典论文中的图解
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调试一下
首先,看下结构类型与两个接口的内存关系
可见,duck与actor的data指针都指向dd
然后是Duck接口的方法集:
以及其动态类型的具体实现:
可以看到,都指向了tanglaoya的具体实现
再看看Actor的方法集:
及其动态类型的具体实现
3、总结
C++在代码编写时就明确了是否实现某个接口,并将接口信息附加在自己的内存中,但is-A的模式越来越限制模块间的解耦;Golang其宽松的接口充分降低了耦合的发生,但可能在代码书写无意中却实现了某个接口.. 此外,其实现可能会比较绕,容易发生其他错误(比如经典的interface与nil的比较等等)
有疑问加站长微信联系(非本文作者)