问题
知乎网友问:在C++中函数指针有什么作用呢,为什么不直接调用函数而要使用函数指针?
南老师回答
函数指针让“函数”可以是一个“变量”。
通常我们认为一件事由“动作”和“数据”组成,比如“小明泡妞”中,小明是数据,妞是数据,泡是动作。动作和数据边界清晰,各自含义也直观。
把动作数据化,真的是各种编程语言一路下来心心念念的要有各种实现各种改进的念想。就以典型面向过程式的C语言为例:函数指针在C语言中就存在;并且相比支持面向对象的其它语言,比如C++ ,函数指针在C语言更加广泛地应用;地位很高。用不好甚至不会用函数指针的C程序员,基本就是“雏”。
渣男泡妞的例子
事实上在现实的生活经验中,人类就有大量需要"动作数据化"的时候。
以“小明泡妞“为例。小明不是雏,相反,他是渣:他同时向5个女生发起约会;这个5个女生和他的亲近关系各不相同,以小红和小白为例:小红每次吃饭都拿脚在餐桌下勾小明……的拖鞋;小白则只要小明盯她超过0.05秒就会甩一巴掌大骂“流氓”。
所以,同样是“泡”由于泡的对象不同,导致五种“泡”的动作包含的实质内容,天差地别,近乎没有共性——尽管从外部看,它们都是一类操作:泡妞。
五个几乎没有共性的操作,你是要分5个函数各自写,还是揉入1个函数用一堆if/else来判断,区分?显然是个正常人都会选前者。于是就有:
void PaoA(char const* name) { cout << "和" << name << "看文艺电影" << endl; }
void PaoB(char const* name) { cout << "陪" << name << "逛街购物,我花钱" << endl; }
void PaoC(char const* name) { cout << name << ",走,到军博看大炮去!" << endl; }
void PaoD(char const* name) { cout << name << ",我们来谈人生好吗?" << endl; }
void PaoE(char const* name) { cout << name << ",川菜还是撸串?" << endl; }
你以为我们是讲多态——小明依据最终成功约到的女生不同,执行不同的Pao行为?
不是不是,我想说的是小明把5位女生都成功约好,并且时间安排得非常合理,于是这个周末,小明要做的,就是逐个调用一遍就对了!
逐个调用?现在有两种做法,一个是:
void WeekendDate()
{
PaoA("丁晓晓");
PaoB("贾玲玲");
PaoC("林子绿");
PaoD("王梦");
PaoE("水原希");
}
这就是你说的“直接调用函数”——但别忘了我们的关键业务需求:小明之渣。小明下周可能又约另外5个甚至6个;并且约的对象也在变;约到后泡的操作也会变……如此,就会想到最好还是做两个数组,一个存储女生(的姓名),一个存储对应的“泡”法:
#include <iostream>
using namespace std;
void PaoA(char const* name) { cout << "和" << name << "看文艺电影" << endl; }
void PaoB(char const* name) { cout << "陪" << name << "逛街购物,我花钱" << endl; }
void PaoC(char const* name) { cout << name << ",走,到军博看大炮去!" << endl; }
void PaoD(char const* name) { cout << name << ",我们来谈人生好吗?" << endl; }
void PaoE(char const* name) { cout << name << ",川菜还是撸串?" << endl; }
char const* names [] =
{
"丁晓晓", "贾玲玲", "林子绿", "王梦", "水原希"
};
using F = void (*)(char const*);
F functions [] =
{
PaoA, PaoB, PaoC, PaoD, PaoE
};
void WeekendDate()
{
for (auto i=0; i<sizeof(names)/sizeof(names[0]); ++i)
{
functions[i](names[i]);
}
}
int main()
{
WeekendDate();
}
以上代码100%可直接跑,输出如下:
和丁晓晓看文艺电影 陪贾玲玲逛街购物,我花钱 林子绿,走,到军博看大炮去! 王梦,我们来谈人生好吗? 水原希,川菜还是撸串?
下周,再下周……当约的人有变化,我们就修改names数组,再对应修改每个人名对应可执行的动作(也已经是数组)就好了;至于那个 “WeekendDate”所要表达的周末约会都不用变啦……
渣中之渣:并发泡妞可以吗?
事主小明表示担心:再下下周,同样是5个人,同样是周末,同样是约会,但我想开群体活动,五个一起来!
多大的事,别说理论上的"同时",就是真要多线程并发(小明有四个分身)约会也没问题啊!事实上业务需求越复杂,往往代表数据之间的关系或耦合也越紧密,此时names和 functions这样的数据化设计,也就越能体现它们的正确性。以并发为例:
#include <iostream>
#include <thread>
using namespace std;
void PaoA(char const* name) { cout << "和" << name << "看文艺电影" << endl; }
void PaoB(char const* name) { cout << "陪" << name << "逛街购物,我花钱" << endl; }
void PaoC(char const* name) { cout << name << ",走,到军博看大炮去!" << endl; }
void PaoD(char const* name) { cout << name << ",我们来谈人生好吗?" << endl; }
void PaoE(char const* name) { cout << name << ",川菜还是撸串?" << endl; }
using F = void (*)(char const*);
F functions [] =
{
PaoA, PaoB, PaoC, PaoD, PaoE
};
char const* names [] =
{
"丁晓晓", "贾玲玲", "林子绿", "王梦", "水原希"
};
constexpr std::size_t N = sizeof(names)/sizeof(names[0]);
void WeekendDate()
{
for (auto i=0; i<N; ++i)
{
functions[i](names[i]);
}
}
void CrazyWeekendDate()
{
std::thread trds [N];
for (std::size_t i=0; i<N; ++i)
{
auto trd = std::thread([i]
{
functions[i](names[i]);
});
trds[i] = std::move(trd);
}
for (auto& trd: trds)
{
trd.join();
}
}
int main()
{
std::cout << "辛苦的周末:\n";
WeekendDate();
std::cout << "\n";
std::cout << "混乱的周末:\n";
CrazyWeekendDate();
}
运行后的一种输出可能是:
辛苦的周末: 和丁晓晓看文艺电影 陪贾玲玲逛街购物,我花钱 林子绿,走,到军博看大炮去! 王梦,我们来谈人生好吗? 水原希,川菜还是撸串? 混乱的周末: 水原希,川菜还是撸串? 王梦,我们来谈人生好吗? 林子绿,走,到军博看大炮去! 陪贾玲玲逛街购物,我花钱 和丁晓晓看文艺电影
如果改回“直 接 调 用”,那个线程创建的过程,大概是这样的:
auto trd = std::thread([i]
{
if (names[i] == std::string("丁晓晓"))
{
paoA(names[i);
}
else if (names[i] == std::string("贾玲玲"))
{
paoB(names[i);
}
else if (names[i] == std::string("林子绿"))
{
paoC(names[i);
}
else if (...)
{
....
}
});
或者改用判断 “i”的值——但那样会更不直观……
小结
1 解决问题,若不能依赖于(优雅的)数据结构, 那就得依赖于(粗鄙的)流程结构。
2 函数通常使用流程结构来组织,一旦函数可以数据化,就能让数据结构也过来帮忙表达复杂的操作。
因此,多数语言以及多数有品的程序员,都希望有将“动作”数据化的能力。更多这方面的讨论,有需要的请阅读《白话C++》。
你说在“混乱的周末”里,没有对cout 对象加锁,屏幕输出可能会混杂在一起——在本例中,这是特性,而不是bug;不信你自己实践一下,同时带5个女人出门试试!
“你们一个接一个的说话,避免并发冲突”
“干嘛要这样有规矩,我们就喜欢七嘴八舌。”