操作指向类成员的指针需要了解的两个操作符->*和.*

前言

关于 ->* 这种写法在很早就在项目代码里见过了,并且还写过,不过当时并没有正确的理解这样写的含义,一直到最近发现这样写很奇怪,于是根据自己的理解,开始改代码,发现无论怎么改都无法通过编译,仔细搜索后才发现这是一种固定的写法,也就是说 ->* 是一个操作符,无法拆分,同时还有一个 .* 也是相同的作用,只不过是用于对象上,而 ->* 是用于对象的指针上。

那么这两个操作符究竟有什么作用呢?实际上它们主要用于操作指向类成员的指针,可能你会说指向类成员的指针直接定义就好了,为什么这么麻烦,还要是用这两个操作符呢?接下来我们举几个例子就明白了。

指向类数据成员的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
class C
{
public:
int m;
};

int main()
{
int C::* p = &C::m; // pointer to data member m of class C
C c = {7};
std::cout << c.*p << '\n'; // prints 7
C* cp = &c;
cp->m = 10;
std::cout << cp->*p << '\n'; // prints 10
}

看到上述代码中的p指针有什么不同了吧,这是一个指向类成员变量的指针,如果我们不这样定义p也想操作c对象的成员变量m要怎么办呢?我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
class C
{
public:
int m;
};

int main()
{
C c = {7};
int *p = &c.m;
std::cout << *p << '\n'; // prints 7
*p = 10;
std::cout << *p << '\n'; // prints 10
}

这样代码中的变量p就变成了一个简单的指向整型数据的指针,我们也可以通过它访问c对象的m变量,并且给它赋值,但是你有没有发现区别,前一种指针p只依赖于类C的定义,可以在类C创建对象之前就给指针p定义赋值,但是后一种数据指针p就只能在类C创建对象之后才能给它赋值,还有一点,前一种指针p可以根据调用它的对象不同而访问不同类C对象的值,而后一种指针p就只能访问它所指向的那个对象的m值,如果要访问其他对象,需要重新给p赋值。

注意指向类成员指针的定义和赋值方法,是int C::* p = &C::m;,取变量m的地址还有两种写法,&(C::m) 或者 &m这两种写法只能写在类C的成员函数中,所表示的也就是一个简单的指向整型变量的指针,即int*,与 &C::m的含义是大不相同的。

而操作符->*.*在代码中起什么作用呢,我们只看这一句std::cout << cp->*p << '\n';,其中表达式cp->*p用到了操作符->*,根据我的理解这个操作符的作用就是将后面的指针解引用,然后再被前面的对象调用,首先我们看cp是一个指向c对象的指针,如果想访问m变量,可以直接使用cp->m,假设现在不想这么写,我们有一个指向类C中m变量的指针p,那么直接写成cp->p肯定是不行的,因为p并不是类C的成员,它只是一个指向类C成员的指针,所以需要将其解引用,转换成真正的成员才能被cp指针引用到,那么*cp其实就是类C中的m,组合到一起就是cp-> *p,这只是理解,其实->*是一个不可分割的操作符,需要紧挨着写成cp->*p才能编译通过。

另外关于指向类成员指针,在操作对象是父类对象和子类对象时有什么不同呢?答案是:指向可访问的非虚拟基类的数据成员的指针可以隐式地转换为指向派生类的同一数据成员的指针,反过来结果就是未定义的了,可以参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
class Base
{
public:
int m;
};
class Derived : public Base {};

int main()
{
int Base::* bp = &Base::m;
int Derived::* dp = bp;
Derived d;
d.m = 1;
std::cout << d.*dp << '\n'; // prints 1
std::cout << d.*bp << '\n'; // prints 1
}

指向类成员函数的指针

其实前面的例子我在工作中还真没遇到过,但是指向类数据成员的指针确实经常用,熟悉函数指针的工程师都知道,类似于void (*func)(); 就是定义了指向一个无返回值无参数函数的指针,调用时只要写成(*func)();就行,但是如果定义指向类成员函数的指针可就麻烦一点了,接下来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C
{
public:
void f(int n) { std::cout << n << '\n'; }
};

