《C++面向对象高效编程(第2版)》——3.5 this 指针和名称重整的进一步说明

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尽管链接器(和开发环境)越来越智能,开发环境为用户显示未经重整的名称也越来越普遍。但是,粗略了解一下名称重整仍然有好处。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2024-12-02 12:28:59

《C++面向对象高效编程(第2版)》——3.5 this 指针和名称重整的进一步说明的相关文章

《C++面向对象高效编程(第2版)》——导读

前言 C++面向对象高效编程(第2版) 面向对象软件开发已逐渐成为开发软件的首选.优秀的面向对象软件开发人员.设计人员.系统架构师对其需求与日俱增.要想成为一名成功的面向对象编程(OOP)人员必须忘却(摈弃)多年来面向程序编程的习惯,从新的角度分析问题. 面向对象编程要求程序员和设计者非常熟悉一些基本范式或概念.理解这些范式是在面向对象软件领域打下牢固基础的基本要求.支持OOP的语言都必须支持这些基本范式.换言之,学习OOP,简单地说,就是学习许多语言(如C++,Eiffel,SmallTalk

《C++面向对象高效编程(第2版)》——4.2 无用单元收集问题

4.2 无用单元收集问题 C++面向对象高效编程(第2版) 在我们讨论无用单元收集1(garbage collection)之前,先了解一下何为无用单元(garbage),何为悬挂引用(dangling reference). 4.2.1 无用单元 所谓无用单元(garbage),是一块存储区(或资源),该存储区虽然是程序(或进程)的一部分,但是在程序中却不可再对其引用.按照C++的规定,我们可以说,无用单元是程序中没有指针指向的某些资源.以下是一个示例: main() { char* p =

《C++面向对象高效编程(第2版)》——4.6 对象赋值的语义

4.6 对象赋值的语义 C++面向对象高效编程(第2版) 赋值与复制的操作非常类似.在C++中,绝大多数的复制操作都由语言隐式调用(当对象按值传递或按值返回时).当通过现有对象创建新对象时,也进行了复制操作(但不是很频繁).与复制相反的是,赋值是必须由程序员显式调用的操作.然而,在Eiffel和Smalltalk中,赋值和复制操作都由程序员显式调用.这也是基于值的语言与基于引用的语言之间的区别. 在C++中,对于对象和基本类型赋值都具有相同的含义.把基本类型变量赋值给另一个(兼容的)基本类型变量

《C++面向对象高效编程(第2版)》——2.21 确保抽象的可靠性——类不变式和断言

2.21 确保抽象的可靠性--类不变式和断言 C++面向对象高效编程(第2版) 任何抽象都必须与客户履行它的契约(contract).当客户使用类时,他希望类的对象像其发布描述的那样运行正常.另一方面,类的实现者必须千方百计地确保对象运行正常.但是,类只有在客户履行自己那部分契约后,才能正确行使它的职责.例如,类的成员函数可能要求传入的参数为非零指针(non-zero pointer).只有满足此前提条件,成员函数才能保证它的行为.因此,客户必须履行一些义务.换言之,如果客户履行了她那部分契约,

《C++面向对象高效编程(第2版)》——2.30 has-a关系的重要性

2.30 has-a关系的重要性 C++面向对象高效编程(第2版) "has-a"关系(也称为关联.聚集.包含.组合)是在OOD(面向对象设计)中频繁使用的重要关系.但是,许多设计者和程序员都没有很好地理解其相关性,从而导致复杂僵化的设计和不必要的继承. 在OOD阶段,软件的复用主要通过两种方式完成:继承和包含.它们各有优缺点.优秀的设计者了解它们的局限性.优点和代价,可以灵活自如地应用它们.继承将在第5章.第6章以及第二部分的第12章中详细讨论. 包含是一项强大的设计技术,它比继承更

《C++面向对象高效编程(第2版)》——2.27 关联

2.27 关联 C++面向对象高效编程(第2版) 关联表示对象与不同类之间的结构关系(structual relationship),大多数关联都是二元关系(binary relation).类之间的多重关联(multiple association)和类本身的自关联(self association)都是合法的(见图2-19). 关联可以有一个名称,表明阅读方向的箭头为可选.注意,方向箭头为可选,但关联名必须显示.关联在不同的方向可以有不同的名称,但是,大多数情况下,没必要注明(特别是在已标出

《C++面向对象高效编程(第2版)》——3.2 类要素的细节

3.2 类要素的细节 C++面向对象高效编程(第2版) 3.2.1 访问区域 客户可以访问在类的public区域中声明的任何成员.我们可以把该区域看做是通用公共(general public)的接口,它没有任何保护,是类限制最少的区域.一个设计良好的类绝不会将数据成员包含在public区域,该区域只能包含成员函数.如果在public区域包含数据成员,那么无需类的实现者,仅通过编译器即可访问这些数据成员.这违反了数据抽象和封装原则.这也是我们为什么总将数据成员放在private或protected

《C++面向对象高效编程(第2版)》——第2章 什么是数据抽象

第2章 什么是数据抽象 C++面向对象高效编程(第2版) 面向对象编程的一项基本任务是创建带有适当功能的类,并隐藏不必要的细节(即抽象数据).下面,我们将用一个现实生活中的例子来解释数据抽象的概念. 绝大多数人都见过影碟播放机(laser disc player)(或LD播放机).现在,提出一个简单的问题:设计一个影碟播放机,要求易于使用和修改,可后续添加更多有用的功能. 注意: 如果难以理解影碟播放机,可以用CD播放机代替LD播放机,其设计原理类似.实际上,影碟播放机的功能是CD播放机功能的超

《C++面向对象高效编程(第2版)》——1.5 什么可以作为类

1.5 什么可以作为类 C++面向对象高效编程(第2版) 用简单的例子详细讨论类和对象非常容易,但是难点在于如何为给定的问题找出合适的类.我们必须理解类代表什么,何时将问题中的某些部分转化为类,而非数据,反之亦然.根据我们的定义,类拥有一组对象的共同属性(或者特性).怎样的共同才是共同?何时说这是一个类,而不是另一个类的对象?这些都是我们在学习OOP时会遇到的,和真正关心的问题. 当我们决定创建一个类时,第一个问题就是"是否确实需要这个类的多个实例?",如果答案为"是&quo