12.3.4 不常用的运算符

还有一些运算符的重载语法有一点不同。

下标运算符operator[],必须是成员函数并且它只接受一个参数。因为它所作用的对象应该像数组一样操作,可以经常从这个运算符返回一个引用,所以它可以被很方便地用于等号左侧。这个运算符经常被重载,可以在本书其他部分看到相关的例子。

运算符new和delete用于控制动态存储分配并能按许多种不同的方法进行重载,这将在第13章中讨论。

12.3.4.1 operator,

当逗号出现在一个对象左右,而该对象的类型是逗号定义所支持的类型时,将调用逗号运算符。然而,“operator,”调用的目标不是函数参数表,而是被逗号分隔开的、没有被括号括起来的对象。除了使语言保持一致性外,这个运算符似乎没有许多实际用途。下面的例子说明了当逗号出现在对象前面以及后面时,逗号函数调用的方式:

12.3.4 不常用的运算符 - 图1

全局函数允许逗号放在被讨论的对象的前面。这里的用法既晦涩又可疑。虽然还可以再把一个逗号分隔的参数表当做更加复杂的表达式的一部分,但这太灵活了,大多数情况下不能使用。

12.3.4.2 operator->

当希望一个对象表现得像一个指针时,通常就要用到operator->。由于这样一个对象比一般的指针有着更多与生俱来的灵巧性,于是常被称作灵巧指针(smart pointer)。如果想用类包装一个指针以使指针安全,或是在迭代器(iterator)普通的用法中,这样做会特别有用。迭代器是一个对象,这个对象可以作用于其他对象的容器或集合上,每次选择它们中的一个,而不用提供对容器的直接访问。(在类函数里经常发现容器和迭代器,例如本书第2卷中描述的标准C++库。)

12.3.4 不常用的运算符 - 图2

指针间接引用运算符一定是一个成员函数。它有着额外的、非典型的限制:它必须返回一个对象(或对象的引用),该对象也有一个指针间接引用运算符;或者必须返回一个指针,被用于选择指针间接引用运算符箭头所指向的内容。下面是一个简单的例子:

12.3.4 不常用的运算符 - 图3

类Obj定义了程序中使用的一些对象。函数f()和g()用静态数据成员打印令人感兴趣的值。使用ObjContainer的函数add()将指向这些对象的指针存储在类型为ObjContainer的容器中。ObjContainer看起来像一个指针数组,但却没有办法取回这些指针。然而,类SmartPointer被声明为友元类,所以它允许进入这个容器内。类SmartPointer看起来像一个聪明的指针—可以使用运算符++向前移动它(也可以定义一个operator—),它不会超出它所指向的容器的范围,它可以返回它指向的内容(通过这个指针间接引用运算符)。注意,不像一个基本指针,SmartPointer是和所创建的容器的配套使用的,不存在一个具有“通用目的”的灵巧指针。我们将在本书最后一章和第2卷中了解更多被称为“迭代器”的灵巧指针的内容。

在main()中,一旦Obj对象装入容器oc,一个SmartPointer类的SP就创建了。灵巧指针按下面的表达式进行调用:

12.3.4 不常用的运算符 - 图4

这里,尽管sp实际上并没有成员函数f()和g(),但指针间接引用运算符自动地为用SmartPointer:operator->返回的Obj*调用那些函数。编译器进行所有检查以保证函数调用正确。

虽然,指针间接运算符的底层机制比其他运算符复杂一些,但目的是一样的—为类的用户提供更为方便的语法。

12.3.4.3 嵌入的迭代器

更常见的是,“灵巧指针”和“迭代器”类嵌入它所服务的类中。前面的例子可按如下重写,以在ObjContainer中嵌入SmartPointer。

12.3.4 不常用的运算符 - 图5

12.3.4 不常用的运算符 - 图6

除了实际上嵌入了类中,另有两点不同之处。首先是在类的声明中说明它是一个友元类。

12.3.4 不常用的运算符 - 图7

编译器首先在被告知类是友元的之前,必须知道该类是存在的。

第二个不同之处是在ObjContainer的成员函数begin()中,begin()产生一个指向ObjContainer序列开头的SmartPointer。虽然实际上仅是方便了,但由于它遵循了在标准C++库中使用的部分形式,所以还是值得的。

12.3.4.4 operator->*

operator->*是一个二元运算符,其行为与所有其他二元运算符类似。它是专为模仿前一章介绍的内建数据类型的成员指针行为而提供的。

与operator->一样,指向成员的指针间接引用运算符通常同某种代表“灵巧指针”的对象一起使用。这里的例子将简单些以便于理解。在定义operator->时要注意它必须返回一个对象,对于这个对象,可以用正在调用的成员函数为参数调用operator()。

operator()的函数调用必须是成员函数,它是惟一的允许在它里面有任意个参数的函数。这使得对象看起来像一个真正的函数。虽然可以定义一些重载的带不同参数的operator()函数,但这常被用于仅有一个单一操作数或至少是一个特别优先的类型。在第2卷中,可以看到标准C++库使用函数调用运算符以创建“函数对象”。

要想创建一个operator->,必须首先创建带有operator()类,这是operator->将返回对象的类。该类必须获取一些必要的信息,以使当operator()被调用时,指向成员的指针可以对对象进行间接引用。在下面的例子中,FunctionObject的构造函数得到并储存指向对象的指针和指向成员函数的指针,然后operator()使用这些指针进行实际指向成员的指针的调用。

12.3.4 不常用的运算符 - 图8

12.3.4 不常用的运算符 - 图9

Dog有三个成员函数,它们的参数和返回类型都是int。PMF是一个typedef,用于简化定义一个指向Dog成员函数的指向成员的指针。

operator->创建并返回一个FunctionObject对象。注意operator->既知道指向成员的指针所调用的对象(this),又知道这个指向成员的指针,并把它们传递给存储这些值的FunctionObject构造函数。当operator->被调用时,编译器立刻转而对operator->返回的值调用operator(),把已经给operator->*的参数传递进去。FunctionObject:operator()得到参数,然后使用存储的对象指针和指向成员的指针间接引用“真实的”指向成员的指针。

注意,正如operator->,这里操作的内容正插入到调用operator->*的中间。如果需要的话,这允许我们执行某些额外的操作。

此处执行的operator->*机制仅作用于参数和返回值是int的成员函数。这是一个局限,但是,如果试着为每一个不同的可能性进行重载,这就像是一个禁止的行为。幸运的是,C++的template机制(在本书的最后一章和第2卷中讲述)被设计用来处理这个问题。