`

C++基本概念之动态联编与静态联编

 
阅读更多
昨个儿,一同学打电话问我,这两个名词,事实上,我以前倒没大注意,在课堂的时候听说过一次,后来就再没印象了。缘故是我看的C++语法概念书就那一本:CTPL, 而且上面并未着重提出,这个概念估计是国内的专属吧,概念是概念,理论还是有的。同学的引出点是关于转型对于函数调用的影响。

Java 的世界里不仅没有指针,更没有虚函数的概念,当然这个是针对使用者而言。且不说指针,虚函数的省略确实减轻了设计的负担,因为就C++的概念放到java里说,普通成员函数的重载都是虚函数重写,所以你只要对父类的某个方法重写了,以为着你达到了C++的虚函数效果了。所以说,Java还是蛮简单的,将一切复杂的东西抽取成简单的表面化。

1C++ 对象模型

这里为了简单,没有考虑继承等因素。

了解对象模型对这个问题会有知其所以然的豁然,Stroustrup 设计的 C++ 对象模型在当前的编译器界仍然广为接受. 首先,我们知道,一个对象在内存中实在存在的(对比类)。从C的层次来讲,我们访问内存区域,需要指定如何访问,这就是类型存在的意义:指明了内存中的存储结构。如我们访问一块儿 char 类型数据,编译器一看是一 char 的形式访问,自然的就提取内存的前1个字节。C++ 的对象与这类 POD 的对象有着实在的差别,因着增加了成员函数,而且有动态识别的信息。对于各个类内各个类型的数据,是如何存储的呢?

1.1普通成员变量

单就这点来说,C的 struct 模型就足够了,而且为了C的兼容,所以普通成员函数仍然排列在对象内存区域。顺带一说,其他类型的成员都在这个内存结构之外。所以一个简单的无虚函数的对象来说,sizeof(object)其实就是C 中struct 的大小,也就是各个成员变量域大小的总和(要考虑对齐因素哦)。

1.2普通成员函数,静态成员函数,静态成员变量

这些被放在对象内存结构之外了。对于静态类的,可以想象成是全局。这些信息是不像虚函数,有动态调用的公用,所以简单的处理就可以了。所以在编译的时候,这些函数调用都实际上生成了一个真是的函数地址。

1.3虚函数

虚函数引来的问题就多了,因为他不仅引入了动态联编,而且引起了动态识别。Stroustrup 的方案是设置了一个虚表,在虚表里存储着这些特殊的信息:虚函数位置,动态类型信息。而这个虚表是不在对象内存结构中,为了能找到这个虚表,在对象的内存结构中有一个固定位置的插槽,这里存放的就是虚表的位置(地址)。 你可以通过这样的例子来证实这个插槽:

1
2
3
4
class TestClass {
int value;
void fun();
};

然后另一个对象:

1
2
3
4
class TestClass {
int value;
virtual void fun();
};

检验两个类的 sizeof, 然后再在第二个类里多加几个虚函数试试,sizeof结果依然不变。vs2005的结构是 4 和 8, 这个插槽(指针)的大小是4个字节,而且通过内存调试知道这个插槽恰恰在第一个DWORD区域里。

在编译的时候,因为对象的类型信息编译器不知道,也不需要知道,所以这种二次寻址的方式恰恰实现了虚函数的动态调用功效。

2转型

普通的转型其实是告诉编译器的,是告诉编译器以某种方式来访问。如果是访问普通成员变量,就以你指定的类型来访问这个内存区域的某个域。如果访问的是普通成员函数,编译器知道你的转型类型,所以到某个类型的普通成员函数中找到这个函数,然后生成这个函数的真实地址,填充到你的程序二进制码中。静态成员函数和变量类似。

但是对于虚函数来说,编译器知道这是虚函数,所以会通过插槽找,首先,这个插槽的位置是固定的,所以你无论怎么转型(继承链上),它(编译器)得到的插槽的值是固定的,所以你访问的虚表是你对象的真实类型的虚表,所以你调用的虚函数永远都是正确的。

总之,这样的原则记住就好:对于普通成员函数,转型只是转换对象内存访问方式;对于虚函数,转型并不会引起虚函数的调用转变,还是真实的;对于其他的类型数据,转型便调用指定类型的函数, 意味着调用的不是真实的自己的,即使调用的在自己内部并不存在。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics