C++ 从入门到精通之十七
大纲
- C++ 从入门到精通之一、C++ 从入门到精通之二、C++ 从入门到精通之三
- C++ 从入门到精通之四、C++ 从入门到精通之五、C++ 从入门到精通之六
- C++ 从入门到精通之七、C++ 从入门到精通之八、C++ 从入门到精通之九
- C++ 从入门到精通之十、C++ 从入门到精通之十一、C++ 从入门到精通之十二
- C++ 从入门到精通之十三、C++ 从入门到精通之十四、C++ 从入门到精通之十五
- C++ 从入门到精通之十六、C++ 从入门到精通之十七
C++ 进阶
可调用对象
函数指针
核心概念
C++ 中的函数指针是一种指向函数的指针变量,它存储的是函数在内存中的入口地址,而非数据。声明函数指针时需要指定其所指函数的返回类型和参数列表,例如 int (*p)(int, int) 表示一个指向 “接受两个 int 并返回 int“ 的函数的指针。使用时可将函数名(会隐式转换为函数地址)赋给函数指针,然后通过 p(args) 或 (*p)(args) 两种等价方式调用该函数,也可将函数指针作为参数传递给其他函数,从而实现回调机制和运行时动态选择函数的功能。函数指针的局限在于它只能指向具有相同签名的普通函数或静态成员函数,无法直接携带状态,也不能指向非静态成员函数(后者需要成员函数指针且调用时须提供对象),因此在现代 C++ 中常被函数对象、Lambda 表达式和 std::function 等更灵活的机制所替代,但在与 C 语言接口交互、底层系统编程以及需要极轻量级回调的场合仍然广泛使用。
案例代码
1 |
|
程序运行的结果如下:
1 | p(3, 4) = 7 |
函数对象
核心概念
C++ 中的函数调用运算符 operator() 可以被重载,使得类的对象能够像普通函数一样通过 对象名(参数列表) 的语法被调用,这种对象称为函数对象(又称仿函数)。重载时需在类内定义一个名为 operator() 的成员函数,其参数列表和返回类型可自由设计,并可包含多个重载版本以支持不同参数。函数对象相比普通函数指针的优势在于可以携带状态(通过成员变量保存数据),并且编译器容易将其内联从而提升性能,因此广泛应用于 C++ 标准库算法(如 std::sort 的比较器、std::for_each 的遍历操作)以及需要自定义行为的回调场景中,同时 Lambda 表达式在底层也正是通过编译器生成一个重载了 operator() 的匿名函数对象来实现的。
案例代码
1 |
|
程序运行的结果如下:
1 | 1 |
function 类模板
核心概念
std::function 是 C++ 11 引入于 <functional> 头文件中的一个通用多态函数包装器类模板,它能够以统一的方式存储、复制和调用任何满足指定签名的可调用对象。其模板参数采用 std::function<返回类型(参数类型...)> 的形式,例如 std::function<int(int, int)> 表示一个 “接受两个 int 并返回 int“ 的可调用对象包装器。与只能指向普通函数或静态成员函数的函数指针不同,std::function 可以容纳普通函数、函数指针、函数对象(重载了 operator() 的类实例)、Lambda 表达式、std::bind 的绑定结果以及成员函数指针(配合对象或绑定器)等几乎所有可调用实体,只要它们的调用签名兼容即可。它内部通过类型擦除(type erasure)机制实现,因此不同类型的可调用对象都能被同一 std::function 变量持有,并在运行时被统一调用。使用上,它可以像普通函数一样通过 operator() 调用,也可以为空(默认构造或赋值为 nullptr 时),调用空 std::function 会抛出 std::bad_function_call 异常,因此调用前通常用 if (f) 检查其是否有效。std::function 的代价是相比函数指针和函数对象有额外的类型擦除与可能的堆分配开销,性能略低且不利于内联,因此在性能敏感或需要极致轻量的场景下仍会优先考虑函数指针或模板;但在需要存储异构回调、实现事件系统、回调注册表或运行时动态选择行为的场合,std::function 提供了远比函数指针灵活且类型安全的解决方案。
特别注意
std::function 无法直接把一个重载函数名(重载集)赋给它并自动完成重载解析,因为其构造函数模板不会以目标函数签名作为推导依据,面对重载集时编译器无法确定该选哪个重载函数,导致推导失败、编译报错;解决方法是先用目标函数签名完成重载解析再交给 std::function,常见做法有三种 —— 用 static_cast<void(*)(int)>(f)(或 C 语言风格转换)显式指定函数签名、先用一个函数签名明确的函数指针变量接收重载函数再赋给 std::function、或用 Lambda 表达式包一层在函数体内调用重载函数让解析在体内发生,此外也可以通过 std::bind 绑定参数后消除重载歧义。简而言之,std::function 不能直接绑定重载函数集,因为它缺少重载解析所需的目标函数签名上下文,必须通过显式转换、函数指针中转或 Lambda 表达式包装先选定具体重载函数,再交由 std::function 保存和调用。
案例代码
案例代码一
std::function与普通函数结合使用
1 |
|
程序运行的结果如下:
1 | f(3, 4) = 7 |
案例代码二
std::function与函数指针结合使用
1 |
|
程序运行的结果如下:
1 | fp(3, 4) = 7 |
案例代码三
std::function与函数对象结合使用
1 |
|
程序运行的结果如下:
1 | f(10) = 15 |
案例代码四
std::function与 Lambda 表达式结合使用
1 |
|
程序运行的结果如下:
1 | f(10) = 11 |
万能引用
万能引用的概述
C++ 中的万能引用(universal reference,正式名称是转发引用 forwarding reference)是 C++ 11 引入的一个概念,指在模板参数推导或 auto 推导语境下、形如 T&& 或 auto&& 的引用形式;它之所以 “万能”,是因为它既能绑定左值也能绑定右值:当传入左值时 T 被推导为左值引用类型(如 int&),经引用折叠后 T&& 成为左值引用;当传入右值时 T 被推导为非引用类型(如 int),T&& 即为右值引用。判定是否为万能引用的关键有两点:一是必须发生在类型推导语境中(如函数模板参数 T&&、auto&&);二是形式必须严格为 T&&,一旦加上 const(如 const T&&)或出现在非类型推导语境(如指定类模板参数、具体类型,类似 std::vector<T>&& 和 std::vector<int>&&),它就只是普通的右值引用。万能引用最常见的用途是配合 std::forward<T> 实现完美转发,把参数的左值 / 右值属性原样传递给下层函数,从而避免不必要的拷贝并支持移动语义。需要注意的是,由于万能引用几乎能匹配一切类型,它在重载解析中往往过于贪婪,容易抢走本应匹配其他重载的调用,因此常需配合 std::enable_if、std::is_same 或 Concepts(C++ 20)加以约束。
左值引用、右值引用、移动语义、完美转发的介绍
万能引用的使用
右值引用案例代码
- 这里的案例代码演示了右值引用的使用,万能引用不会生效
1 |
|
程序运行的结果如下:
1 | takeRvalue(int&&) : 10 |
万能引用案例代码
- 这里的案例代码演示了万能引用的使用
1 |
|
程序运行的结果如下:
1 | takeRvalue(T&&) : 10 |
引用折叠的说明
引用折叠只发生在 "引用的引用" 这种形式上,即 & &、& &&、&& &、&& && 这四种组合。只有当 T 本身是引用类型(比如左值推导出 int&)时,T&& 代入后才会形成 int& &&,才需要折叠。如果右值推导出的 T 是 int,代入 T&& 直接就是 int&&,没有嵌套引用,自然不会发生引用折叠。
与右值引用的区别
万能引用与右值引用的区别如下:
- 本质不同:右值引用是具体类型(如
int&&),万能引用是模板 /auto类型推导语境下T&&的表现形式。 - 推导条件:万能引用必须发生在类型推导语境(如函数模板参数、
auto&&);右值引用出现在具体类型或非类型推导语境。 - 形式要求:万能引用必须严格是
T&&;如果写成const T&&、std::vector<int>&&等就是普通右值引用。 - 绑定能力:万能引用既能绑定左值也能绑定右值;右值引用只能绑定右值。
- 推导结果:万能引用传左值推导为
T = int&,经引用折叠得到int&;传右值推导为T = int,得到int&&。右值引用类型固定不变。 - 用途不同:万能引用主要用于完美转发(配合
std::forward);右值引用主要用于移动语义和移动构造 / 移动赋值。 - 重载影响:万能引用几乎匹配一切类型,容易在重载中 “抢走” 调用,需用
enable_if/ Concepts(C++ 20)约束;右值引用不会如此贪婪。
总结
- 右值引用是固定类型的右值引用。
- 万能引用是会随实参推导、既能代表左值引用又能代表右值引用的模板形式。
