C++ 从入门到精通之十七

大纲

C++ 进阶

可调用对象

函数指针

核心概念

C++ 中的函数指针是一种指向函数的指针变量,它存储的是函数在内存中的入口地址,而非数据。声明函数指针时需要指定其所指函数的返回类型和参数列表,例如 int (*p)(int, int) 表示一个指向 “接受两个 int 并返回 int“ 的函数的指针。使用时可将函数名(会隐式转换为函数地址)赋给函数指针,然后通过 p(args) 或 (*p)(args) 两种等价方式调用该函数,也可将函数指针作为参数传递给其他函数,从而实现回调机制和运行时动态选择函数的功能。函数指针的局限在于它只能指向具有相同签名的普通函数或静态成员函数,无法直接携带状态,也不能指向非静态成员函数(后者需要成员函数指针且调用时须提供对象),因此在现代 C++ 中常被函数对象、Lambda 表达式和 std::function 等更灵活的机制所替代,但在与 C 语言接口交互、底层系统编程以及需要极轻量级回调的场合仍然广泛使用。

案例代码
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
#include <iostream>
#include <map>
#include <string>

// 定义几个普通函数
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int multiply(int a, int b) {
return a * b;
}

// 接受函数指针作为参数的函数(回调函数)
int compute(int x, int y, int (*op)(int, int)) {
return op(x, y);
}

