liangbm3's blog

Back

1. 函数指针#

函数指针就是一个指向函数的指针变量,函数是是存放在代码段的,函数指针指向的就是函数在代码段中的入口地址。通过函数指针,我们可以间接地调用这个函数。

语法:

返回类型 (*指针名称)(参数类型1, 参数类型2, ...);
cpp

假设我们有一个普通函数:

int add(int a, int b) 
{
    return a + b;
}
cpp

我们可以声明一个指向该函数的函数指针:

int (*p_func)(int, int);
p_func = &add; // & 是可选的,直接写函数名也可以
cpp

通过解引用函数指针,或者直接使用指针名,就可以调用它所指向的函数。

// 使用函数指针调用函数
int result1 = (*p_func)(10, 5); // 解引用方式 (C-style)
int result2 = p_func(10, 5);   // 直接使用 (更现代的 C++ 风格)
cpp

对于普通函数,add 是函数名,本质是一个标签,指向该函数的入口地址,是静态的,不可改变的。而函数指针是一个变量,它存储的函数的地址,可以通过这个指针去调用函数,是动态的,可以指向不同的函数。基于这个特性,函数指针通常的用途如下:

  • 回调函数 (Callbacks): 一个函数接受另一个函数作为参数,在适当的时候调用它。
    void performOperation(int x, int y, int (*op)(int, int)) 
    {
    std::cout << "Result: " << op(x, y) << std::endl;
    }
    
    // 调用
    performOperation(20, 10, &add);      // 传入 add 函数
    performOperation(20, 10, &subtract); // 传入另一个兼容的函数
    cpp
  • 实现函数表/跳转表 (Jump Tables): 可以用函数指针数组来代替一长串的 if-elseswitch-case 语句,根据输入选择要执行的函数。函数指针数组的声明语法如下:
    返回类型 (*数组名[数组大小])(参数列表);
    cpp
    这种声明形式比较晦涩难懂,我们可以使用typedefusing来进行简化:
    typedef int (*FuncPtr)(int, int);
    // 或 C++11 写法:using FuncPtr = int(*)(int, int);
    
    FuncPtr funcArr[3] = {add, sub, mul};
    cpp

函数指针是C语言中的用法,实现简单,调用的开销小。但是在C++中是不推荐使用函数指针的,因为函数指针既不能携带状态信息,也不能捕获或者存储任何上下文的信息。在C++中,通常使用的是函数对象。

2. 函数对象#

函数对象又叫仿函数,本质上是一个类或者结构体的实例,由于它重载了operator(),因此我们可以像调用普通函数一样使用这个对象。

一个示例如下:

class Adder 
{
public:
    int operator()(int a, int b) const 
    {
        return a + b;
    }
};
Adder add; // 创建一个 Adder 对象
int sum1 = add(5, 3);       // 像调用函数一样调用对象 add
// 也可以直接创建临时对象并调用
int sum2 = Adder()(10, 20);
cpp

函数对象的核心优势在于可以拥有独立的状态,而函数指针和普通函数不具备这个能力。例如:

#include <iostream>
using namespace std;

class Counter 
{
    int count = 0;  // 每个对象独有的状态
public:
    int operator()(int x) {
        return ++count + x;
    }
};

int main() 
{
    Counter c1, c2;
    cout << c1(5) << endl;  // count = 1
    cout << c1(5) << endl;  // count = 2
    cout << c2(5) << endl;  // c2独立,count = 1
}
cpp

函数模板允许我们编写与类型无关的泛型代码,当函数模板的行为需要某种可定制的操作时,函数对象便是最理想的参数类型。例如可以写一个通用的getMax函数,可以根据传入的比较策略(函数对象)返回两个值中的较大者。

#include <iostream>
using namespace std;

// 通用比较模板函数
template <typename T, typename Compare>
T getMax(T a, T b, Compare comp) 
{
    return comp(a, b) ? b : a;
}

// 函数对象:比较大小
struct Less 
{
    bool operator()(int a, int b) 
    {
        return a < b; // 返回 true 时,b 是较大者
    }
};

// 函数对象:比较绝对值大小
struct AbsLess 
{
    bool operator()(int a, int b) 
    {
        return abs(a) < abs(b);
    }
};

int main() {
    int x = -5, y = 3;

    // 使用 Less 比较器
    cout << "Max: " << getMax(x, y, Less()) << endl;

    // 使用 AbsLess 比较器
    cout << "Max by abs: " << getMax(x, y, AbsLess()) << endl;

    return 0;
}
cpp

在C++的STL算法中,和上面所举例子一样,大量使用了函数对象作为可传递的比较器或操作器,因此STL的使用非常灵活,是泛型编程的典范。例如,C++ 的STL算法std::sort默认的是std::less<T>来进行升序排序,可以使用预定义的函数对象std::greater<T>来进行降序排序:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional> // 包含预定义函数对象

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9};

    // 使用 std::greater<> 进行降序排序
    std::sort(nums.begin(), nums.end(), std::greater<int>());

    for (int n : nums) {
        std::cout << n << " "; // 输出: 9 5 4 3 1 1
    }
    std::cout << std::endl;
}
cpp

