关于C++中的非静态类成员函数指针
昨天发现了一个问题,就是使用对类中的非静态成员函数使用std::bind时,不能像普通函数一样直接传递函数名,而是必须显式地调用&(取地址),于是引申出我们今天的问题:非静态类成员函数指针和普通函数指针有什么区别?
一.C++中对函数到指针的隐式转换
以前在C语言程序设计课上,老师都会说:“函数名就是指向这个函数的指针”。实际上通过查阅cppreference中的隐式转换规则,其中有这么一句关键的话道出了玄机:
函数类型
T的左值能隐式转换成指向该函数的指针纯右值。这不作用于非静态成员函数,因为不存在指代非静态成员函数的左值。
这一句话表明,实际上我们在把普通函数的函数名当作右值使用的过程中,C++隐式地将其转换成了指向该函数的的指针。这也就是我们在使用std::bind的过程中可以直接传递普通函数的函数名的原因,例如:
int foo(int a,int b)
{
return (a+b);
} int main()
{
auto function = std::bind(foo,std::placeholders::_1,std::placeholders::_2);
std::cout << function(,) << std::endl; //
return ;
}
但是,这一条隐式转换规则不作用于非静态成员函数,因为不存在指代非静态成员函数的左值。因此我们能看出,指向非静态成员函数的指针和指向普通函数的指针是有区别的。接下来我们将会讨论类成员函数指针(除非有特别说明,下文中的“类成员”都指代“非静态类成员”)。
二.类成员函数指针的声明和调用
类成员函数指针的声明有些类似普通函数指针。但是区别在于,类成员函数是类成员的一部分,所以需要将类成员函数指针使用 ::* 声明成 成员指针类型。这也表示了类成员函数指针不能单独地被调用,需要 该类型的实例使用.*(对象的成员指针) 或 指向该类型的指针使用->*(指针的成员指针) 进行调用。同时由于上文说到的C++隐式转换的规则,我们需要显示地调用 &(取地址) 获取非静态成员函数指针:
class A
{
public:
void FunA()
{
std::cout << "Function A" << endl;
}
}; int main()
{
void(A::*p1)() = &A::FunA;
//p1(); Error
A a;
A *p2 = &a;
(a.*p1)(); // Succ
(p2->*p1)(); // Succ
return ;
}
三.探究类成员函数指针
我们都知道,普通函数指针实际上指向的是函数代码的起始地址。然而类成员函数指针不仅仅是类成员函数的内存起始地址,它还解决C++中涉及到的多重继承、虚函数等导致的地址偏移问题。因此,普通函数指针的内存大小是普通指针的大小(32位系统4字节,64位系统8字节),但是成员函数指针却不一定。
1.多继承中的使用
现在有三个类A B C,其中类C public继承 了A B两个类。它们的声明和定义如下:
class A
{
public:
int a;
void funA()
{
std::cout << "function A" << std::endl;
}
}; class B
{
public:
int b;
void funB()
{
std::cout << "function B" << std::endl;
}
}; class C : public A, public B
{
public:
int c;
void funC()
{
std::cout << "function C" << std::endl;
}
};
现在我们声明一个函数指针:
int main()
{
void (C::*mfpC)() = &B::funB;
return ;
}
这个指针很特别,它是一个指向C的类成员的指针,但实际上它指向了C的基类B的成员函数B::funB。现在我们编译代码并调用gdb调试查看mfpC的信息:
(gdb) p mfpC
$1 = (void (C::*)(C * const)) 0x8000966 <B::funB()>, this adjustment 4
(gdb) p sizeof(mfpC)
$2 = 16
(gdb) x/16xb &mfpC
0x7ffffffee830: 0x66 0x09 0x00 0x08 0x00 0x00 0x00 0x00
0x7ffffffee838: 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
可以看见,mfpC实际占用了16个字节(运行环境为64位操作系统),这16个字节中不仅包含了它所指向的函数的起始地址,还包含了this指针的调整值。因为对于多重继承来说,如果类成员函数指针保存的是非最左基类的成员函数地址,根据C++标准,非最左基类实例的开始地址肯定不同于派生类实例的开始地址,所以需要调整this指针,使其指向非最左基类实例。
2.有虚函数的使用
现在修改代码,在类B中定义一个虚函数funC,并在类C中重写它:
class A
{
public:
int a;
void funA()
{
std::cout << "function A" << std::endl;
}
}; class B
{
public:
int b;
void funB()
{
std::cout << "function B" << std::endl;
} virtual void funC()
{
std::cout << "function C in class B" << std::endl;
}
}; class C : public A, public B
{
public:
int c;
void funC()
{
std::cout << "function C in class C" << std::endl;
}
};
然后我们实例化一个类C的对象c和一个指向B::funC的成员函数指针mfpB,最后通过c调用它:
int main()
{
C c;
void (B::*mfpB)() = &B::funC;
(c.*mfpB)();
return 0;
}
结果显而易见,输出的不是"function C in class B",而是"function C in class C"。
由此可见,成员函数指针虽然看起来和普通的函数指针有些类似,但实际上两者还是有很大的差别的。
关于C++中的非静态类成员函数指针的更多相关文章
- C++中的Thunk技术 / 非静态类成员函数作为回调函数 的实现方法
原文:https://blog.twofei.com/616/ 用我的理解通俗地解释一下什么是C++中的Thunk技术吧! Thunk技术就是申请一段可执行的内存, 并通过手动构造CPU指令的形式来生 ...
- 为什么const对象只能调用const成员函数,而不能调用非const成员函数?
在c++中,我们可以用const来定义一个const对象,const对象是不可以调用类中的非const成员函数,这是为什么呢?下面是我总结的一些原理. 假设有一个类,名字为test代码如下: clas ...
- 成员函数指针与高性能C++委托
1 引子 标准C++中没有真正的面向对象的函数指针.这一点对C++来说是不幸的,因为面向对象的指针(也叫做“闭包(closure)”或“委托(delegate)”)在一些语言中已经证明了它宝贵的价值. ...
- [转]成员函数指针与高性能的C++委托
原文(作者:Don Clugston):Member Function Pointers and the Fastest Possible C++ Delegates 译文(作者:周翔): 成员函数指 ...
- C++ 指向类成员函数指针的用法(转自维基百科)
类成员函数指针 类成员函数指针(member function pointer),是C++语言的一类指针数据类型,用于存储一个指定类具有给定的形参列表与返回值类型的成员函数的访问信息. 目录 1 语法 ...
- 函数指针和成员函数指针有什么不同,反汇编带看清成员函数指针的本尊(gcc@x64平台)
函数指针是什么,可能会答指向函数的指针. 成员函数指针是什么,答指向成员函数的指针. 成员函数指针和函数指针有什么不同? 虚函数指针和非虚成员函数指针有什么不同? 你真正了解成员函数指针了吗? 本篇带 ...
- 从汇编看c++成员函数指针(二)
下面先看一段c++源码: #include <cstdio> using namespace std; class X { public: virtual int get1() { ; } ...
- 为什么 C++ 中成员函数指针是 16 字节?
当我们讨论指针时,通常假设它是一种可以用 void * 指针来表示的东西,在 x86_64 平台下是 8 个字节大小.例如,下面是来自 维基百科中关于 x86_64 的文章 的摘录: Pushes a ...
- 从汇编看c++中成员函数指针(一)
下面先来看c++的源码: #include <cstdio> using namespace std; class X { public: int get1() { ; } virtual ...
随机推荐
- Git--03 git分支
目录 Git分支 1.新建testing分支 2.合并分支 3.合并冲突 4.删除分支 Git标签使用 1.查看标签 02.删除标签 Git分支 分支即是平行空间,假设你在为某个手机系统研发拍照功 ...
- Java虚拟机——Java内存区域与内存溢出
内存区域 Java虚拟机在执行Java程序的过程中会把他所管理的内存划分为若干个不同的数据区域.Java虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器.Java虚拟机栈.本地方法栈 ...
- ubuntu安装Scala
参考地址:https://blog.csdn.net/qq_36743482/article/details/78317549 1.下载Scala:http://www.scala-lang.org/ ...
- mysql数据库进阶
一.索引 索引,是数据库中专门用于帮助用户快速查询数据的一种数据结构.类似于字典中的目录,查找字典内容时可以根据目录查找到数据的存放位置,然后直接获取即可. 分类: 普通索引 唯一索引 全文索引 组合 ...
- JavaScript实例之计算器
代码实例: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...
- 使用axios上传文件到阿里云对象文件存储服务器oss
背景 OSS可用于图片.音视频.日志等海量文件的存储.各种终端设备.Web网站程序.移动应用可以直接向OSS写入或读取数据.OSS支持流式写入和文件写入两种方式.使用阿里云oss做文件存储的时候,不可 ...
- 人生苦短_我用Python_dict(字典)_003
# coding=utf-8 # 字典 {"key":"values"} 无序的 # dict 可以包含任何类型的数据 values可以是任何数据类型 key必 ...
- restful风格接口和spring的运用
Restful风格的API是一种软件架构风格,设计风格而不是标准,只是提供了一组设计原则和约束条件.它主要用于客户端和服务器交互类的软件.基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机 ...
- CentOS7安装配置redis5.0.5
一.安装必需包gcc yum install gcc 二.下载redis,并解压 wget http://download.redis.io/releases/redis-5.0.5.tar.gz t ...
- 使用UncaughtExceptionHandler重启线程
先复习Java中的异常 java.lang.Throwable 顶层父类 |– Error错误:JVM内部的严重问题,如OOM,程序员无法在代码中无法处理. |–Exception异常:普通的问题.通 ...