13.10 - 隐藏的this指针
新手面向对象程序员经常问的一个关于类的问题是,“当调用成员函数时,C++如何确定调用它的对象?“ 实际上,C++ 使用一个名为 “this”的隐藏指针来跟踪调用成员的对象。
下面是一个保存整数并提供构造函数和访问函数的简单类。注意,这个类不需要析构函数,因为C++自动清理整数成员变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
下面例子显示了Simple的使用方法:
1 2 3 4 5 6 7 8 9 10 |
|
程序输出结果如下:
1 |
|
当我们调用 simple.setID(2);
时,C++ 就能够知道调用的是simple
的 setID()
,而 m_id
则指的是 simple.m_id
。接下来让我们看看这背后的工作原理。
隐藏的 *this
指针
观察上面例子中的这一行代码:
1 |
|
尽管函数 setID()
函数看上去只接受了一个参数,而实际上它接受了两个参数!当程序编译时,编译器会将上述代码转变成下面的形式 :
1 |
|
注意,上面的代码已经变成了标准的函数调用,simple 对象(以前是一个对象前缀)则通过按地址传递的方式传入了函数。
但这只是答案的一半,由于函数调用现在增加了一个实参,因此需要修改成员函数定义才能接受(并使用)这个实参作为形参。因此,下面的成员函数:
1 |
|
会被编译器转换为下面的形式:
1 |
|
编译器在编译普通的成员函数时,会隐式地为其添加一个新的参数this
。this
指针是一个隐藏的常量指针,它保存着该函数对应对象的地址。
此外,还有一点需要注意。在成员函数中,在成员函数中,任何类成员(包括函数和变量)也需要被更新为引用对象的形式。要完成这一点很简单,只需要为它们添加this->
前缀即可。因此在函数体中,m_id
(成员变量)被转变成 this->m_id
。由于 this 指针指向的是simple
对象的地址,因此 this->m_id
会被解析为 simple.m_id
。
总结一下:
- 当调用
simple.setID(2)
时,编译器实际调用的是setID(&simple, 2)
; - 在
setID()
中,this 指针保存着simple
对象的地址; setID()
函数中的任何成员都会被添加 “this->
” 前缀,所以,对于m_id = id
这条语句,编译器实际上执行的是this->m_id = id
,在这个例子中即将simple.m_id
的值更新为id
。
好消息是,所有这些都是自动发生的,你是否记得它是如何工作的并不重要。你只需记住,所有普通成员函数都有一个“this”指针,该指针指向调用函数的对象。
“this” 指针总是指向操作对象自己
新程序员有时会对存在多少个“this”指针感到困惑。每个成员函数都有一个“this”指针形参,它被设置为正在操作的对象的地址。例如:
1 2 3 4 5 6 7 8 9 |
|
在上面的代码中,this指针交替地指代对象A或者B的地址,取决于调用成员函数的是A还是B。
因为“this”只是一个函数形参,它不会给类增加任何内存使用量(只是给成员函数调用增加内存使用量,因为这个形参需要传递给函数并存储在内存中)。
显式地使用 this 指针
大多数情况下,你不需要显式引用“this”指针。然而,在一些情况下直接使用 this 指针也是有用的。
首先,如果构造函数(或成员函数)的形参与成员变量同名,你可以使用" this "来消除歧义:
1 2 3 4 5 6 7 8 9 10 11 |
|
上面代码中的构造函数,其形参和类成员变量同名。在这种情况下,data
将表示形参,所以你必须使用 “this->data” 来表示成员变量。尽管这么做也是可以被接受的编程风格,但是使用m_
作为成员变量的前缀就可以从源头上消除这种歧义。
链式调用成员函数
另一方面,有时候让类成员函数返回对象本身也是很有用的。这么做的一个主要原因就是可以允许函数的链式调用,使得多个成员函数可以作用于一个相同的对象!其实你已经多次使用该特性了。对于下面这个例子来说,std::cout
输出了不止一个字符串。
1 |
|
在这个例子中,std::cout
其实是一个对象,而运算符 <<
是该对象的一个成员函数。编译器会按照下面的方式对上述代码求值:
1 |
|
首先,<<
使用 std::cout
和字符串字面量 “Hello, ” 将 “Hello, ” 打印到终端。不过,这只是上述表达式的一部分,<<
还需要返回一个值(或void)。如果它返回的是 void,则结果会变为下面的形式:
1 |
|
这显然不合逻辑(编译器会抛出错误)。实际上,<<
的返回值是*this
,在上面的语境下,this 就是std::cout
对象。因此,当第一个<<
执行完成后,语句等价于如下形式:
1 |
|
上述代码进而会打印用户名。
通过这种方式,我们只需要指定对象一次(此例中为 std::cout
),然后每次函数调用都可以把它传递给接下来的函数,是我们可以链式执行多个指令。
在用户自定义类中我们也可以实现上述行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
如果你希望先加5,再减3,然后再乘以4,那么可以像下面这样做:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
不过,如果每个函数都返回 *this
的话,在可以链式地调用它们,下面是一个可链式调用的例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
注意,函数add()
,sub()
和 mult()
现在都返回*this
。这样一来就可以向下面这样调用函数:
1 2 3 4 5 6 7 8 9 10 |
|
三行代码被压缩成了一行!我们再仔细研究一下它是如何工作的。
首先,调用calc.add(5)
将 5 加到 m_value
,然后返回 *this
,它是 calc
的引用,所以 calc
会被用于调用接下来的函数。再接下来,对 calc.sub(3)
求值,它将 3 从 m_value
减去后,继续返回calc
。最后,calc.mult(4)
将 m_value
乘以四并返回calc
,由于不需要继续调用函数,这个返回值也就被忽略掉了。
因为每个函数都会修改对象 calc
,所以calc
的成员 m_value
的值此时为 (((0 + 5) - 3) * 4)
,即8。
小结
“this” 指针是一个被编译器隐式添加到非静态成员函数的隐藏参数。多数情况下你并不会直接访问它,但是当有需要时你会用的着的。需要注意的是,this是一个常量指针,你可以通过它修改它所指的对象,但是你不能让它指向别的对象。
如果我们让原本返回 void 的函数返回*this
,则可以使这些函数能够被链式调用。通常在重载类的运算符时会用到这个特性(14章中会详细介绍运算符重载)。