3. Lambda 表达式#

C++11 引入的 Lambda 表达式在很多情况下可以看作是创建匿名函数对象的便捷语法,通常编译器会将 Lambda 表达式转换为一个未命名的函数对象。Lambda 表达式的基本格式如下:

[捕获列表](参数列表) -> 返回类型 {
    函数体
};
cpp

其中:

  • [] 捕获列表(Capture List):捕获列表决定了 Lambda 如何访问外部作用域的变量
    • 按值捕获 [=] 或显式 [x, y]:值捕获的变量在 Lambda 表达式定义时就已经确定,不会随着外部变量的变化而变化,例如:
      int x = 10;
      auto f = [x] (int y) -> int { return x + y; }; // 值捕获 x
      x = 20; // 修改外部的 x
      cout << f(5) << endl; // 输出 15,不受外部 x 的影响
      cpp
      默认情况下,值捕获的变量在lambda内部是只读的,但是可以通过mutable修改,例如:
      int a = 10;
      auto f = [a]() mutable { a++; std::cout << a << std::endl; };
      f(); // 输出 11,但外部a仍然是10
      cpp
    • 按引用捕获 [&] 或显式 [&x, &y]:引用表达式的变量在 Lambda 表达式调用时才确定,会随着外部变量变化而变化,反之亦然。使用引用捕获要注意生命周期的问题,避免悬空引用。
      int x = 10;
      auto f = [&x] (int y) -> int { return x + y; }; // 引用捕获 x
      x = 20; // 修改外部的 x
      cout << f(5) << endl; // 输出 25,受外部 x 的影响
      cpp
    • 混合捕获:[=, &x]x按引用,其余按值)或 [&, y]y按值,其余按引用)。例如:
      int x = 10;
      int y = 20;
      auto f = [=, &y] (int z) -> int { return x + y + z; }; // 隐式按值捕获 x,显式按引用捕获 y
      x = 30; // 修改外部的 x
      y = 40; // 修改外部的 y
      cout << f(5) << endl; // 输出 55,不受外部 x 的影响,受外部 y 的影响
      cpp
    • 初始化捕获(C++14):它允许在捕获列表中使用初始化表达式,从而在捕获列表中创建并初始化一个新的变量,而不是捕获一个已存在的变量。这种方式可以使用 auto 关键字来推导类型,也可以显式指定类型。这种方式可以用来捕获只移动的变量,或者捕获 this 指针的值。下面是一个捕获unique_ptr的例子:
      auto lambda = [p = std::make_unique<int>(42)]() 
      {
          std::cout << "Value: " << *p << std::endl;
      };
      cpp
    • 捕获this指针,[this][*this]:这允许在 Lambda 内部访问当前类的成员变量和成员函数。this是C++11引入的,这种方式会按值捕获this指针,在 Lambda 表达式的内部会有一份this指针的拷贝,实际上这相当于按引用捕获了对象本身,Lambda 内部对成员变量的修改会直接影响到原始对象。而*this是C++17引入的,这种方式是对当前对象进行值捕获,这就意味着 Lambda 表达式的内部操作的是一个独立的副本,对副本成员的任何修改都不会影响到原始对象。
  • () 参数列表(Parameter List):可选,类似于普通函数的参数。
    • 与普通函数参数类似,支持默认参数(C++14起)。
    • 泛型Lambda(C++14):使用 auto 声明参数,使 Lambda 可接受任意类型。
  • -> 返回类型(Return Type):可选,返回类型通常可以省略,由编译器推导。
  • {} 函数体(Function Body):Lambda 的执行逻辑。

Lambda 表达式为我们创建匿名函数对象提供了便捷的语法糖,当我们编写一个Lambda表达式时,C++编译器会自动生成一个独一无二、匿名的类(这个类被称为闭包类型),然后根据这个类创建一个对象,这个对象就是我们所说的函数对象。例如这个 Lambda 表达式:

auto myLambda = [](int x) { return x * 2; };
cpp

编译器内部通常会生成如下的匿名类:

// 编译器在内部生成的匿名类
class __SomeInternalUniqueNameForLambda {
public:
    // 1. operator() 的重载
    // Lambda的参数列表和返回类型决定了这个成员函数的签名
    // 默认情况下,这个operator()是const的,因为Lambda默认是不可变的
    int operator()(int x) const {
        return x * 2;
    }
};

// 2. 创建该类的实例(函数对象)
auto myLambda = __SomeInternalUniqueNameForLambda{};
cpp

4. 函数包装器和函数适配器#

