本节书摘来自异步社区出版社《C++面向对象高效编程(第2版)》一书中的第3章,第3.5节,作者: 【美】Kayshav Dattatri,更多章节内容可以访问云栖社区“异步社区”公众号查看。
3.5 this 指针和名称重整的进一步说明
C++面向对象高效编程(第2版)
在前面介绍的实现中,我们有时使用this指针访问对象的数据成员。如第2章所述,成员函数中的this指针指向调用该成员函数的对象。在前述的main()程序中,如下代码
`
a.Push(i);`
通过a对象调用Push成员函数。在Push成员函数内部,this指针持有a对象的地址。以这样的方式,成员函数可以访问对象内的任何元素(数据成员和成员函数)。如第2章所述,编译器像实现其他函数那样,实现每个成员函数,但是,每个成员函数应该可以通过某种方法访问调用它的对象。为达到这个目的,this指针将作为隐藏的参数传递给每个成员函数,且this指针通常是函数接收的第1个参数(其后是已声明的其他参数)。假定Push成员函数的实现如下:
void Push(TIntStack* this, int what)
{
// 实现代码如前所述
// 通过a调用Push时,即a.Push(10),
// “this”指针指向a对象。
}```
然而,Push可能是一个已经使用的函数名。或者说,在其他类中可能也包含了Push函数。编译器(或链接器)如何区别它们?我们如何表示重载的构造函数和析构函数?要回答这些问题,必须使用函数名重整(function name mangling)的概念。
注意:
ANSI C++ 语言标准对名称重整的样式未作要求(稍后讨论)。编译器的实现者可以自行规定名称重整的方案(甚至包括所有权)。了解一下名称重整有好处,但并不需要理解详细的重整方案。以下示例仅用于介绍概念,其重整方案依不同的编译器而已。
类的每个成员函数都包含类名(实际上是this指针的类型)和一些其他的信息。Push成员函数可能变成:
`
void Push 9TIntStackFi()`
在数字9前面有两条下划线(ASCII _,十进制为95)。数字代表类名的字符个数(TIntStack为9),这对于解析名称很有帮助。如果程序员不得不查找类(某函数所属)的名称,那么只需先查找 和其后的数字(假设为N),然后提取数字后的N个字符,即可获得该函数所属的类名。剩余的字符代表参数类型和返回值类型。注意,只有参数类型(不是参数名)才用于名称重整。 后的字符序列按照该函数已声明的所有参数顺序编码。在上面的重整名称中,Push为原始名,`9TIntStack`表明该函数的第1个参数(即this指针)类型为`TIntStack`,F 1表明该函数为全局函数,i表明该函数接受1个整数参数。返回值类型不是重整名称的一部分。
构造函数并没有特别的名称!它的名称与类名相同。为表示构造函数,在重整过程中,会在类的重整名称前加上
`__ct__`
因此,重整后的默认构造函数应该是:
TIntStack(); // 普通的未重整名称
__ct__9TIntStackFv(); // 重整后的名称`
同样地,此处的9TIntStack仍代表this指针的类型,F表明该函数为全局函数,v表明该函数不接受任何参数(void)。
其他的构造函数(如果有的话)也会包含参数,因此,它们确切的名称应该不同。例如,复制构造函数应该是:
TIntStack(const TIntStack&); //未重整名称
__ct__9TIntStackF9TIntStackRC(); // 重整名称
// R 代表引用,C 代表const。```
第二个9TIntStack代表参数,RC表明该参数类型为`const`的引用。欲了解名称重整的细节,详见ARM 2。
根据以上的分析,析构函数(每个类只有一个)应该为:
~TIntStack(); // 未重整名称
__dt__9TIntStackFv(); // 重整名称`
类似地,操作符函数也有经过特定编码的重整名。例如,赋值操作符被编码成:
__as__9TIntStack();
全局new()操作符以__nw开始,全局delete操作符以__dl开始。
语言中所有的操作符都有预定义的(pre-defined)编码。
你可能会问,为什么要知道关于名称重整的这些细节?
想象一下,你实现了一个类,但忘记实现一些成员函数。编译过程可能没什么问题,但链接器会对未定义的函数报错。这些显示在报错信息中的函数,看上去与你在类头文件3中的函数不一样。现在,你彻底糊涂了!
这是因为名称重整的缘故。链接器在报错时绝不会打印原始的函数名,它只会给出由编译器提供的等价重整名。链接器对重整一无所知(至少到目前为止,我还未见过链接器了解此事),它只会抱怨那些未定义的名称。此时,如果了解一些名称重整会对你有帮助。如果链接器打印出的名称以 ct 开始,则说明出问题的是某个构造函数,这缩小了查找未定义成员函数的范围。随着你编写的C++代码越来越多,重整名称会成为你破译报错信息的秘密武器!相信我。
警告:
记住,this指针是个非常神圣的指针。不能修改this指针,绝不能给它赋值!需要在成员函数内修改this
指针的情况非常少见,稍后将会介绍这样的示例。
你可能也注意到,在每个成员函数内,我们并未用this指针访问数据成员。例如,在Pop函数中,无需任何限定符,即可直接访问数据成员_count
。无论何时在成员函数中使用数据成员的名称,编译器都会为其预先添加“this”限定符。因此,_count
成为this->_count
。除非我们明确需要指向对象的指针,否则不必显式使用它。
样式:
在本书中,只要有可能出现混淆的地方,都会显式使用this限定符。例如,在之前的赋值操作符中,有如下语句:
this->_count = source._count;
也可以写成:
_count = source._count; // 这样写也正确!
但是,同一语句中,通过不同的对象多次用相同的名称_count会令人困惑。因此,要显式使用this限定符。
什么时候必须使用this指针?当我们希望返回对调用某函数的对象的引用时,必须使用*this。否则,如何显式表示“我的对象”这个概念?另一种情况是(如前所述的赋值操作符中),我们希望获得对象的地址,也必须显式使用this名称。到目前为止,这是显式使用this名称最常见的两种情况。
1一些编译器用N和F区别near和far函数,即用N代表near函数,用F代表far函数。
2译者注:ARM指的是Margaret A. Ellis与Bjarne Stroustrup合著的书:Annotated C++ Reference Manual。ISBN 0-201-51459-1。
3尽管链接器(和开发环境)越来越智能,开发环境为用户显示未经重整的名称也越来越普遍。但是,粗略了解一下名称重整仍然有好处。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。