当前位置: 首页 > news >正文

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

前言

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

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


指向类数据成员的指针

#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要怎么办呢?我们可以这样写:

#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才能编译通过。

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

#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)();就行,但是如果定义指向类成员函数的指针可就麻烦一点了,接下来看一个例子:

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的。

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

#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四种状态,下面是实现代码:

#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;
}

运行结果

state = idle
state = idle
state = run
state = jump
state = jump
state = jump
state = walk
state = idle
state = idle

总结

  • 牢记->*.*也是一种操作符,使用的时候不要拆开
  • 理解操作符中的*符号的解引用的作用
  • 如果有理解不正确的地方欢迎大家批评指正

相关文章:

  • VS2015调试dump文件时提示未找到xxx.exe或xxx.dll
  • 结构体sockaddr、sockaddr_in、sockaddr_in6之间的区别和联系
  • 简述TCP三次握手和四次挥手流程
  • 智能指针(零):分类及简单特性
  • 智能指针(一):auto_ptr浅析
  • 智能指针(二):shared_ptr浅析
  • 智能指针(四):unique_ptr浅析
  • Lua中关于table对象引用传递的注意事项
  • VS2015调试dump文件时提示打不开KERNELBASE.dll
  • Mysql中使用select into语句给变量赋值没有匹配记录时的结果
  • 排序算法系列之(四)——抓扑克牌风格的插入排序
  • linux环境下服务器程序的查看与gdb调试
  • linux环境下运行程序常用的nohup和的区别
  • 排序算法系列之(五)——为目标打好基础的希尔排序
  • linux环境下查找包含指定内容的文件及其所在行数
  • [译]前端离线指南(上)
  • Angular 响应式表单之下拉框
  • interface和setter,getter
  • Javascript设计模式学习之Observer(观察者)模式
  • Java面向对象及其三大特征
  • jquery ajax学习笔记
  • leetcode388. Longest Absolute File Path
  • Mocha测试初探
  • Rancher如何对接Ceph-RBD块存储
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • uva 10370 Above Average
  • 从零开始学习部署
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 问:在指定的JSON数据中(最外层是数组)根据指定条件拿到匹配到的结果
  • 想晋级高级工程师只知道表面是不够的!Git内部原理介绍
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • ​二进制运算符:(与运算)、|(或运算)、~(取反运算)、^(异或运算)、位移运算符​
  • ## 临床数据 两两比较 加显著性boxplot加显著性
  • #《AI中文版》V3 第 1 章 概述
  • #define用法
  • #HarmonyOS:软件安装window和mac预览Hello World
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • #QT(串口助手-界面)
  • (1)(1.13) SiK无线电高级配置(五)
  • (2)关于RabbitMq 的 Topic Exchange 主题交换机
  • (二)fiber的基本认识
  • (附源码)springboot宠物医疗服务网站 毕业设计688413
  • (教学思路 C#之类三)方法参数类型(ref、out、parmas)
  • (深度全面解析)ChatGPT的重大更新给创业者带来了哪些红利机会
  • (十二)python网络爬虫(理论+实战)——实战:使用BeautfulSoup解析baidu热搜新闻数据
  • (学习日记)2024.04.10:UCOSIII第三十八节:事件实验
  • (一)硬件制作--从零开始自制linux掌上电脑(F1C200S) <嵌入式项目>
  • (转)Linq学习笔记
  • .NET 8 编写 LiteDB vs SQLite 数据库 CRUD 接口性能测试(准备篇)
  • .NET Core 2.1路线图
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .NET 的静态构造函数是否线程安全?答案是肯定的!
  • .Net 中的反射(动态创建类型实例) - Part.4(转自http://www.tracefact.net/CLR-and-Framework/Reflection-Part4.aspx)...
  • .Net各种迷惑命名解释
  • .NET连接MongoDB数据库实例教程