函数
# 第 7 章 C++ 函数
# 目录介绍
# 7.1 概述和定义
# 7.1.1 函数概念
函数 是一段可重用的代码块,用于执行特定的任务。函数可以提高代码的模块化、可读性和可维护性。C++ 函数包括函数声明、函数定义和函数调用。
一个函数通常包括以下部分:
- 返回类型:函数返回值的类型(如
int、void等)。 - 函数名:函数的名称,用于调用函数。
- 参数列表:函数接受的输入参数(可选)。
- 函数体:函数的具体实现代码。
# 7.1.2 函数声明和定义
语法
返回类型 函数名(参数列表) {
// 函数体语句
return 返回值; // 如果返回类型不是 void
}
2
3
4
- 返回类型 :一个函数可以返回一个值。在函数定义中
- 函数名:给函数起个名称
- 参数列表:使用该函数时,传入的数据
- 函数体语句:花括号内的代码,函数内需要执行的语句
- return表达式: 和返回值类型挂钩,函数执行完后,返回相应的数据
示例
#include <iostream>
using namespace std;
// 函数声明
int add(int a, int b);
int main() {
int result = add(3, 5); // 函数调用
cout << "Result: " << result << endl; // 8
return 0;
}
// 函数定义
int add(int a, int b) {
return a + b;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意:函数的声明可以多次,但是函数的定义只能有一次
# 7.1.3 函数分类
常见的函数样式有4种
- 无参无返
- 有参无返
- 无参有返
- 有参有返
示例:
//1、 无参无返
void test01() {
//void a = 10; //无类型不可以创建变量,原因无法分配内存
cout << "this is test01" << endl;
//test01(); 函数调用
}
//2、 有参无返
void test02(int a) {
cout << "this is test02" << endl;
cout << "a = " << a << endl;
}
//3、无参有返
int test03() {
cout << "this is test03 " << endl;
return 10;
}
//4、有参有返
int test04(int a, int b) {
cout << "this is test04 " << endl;
int sum = a + b;
return sum;
}
int main() {
test01();
test02(10);
int a3 = test03();
int a4 = test04(1,3);
return 0;
}
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
# 7.1.4 Lambda函数
C++11 引入了 Lambda 函数,用于定义匿名函数。
语法
[捕获列表](参数列表) -> 返回类型 {
// 函数体
}
2
3
示例
#include <iostream>
using namespace std;
int main() {
auto add = [](int a, int b) -> int {
return a + b;
};
cout << "add(3, 5): " << add(3, 5) << endl; // 8
return 0;
}
2
3
4
5
6
7
8
9
10
# 7.1.5 综合案例与思考
综合案例:函数的声明、定义与Lambda综合运用
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
// 函数声明(放在前面)
int factorial(int n);
void printResult(const string& label, int value);
// 四种函数分类演示
void greet() { cout << "欢迎使用计算器!" << endl; } // 无参无返
void showMenu(int count) { cout << "共有" << count << "个功能" << endl; } // 有参无返
int getVersion() { return 1; } // 无参有返
int power(int base, int exp) { // 有参有返
int result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
int main() {
// 1. 普通函数调用
greet();
showMenu(3);
printResult("2^10", power(2, 10));
printResult("5!", factorial(5));
// 2. Lambda函数:作为排序的自定义比较器
vector<int> nums = {5, 2, 8, 1, 9, 3};
sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b; // 降序排序
});
cout << "降序排序: ";
for (int n : nums) cout << n << " ";
cout << endl;
// 3. Lambda捕获外部变量
int threshold = 5;
auto filter = [threshold](int x) { return x > threshold; };
cout << "大于" << threshold << "的数: ";
for (int n : nums) {
if (filter(n)) cout << n << " ";
}
cout << endl;
// 4. 用std::function存储不同类型的可调用对象
function<int(int, int)> op;
op = [](int a, int b) { return a + b; };
cout << "Lambda加法: " << op(3, 5) << endl;
op = power; // 也能存储普通函数(通过包装)
// 注意:power有两个参数但语义不同,这里仅作演示
return 0;
}
// 函数定义(放在后面)
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 递归
}
void printResult(const string& label, int value) {
cout << label << " = " << value << endl;
}
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
案例知识融合:这个案例综合展示了函数的核心知识——声明与定义的分离、四种函数分类(无参无返/有参无返/无参有返/有参有返)、递归函数、以及C++11的Lambda表达式(作为算法参数、捕获外部变量、与std::function配合使用)。
思考题:
- 函数声明和定义为什么要分离?在什么情况下函数必须先声明后使用?
- Lambda表达式中的捕获列表
[=]、[&]、[this]分别是什么含义?值捕获和引用捕获各有什么风险? - 递归函数的栈空间有限制吗?如果递归层数太深会发生什么?如何用迭代替代递归?
# 7.2 函数参数
函数可以接受零个或多个参数。参数可以是值传递、引用传递或指针传递。
# 7.2.1 值传递
传递参数的副本,函数内对参数的修改不会影响原始值。
- 所谓值传递,就是函数调用时实参将数值传入给形参。
- 值传递时,==如果形参发生,并不会影响实参==
void increment(int x) {
x++;
cout << "Inside function: " << x << endl; // 11
}
int main() {
int a = 10;
increment(a);
cout << "Outside function: " << a << endl; // 10
return 0;
}
2
3
4
5
6
7
8
9
10
11
总结: 值传递时,形参是修饰不了实参的
在C++中,值传递(Pass by Value)是一种参数传递的方式,它指的是将实际参数的值复制给函数或方法的形式参数。在值传递中,函数或方法使用的是形式参数的副本,而不是直接操作实际参数本身。
- 形式参数是实际参数的副本:在函数或方法调用时,实际参数的值会被复制到对应的形式参数中。这意味着函数或方法内部对形式参数的修改不会影响到实际参数的值。
- 拷贝开销:每次调用函数时,都会创建参数对象的一个副本。如果对象很大或包含动态内存,这会带来额外的性能开销。
- 无法修改原始对象:函数内部对参数的修改只会影响副本,不会影响原始对象。
- 潜在的性能问题:对于大型对象或频繁调用的函数,值传递会导致性能下降。
# 7.2.2 引用传递
引用传递意味着函数参数是原始对象的别名,而不是对象的副本。函数内对参数的修改会影响原始值。
void increment(int &x) {
x++;
cout << "Inside function: " << x << endl; // 11
}
int main() {
int a = 10;
increment(a);
cout << "Outside function: " << a << endl; // 11
return 0;
}
2
3
4
5
6
7
8
9
10
11
- 避免拷贝开销:如果
参数是一个复杂的对象(例如包含大量数据或动态内存),引用传递可以避免拷贝整个对象的开销。 - 允许修改原始对象:如果去掉 const,引用传递还可以让函数修改原始对象。
- 提高性能:引用传递避免了不必要的拷贝操作,尤其对于大型对象或频繁调用的函数,性能提升显著。
void increment(const int &x) {
x++;
cout << "Inside function: " << x << endl; // 11
}
int main() {
int a = 10;
increment(a);
cout << "Outside function: " << a << endl; // 11
return 0;
}
2
3
4
5
6
7
8
9
10
11
为什么这里使用 const 和 & 结合?
使用 & 是为了避免不必要的拷贝开销,提高性能,同时使用 const 确保函数不会修改原始对象。
# 7.2.3 指针传递
传递参数的地址,函数内通过指针修改原始值。
void increment(int *x) {
(*x)++;
cout << "Inside function: " << *x << endl; // 11
}
int main() {
int a = 10;
increment(&a);
cout << "Outside function: " << a << endl; // 11
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 7.2.4 综合案例与思考
综合案例:三种参数传递方式对比
#include <iostream>
#include <string>
using namespace std;
struct BigData {
int arr[1000];
string name;
};
// 值传递:拷贝整个对象,不影响原对象
void modifyByValue(BigData data) {
data.name = "修改后(值传递)";
data.arr[0] = 999;
}
// 引用传递:直接操作原对象,零拷贝
void modifyByRef(BigData& data) {
data.name = "修改后(引用传递)";
data.arr[0] = 999;
}
// const引用传递:只读访问,零拷贝+安全
void readByConstRef(const BigData& data) {
cout << "名称: " << data.name << ", arr[0]=" << data.arr[0] << endl;
// data.name = "不能修改"; // 编译错误!
}
// 指针传递:可以传nullptr,灵活但需手动解引用
void modifyByPointer(BigData* data) {
if (data == nullptr) {
cout << "空指针,跳过" << endl;
return;
}
data->name = "修改后(指针传递)";
data->arr[0] = 888;
}
int main() {
BigData original;
original.name = "原始数据";
original.arr[0] = 100;
// 1. 值传递:不影响原对象
modifyByValue(original);
cout << "值传递后: " << original.name << endl; // 仍是"原始数据"
// 2. 引用传递:修改原对象
modifyByRef(original);
cout << "引用传递后: " << original.name << endl; // "修改后(引用传递)"
// 3. const引用:只读
readByConstRef(original);
// 4. 指针传递:可传nullptr
modifyByPointer(&original);
cout << "指针传递后: " << original.name << endl;
modifyByPointer(nullptr); // 安全处理空指针
return 0;
}
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
案例知识融合:这个案例通过一个大型结构体BigData直观展示了三种传递方式的差异——值传递(完整拷贝、不影响原对象但有性能开销)、引用传递(零拷贝、直接修改原对象)、const引用传递(零拷贝+只读保护)、指针传递(可以传nullptr、需要手动解引用和空指针检查)。
思考题:
- 对于大型对象(如包含1000个int的结构体),值传递的性能开销有多大?在什么场景下值传递反而更合适?
const T&和T作为函数参数的选择标准是什么?对于基本类型(如int)推荐哪种?- C++11引入了"右值引用"(
T&&)和"移动语义",它提供了第四种传递方式。它解决了什么问题?
# 7.3 函数返回值
函数可以返回一个值,返回类型由函数声明指定。如果函数不需要返回值,可以使用 void。示例
#include <iostream>
using namespace std;
int square(int x) {
return x * x;
}
void printMessage() {
cout << "This function has no return value." << endl;
}
int main() {
int result = square(5);
cout << "Square: " << result << endl; // 25
printMessage();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.7.2 函数占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法: 返回值类型 函数名 (数据类型){}
在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术
示例:
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
cout << "this is func" << endl;
}
int main() {
func(10,10); //占位参数必须填补
return 0;
}
2
3
4
5
6
7
8
9
# 7.3.1 综合案例与思考
综合案例:函数返回值的多种方式
#include <iostream>
#include <string>
#include <tuple>
#include <optional>
using namespace std;
// 1. 返回基本类型
int add(int a, int b) { return a + b; }
// 2. 返回结构体(多值返回)
struct DivResult {
int quotient;
int remainder;
};
DivResult divide(int a, int b) {
return {a / b, a % b};
}
// 3. 用tuple返回多个值(C++11)
tuple<double, double> getMinMax(double arr[], int n) {
double minVal = arr[0], maxVal = arr[0];
for (int i = 1; i < n; ++i) {
if (arr[i] < minVal) minVal = arr[i];
if (arr[i] > maxVal) maxVal = arr[i];
}
return {minVal, maxVal};
}
// 4. 返回引用(允许左值使用)
int data[5] = {10, 20, 30, 40, 50};
int& getElement(int index) {
return data[index];
}
int main() {
// 基本返回
cout << "3 + 5 = " << add(3, 5) << endl;
// 结构体返回
auto [q, r] = divide(17, 5); // C++17结构化绑定
cout << "17/5 = " << q << " 余 " << r << endl;
// tuple返回
double arr[] = {3.14, 1.41, 2.71, 0.57};
auto [minV, maxV] = getMinMax(arr, 4);
cout << "最小: " << minV << ", 最大: " << maxV << endl;
// 引用返回(可作为左值)
getElement(2) = 99; // 修改data[2]
cout << "data[2] = " << data[2] << endl; // 99
return 0;
}
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
案例知识融合:这个案例展示了函数返回值的多种方式——返回基本类型、返回结构体实现多值返回、用tuple返回多个值配合C++17结构化绑定、以及返回引用允许函数调用作为左值。同时包含了占位参数的概念。
思考题:
- 函数返回局部变量时,C++编译器会执行"返回值优化"(RVO/NRVO),这是什么意思?它如何避免不必要的拷贝?
- 返回
tuple和返回结构体,哪种方式更好?各有什么优缺点? - 为什么返回局部变量的引用是危险的?编译器能检测到这个错误吗?
# 7.4 函数重载
# 7.4.1 函数重载案例
函数重载(Function Overloading)是指在同一个作用域内,可以定义多个同名函数,但它们的参数列表不同。函数重载允许使用相同的函数名来实现不同的功能!
- 函数名相同:重载函数具有相同的函数名,但参数列表不同。
- 参数列表不同:参数列表可以通过参数的类型、个数或顺序的不同来区分。
- 返回类型不是重载的依据:函数重载不依赖于函数的返回类型,只依赖于参数列表。
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
int main() {
int result1 = add(3, 5); // 调用第一个add函数
double result2 = add(2.5, 3.7); // 调用第二个add函数
int result3 = add(1, 2, 3); // 调用第三个add函数
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
函数重载使得代码更加灵活,可以根据不同的需求使用相同的函数名来实现不同的功能。
# 7.4.2 重载注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
示例:
//函数重载注意事项
//1、引用作为重载条件
void func(int &a) {
cout << "func (int &a) 调用 " << a << endl;
}
void func(const int &a) {
cout << "func (const int &a) 调用 " << a << endl;
}
//2、函数重载碰到函数默认参数
void func2(int a, int b = 10) {
cout << "func2(int a, int b = 10) 调用" << a + b << endl;
}
void func2(int a) {
cout << "func2(int a) 调用" << a << endl;
}
int main() {
int a = 10;
func(a); //调用无const
func(10);//调用有const
//func2(10); //碰到默认参数产生歧义,需要避免
return 0;
}
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
# 7.4.3 综合案例与思考
综合案例:用函数重载实现通用打印功能
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 重载1:打印整数
void print(int value) {
cout << "[int] " << value << endl;
}
// 重载2:打印浮点数
void print(double value) {
cout << "[double] " << value << endl;
}
// 重载3:打印字符串
void print(const string& value) {
cout << "[string] " << value << endl;
}
// 重载4:打印数组
void print(const int arr[], int size) {
cout << "[array] ";
for (int i = 0; i < size; ++i)
cout << arr[i] << " ";
cout << endl;
}
// 重载5:打印vector
void print(const vector<int>& vec) {
cout << "[vector] ";
for (int v : vec) cout << v << " ";
cout << endl;
}
int main() {
print(42); // 调用重载1
print(3.14); // 调用重载2
print(string("Hello")); // 调用重载3
int arr[] = {1, 2, 3};
print(arr, 3); // 调用重载4
print(vector<int>{4, 5, 6}); // 调用重载5
return 0;
}
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
案例知识融合:这个案例通过重载print函数展示了函数重载的核心——同名函数通过参数类型(int/double/string)、参数个数(arr+size vs vector)来区分。编译器根据调用时的参数自动选择匹配的版本。
思考题:
- 如果调用
print(42.0f)会匹配到哪个重载?为什么?如果同时存在print(int)和print(double),float参数会怎样? - 为什么C++不允许仅通过返回类型来区分重载函数?从编译器的角度思考这个问题。
- C++的函数重载底层是通过"名称修饰"(Name Mangling)实现的。
extern "C"为什么能禁止名称修饰?这对与C语言库交互有什么影响?
# 7.5 内联函数
# 7.5.1 内联函数是什么
C++ 中的 内联函数(Inline Function) 是一种优化技术,用于减少函数调用的开销。
通过将函数体直接插入到调用处,内联函数可以避免函数调用的额外开销(如栈帧的创建和销毁),从而提高程序的执行效率。
# 7.5.2 内联函数定义
在函数声明或定义前加上 inline 关键字:编译器会尝试将函数调用替换为函数体,以减少函数调用的开销。
inline 返回类型 函数名(参数列表) {
函数体
}
2
3
示例 1:基本用法
#include <iostream>
using namespace std;
// 定义内联函数
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 5); // 调用内联函数
cout << "Result: " << result << endl;
// 输出:
// Result: 8
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
说明:编译器会将 add(3, 5) 替换为 3 + 5,从而避免函数调用。
示例 2:类中的内联函数
在类中定义的成员函数默认是内联的。
#include <iostream>
using namespace std;
class Math {
public:
// 内联成员函数
inline int multiply(int a, int b) {
return a * b;
}
};
int main() {
Math math;
int result = math.multiply(4, 5); // 调用内联函数
cout << "Result: " << result << endl;
// Result: 20
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
示例 3:类中的静态内联函数
// 调用
auto* heart_thread = Threads::HeartBeatThread();
// 类中的静态
class Threads {
private:
static Threads *Instance();
public:
static inline Thread *HeartBeatThread() { return GlobalThread(kHeartBeatThread); } // 主要是周期上报使用,避免阻塞
};
2
3
4
5
6
7
8
9
10
# 7.5.3 内联函数特点
- 减少函数调用开销:内联函数将函数体直接插入到调用处,避免了函数调用的额外开销。
- 适用于小型函数:内联函数通常用于代码量较小的函数,因为过大的函数会导致代码膨胀。
- 由编译器决定:
inline关键字只是对编译器的建议,编译器可以选择忽略内联请求。
# 7.5.4 注意事项
- 代码膨胀:内联函数会将函数体插入到每个调用处,如果函数体较大,会导致代码膨胀,反而降低性能。
- 递归函数不能内联:递归函数无法完全展开,因此不能使用内联。
- 虚函数不能内联:虚函数的调用需要在运行时确定,因此不能内联。
- 编译器决定:
inline关键字只是建议,编译器可能会忽略内联请求。
# 7.5.5 内联函数 vs 宏
内联函数是 C++ 对 C 语言宏(#define)的改进,具有以下优势:
- 类型安全: 内联函数是类型安全的,而宏没有类型检查。
- 调试方便: 内联函数可以调试,而宏在预处理阶段被替换,无法调试。
- 作用域规则: 内联函数遵循作用域规则,而宏是全局的。
# 7.5.6 内联函数场景
- 小型函数: 函数体较小且频繁调用时,使用内联函数可以提高性能。
- 性能关键代码: 在性能关键路径上,使用内联函数可以减少函数调用开销。
- 替代宏: 在需要类型安全和调试支持的场景中,使用内联函数替代宏。
# 7.5.7 综合案例与思考
综合案例:内联函数 vs 宏 vs 普通函数
#include <iostream>
using namespace std;
// 宏定义:简单但有陷阱
#define SQUARE_MACRO(x) ((x) * (x))
// 内联函数:类型安全,行为可预测
inline int squareInline(int x) {
return x * x;
}
// 普通函数:有调用开销
int squareNormal(int x) {
return x * x;
}
int main() {
int a = 5;
// 1. 三种方式对比
cout << "=== 基本对比 ===" << endl;
cout << "宏: " << SQUARE_MACRO(a) << endl;
cout << "内联: " << squareInline(a) << endl;
cout << "普通: " << squareNormal(a) << endl;
// 2. 宏的经典陷阱
cout << "\n=== 宏的陷阱 ===" << endl;
int b = 3;
// 宏展开:((b++) * (b++)),b被递增两次!
cout << "SQUARE_MACRO(b++): " << SQUARE_MACRO(b++) << endl;
cout << "b现在是: " << b << " (被递增了两次!)" << endl;
// 内联函数不会有这个问题
b = 3;
cout << "squareInline(b++): " << squareInline(b++) << endl;
cout << "b现在是: " << b << " (只递增一次)" << endl;
// 3. 类中默认内联
cout << "\n=== 类中内联 ===" << endl;
struct Point {
int x, y;
// 类内定义的函数默认是内联的
int distSquared() const { return x * x + y * y; }
};
Point p = {3, 4};
cout << "距离²: " << p.distSquared() << endl;
return 0;
}
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
案例知识融合:这个案例对比了三种"轻量级函数"的实现方式——宏定义(预处理替换,有参数多次求值陷阱)、内联函数(编译器展开,类型安全)、普通函数(有调用开销)。特别展示了宏的SQUARE(b++)陷阱,内联函数完美避免了这个问题。
思考题:
inline关键字只是对编译器的"建议",编译器可以忽略。那么在什么条件下编译器会拒绝内联?- 现代编译器(如GCC -O2)即使没有
inline关键字也会自动内联小函数,那么inline关键字还有什么实际作用?(提示:考虑链接问题) - 为什么虚函数不能内联?如果编译器能确定虚函数调用的具体对象,是否可以内联?
# 7.6 默认参数
# 7.6.1 默认参数示例
函数默认参数是一种允许在函数声明中为某些参数指定默认值的机制。如果调用函数时没有为这些参数提供实参,那么函数会自动使用默认值。
语法:返回值类型 函数名 (参数= 默认值){}
示例:
int func(int a , int b =10 , int c= 10) {
return a + b + c;
}
//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
return a + b;
}
int main() {
cout << "ret = " << func(20, 20) << endl;
cout << "ret = " << func(100) << endl;
cout << "ret = " << func2(10,20) << endl;
cout << "ret = " << func2(10) << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
调用函数时的行为
- 如果调用函数时没有提供某个参数的值,函数会使用默认值。
- 如果提供了参数值,则会覆盖默认值。
# 7.6.2 默认参数注意点
从右向左设置默认参数,默认参数必须从右到左依次设置,不能跳过。例如:
void example(int a, int b = 10, int c = 20); // 合法
void example(int a = 10, int b, int c = 20); // 不合法
2
与函数重载的冲突,默认参数可能与函数重载产生冲突,导致编译器无法确定调用哪个函数。例如:
void example(int a);
void example(int a, int b = 10);
example(5); // 编译器无法确定调用哪个函数
2
3
# 7.6.3 综合案例与思考
综合案例:默认参数在实际开发中的应用
#include <iostream>
#include <string>
using namespace std;
// 日志函数:level和tag有默认值
void log(const string& message, const string& level = "INFO", const string& tag = "APP") {
cout << "[" << level << "][" << tag << "] " << message << endl;
}
// 创建窗口:宽高有默认值
struct Window {
string title;
int width, height;
bool fullscreen;
};
Window createWindow(const string& title, int width = 800, int height = 600, bool fullscreen = false) {
return {title, width, height, fullscreen};
}
int main() {
// 1. 使用全部默认参数
log("程序启动");
// 2. 覆盖部分默认参数
log("连接失败", "ERROR");
log("网络请求", "DEBUG", "NET");
// 3. 窗口创建
auto w1 = createWindow("游戏"); // 800x600, 窗口模式
auto w2 = createWindow("编辑器", 1920, 1080); // 自定义分辨率
auto w3 = createWindow("全屏", 1920, 1080, true);
cout << w1.title << ": " << w1.width << "x" << w1.height << endl;
cout << w2.title << ": " << w2.width << "x" << w2.height << endl;
cout << w3.title << ": " << w3.width << "x" << w3.height
<< (w3.fullscreen ? " 全屏" : " 窗口") << endl;
return 0;
}
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
案例知识融合:这个案例展示了默认参数在实际开发中的典型应用——日志函数的级别和标签默认值、窗口创建函数的宽高和全屏模式默认值。调用者可以只提供必要的参数,极大简化了API的使用。
思考题:
- 默认参数必须从右到左设置,不能跳过中间的参数。如果需要只覆盖最后一个参数(跳过中间的),有什么替代方案?
- 默认参数应该写在声明中还是定义中?如果两处都写会怎样?
- 函数重载和默认参数都能实现"少传参数也能调用"的效果,它们的适用场景有何不同?如何避免二者之间的歧义?
# 7.7 结构体与函数
# 7.7.1 结构体做参数
作用:将结构体作为参数向函数中传递
传递方式有两种:
- 值传递
- 地址传递
示例:
//学生结构体定义
struct student {
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
};
//值传递
void printStudent(student stu) {
stu.age = 28;
cout << "子函数中 姓名:" << stu.name << " 年龄: " << stu.age << " 分数:" << stu.score << endl;
}
//地址传递
void printStudent2(student *stu) {
stu->age = 28;
cout << "子函数中 姓名:" << stu->name << " 年龄: " << stu->age << " 分数:" << stu->score << endl;
}
int main() {
student stu = { "张三",18,100};
//值传递
printStudent(stu);
cout << "主函数中 姓名:" << stu.name << " 年龄: " << stu.age << " 分数:" << stu.score << endl;
cout << endl;
//地址传递
printStudent2(&stu);
cout << "主函数中 姓名:" << stu.name << " 年龄: " << stu.age << " 分数:" << stu.score << endl;
return 0;
}
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
总结:如果不想修改主函数中的数据,用值传递,反之用地址传递
# 7.7.2 结构体做返回值
#include <iostream>
#include <string>
using namespace std;
struct Person {
string name;
int age;
double height;
};
// 创建一个结构体并返回
Person createPerson(string name, int age, double height) {
Person p;
p.name = name;
p.age = age;
p.height = height;
return p;
}
int main() {
Person person = createPerson("Bob", 30, 1.75);
cout << "Name: " << person.name << endl;
cout << "Age: " << person.age << endl;
cout << "Height: " << person.height << endl;
return 0;
}
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
输出:
Name: Bob
Age: 30
Height: 1.75
2
3
# 7.7.3 结构体const场景
作用:用const来防止误操作
使用 const 关键字修饰结构体意味着该结构体实例是不可修改的,即其成员变量在声明后不能被修改。确保结构体实例在函数中被传递时不会被修改!
示例:
//学生结构体定义
struct student {
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
};
//const使用场景
//加const防止函数体中的误操作
void printStudent(const student *stu) {
//stu->age = 100; //操作失败,因为加了const修饰
cout << "姓名:" << stu->name << " 年龄:" << stu->age << " 分数:" << stu->score << endl;
}
int main() {
student stu = { "张三",18,100 };
printStudent(&stu);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 7.7.4 综合案例与思考
综合案例:结构体与函数的综合运用——员工管理
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Employee {
string name;
int age;
double salary;
};
// 值传递:创建新员工(返回结构体)
Employee createEmployee(const string& name, int age, double salary) {
return {name, age, salary};
}
// const引用传递:只读打印
void printEmployee(const Employee& emp) {
cout << emp.name << " | " << emp.age << "岁 | 薪资: " << emp.salary << endl;
}
// 引用传递:修改员工信息
void raiseSalary(Employee& emp, double percent) {
emp.salary *= (1.0 + percent / 100.0);
}
// 指针传递:可能为空的场景
bool findEmployee(const vector<Employee>& list, const string& name, Employee* result) {
for (const auto& emp : list) {
if (emp.name == name) {
if (result) *result = emp;
return true;
}
}
return false;
}
int main() {
vector<Employee> team;
team.push_back(createEmployee("张三", 28, 15000));
team.push_back(createEmployee("李四", 32, 20000));
team.push_back(createEmployee("王五", 25, 12000));
cout << "=== 员工列表 ===" << endl;
for (const auto& emp : team) printEmployee(emp);
cout << "\n=== 加薪10% ===" << endl;
for (auto& emp : team) raiseSalary(emp, 10);
for (const auto& emp : team) printEmployee(emp);
cout << "\n=== 查找员工 ===" << endl;
Employee found;
if (findEmployee(team, "李四", &found)) {
cout << "找到: "; printEmployee(found);
}
return 0;
}
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
案例知识融合:这个案例在一个员工管理场景中综合运用了结构体与函数的所有传递方式——返回结构体(createEmployee创建新对象)、const引用传递(printEmployee只读打印)、引用传递(raiseSalary修改原对象)、指针传递(findEmployee可能无结果时传nullptr)。
思考题:
- 结构体作为函数返回值时,现代编译器会进行RVO优化避免拷贝。那如果返回的是一个很大的结构体,是否还需要担心性能?
const Employee&和const Employee*都能表示"只读访问",哪种方式在语义上更好?如何选择?- 这个案例中
Employee是一个简单的数据容器(POD类型),如果它有复杂的构造函数和析构函数,传递方式的选择会有什么变化?
# 7.8 函数底层原理
# 7.8.1 函数调用的栈帧机制
函数调用的本质:每次函数调用,CPU 会在栈上创建一个栈帧(Stack Frame),包含返回地址、参数、局部变量等信息。函数返回时,栈帧被销毁。
; int result = add(3, 5); 的汇编伪码(x86-64)
; 1. 准备参数(现代x86-64用寄存器传递前几个参数)
mov edi, 3 ; 第一个参数放入edi
mov esi, 5 ; 第二个参数放入esi
; 2. 调用函数(push返回地址+跳转)
call add ; 等价于: push rip; jmp add
; 3. 使用返回值(在eax中)
mov [result], eax
add:
; 4. 创建栈帧
push rbp ; 保存旧的栈帧指针
mov rbp, rsp ; 设置新的栈帧指针
; 5. 执行函数体
lea eax, [edi+esi] ; eax = edi + esi = 3 + 5
; 6. 销毁栈帧并返回
pop rbp ; 恢复旧栈帧
ret ; pop rip; jmp rip(返回调用处)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
栈帧结构(从高地址到低地址):
+------------------+
| 调用者的栈帧 |
+------------------+
| 返回地址(rip) | <- call指令自动压入
| 旧的rbp | <- push rbp
+------------------+
| 局部变量1 | <- rbp - 8
| 局部变量2 | <- rbp - 16
| ... |
+------------------+ <- rsp(栈顶)
2
3
4
5
6
7
8
9
10
疑惑:函数调用的开销到底有多大?值得担心吗?
答疑:一次函数调用的开销包括:压栈、跳转、弹栈、返回,大约需要 5-10 个时钟周期。对于现代 CPU(GHz级别),这几乎可以忽略。
论证:但在极端性能敏感的场景中(如游戏引擎的每帧渲染循环中被调用百万次的小函数),这个开销会累积。这就是内联函数存在的意义——编译器将函数体直接嵌入调用处,消除栈帧的创建和销毁。
结果展示:现代编译器在 -O2 以上会自动内联小函数,即使你没有写 inline 关键字。你可以用 __attribute__((noinline)) (GCC) 或 __declspec(noinline) (MSVC) 来强制禁止内联,然后对比性能差异。
# 7.8.2 参数传递的底层差异
值传递的底层:将实参的值复制到栈上(小类型用寄存器),函数操作的是副本。
; void func(int x) 值传递
mov edi, [a] ; 将a的值复制到edi寄存器
call func ; 函数内操作edi,不影响a
2
3
引用传递的底层:传递的是地址(和指针传递完全相同的机器码!)引用只是语法糖。
; void func(int& x) 引用传递
lea rdi, [a] ; 将a的地址放入rdi
call func ; 函数内通过地址修改a
; 与 void func(int* x) 生成的汇编码完全相同!
2
3
4
疑惑:引用和指针在底层完全一样,那为什么 C++ 同时保留了两种机制?
答疑:虽然底层实现相同,但它们的语义不同:
| 特性 | 引用 | 指针 |
|---|---|---|
| 能否为空 | 不能,必须绑定对象 | 可以为 nullptr |
| 能否重新绑定 | 不能,一旦绑定就固定 | 可以随时指向其他对象 |
| 语法 | 透明,像使用原对象 | 需要 * 和 -> |
| 安全性 | 更高(不会空悬) | 更低(可能空指针) |
结果展示:C++ Core Guidelines 建议:当"不可能为空"时用引用,当"可能为空"或"需要重新绑定"时用指针。const T& 是只读传大对象的首选方式。
# 7.8.3 名称修饰与函数重载
名称修饰(Name Mangling):C++ 编译器为了支持函数重载,会将函数名和参数类型编码成一个唯一的符号名。
int add(int a, int b); // 编译后符号: _Z3addii
double add(double a, double b); // 编译后符号: _Z3adddd
int add(int a, int b, int c); // 编译后符号: _Z3addiii
2
3
这就是为什么仅靠返回类型不能区分重载——返回类型不参与名称修饰编码。
extern "C" 的作用:禁用名称修饰,让 C++ 函数生成与 C 兼容的符号名:
extern "C" {
int add(int a, int b); // 编译后符号: add(无修饰)
}
2
3
这是 C++ 调用 C 语言库(如系统 API)的必要手段。
# 7.9 函数训练题
训练1:实现一组数学工具函数,练习函数的声明、定义和重载:
#include <iostream>
#include <cmath>
using namespace std;
// 重载abs函数
int myAbs(int x) { return x >= 0 ? x : -x; }
double myAbs(double x) { return x >= 0 ? x : -x; }
// 重载max函数
int myMax(int a, int b) { return a > b ? a : b; }
int myMax(int a, int b, int c) { return myMax(myMax(a, b), c); }
double myMax(double a, double b) { return a > b ? a : b; }
// 默认参数:带精度的四舍五入
double roundTo(double value, int decimals = 2) {
double factor = pow(10, decimals);
return round(value * factor) / factor;
}
int main() {
cout << "myAbs(-5) = " << myAbs(-5) << endl;
cout << "myAbs(-3.14) = " << myAbs(-3.14) << endl;
cout << "myMax(3, 7) = " << myMax(3, 7) << endl;
cout << "myMax(3, 7, 5) = " << myMax(3, 7, 5) << endl;
cout << "roundTo(3.14159) = " << roundTo(3.14159) << endl;
cout << "roundTo(3.14159, 4) = " << roundTo(3.14159, 4) << endl;
return 0;
}
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
思考:标准库已经有 std::abs 和 std::max,为什么我们还要自己实现?在什么场景下自定义版本更有价值?
训练2:使用 Lambda 和 std::function 实现一个回调机制:
#include <iostream>
#include <functional>
#include <vector>
using namespace std;
// 事件处理器:接受回调函数
void processData(const vector<int>& data, function<void(int)> callback) {
for (int val : data) {
callback(val);
}
}
int main() {
vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 回调1:打印所有元素
cout << "所有元素: ";
processData(numbers, [](int x) { cout << x << " "; });
cout << endl;
// 回调2:只打印偶数
cout << "偶数: ";
processData(numbers, [](int x) { if (x % 2 == 0) cout << x << " "; });
cout << endl;
// 回调3:带状态的回调(捕获外部变量)
int sum = 0;
processData(numbers, [&sum](int x) { sum += x; });
cout << "总和: " << sum << endl;
// 回调4:使用普通函数
processData(numbers, [](int x) {
if (x > 5) cout << x << " 大于5" << endl;
});
return 0;
}
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
思考:std::function 有运行时开销(类型擦除),在性能关键路径中,用模板参数接受回调是否更好?
训练3:实现一个递归 vs 迭代的性能对比:
#include <iostream>
#include <chrono>
using namespace std;
// 递归版斐波那契(指数级复杂度)
long long fibRecursive(int n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
// 迭代版斐波那契(线性复杂度)
long long fibIterative(int n) {
if (n <= 1) return n;
long long a = 0, b = 1;
for (int i = 2; i <= n; i++) {
long long temp = a + b;
a = b;
b = temp;
}
return b;
}
int main() {
int n = 40;
auto start = chrono::high_resolution_clock::now();
long long r1 = fibRecursive(n);
auto end = chrono::high_resolution_clock::now();
auto dur1 = chrono::duration_cast<chrono::milliseconds>(end - start).count();
cout << "递归 fib(" << n << ") = " << r1 << ",耗时 " << dur1 << " ms" << endl;
start = chrono::high_resolution_clock::now();
long long r2 = fibIterative(n);
end = chrono::high_resolution_clock::now();
auto dur2 = chrono::duration_cast<chrono::milliseconds>(end - start).count();
cout << "迭代 fib(" << n << ") = " << r2 << ",耗时 " << dur2 << " ms" << endl;
return 0;
}
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
思考:递归版本慢在哪里?有没有办法优化递归版本(提示:记忆化搜索)?尾递归优化在 C++ 中可靠吗?
# 7.10 综合思考题
函数指针 vs Lambda vs std::function:C++ 中有三种"可调用对象"的表示方式。函数指针最轻量但不能捕获状态;Lambda可以捕获但每个Lambda类型唯一;
std::function最灵活但有运行时开销。你在什么场景下会选择哪种?调用约定(Calling Convention):在不同平台上,函数的参数传递方式不同——Windows x64 用
rcx, rdx, r8, r9传前4个参数,Linux x64 用rdi, rsi, rdx, rcx, r8, r9传前6个参数。这种差异对跨平台编程有什么影响?constexpr 函数:C++11 引入了
constexpr函数,它可以在编译时求值。constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n-1); }这段代码在编译时就能计算出结果。你认为constexpr函数和普通函数的边界在哪里?什么样的函数适合用constexpr?C++20 的概念(Concepts):C++20 引入了 Concepts,可以对模板函数的参数类型进行约束。这和函数重载的"参数类型不同"有什么区别?Concepts 解决了模板编程中的哪些痛点?
# 7.11 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | Lambda 引用捕获悬挂 | [&] 捕获了局部变量却让 lambda 跨作用域使用 |
| 2 | 不写返回类型导致重载错误 | auto 推导有歧义时编译失败;显式标返回类型 |
| 3 | 默认参数写在定义而非声明 | 客户端看不到默认值;统一写在头文件声明处 |
| 4 | 重载与默认参数冲突 | 调用 f() 既可匹配无参重载也可匹配有默认参数版本 |
| 5 | 形参用 T&& 但其实想要值 | 万能引用要配 std::forward 才正确转发 |