int main()
{
void (C::* p)(int) = &C::f; // pointer to member function f of class C
C c;
(c.*p)(1); // prints 1
C* cp = &c;
(cp->*p)(2); // prints 2
}

这个例子中的函数指针p是有作用域的,也就是只能指向类C中的无返回值并且有一个整型参数的函数,代码中赋值为&C::f,这个形式与数据成员指针的赋值一样,其实函数f就是类C的一个成员而已。

那么它是怎么通过p指针调用到函数f的呢?我们看一句代码(cp->*p)(2);其实->*在这里还是起到了解引用并访问的作用,如果要访问f函数,只要cp->f(2)即可,但是这里没有f只有一个指向f的指针p,所以将f替换成*p编程cp->*p(2);但是这样无法通过编译,它无法区分那一部分是函数体,那一部分是参数,所以加个括号指明一下变成(cp->*p)(2);就可以正常访问f函数了。

实际上对面向对象编程了解的深入一点就会知道,调用对象的成员函数,实际上就是把对象的指针this作为函数第一个参数传进去,比如cp->f(2),假如函数f的函数指针是func,那么cp->f(2)就是调用func(cp, 2),这样在函数f中就可以调用对象的成员变量或者其他的成员函数了,但是如果你的成员函数中没有访问成员内容,那么这个this指针传什么都可以,也就是说func(cp, 2)func(0, 2)func(0x1234567890, 2)都是等价的,在这个例子中就是这样,所有你可以这样来写一段代码:(((C*)0)->*p)(2),也是可以打印出数字2的。

另外关于指向类成员函数指针,在操作对象是父类对象和子类对象时与成员变量的规则一致:指向可访问的非虚拟基类的成员函数的指针可以隐式地转换为指向派生类的同一成员函数的指针,反过来也是未定义,可以参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
class Base
{
public:
void f(int n) { std::cout << n << '\n'; }
};
class Derived : public Base {};

int main()
{
void (Base::* bp)(int) = &Base::f;
void (Derived::* dp)(int) = bp;
Derived d;
(d.*dp)(1);
(d.*bp)(2);
}

具体使用

前面提到过指向类数据成员的指针我之前真的没用到过,但是指向成员函数的指针,我却用了不少,一般都是放在函数数组中使用,比如有这样一个场景,游戏npc根据状态执行对应的状态函数,这些状态函数是成员函数,为此我们需要将npc所有的状态函数添加到一个函数数组中,假设有idle、run、walk、jump四种状态,下面是实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>

class CNpc
{
typedef void (CNpc::*StateFunction)();
public:
int state; // 0,1,2,3 对应 idle、run、walk、jump
StateFunction state_function_array[4];

public:
CNpc()
{
state = 0;
state_function_array [0] = &CNpc::idle;
state_function_array [1] = &CNpc::run;
state_function_array [2] = &CNpc::walk;
state_function_array [3] = &CNpc::jump;
}

void change_state(int new_state)
{
state = new_state;
}

void process_state()
{
if (state_function_array[state])
{
(this->*state_function_array[state])(); // 调用函数指针的地方
}
}

private:
void idle() { std::cout << "state = idle" << std::endl; }
void run() { std::cout << "state = run" << std::endl; }
void walk() { std::cout << "state = walk" << std::endl; }
void jump() { std::cout << "state = jump" << std::endl; }
};

int main()
{
CNpc npc;

npc.process_state();
npc.process_state();

npc.change_state(1);
npc.process_state();

npc.change_state(3);
npc.process_state();
npc.process_state();
npc.process_state();

npc.change_state(2);
npc.process_state();

npc.change_state(0);
npc.process_state();
npc.process_state();

return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
state = idle
state = idle
state = run
state = jump
state = jump
state = jump
state = walk
state = idle
state = idle

总结

  • 牢记->*.*也是一种操作符,使用的时候不要拆开
  • 理解操作符中的*符号的解引用的作用
  • 如果有理解不正确的地方欢迎大家批评指正
Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客