int main() {
// 1. 声明并初始化函数指针
int (*p)(int, int) = add;

// 2. 通过函数指针调用
std::cout << "p(3, 4) = " << p(3, 4) << std::endl;
std::cout << "(*p)(3, 4) = " << (*p)(3, 4) << std::endl;

// 3. 让函数指针指向另一个函数
p = subtract;
std::cout << "p(10, 4) = " << p(10, 4) << std::endl;

// 4. 将函数指针作为参数传递(回调)
std::cout << "compute(5, 6, add) = " << compute(5, 6, add) << std::endl;
std::cout << "compute(5, 6, multiply) = " << compute(5, 6, multiply) << std::endl;

// 5. 用函数指针数组实现简单的 "分派表"
int (*ops[])(int, int) = {add, subtract, multiply};
for (int i = 0; i < 3; ++i) {
std::cout << "ops[" << i << "](8, 2) = " << ops[i](8, 2) << std::endl;
}

// 6. 将多个函数指针放入同一个容器
std::map<std::string, int (*)(int, int)> container;
container.insert(std::make_pair("add", ops[0]));
container.insert(std::make_pair("subtract", ops[1]));
container.insert(std::make_pair("multiply", ops[2]));
for (auto it = container.begin(); it != container.end(); ++it) {
std::cout << it->first << "(3, 4) = " << it->second(3, 4) << std::endl;
}

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
p(3, 4) = 7
(*p)(3, 4) = 7
p(10, 4) = 6
compute(5, 6, add) = 11
compute(5, 6, multiply) = 30
ops[0](8, 2) = 10
ops[1](8, 2) = 6
ops[2](8, 2) = 16
add(3, 4) = 7
multiply(3, 4) = 12
subtract(3, 4) = -1

函数对象

核心概念

C++ 中的函数调用运算符 operator() 可以被重载,使得类的对象能够像普通函数一样通过 对象名(参数列表) 的语法被调用,这种对象称为函数对象(又称仿函数)。重载时需在类内定义一个名为 operator() 的成员函数,其参数列表和返回类型可自由设计,并可包含多个重载版本以支持不同参数。函数对象相比普通函数指针的优势在于可以携带状态(通过成员变量保存数据),并且编译器容易将其内联从而提升性能,因此广泛应用于 C++ 标准库算法(如 std::sort 的比较器、std::for_each 的遍历操作)以及需要自定义行为的回调场景中,同时 Lambda 表达式在底层也正是通过编译器生成一个重载了 operator() 的匿名函数对象来实现的。

案例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

// 函数对象(仿函数)
class BiggerThanZero {
public:
// 函数运算符 operator() 重载
bool operator()(const int val) const {
return val > 0;
}
};

int main() {
const BiggerThanZero btz;
const int result = btz(3);
std::cout << result << std::endl;
return 0;
}

程序运行的结果如下:

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
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
#include <functional>
#include <iostream>
#include <string>
#include <vector>

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

int subtract(int a, int b) {
return a - b;
}

int multiply(int a, int b) {
return a * b;
}

// 接受 std::function 作为参数的函数
int compute(int x, int y, std::function<int(int, int)> op) {
return op(x, y);
}

int main() {
// 1. 用 std::function 包装普通函数
std::function<int(int, int)> f = add;
std::cout << "f(3, 4) = " << f(3, 4) << std::endl;

// 2. 重新赋值为另一个普通函数
f = subtract;
std::cout << "f(10, 4) = " << f(10, 4) << std::endl;

// 3. 作为函数参数传递(回调)
std::cout << "compute(5, 6, add) = " << compute(5, 6, add) << std::endl;
std::cout << "compute(5, 6, multiply) = " << compute(5, 6, multiply) << std::endl;

// 4. 将普通函数存入容器,实现分派表
std::vector<std::function<int(int, int)>> ops = {add, subtract, multiply};
std::vector<std::string> names = {"add", "subtract", "multiply"};
for (size_t i = 0; i < ops.size(); ++i) {
std::cout << names[i] << "(8, 2) = " << ops[i](8, 2) << std::endl;
}

// 5. 空 std::function 检查
std::function<int(int, int)> g;
if (!g) {
std::cout << "g is empty" << std::endl;
}
g = add;
if (g) {
std::cout << "g(1, 2) = " << g(1, 2) << std::endl;
}

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
f(3, 4) = 7
f(10, 4) = 6
compute(5, 6, add) = 11
compute(5, 6, multiply) = 30
add(8, 2) = 10
subtract(8, 2) = 6
multiply(8, 2) = 16
g is empty
g(1, 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
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
#include <functional>
#include <iostream>

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

int subtract(int a, int b) {
return a - b;
}

// 接受函数指针作为参数的函数
int applyPtr(int x, int y, int (*op)(int, int)) {
return op(x, y);
}

// 接受 std::function 作为参数的函数
int applyFunc(int x, int y, std::function<int(int, int)> op) {
return op(x, y);
}

int main() {
// 1. 定义函数指针并指向普通函数
int (*fp)(int, int) = add;
std::cout << "fp(3, 4) = " << fp(3, 4) << std::endl;

// 2. 用 std::function 包装该函数指针
std::function<int(int, int)> f = fp;
std::cout << "f(3, 4) = " << f(3, 4) << std::endl;

// 3. 让函数指针改指向另一函数,再赋给 std::function
fp = subtract;
f = fp;
std::cout << "f(10, 4) = " << f(10, 4) << std::endl;

// 4. 把函数指针直接传给接受 std::function 的函数
std::cout << "applyFunc(5, 6, add) = " << applyFunc(5, 6, add) << std::endl;

// 5. 把 std::function 取出为函数指针再传给接受函数指针的函数
int (*fp2)(int, int) = add;
std::function<int(int, int)> f2 = fp2;
std::cout << "applyPtr(7, 2, f2.target<int(*)(int,int)>() ? "
"*f2.target<int(*)(int,int)>() : nullptr) = ";
auto extracted = f2.target<int (*)(int, int)>();
if (extracted) {
std::cout << applyPtr(7, 2, *extracted) << std::endl;
}

// 6. 二者混用:先用函数指针选择,再用 std::function 统一保存
int (*ops[])(int, int) = {add, subtract};
std::function<int(int, int)> selected = ops[1];
std::cout << "selected(9, 3) = " << selected(9, 3) << std::endl;

// 7. 空 std::function 与函数指针的对比
std::function<int(int, int)> empty;
int (*nullFp)(int, int) = nullptr;
if (!empty && !nullFp) {
std::cout << "both empty and null" << std::endl;
}

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
fp(3, 4) = 7
f(3, 4) = 7
f(10, 4) = 6
applyFunc(5, 6, add) = 11
applyPtr(7, 2, f2.target<int(*)(int,int)>() ? *f2.target<int(*)(int,int)>() : nullptr) = 9
selected(9, 3) = 6
both empty and null
案例代码三
  • 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
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
70
71
72
73
74
75
76
77
78
79
80
#include <functional>
#include <iostream>
#include <vector>

// 函数对象:加法器,可携带状态
class Adder {
int base_;

public:
explicit Adder(int base) : base_(base) {
}
int operator()(int x) const {
return base_ + x;
}
};

// 函数对象:乘法器,可携带状态
class Multiplier {
int factor_;

public:
explicit Multiplier(int factor) : factor_(factor) {
}
int operator()(int x) const {
return factor_ * x;
}
};

// 函数对象:计数器,调用时递增并返回调用次数
class Counter {
int count_ = 0;

public:
int operator()() {
return ++count_;
}
};

// 接受 std::function 作为参数的函数
int apply(int x, std::function<int(int)> op) {
return op(x);
}

int main() {
// 1. 用 std::function 包装函数对象
Adder add5(5);
std::function<int(int)> f = add5;
std::cout << "f(10) = " << f(10) << std::endl;

// 2. 重新赋值为另一个函数对象
Multiplier mul3(3);
f = mul3;
std::cout << "f(10) = " << f(10) << std::endl;

// 3. 直接构造临时函数对象并赋给 std::function
f = Adder(100);
std::cout << "f(1) = " << f(1) << std::endl;

// 4. 将函数对象作为参数传递
std::cout << "apply(7, Adder(2)) = " << apply(7, Adder(2)) << std::endl;

// 5. 函数对象携带状态:同一个对象被多次调用
Counter counter;
std::function<int()> c = std::ref(counter);
std::cout << "c() = " << c() << std::endl;
std::cout << "c() = " << c() << std::endl;
std::cout << "c() = " << c() << std::endl;
std::cout << "counter() = " << counter() << std::endl;

// 6. 将不同函数对象存入同一容器
std::vector<std::function<int(int)>> ops;
ops.push_back(Adder(1));
ops.push_back(Multiplier(2));
ops.push_back(Adder(10));
for (size_t i = 0; i < ops.size(); ++i) {
std::cout << "ops[" << i << "](5) = " << ops[i](5) << std::endl;
}

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
f(10) = 15
f(10) = 30
f(1) = 101
apply(7, Adder(2)) = 9
c() = 1
c() = 2
c() = 3
counter() = 4
ops[0](5) = 6
ops[1](5) = 10
ops[2](5) = 15
案例代码四
  • std::function 与 Lambda 表达式结合使用
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
#include <functional>
#include <iostream>
#include <vector>

// 接受 std::function 作为参数的函数
int apply(int x, std::function<int(int)> op) {
return op(x);
}

int main() {
// 1. 用 std::function 包装无捕获 Lambda
std::function<int(int)> f = [](int x) { return x + 1; };
std::cout << "f(10) = " << f(10) << std::endl;

// 2. 重新赋值为另一个 Lambda
f = [](int x) { return x * x; };
std::cout << "f(10) = " << f(10) << std::endl;

// 3. 包装带捕获的 Lambda,携带状态
int base = 100;
f = [base](int x) { return base + x; };
std::cout << "f(5) = " << f(5) << std::endl;

// 4. 按引用捕获,共享外部变量
int counter = 0;
std::function<int()> tick = [&counter]() { return ++counter; };
std::cout << "tick() = " << tick() << std::endl;
std::cout << "tick() = " << tick() << std::endl;
std::cout << "counter = " << counter << std::endl;

// 5. 将 Lambda 作为参数传递
std::cout << "apply(7, [](int x){ return x - 3; }) = " << apply(7, [](int x) { return x - 3; }) << std::endl;

// 6. 将多个 Lambda 存入同一容器
std::vector<std::function<int(int)>> ops;
ops.push_back([](int x) { return x + 1; });
ops.push_back([](int x) { return x * 2; });
ops.push_back([](int x) { return x - 10; });
for (size_t i = 0; i < ops.size(); ++i) {
std::cout << "ops[" << i << "](6) = " << ops[i](6) << std::endl;
}

// 7. 返回 Lambda 的工厂函数
auto makeAdder = [](int n) -> std::function<int(int)> { return [n](int x) { return n + x; }; };
std::function<int(int)> add5 = makeAdder(5);
std::function<int(int)> add20 = makeAdder(20);
std::cout << "add5(1) = " << add5(1) << std::endl;
std::cout << "add20(1) = " << add20(1) << std::endl;

// 8. 空 std::function 检查
std::function<int(int)> empty;
if (!empty) {
std::cout << "empty is not callable" << std::endl;
}
empty = [](int x) { return x; };
if (empty) {
std::cout << "empty(42) = " << empty(42) << std::endl;
}

return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
f(10) = 11
f(10) = 100
f(5) = 105
tick() = 1
tick() = 2
counter = 2
apply(7, [](int x){ return x - 3; }) = 4
ops[0](6) = 7
ops[1](6) = 12
ops[2](6) = -4
add5(1) = 6
add20(1) = 21
empty is not callable
empty(42) = 42

万能引用

万能引用的概述

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

// 模板参数 T 只用于返回类型,形参是具体类型的右值引用
template <typename T>
T takeRvalue(int&& x) {
std::cout << "takeRvalue(int&&) : " << x << std::endl;
return x;
}

int main() {
// 传右值,正常调用;右值引用生效(int&& 绑定右值)
takeRvalue<int>(10);

int a = 20;

// 传左值,编译失败:int&& 是右值引用,不能绑定左值;右值引用生效(拒绝左值)
// takeRvalue<int>(a);

// 传右值,正常调用;右值引用生效(int&& 绑定右值)
takeRvalue<int>(25);

return 0;
}

程序运行的结果如下:

1
2
takeRvalue(int&&) : 10
takeRvalue(int&&) : 25
万能引用案例代码
  • 这里的案例代码演示了万能引用的使用
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>

// 模板参数 T 参与形参推导,形参 T&& 是万能引用
template <typename T>
T takeRvalue(T&& x) {
std::cout << "takeRvalue(T&&) : " << x << std::endl;
return x;
}

int main() {
// 显式指定 T=int,形参为 int&&,绑定右值;万能引用未生效(表现为普通右值引用)
takeRvalue<int>(10);

int a = 20;

// 显式指定 T=int,形参是 int&&,左值 a 不能绑定到 int&&,会编译失败;也就是说,显式指定 T 后它表现得像普通右值引用,万能引用未生效
// takeRvalue<int>(a);

// 显式指定 T=int&,形参折叠为 int&,绑定左值;万能引用未生效(表现为普通左值引用)
takeRvalue<int&>(a);

// 不显式指定 T,传左值:T 推导为 int&,形参 T&& 代入得 int& &&,经引用折叠为 int&,绑定左值;万能引用生效
takeRvalue(a);

// 不显式指定 T,传右值:T 推导为 int,形参 T&& 代入即 int&&,无需折叠,绑定右值;万能引用生效
takeRvalue(25);

return 0;
}

程序运行的结果如下:

1
2
3
4
takeRvalue(T&&) : 10
takeRvalue(T&&) : 20
takeRvalue(T&&) : 20
takeRvalue(T&&) : 25

引用折叠的说明

引用折叠只发生在 "引用的引用" 这种形式上,即 & &、& &&、&& &、&& && 这四种组合。只有当 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)约束;右值引用不会如此贪婪。

总结

  • 右值引用是固定类型的右值引用。
  • 万能引用是会随实参推导、既能代表左值引用又能代表右值引用的模板形式。

模板类型推断

auto 类型推断

引用折叠、完美转发

decltype 声明类型说明符