当我得到成员函数的地址时,我无法将地址分配给 void*:
void* ptr = &object::function; // <-- doesn't work!
我知道这是因为成员函数为您提供了这些指向成员的奇怪指针,而不仅仅是像普通函数一样运行,但我不明白为什么会这样。
是的,该函数为 this 指针提供了一个额外的参数。是的,该函数在相关对象的上下文之外并没有真正的意义。但最终,该函数仍然位于特定的内存地址,就像其他所有函数一样。考虑到这一点,我不明白为什么语言设计者让我跳过箍只是为了获取成员函数的内存地址:
对于正常功能:
void function() { }
std::cout << (size_t)&function << 'n';
不需要有趣的业务,但对于成员函数:
struct object {
void function() { }
}
void object::* ptr = &object::function;
std::cout << *(size_t*)&ptr << 'n';
它只是无缘无故地变得奇怪。我有点理解成员变量上下文中指向成员的指针,在创建实例之前,它们实际上没有地址,因此在此之前,您将指向成员的指针用作一种伪指针。但我不明白指向成员函数的指针。它们仍然是函数,为什么它们会得到这种奇怪的特殊处理,这些指向成员函数的指针有什么实际优势?
请注意,根据 C 和 C++ 规范,您不能将任何函数指针强制转换为void*
。 这是一个常见的扩展,允许它用于普通函数(并且该扩展是 POSIX 标准所要求的,因此可能仍然很常见),但对于成员函数指针来说则不那么多。
原因是成员函数的许多实现除了调用函数实现之外还需要额外的修正/查找——对于虚函数,您需要根据this
的动态类型(通常是 vtable 查找)查找实现,并且支持多重继承可能需要在调用函数之前对this
进行一些调整。 因此,许多实现使用的方法指针的表示形式大于void *
并且无法容纳在其中。
我无法将地址分配给空白*
但我不明白为什么会这样。
最直接的解释是指向成员函数的指针不能转换为void*
。对此最直接的解释是语言不允许这样做。指向成员的指针不是指针(这适用于指向数据成员的指针以及指向成员函数的指针)。
函数仍位于特定内存地址
事情没那么简单。如果成员函数是虚拟的,则实际上可能有许多实现,并且每个实现将位于不同的内存地址中。语言实现必须以某种方式确保当您通过指向派生对象上的成员函数的指针调用时,使用正确的this
地址调用正确的重写,这不一定是实例参数的地址。当涉及多重继承时,这变得更加复杂。指向成员函数的指针通常包含比单个地址更多的信息。
如果在语言实现中检查指向成员函数的指针sizeof
,则可能会发现它大于void*
的大小。转换为void*
的任何内容都必须可转换回相同的值,当原始类型的状态大于void*
可以表示的状态时,这是不可能的。
旁注 1:从指针到函数再到void*
的转换是一项有条件支持的功能,因此在所有(旧的、深奥的?)语言实现上也可能无法实现。
旁注2:
std::cout << (size_t)&function << 'n';
这不仅依赖于上述条件特征,而且还依赖于从void*
到std::size_t
的转换。这也不能保证。如果要将指针转换为整数,应改用std::uintptr_t
。
旁注3:
void object::* ptr = &object::function;
这是格式不正确的。它看起来像一个指向类型void
的数据成员的指针,这当然是不允许的。我想你打算写:
void (object::* ptr)() = &object::function;
旁注4:
*(size_t*)&ptr
&ptr
指向类型为void (object::*)()
的对象,因此将其重新解释为size_t*
并通过重新解释的指针访问指向的对象会导致未定义的行为。不要在真正的程序中执行此操作。
但最终,该函数仍然位于特定的内存地址,就像其他所有函数一样。
假。(嗯,每个函数都有一个特定的地址是真的,但是对于指向成员函数的特定值,总是有一个"函数"的想法使这是错误的。
指向成员的指针不存储特定的内存地址。相反,它存储了一些可以与正确类型的对象组合以生成特定内存地址的东西。
例如,以下代码
#include <iostream>
// Define some types to use in the example
struct Base {
void function() { std::cout << "Functionn"; }
virtual void virtualFunction() { std::cout << "Basen"; }
};
struct Derived : Base {
void virtualFunction() override { std::cout << "Derivedn"; }
};
int main()
{
Base base;
Derived derived;
// Demonstrate pointing to a virtual function.
void (Base::*pointer)() = &Base::virtualFunction;
(base.*pointer)();
(derived.*pointer)();
// Demonstrate pointing to a non-virtual function.
pointer = &Base::function;
(base.*pointer)();
(derived.*pointer)();
}
生成以下输出
Base
Derived
Function
Function
调用变量pointer
的前两次,将调用两个不同的函数,即使pointer
没有更改。这表明指向成员函数的指针不能简单地保存单个函数的地址。它指向什么可能取决于它与之组合的对象。
此示例的第二部分是演示同一变量同样能够保存指向非虚函数的指针。相同的类型,甚至相同的变量,需要能够处理需要动态调度的情况和不需要动态调度的情况。仅仅存储非虚拟成员函数的地址是不够的,因为该值还必须包含"我不指向虚函数"的信息。
我倾向于将指向成员的指针视为偏移量。指向数据成员的指针可以实现为对象的偏移量,指向成员的指针函数可以作为偏移量实现到虚函数表中,其中 vtable 被视为已扩展以包含非虚拟成员函数。如果您有(地址)可接受类型的对象,则可以(由编译器)使用这些偏移量来获取特定地址。
请记住,将指向成员的指针视为偏移量只是一种方法(在处理成员模板时并不是特别好的方法)。其他方法也是可能的,特别是如果sizeof(pointer)
恰好是两倍sizeof(std::ptrdiff_t)
。因此,不要试图根据这种观点得出结论,某些事情必须发生。