我一直在讨论与previous question相关的成员函数指针。在下面的代码中,我调用类(B)上的方法来更改其中的变量(count),但我从不创建此类的实例。为什么这样做?
#include <iostream>
#include <string>
#include <map>
class A;
typedef int (A::*MEMFUNC)(int, int);
#define HANDLER(aclass, aproc) (MEMFUNC)(&aclass::aproc)
enum
{
ADD=1,
MUL,
SUB,
DIV
};
class B
{
int count;
public:
B() : count(0) {}
~B() {}
int multiply(int x, int y) { count++; return x*y*count; }
int divide(int x, int y) { count++; if (y!=0) return (x/y)*count; else return 0; }
};
class A
{
std::map< int, MEMFUNC > funcs;
public:
A() { AddLocals(); }
~A() {}
int CallLocal(int nID, int x, int y)
{
MEMFUNC f = funcs[nID];
if (f) return (this->*f)(x, y);
else return 0;
}
void AddLocals()
{
Add(ADD, HANDLER(A, plus));
Add(MUL, HANDLER(B, multiply));
Add(SUB, HANDLER(A, subtract));
Add(DIV, HANDLER(B, divide));
}
void Add(int nID, MEMFUNC f) { funcs[nID] = f; }
int plus(int x, int y) { return x+y; }
int subtract(int x, int y) { return x-y; }
};
int main()
{
A aA;
int a,b,c,d;
a = aA.CallLocal(ADD,8,2);
b = aA.CallLocal(MUL,8,2);
c = aA.CallLocal(SUB,8,2);
d = aA.CallLocal(DIV,8,2);
std::cout << "a = " << a << "\n"
<< "b = " << b << "\n"
<< "c = " << c << "\n"
<< "d = " << d << "\n";
return 0;
}
(对不起,我再次,但是这个成员函数指针让我发痒)
答案 0 :(得分:8)
你在HANDLER
宏def中的演员告诉编译器“闭嘴!我知道我在做什么!”。
所以编译器关闭了。
你仍然有未定义的行为,但UB的一个属性是,在某些情况下,它会做你天真期望的,或者你想要它做什么。
但是,如果这些代码崩溃,或者在显然完全不相关的代码中导致崩溃或神秘的错误结果,请不要感到惊讶。
或者,例如,导致鼻子恶魔飞出你的鼻子。
干杯&amp;第h
答案 1 :(得分:1)
C-casting可以让你摆脱各种可怕的行为,但并不意味着可以这样做,所以根本不这样做。
彻底摆脱你的宏,不要施放。你可以使用boost :: function和boost :: bind来获得你真正想要的行为。
答案 2 :(得分:1)
结果只是未定义的行为。例如,我得到b = 2083899728
和d = -552766888
。
你正在操作的持久性事物很可能是A的映射实例中int的字节值(因为如果对象确实是B,那么这就是count
成员所在的偏移量。 / p>
在我的stdlib实现中,map的第一个成员是比较函数,在本例中是std::less<int>
的实例。它的大小为1,但之后必须有未使用的填充字节以对齐映射的其他成员。也就是说,(至少)std::map
的这个实例化的前四个字节只包含没有用于任何东西的垃圾(std :: less没有数据成员且不存储状态,它只需要地图中的空间)。这可以解释为什么代码不会崩溃 - 它正在修改地图实例的一部分,这不会影响地图的功能。
在count
之前在B中添加更多数据成员,现在count++
将影响地图内部表示的关键部分,您可能会崩溃。
答案 3 :(得分:1)
您的代码通过尝试使用类A的对象调用类B的成员来调用未定义的行为。 我们可以尝试解释编译器如何处理您观察到的行为,但如果您更改任何内容(添加/删除成员,更改编译器设置或使用其他编译器),则无法保证您将获得相同的行为。
使用HANDLER
宏中的强制转换,您告诉编译器不要警告您使用不兼容的类型,而只是按照您的说法进行操作。在这种情况下,您告诉编译器将任何类的成员的地址重新解释为A类成员的地址。
当您稍后尝试调用B::multiply
时,该函数不知道它没有处理类B的对象,所以它会很高兴地破坏aA
的字节如果它是B::count
对象,它将对应B
成员。最有可能的是,A::funcs
实际上正在使用这些字节,但显然不是任何关键的。如果您将A类更改为:
class A
{
int count;
std::map< int, MEMFUNC > funcs;
public:
A() : count(0) { AddLocals(); }
~A() {}
int CallLocal(int nID, int x, int y)
{
MEMFUNC f = funcs[nID];
if (f) return (this->*f)(x, y);
else return 0;
}
int Count()
{
return count;
}
void AddLocals()
{
Add(ADD, HANDLER(A, plus));
Add(MUL, HANDLER(B, multiply));
Add(SUB, HANDLER(A, subtract));
Add(DIV, HANDLER(B, divide));
}
void Add(int nID, MEMFUNC f) { funcs[nID] = f; }
int plus(int x, int y) { return x+y; }
int subtract(int x, int y) { return x-y; }
};
然后在各个地方打印aA.Count()
的结果可能会显示效果。
编译器正在调用期望的函数,因为它们是非虚拟成员函数
非成员函数和非虚成员函数之间的唯一区别在于隐藏参数,该参数在成员函数中提供this
指针。因此,如果您获取非虚拟成员函数的地址,您将获得一个与每个函数不同的固定地址
如果成员函数是虚拟的,那么编译器很可能已经将索引作为该函数的指针返回到v表中(以及某种表示它是v表偏移的指示)。然后代码可以在调用站点确定它是否可以直接调用成员函数,或者是否需要通过调用函数的对象的v-table进行间接调用。