如果你正在寻找性能、复杂性,或者解决问题的多种可能方案,那么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 条评论