函数 Function与函数指针
[TOC]
1. 函数指针
1.1 含义
函数指针(Function Pointer)用于实现动态函数调用、回调函数、函数指针数组。
普通函数指针:
1
2
3
4
5
int normal_func(int x) { /* ... */ }
// 它的类型是:int (*)(int)
// 可以直接用函数指针存储:
int (*func_ptr)(int) = &normal_func;
func_ptr(10); // 直接调用
成员函数指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
public:
void member_func(int x) { /* ... */ }
};
void main() {
// 成员函数指针
void (MyClass::*mem_ptr)(int) = &MyClass::member_func;
// 调用时必须绑定一个对象:
MyClass obj;
(obj.*mem_ptr)(10); // 必须用 .* 或 ->* 语法,非常反直觉
}
函数指针与 void* 互相转换:
- 函数指针 =>
void*1 2
int (*funcPtr)(int) = Func; // Func为符合该类型的函数 void* pTemp = reinterpret_cast<void*>(funcPtr);
void*=> 函数指针1
int (*funcPtr)(int) =reinterpret_cast<int(*)(int)>(pTemp);
1.2 动态函数调用
函数指针是指向函数的指针。它可以用来存储函数的地址,然后通过这个指针调用函数。这在实现回调函数或函数表(如在策略模式中)时非常有用。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
void HelloWorld() {
std::cout << "Hello, World!" << std::endl;
}
int Add(int a, int b) {
return a + b;
}
int main() {
// 函数指针 fp 指向 HelloWorld 函数
void (*fp)() = HelloWorld;
fp(); // 通过函数指针调用 HelloWorld
// 函数指针 fp2 指向 Add 函数
int (*fp2)(int, int) = Add;
int result = fp2(3, 4); // 通过函数指针调用 Add
std::cout << "Result: " << result << std::endl;
return 0;
}
在这个例子中,fp 是一个指向 HelloWorld 函数的指针,而 fp2 是一个指向 Add 函数的指针。
1.3 回调函数
回调函数(Callback Function)是一个 通过函数指针传递给另一个函数的函数。它允许在一个函数内部调用另一个函数。回调函数是一种实现反向控制的技术,通常用于响应某些事件或处理异步操作。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
void ProcessData(void (*callback)()) {
// ... 处理一些数据 ...
callback(); // 调用回调函数
}
void OnDataProcessed() {
std::cout << "Data processed." << std::endl;
}
int main() {
ProcessData(OnDataProcessed); // 将 OnDataProcessed 作为回调函数传递
return 0;
}
在这个例子中,OnDataProcessed 是一个回调函数,它被传递给 ProcessData 函数,并在 ProcessData 内部被调用。
另外,对于回调函数,也可以使用std::function来进行包装使用,具体例子见对应的文章。
1.4 函数指针数组
函数指针数组,就是函数指针的数组,就是一个存放函数指针的数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int Add(int x, int y){// 加法
return x + y;
}
int Sub(int x, int y){// 减法
return x - y;
}
int Che(int x, int y){// 乘法
return x * y;
}
int Div(int x, int y){// 除法
return x / y;
}
int main()
{
// 创建一个函数指针数组,将所有的算数函数放入数组:
int (*p[4])() = {Add,Sub,Che,Div};
int x = 3, y = 23;
for(int i = 0; i < 4; i++)
{
p[i](x, y); // 调用算数函数
}
return 1;
}
2. std::function
std::function是C++11标准库中增加的一个非常有用的工具。它提供了一种通用的、类型安全的函数包装器,可以容纳各种可调用对象,并在需要时进行调用。1
std::function位于<functional>头文件中。它是对C++语言中函数类型的通用封装,类似于函数指针,提供了一种机制来存储、传递和调用各种可调用对象(函数指针、函数对象、Lambda表达式、成员函数指针等)。通过使用std::function,我们可以将函数作为一种数据类型来处理,使得代码更加灵活和可扩展。
2.1 主要特性
- 函数签名和调用方式的灵活性:
std::function可以存储各种不同函数类型的对象,只要它们的参数类型和返回类型与std::function的模板参数匹配。这包括函数指针、函数对象、Lambda表达式、成员函数指针等。只要函数的签名(参数类型和返回类型)匹配,std::function可以用来存储和调用它们。 - 类型安全:
std::function提供了编译时类型检查,可以避免在运行时发生类型错误。如果试图将不兼容的函数或可调用对象赋值给std::function,编译器将抛出错误。 - 函数对象的拷贝和移动:
std::function可以拷贝和移动函数对象。这意味着我们可以在程序中传递和存储std::function对象,而不必担心对象的生命周期和所有权。 - 函数调用:通过使用
()运算符,我们可以调用存储在std::function中的函数对象,就像直接调用函数一样。std::function会根据存储的函数对象的类型自动调用正确的函数。
2.2 调用成员函数
下面是一个完整的例子,将成员函数的指针作为回调函数传递给另一个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <functional>
// 在 MyClass 类中定义成员函数
class MyClass{
public:
void callback(){
std::cout << "callback called" << std::endl;
}
};
void SomeFunction(std::function<void()> func){
// 调用回调函数
func();
}
// 调用 SomeFunction,将成员函数作为回调函数传递
void main() {
std::function<void()> CallFunc = std::bind(&MyClass::callback, this);
SomeFunction(CallFunc);
}
2.3 调用其他函数
下面是一个简单的示例,展示了如何使用std::function来存储和调用不同类型的可调用对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <functional>
int add(int a, int b){
return a + b;
}
struct Multiply{
int operator()(int a, int b){
return a * b;
}
};
int main(){
std::function<int(int, int)> func;
func = add; // 全局函数
std::cout << "Addition result: " << func(5, 3) << std::endl;
Multiply multiply;
func = multiply; // 函数对象
std::cout << "Multiplication result: " << func(5, 3) << std::endl;
auto lambda = [](int a, int b) { return a - b; };
func = lambda; // Lamda函数
std::cout << "Subtraction result: " << func(5, 3) << std::endl;
return 0;
}
2.4 std::function与函数指针的联系
std::function和函数指针都用于存储可调用对象(函数、函数对象、成员函数等),但它们有一些区别和联系。
- 区别:
- 类型灵活:
std::function可以用于存储任意可调用对象,包括函数指针、函数对象、成员函数指针、lambda表达式等,并提供一致的调用方法;而函数指针只能存储函数指针类型(一个指向函数的地址)。 - 对象管理:
std::function可以拷贝和移动,可以作为函数参数或返回值传递,而函数指针只是一个指向函数的指针,不能拷贝或移动。 - 多态支持:
std::function可以用于实现多态调用,即可以将不同的函数对象存储在同一个std::function对象中,并通过相同的调用方式来调用它们。函数指针不提供多态支持。
- 类型灵活:
- 联系:
- 调用:
std::function和函数指针可以通过调用运算符()来调用所存储的函数或函数对象。 - 转换:可以将函数指针转换为
std::function对象进行存储和调用。
- 调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <functional>
void foo() {
std::cout << "foo" << std::endl;
}
void bar(int x) {
std::cout << "bar: " << x << std::endl;
}
int main() {
std::function<void()> func1 = foo;
func1(); // 调用 foo
std::function<void(int)> func2 = bar;
func2(42); // 调用 bar
void (*funcPtr)() = foo;
funcPtr(); // 调用 foo,通过函数指针调用
return 0;
}
在上面的示例中,我们定义了两个函数foo和bar,分别无参和带一个整型参数。然后,我们使用std::function分别创建了存储无参函数和带参函数的对象,并通过调用运算符 () 调用它们。同时,我们也定义了一个函数指针funcPtr,并通过函数指针调用foo。
3. std::bind
当您需要将一个函数与其参数绑定以创建一个可调用的对象时(也就是一个std::function的对象),C++11标准库提供了std::bind函数模板。std::bind允许您将函数的一部分参数绑定,从而创建一个新的可调用对象,这个对象可以在稍后的时候以指定的参数来执行函数。1
3.1 std::bind的基本语法
1
auto boundFunc = std::bind(func, arg1, arg2, ...);
其中,func是待绑定的函数,arg1、arg2等是需要绑定的参数。
3.2 绑定普通函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <functional>
void printSum(int a, int b) {
std::cout << a + b << std::endl;
}
int main() {
// 下面auto的类型对应于std::function<void(int, int)>
auto boundFunc = std::bind(printSum, 10, std::placeholders::_1);
boundFunc(5); // 输出 15
return 0;
}
在这个示例中,我们绑定了函数printSum的第一个参数为10,而第二个参数使用占位符std::placeholders::_1表示稍后提供。然后,我们调用boundFunc并提供第二个参数为5,输出了15。
3.3 绑定成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <functional>
class MyClass {
public:
void printMessage(const std::string& message) {
std::cout << "Message: " << message << std::endl;
}
};
int main() {
MyClass obj;
auto boundFunc = std::bind(&MyClass::printMessage, &obj, std::placeholders::_1);
boundFunc("Hello"); // 输出 "Message: Hello"
return 0;
}
在这个示例中,我们定义了一个MyClass类,并创建了一个MyClass的对象obj。然后,我们使用std::bind绑定了obj的成员函数printMessage,并将obj作为第一个参数传递给std::bind。通过占位符std::placeholders::_1表示稍后提供的参数。最后,我们调用boundFunc并提供参数为 “Hello”,输出了 “Message: Hello”。
3.4 绑定函数对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <functional>
struct Adder {
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Adder add;
auto boundFunc = std::bind(add, std::placeholders::_1, 5);
int result = boundFunc(10); // result 的值为 15
std::cout << result << std::endl;
return 0;
}
在这个示例中,我们定义了一个函数对象Adder,它重载了函数调用运算符operator()用于实现加法操作。然后,我们创建了一个Adder对象add,并使用std::bind绑定了add的函数调用运算符,将std::placeholders::_1作为第一个参数,5作为第二个参数。最后,我们调用boundFunc并提供参数为10,得到了结果15。
需要注意的是,std::bind还支持其他一些特性,例如参数重排、常量绑定、函数指针绑定等,可以根据实际需求使用。此外,绑定的参数可以是左值引用或右值引用,也可以是std::cref或std::ref等引用包装器。更多关于std::bind的详细信息可以参考C++的相关文档和教程。
4. std::ref
std::ref 是 C++11 标准中引入的一个函数模板,用于 将一个对象包装为一个引用。它可以让我们将一个对象作为引用传递给函数或算法,而不是按值传递,以实现对原始对象的直接修改。它在需要引用传递的情况下非常有用。1
std::ref 的作用是 创建一个可以传递给接受引用参数的函数或算法的可调用对象。通过使用 std::ref,我们可以将对象传递给接受引用参数的函数,并确保在函数内部对该对象的操作能够影响到原始对象,而不是创建对象的副本。
4.1 常用于以下情况
- 作为 函数或算法 的参数传递:当函数或算法接受引用作为参数时,使用
std::ref可以将对象包装为引用,并将其传递给函数或算法。这样,函数或算法就能够直接修改原始对象,而不是创建对象的副本。 - 作为 线程 的参数传递:如果您使用线程库创建线程,并且想要在线程执行的函数中对某个对象进行修改,您可以使用
std::ref将对象作为引用传递给线程的函数。这样,线程函数就能够直接修改原始对象,而不是创建副本。 - 作为 容器 元素的引用:当您将对象存储在容器(如
std::vector或std::list)中,并且希望通过容器访问和修改对象时,可以使用std::ref将对象包装为引用,并将其添加到容器中。这样,容器中存储的是对象的引用,对容器中的引用进行操作会直接影响原始对象。
4.2 用法示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 需要多线程做的函数
void func(std::atomic<int>& progress, std::mutex& mutex)
{
std::lock_guard<std::mutex> lock(mutex);
++progress;
/*做点儿啥*/
}
int main()
{
std::atomic<int> progress(0);
std::mutex mutex;
std::jthread Thread(func, std::ref(progress), std::ref(mutex));
return 0;
}
上面的实例中,为了保证能将引用的参数正确传递给线程函数,使用了ref()函数。
5. TKClassFunctor
详细代码见本地文件《TKClassFunctor.h》
TKClassFunctor.h 是一个 C++ 03 风格 的早期仿函数实现,利用多态 + 模板实现了一个 轻量级的 C++ 成员函数绑定器(Member Function Functor),用于解决成员函数回调问题。
它的核心作用是将 类的成员函数 和 类的实例对象绑定在一起,封装成一个可以像普通函数一样调用的 “仿函数对象”(Functor)。
但在现代 C++ 工程中,这个模板类由于 缺乏内存管理机制 且 功能受限(仅支持1-2个参数),不建议直接在生产环境使用,推荐使用标准库的 std::function。
TKClassFunctor 解决了什么问题
具体解决了什么
TKClassFunctor 做了一个非常经典的 “类型擦除 + 封装” 工作:
- 封装数据:它把「成员函数指针」和「对象指针」这两样东西强行绑在了一个类对象里(
function_impl)。 - 统一接口:它用一个抽象基类(
function_base)定义了统一的operator()接口。 - 隐藏复杂性:对外只暴露基类指针,让你可以像用普通函数指针一样用它。
简单来说:它把丑陋的 (obj.*mem_ptr)(arg) 包装成了漂亮的 (*func_ptr)(arg)。
除了 “记录指针”,还解决了什么附带问题
虽然核心是为了 “记录”,但它顺便解决了传递和调用的问题:
- 可以作为参数传递:你可以把
function_base*传给一个普通函数,实现回调机制(这在 C++03 时代是刚需)。 - 可以存入容器:你可以创建一个
std::vector<function_base*>,把一堆不同类的成员函数放在同一个列表里。
总结
TKClassFunctor 就是一个 C++03 时代的 “补丁”,专门用来填补「成员函数指针不好用」这个坑。只不过在 C++11 引入 std::function 和 Lambda 之后,这个补丁就被标准库收编并做得更好了。
代码逻辑详细分析
这段代码通过 C++ 模板和多态实现了类型擦除。它分为两个版本:
- 基础版:支持 1 个参数的成员函数。
- 扩展版 (
_ex):支持 2 个参数的成员函数。
我们以基础版为例拆解核心组件:
1. function_base:抽象接口定义
1
2
3
4
5
6
template<class ret_type, class arg_type>
class function_base {
public:
// ...
virtual ret_type operator() (arg_type) = 0; // 纯虚函数
};
- 作用:定义了一个 “调用接口”。它是一个抽象基类,只规定了 “这个对象必须能像函数一样调用(
operator())”。 - 意义:利用 多态,使得后续我们可以用
function_base*指针来指向任意具体的实现,而不用关心具体是哪个类的成员函数。
2. function_impl:具体实现包装器
1
2
3
4
5
6
7
8
9
10
11
12
13
template<class CS, class ret_type, class arg_type>
class function_impl : public function_base<ret_type, arg_type> {
public:
typedef ret_type (CS::* PROC)(arg_type); // 定义成员函数指针类型
// 构造函数:保存【对象指针】和【成员函数指针】
function_impl(CS* obj, PROC proc): obj_(obj), proc_(proc) { }
// 重载调用运算符:执行真正的成员函数调用
ret_type operator() (arg_type arg) {
return (obj_->*proc_)(arg);
}
};
- 作用:这是核心实现。它把“对象指针”(
obj_)和“成员函数指针”(proc_)绑在了一起。 - 关键点:
(obj_->*proc_)(arg)是 C++ 中通过成员函数指针调用成员函数的标准语法。
3. bind:工厂函数
1
2
3
4
template<class CS, class ret_type, class arg_type>
function_base<ret_type, arg_type>* bind(ret_type (CS::* proc)(arg_type), CS* pc) {
return new function_impl<CS, ret_type, arg_type>(pc, proc);
}
- 作用:为了方便使用而提供的辅助函数。
- 用法:你不需要手动写
new function_impl<...>(...),只需要传 成员函数地址 和 对象地址,它会自动帮你 new 出一个对象并返回基类指针。
4. function_base_ex / function_impl_ex / bind_ex
- 逻辑与上面完全一致,只是支持传入两个参数的成员函数。
使用场景
这段代码主要用于 回调机制 (Callback)。
在 C++ 中,普通的函数指针很难直接指向一个类的非静态成员函数(因为非静态成员函数需要一个 this 指针才能执行)。
典型应用场景:
- 事件处理:比如 GUI 编程中,“当按钮被点击时,调用某个类的
OnClick方法”。 - 异步任务:“当网络数据接收完毕时,调用 Controller 类的
ProcessData方法”。 - 算法策略:将不同类的处理方法作为策略传入算法中。
代码使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
// 假设上面的头文件叫 "tkclassfunctor.h"
#include "tkclassfunctor.h"
class Printer {
public:
void print(int num) {
std::cout << "Printer Output: " << num << std::endl;
}
};
int main() {
// 1. 创建对象
Printer my_printer;
// 2. 绑定成员函数
// 这就生成了一个仿函数指针,它封装了 my_printer 对象和 print 方法
tkclsfunctor::function_base<void, int>* func =
tkclsfunctor::bind(&Printer::print, &my_printer);
// 3. 像普通函数一样调用!
(*func)(100); // 输出: Printer Output: 100
// 4. 清理内存 (下面会讲)
delete func;
return 0;
}
内存泄漏分析
这是一个设计上的明显缺陷,这个类很容易造成内存泄漏。
在实际项目 TKTGPLoadService 中,泳道在使用这个方法时,就很明显没有考虑到内存泄漏的风险。
原因分析:
-
显式 New,无显式 Delete:
bind函数内部使用了new来分配内存。但是这个类并没有提供对应的delete机制,也没有使用 RAII(智能指针)。 -
所有权不清晰:
函数返回一个裸指针(Raw Pointer),调用者很容易忘记这是一个需要手动管理的资源。
什么情况下会泄漏
- 正常使用忘记 delete:就像上面的例子,如果不写
delete func;,内存直接泄漏。 - 异常安全:如果在
new之后、delete之前发生了异常,程序跳转导致delete没执行到。
如何改进
如果要在现代 C++ 中使用或改进这段代码,建议:
-
使用
std::unique_ptr管理返回值:修改
bind函数,不再返回裸指针,而是返回std::unique_ptr<function_base<...>>。 -
或者参考标准库:
实际上,C++11 引入的
std::function和std::bind已经完美解决了这个问题,而且功能更强大(支持 lambda、普通函数、任意参数数量)。
使用 function 代替
以下是完整的示例,展示了如何用标准库替代手写 Functor 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <functional> // 核心头文件:包含 std::function
// 1. 定义一个测试用的类(与之前的例子保持一致)
class Printer
{
public:
// 对应原代码中的 bind (1个参数)
void print(int num)
{
std::cout << "[Printer] 单参数输出: " << num << std::endl;
}
// 对应原代码中的 bind_ex (2个参数)
void print_ex(int num, const std::string& msg)
{
std::cout << "[Printer] 双参数输出: " << msg << " -> " << num << std::endl;
}
};
int main()
{
Printer my_printer; // 创建对象实例
// ==========================================
// 方案一:使用 Lambda 表达式(推荐,最灵活)
// ==========================================
std::cout << "--- 方案 A: Lambda 表达式 ---" << std::endl;
// 1. 绑定单参数成员函数
// std::function<返回值(参数类型)>
std::function<void(int)> func_a1 = [&my_printer](int n) {
my_printer.print(n);
};
func_a1(100);
// 2. 绑定双参数成员函数
std::function<void(int, const std::string&)> func_a2 =
[&my_printer](int n, const std::string& s) {
my_printer.print_ex(n, s);
};
func_a2(200, "Hello C++17");
// ==========================================
// 方案二:使用 std::bind + C++17 CTAD
// ==========================================
std::cout << "\n--- 方案 B: std::bind ---" << std::endl;
// 1. 绑定单参数
// 注意:C++17 的 CTAD 可以自动推导 std::function 的模板参数,
// 不需要写 std::function<void(int)>,直接写 std::function 即可。
std::function func_b1 = std::bind(
&Printer::print, // 成员函数指针
&my_printer, // 对象指针
std::placeholders::_1 // 占位符,表示第一个参数
);
func_b1(300);
// 2. 绑定双参数
std::function func_b2 = std::bind(
&Printer::print_ex,
&my_printer,
std::placeholders::_1, // 对应 print_ex 的第1个参数
std::placeholders::_2 // 对应 print_ex 的第2个参数
);
func_b2(400, "Using Bind");
return 0;
}
代码详解
为什么推荐 Lambda 而不是 std::bind
虽然上面提供了两种写法,但在现代 C++ 开发中,Lambda 表达式通常是首选:
- 可读性:Lambda 的代码逻辑一目了然,而
bind使用_1,_2占位符,代码读起来像“谜语”。 - 调试性:编译器对 Lambda 的优化通常更好,报错信息更清晰。
- 灵活性:Lambda 可以在捕获时做更多逻辑(比如加个判断、修改参数等),不需要重新写一个函数。
内存安全如何保证
在原代码中,需要手动 delete 那个返回的指针。
在现代方案中:
std::function是在栈上声明的对象(或者作为类成员存储)。- 当它离开作用域时,会自动调用析构函数,清理内部捕获的资源(如 Lambda 捕获的变量)。
- 完全不需要手写
delete,从根源上杜绝了内存泄漏。