上一篇笔记里面说到,如果c++的成员函数都是全局的,怎么区分两个类中的同名的成员函数调用,例如:下面定义了2个类Point1,Point2的对象p1,p2;其中Point1,Point2都有成员函数print:
Point1 p1; Point2 p2; p1.print( ); p2.print( );
编译器怎么区分呢?
其实,类中的成员函数print在编译器转换成全局函数的时候就已经不叫print了,举个例子:
比如你现在在301教室,但是出了你现在所在的这栋楼,就不能笼统的叫301了,要叫2栋301……如果出了中国,刚才那个301教室要叫全局名称:中华人民共和国**省**市**….301教室,是不是?
所以,在两个不同类里面的同名函数print,化成全局函数的时候就不叫print了,转换的技术就叫做:Name Mangling ! 查英文词典,mangling居然是乱砍的意思,够亲切的,就是乱砍,呵呵。
Name mangling的目的就是避免重复,原理就是:找到一种编码方法,使得
1)、每一个名称经过转换后,要有唯一的名字;
2)、这种编码必须简单,而且要可逆,就是能由全局名称很快的知道局部的名称~
具体的编码方法每个编译器(g++,vs2005,VCL…)都不相同,而且比较烦琐,这里不再详细阐述,想知道的可以google:Name Mangling
如果你想偷懒,又想知道经过编译器转换后的全局名称,可以也很容易,就是不要去写函数的定义,仅仅是写个声明,然后在主函数中调用它,生成工程的时候就会出现连接错误,具体例子如下:
class Point1 { public: void print(); void print(float); void print(int, int); }; class Point2 { public: void print(); }; int main() {//注意,上面的函数都不需要定义,故意让它产生连接错误的。 Point1 p1; p1.print(); p1.print(1.0f); p1.print(1, 2); Point2 p2; p2.print(); return 0; }
上面的例子能通过编译,但是链接的时候会出错,下面给出vs2005中的错误提示:
1>------ 已启动生成: 项目: funobject, 配置: Debug Win32 ------
1>正在编译...
1>main.cpp
1>正在链接...
1>main.obj : error LNK2019: 无法解析的外部符号"public: void __thiscall Point2::print(void)" (?print@Point2@@QAEXXZ),该符号在函数_main 中被引用
1>main.obj : error LNK2019: 无法解析的外部符号"public: void __thiscall Point1::print(int,int)" (?print@Point1@@QAEXHH@Z),该符号在函数_main 中被引用
1>main.obj : error LNK2019: 无法解析的外部符号"public: void __thiscall Point1::print(float)" (?print@Point1@@QAEXM@Z),该符号在函数_main 中被引用
1>main.obj : error LNK2019: 无法解析的外部符号"public: void __thiscall Point1::print(void)" (?print@Point1@@QAEXXZ),该符号在函数_main 中被引用
1>D:/MYY/program/VC/funobject/Debug/funobject.exe : fatal error LNK1120: 4 个无法解析的外部命令
1>生成日志保存在“file://d:/MYY/program/VC/funobject/Debug/BuildLog.htm”
1>funobject - 5 个错误,个警告
========== 生成: 0 已成功, 1 已失败, 0 最新, 0 已跳过==========
从上面的提示里面的括号我们就可以知道那些函数的全局命名,从而也可以可以见一斑了(管中窥豹嘛):
?print@Point1@@QAEXHH@Z ?print@Point1@@QAEXM@Z ?print@Point1@@QAEXXZ
我们来看一下在Point1中重载的那三个函数,从@@QAEX后面的就不同了,分别是:
带两个int参数的:HH@Z; 带一个float参数的:M@Z 不带参数的: XZ
我们可以猜想,VS2005中,带参数的在最后要用@Z作后缀,float以M来表示,int以H来表示,带几个参数就用几个简化的字母……(我也没仔细看过vs2005的name mangling的说明文档啊,只是猜想,不足为训);至于是不是真的,要靠各位去看文档和多做几个例子了,呵呵。
在g++中,又是另外一番风景:
(注意:在g++里面,你不能从连接错误的提示里面得到上面的这些信息,所以要用另外的办法,如下:假设以上的代码在test.cpp文件中,先编译它,g++ -c test.cpp 得到test.o文件,然后用nm命令来查看test.o文件里面的符号即可:nm test.o)
_ZN6Point15printEf _ZN6Point15printEii _ZN6Point15printEv _ZN6Point25printEv
其中,我们也可以作如是猜测,并通过阅读文档去验证它:ii表示两个int,v代表void,f代表float;等等(再次强调,我没有读过相关规格文档去验证,想知道的就自己上google去看看,呵呵。)
下面说相关的几个问题:
1、 有了name mangling之后,下面的语句:pt.print(); 编译器就会先看pt在符号表里面(至于什么是符号表,可以看编译原理,我已经忘记了,也写不出来,呵呵)的相关类型,查到是Point,然后,就用name mangling去转换为全局函数名称调用。这就叫做“编译期绑定”,也叫做“静态绑定”(static binding)。
2、 关于函数重载,上面也说了,重载的函数经过name mangling之后,全局名称后面的部分会随着参数的个数,类型,和类型的顺序而有所不同。正是因为这种机制能区分出不同的函数,所以函数才可以用这三点作为重载的依据,至于返回值信息?很遗憾,至少我在转换过后的全局名称里面看不到(技术文档里面的说明我就不清楚了)。现在有个问题,为什么返回值类型不能作为重载的依据?下面看例子吧,假设允许返回值类型不同,则Point类里面有两个print成员函数,参数都为空,一个返回int,另一个返回值为void,那么下面的调用语句:Point pt; pt.print(); 该调用哪一个print?(注意,返回值可以不用接收的,为了灵活的缘故)。为了避免出现二义性,所以返回值类型不作为函数重载的依据。
3、 运算符重载:运算符重载也是一种函数重载。这里要注意无论是C还是C++,标识符的名称都只能由
数字,字母或下划线组成,但是运算符重载好像是例外?(注意:我是说好像)下面看看vs2005下对于下面代码的链接错误输出:
class Point { public: int operator +(int); }; int main() { Point p; int num = p + 4; return 0; }
1>main.obj : error LNK2019: 无法解析的外部符号"public: int __thiscall Point::operator+(int)" (??HPoint@@QAEHH@Z),该符号在函数_main 中被引用
1> D:/MYY/program/VC/funobject/Debug/funobject.exe : fatal error LNK1120: 1 个无法解析的外部命令
看到了嘛,是??Hpoint@@QAEHH@Z,没有加号。所以得出以下两点推论:
第一点,为什么标识符只能以数字,字母和下划线组成:因为其他字符在编译器里面有别的重要的用途(看上面的全局名称可知一二);还有就是因为如果允许其它字符,那看一下下面的语句:a=b=c;标识符是a,b,c呢,还是a=b呢…显然增加了编译器的复杂度。
第二点,为什么运算符重载要求只能重载原有的运算符:因为编译器内部的符号表已经把这些运算符与某个符号对应起来了,比如+,编译器里面可能在符号表里面有个符号对应这个+,假设是PLUS(这只是我的假设啊);如果现在允许你用新的符号重载,新的符号在符号表里面没有对应的选项,那么用name mangling的时候,怎么转换那些符号?(再说一遍,C++标准的标识符是只有数字,字母和下划线,从这个角度来说,VS2005也不是100%遵循标准的,可能吧)
以上的是我的推测,没有经过验证,也没有经过任何C++大师的肯定;所以可能是错误的,究竟如何,还请大家一起讨论吧。
还有一个问题,就是C++和C的混合编程,因为C++比较复杂的原因,所以它的name mangling 技术也比较复杂,而C语言的name mangling技术就相对简单(也许有人惊讶:“C也有这个啊”,当然有了,现在不是告诉你了嘛,呵呵),所以在C++中调用C的函数往往会在链接期间出错,这个C的函数是指已经做成库的形式提供的函数。
下面给个例子。(下面的例子在vs2005上进行说明)
先成一个在test.c中写个C函数func:
#include <stdio.h> void func() { printf(“I am a C’s funtion!/n”); };
编译它(确保后缀是.c,这样vs2005会以C的方式编译这个文件,注意是编译,不是生成,快捷键是ctrl + F7)
然后再创建个main.cpp中写个调用语句:
void func(); int main() { func(); return 0; };
现在可以生成解决方案了(快捷键是F7),看到连接错误了吧:
无法解析的外部符号"void __cdecl func(void)" (?func@@YAXXZ),该符号在函数_main 中被引用
那么,这个问题怎么解决呢?在main.c最上面声明这个语句的时候在其前加上extern “C”就可以了,它会告诉编译器,我这个函数名在转换的时候用C式的name mangling~,这样就能匹配,并而通过链接了,不信你试试?
extern “C” void func(); int main() ...
至于C和C++混合编程时更一般的解决方案,参考别的文章。
好了,C++程序在编译期间把类的成员函数通过name mangling转换成了唯一的全局函数,再通过符号表等技术把每条调用语句都转换成为调用相应的函数,好像问题都OK了,现在看一下以下的代码:
class Point { public: virtual void print(){...}; ... }; class Point2D : public Point { public: virtual void print(){...}; ... }; Point2D pt2; Point *p = &pt2; p->print();
对于上面程序的最后一行,我们想要的结果是调用Point2D中的print函数(这是多态的要求),但是,如果按照上面的讨论,编译器先在符号表里面找到p的类型是Point,然后进行name mangling转换…最后的结果却是Point里面的print函数!!,那么对于这种情况,我们聪明的编译器又怎么办呢?它又会为我们提供了怎样的机制来解决这类问题,以正确的提供多态的语义呢?
请看下篇,动态绑定(dynamic binding),谢谢。
PS.
1、上面说的符号表可能相当于一种数据结构,把名称,类型,和相关的属性绑在一起的结构体,至于详细的说明,请参考其他资料。
2、以上知识点在《深度探索c++对象模型》第4.1节中的:Nonstatic member function里面
主要知识点如下:
c++的设计准则之一是非静态成员函数必须和一般的全局函数有相同的效率。
也就是:
float Point::print(); 和 float Point_print(Point *p); 在执行上要达到一样的效率;
而达到一样的效率的唯一方法就是将前者通过编译器转换成后者。这一节里面就说明了相关的东西,相信会令你大开眼界的。