函数包装器std::function和函数适配器std::bind是C++11引入的两大处理可调用对象的强大工具,可以看作是函数指针和函数对象的现代化、类型安全的替代方案。

4.1 std::function#

std::function 是一个定义在 <functional> 头文件中的类模板。它是一个通用的、多态的函数包装器,它可以存储、复制和调用任何签名兼容的可调用对象。std::function 的模板参数就是它要包装的函数的签名,语法如下:

#include <functional>

std::function<返回类型(参数类型1, 参数类型2, ...)> func;
cpp

下面是简单的使用示例:

#include <iostream>
#include <functional>

// 1. 普通函数
int add(int a, int b) { return a + b; }

// 2. 函数对象 (Functor)
struct Subtract 
{
    int operator()(int a, int b) const { return a - b; }
};

int main() 
{
    std::function<int(int, int)> operation;

    // a. 包装普通函数
    operation = add; // 或者 &add
    std::cout << "add(10, 5) = " << operation(10, 5) << std::endl; // 输出 15

    // b. 包装函数对象
    Subtract sub;
    operation = sub;
    std::cout << "subtract(10, 5) = " << operation(10, 5) << std::endl; // 输出 5

    // c. 包装 Lambda 表达式
    operation = [](int a, int b) { return a * b; };
    std::cout << "multiply(10, 5) = " << operation(10, 5) << std::endl; // 输出 50
}
cpp

可以看到,它提供了一个统一的接口来存储不同种类的可调用对象,std::function会擦除原始可调用对象的具体类型,只保留其调用的接口。这在设计需要回调的 API 时非常有用。API 的设计者不必为函数指针、函数对象等分别设计接口,只需使用 std::function 即可。由于会进行类型擦除以及可能进行堆分配,std::function的调用可能比直接调用或者通过函数指针调用有轻微的性能开销,std::function对象本身的大小可能比函数指针大。

4.2 std::bind#

std::bind 也是一个定义在 <functional> 头文件中的函数模板。它的作用是将一个可调用对象(如函数、成员函数、函数对象)与其部分或全部参数进行绑定,从而创建一个新的可调用对象(一个函数对象)。语法如下:

auto new_callable = std::bind(callable, arg1, arg2, ...);
cpp
  • callable_object: 原始的可调用对象。
  • arg1, arg2, ...:
    • 可以是具体的值,这些值将被“绑定”到 callable_object 对应位置的参数。
    • 可以是占位符 (Placeholders),如 std::placeholders::_1, std::placeholders::_2 等。这些占位符表示新生成的可调用对象的参数。_1 对应新可调用对象的第一个参数,_2 对应第二个,以此类推。

一些使用示例如下:

  • 参数绑定(固定部分参数,得到一个参数更少的新函数):
    int add(int a, int b, int c) 
    {
        return a + b + c;
    }
    // 绑定第一个参数为 10
    auto add_10 = std::bind(add, 10, std::placeholders::_1, std::placeholders::_2);
    cpp
  • 重新排列参数顺序:
    void print_ordered(int a, const std::string& b, double c) 
    {
        std::cout << "Int: " << a << ", String: " << b << ", Double: " << c << std::endl;
    }
    
    // 原本参数顺序: int, string, double
    // 新函数参数顺序: string, double, int (传入_3)
    auto print_reordered = std::bind(print_ordered,
                                    std::placeholders::_3, // 第三个参数给 a
                                    std::placeholders::_1, // 第一个参数给 b
                                    std::placeholders::_2  // 第二个参数给 c
                                    );
    cpp
  • 绑定成员函数:这是std::bind的一个经典用途,用于将成员函数适配为可以独立调用的函数对象。对于非静态成员函数,第一个绑定的参数必须是对象实例的指针或引用,或者是一个包装了对象的std::shared_ptrstd::reference_wrapper
    struct Greeter 
    {
        std::string name;
        Greeter(std::string n) : name(n) {}
        void greet(const std::string& salutation) 
        {
            std::cout << salutation << ", " << name << "!" << std::endl;
        }
    };
    
    Greeter world_greeter("World");
    Greeter cpp_greeter("C++");
    
    // 绑定到 world_greeter 实例的 greet 方法,并预设 salutation 参数
    auto greet_world_morning = std::bind(&Greeter::greet, &world_greeter, "Good morning");
    greet_world_morning(); // 输出: Good morning, World!
    
    // 绑定到 cpp_greeter 实例的 greet 方法,salutation 通过占位符传入
    auto greet_cpp = std::bind(&Greeter::greet, &cpp_greeter, std::placeholders::_1);
    greet_cpp("Hello"); // 输出: Hello, C++!
    cpp
C++ 的函数指针与函数对象
https://liangbm3.site/blog/c-de-han-shu-zhi-zhen-yu-han-shu-dui-xiang
Author liangbm3
Published at 2025年7月3日
Comment seems to stuck. Try to refresh?✨