如果您正在寻找性能、复杂性或解决问题的多种可能方案,那么在极端情况下,C++ 始终是一个不错的选择。当然,功能通常伴随着复杂性,但一些 C++ 的特性几乎难以辨认。在我看来,C++ 方法指针 可能是我见过的最复杂的表达式,但我将从一些更简单的东西开始。
本文中的示例可在我的 GitHub 存储库 中找到。
C:指向函数的指针
让我们从一些基础知识开始:假设您有一个函数,它接受两个整数作为参数并返回一个整数
int sum(int a, intb){
return a+b;
}
在纯 C 语言中,您可以创建一个指向此函数的指针,将其分配给您的 sum(...)
函数,并通过解引用来调用它。函数的签名(参数、返回类型)必须与指针的签名一致。除此之外,函数指针的行为类似于普通指针
int (*funcPtrOne)(int, int);
funcPtrOne = ∑
int resultOne = funcPtrOne(2, 5);
如果您将指针作为参数并返回指针,则会变得更难看
int *next(int *arrayOfInt){
return ++arrayOfInt;
}
int *(*funcPtrTwo)(int *intPtr);
funcPtrTwo = &next;
int resultTwo = *funcPtrTwo(&array[0]);
C 语言中的函数指针存储子例程的地址。
指向方法的指针
让我们进入 C++:好消息是,您可能不需要使用指向方法的指针,除非在少数罕见的情况下,例如以下情况。首先,定义一个具有您已经知道的成员函数的类
class MyClass
{
public:
int sum(int a, int b) {
return a+b;
}
};
1. 定义一个指向特定类类型方法的指针
声明一个指向 MyClass
类型方法的指针。此时,您不知道要调用的确切方法。您只声明了一个指向某些任意 MyClass
方法的指针。当然,签名(参数、返回类型)与您稍后要调用的 sum(…)
方法匹配
int (MyClass::*methodPtrOne)(int, int);
2. 分配特定方法
与 C(或 静态成员函数)相反,方法指针不指向绝对地址。C++ 中的每个类类型都有一个虚拟方法表 (vtable),用于存储每个方法的地址偏移量。方法指针指向 vtable 中的特定条目,因此它也只存储偏移值。此原理也实现了动态调度。
由于 sum(…)
方法的签名与您的指针声明匹配,因此您可以将签名分配给它
methodPtrOne = &MyClass::sum;
3. 调用方法
如果您想使用指针调用方法,则必须提供类类型的实例
MyClass clsInstance;
int result = (clsInstance.*methodPtrOne)(2,3);
您可以使用 .
运算符访问实例,使用 *
解引用指针,从而通过提供两个整数作为参数来调用该方法。很丑陋,对吧?但您仍然可以更进一步。
在类中使用方法指针
假设您正在创建一个具有 客户端/服务器 原则架构的应用程序,其中包含后端和前端。您暂时不关心后端;相反,您将专注于前端,它基于 C++ 类。前端的完整初始化依赖于后端提供的数据,因此您需要额外的初始化机制。此外,您希望以通用方式实现此机制,以便将来可以使用其他初始化方法(可能是动态的)来扩展前端。
首先,定义一个数据类型,它可以存储指向初始化方法 (init
) 的方法指针以及描述何时应调用此方法的信息 (ticks
)
template<typename T>
struct DynamicInitCommand {
void (T::*init)(); // Pointer to additional initialization method
unsigned int ticks; // Number of ticks after init() is called
};
以下是 Frontend
类的外观
class Frontend
{
public:
Frontend(){
DynamicInitCommand<Frontend> init1, init2, init3;
init1 = { &Frontend::dynamicInit1, 5};
init2 = { &Frontend::dynamicInit2, 10};
init3 = { &Frontend::dynamicInit3, 15};
m_dynamicInit.push_back(init1);
m_dynamicInit.push_back(init2);
m_dynamicInit.push_back(init3);
}
void tick(){
std::cout << "tick: " << ++m_ticks << std::endl;
/* Check for delayed initializations */
std::vector<DynamicInitCommand<Frontend>>::iterator it = m_dynamicInit.begin();
while (it != m_dynamicInit.end()){
if (it->ticks < m_ticks){
if(it->init)
((*this).*(it->init))(); // here it is
it = m_dynamicInit.erase(it);
} else {
it++;
}
}
}
unsigned int m_ticks{0};
private:
void dynamicInit1(){
std::cout << "dynamicInit1 called" << std::endl;
};
void dynamicInit2(){
std::cout << "dynamicInit2 called" << std::endl;
}
void dynamicInit3(){
std::cout << "dynamicInit3 called" << std::endl;
}
unsigned int m_initCnt{0};
std::vector<DynamicInitCommand<Frontend> > m_dynamicInit;
};
在实例化 Frontend
后,后端会以固定的时间间隔调用 tick()
方法。例如,您可以每 200 毫秒调用一次
int main(int argc, char* argv[]){
Frontend frontendInstance;
while(true){
frontendInstance.tick(); // just for simulation purpose
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
}
Frontend
有三个额外的初始化方法,必须根据 m_ticks
的值来调用。关于在哪个 tick 调用哪个初始化方法的信息存储在向量 m_dynamicInit
中。在构造函数 (Frontend()
) 中,将此信息附加到向量,以便在五个、十个和十五个 tick 后调用其他初始化函数。当后端调用 tick()
方法时,值 m_ticks
会递增,并且您迭代向量 m_dynamicInit
以检查是否必须调用初始化方法。
如果是这种情况,则必须通过引用 this
来解引用方法指针
((*this).*(it->init))()
总结
如果您不熟悉方法指针,它们可能会变得有点复杂。我做了很多试验和错误,并且花了很多时间才找到正确的语法。但是,一旦您理解了一般原则,方法指针就不再那么可怕了。
这是迄今为止我在 C++ 中发现的最复杂的语法。您知道更糟糕的东西吗?请在评论中发布!
3 条评论