文章

函数 Function与函数指针

函数 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 主要特性

  1. 函数签名和调用方式的灵活性std::function可以存储各种不同函数类型的对象,只要它们的参数类型和返回类型与std::function的模板参数匹配。这包括函数指针、函数对象、Lambda表达式、成员函数指针等。只要函数的签名(参数类型和返回类型)匹配,std::function可以用来存储和调用它们。
  2. 类型安全std::function提供了编译时类型检查,可以避免在运行时发生类型错误。如果试图将不兼容的函数或可调用对象赋值给std::function,编译器将抛出错误。
  3. 函数对象的拷贝和移动std::function可以拷贝和移动函数对象。这意味着我们可以在程序中传递和存储std::function对象,而不必担心对象的生命周期和所有权。
  4. 函数调用:通过使用()运算符,我们可以调用存储在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和函数指针都用于存储可调用对象(函数、函数对象、成员函数等),但它们有一些区别和联系。

  • 区别:
    1. 类型灵活:std::function可以用于存储任意可调用对象,包括函数指针、函数对象、成员函数指针、lambda表达式等,并提供一致的调用方法;而函数指针只能存储函数指针类型(一个指向函数的地址)。
    2. 对象管理:std::function可以拷贝和移动,可以作为函数参数或返回值传递,而函数指针只是一个指向函数的指针,不能拷贝或移动。
    3. 多态支持:std::function可以用于实现多态调用,即可以将不同的函数对象存储在同一个std::function对象中,并通过相同的调用方式来调用它们。函数指针不提供多态支持。
  • 联系:
    1. 调用:std::function和函数指针可以通过调用运算符 () 来调用所存储的函数或函数对象。
    2. 转换:可以将函数指针转换为 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;
}

在上面的示例中,我们定义了两个函数foobar,分别无参和带一个整型参数。然后,我们使用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是待绑定的函数,arg1arg2等是需要绑定的参数。

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::crefstd::ref等引用包装器。更多关于std::bind的详细信息可以参考C++的相关文档和教程。

4. std::ref

std::ref 是 C++11 标准中引入的一个函数模板,用于 将一个对象包装为一个引用。它可以让我们将一个对象作为引用传递给函数或算法,而不是按值传递,以实现对原始对象的直接修改。它在需要引用传递的情况下非常有用。1

std::ref 的作用是 创建一个可以传递给接受引用参数的函数或算法的可调用对象。通过使用 std::ref,我们可以将对象传递给接受引用参数的函数,并确保在函数内部对该对象的操作能够影响到原始对象,而不是创建对象的副本。

4.1 常用于以下情况

  1. 作为 函数或算法 的参数传递:当函数或算法接受引用作为参数时,使用 std::ref 可以将对象包装为引用,并将其传递给函数或算法。这样,函数或算法就能够直接修改原始对象,而不是创建对象的副本。
  2. 作为 线程 的参数传递:如果您使用线程库创建线程,并且想要在线程执行的函数中对某个对象进行修改,您可以使用 std::ref 将对象作为引用传递给线程的函数。这样,线程函数就能够直接修改原始对象,而不是创建副本。
  3. 作为 容器 元素的引用:当您将对象存储在容器(如 std::vectorstd::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 做了一个非常经典的 “类型擦除 + 封装” 工作:

  1. 封装数据:它把「成员函数指针」和「对象指针」这两样东西强行绑在了一个类对象里(function_impl)。
  2. 统一接口:它用一个抽象基类(function_base)定义了统一的 operator() 接口。
  3. 隐藏复杂性:对外只暴露基类指针,让你可以像用普通函数指针一样用它。

简单来说:它把丑陋的 (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. 基础版:支持 1 个参数的成员函数。
  2. 扩展版 (_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 指针才能执行)。

典型应用场景:

  1. 事件处理:比如 GUI 编程中,“当按钮被点击时,调用某个类的 OnClick 方法”。
  2. 异步任务:“当网络数据接收完毕时,调用 Controller 类的 ProcessData 方法”。
  3. 算法策略:将不同类的处理方法作为策略传入算法中。

代码使用示例:

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 中,泳道在使用这个方法时,就很明显没有考虑到内存泄漏的风险。

原因分析:

  1. 显式 New,无显式 Delete

    bind 函数内部使用了 new 来分配内存。但是这个类并没有提供对应的 delete 机制,也没有使用 RAII(智能指针)。

  2. 所有权不清晰

    函数返回一个裸指针(Raw Pointer),调用者很容易忘记这是一个需要手动管理的资源。

什么情况下会泄漏

  • 正常使用忘记 delete:就像上面的例子,如果不写 delete func;,内存直接泄漏。
  • 异常安全:如果在 new 之后、delete 之前发生了异常,程序跳转导致 delete 没执行到。

如何改进

如果要在现代 C++ 中使用或改进这段代码,建议:

  1. 使用 std::unique_ptr 管理返回值

    修改 bind 函数,不再返回裸指针,而是返回 std::unique_ptr<function_base<...>>

  2. 或者参考标准库

    实际上,C++11 引入的 std::functionstd::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 那个返回的指针。

在现代方案中:

  1. std::function 是在上声明的对象(或者作为类成员存储)。
  2. 当它离开作用域时,会自动调用析构函数,清理内部捕获的资源(如 Lambda 捕获的变量)。
  3. 完全不需要手写 delete,从根源上杜绝了内存泄漏。

参考文章234

本文由作者按照 CC BY 4.0 进行授权