1. 函数简介
定义
函数是执行特定任务的代码块,可以被程序中的多个地方调用。使用函数可以增加代码的可重用性、可读性和可维护性。
功能
- 封装:将特定功能封装在函数中,便于管理和修改。
- 复用:同一段功能代码可以在程序中多次调用,减少代码重复。
- 结构化:通过函数组织代码,提高程序的结构清晰度。
基本语法
1 | return_type function_name(parameter_list) { |
示例:
1 |
|
输出:
1 | 5 + 3 = 8 |
2. 函数的声明与定义
函数声明(Function Declaration)
告诉编译器函数的名称、返回类型和参数类型,但不包含函数体。通常放在头文件中或在使用函数前进行声明。
语法:
1 | return_type function_name(parameter_list); |
示例:
1 | int multiply(int a, int b); // 函数声明 |
函数定义(Function Definition)
提供函数的具体实现,包括函数体。函数定义可以在源文件中或与声明一起在头文件中出现(推荐仅声明在头文件中)。
语法:
1 | return_type function_name(parameter_list) { |
示例:
1 | int multiply(int a, int b) { // 函数定义 |
函数分离编译
- 声明:放在头文件(如
multiply.h
)。 - 定义:放在源文件(如
multiply.cpp
)。
multiply.h:
1 |
|
multiply.cpp:
1 |
|
main.cpp:
1 |
|
编译命令(假设使用g++):
1 | g++ main.cpp multiply.cpp -o program |
输出:
1 | 4 * 6 = 24 |
3. 函数调用
基本调用
通过函数名称和必要的参数来调用函数。
示例:
1 |
|
输出:
1 | Hello, World! |
多次调用
同一函数可以在程序中被调用多次。
示例:
1 |
|
输出:
1 | Hello! |
4. 参数传递机制
C++中函数参数的传递方式主要有以下三种:
- 传值调用(Pass by Value)
- 传引用调用(Pass by Reference)
- 传指针调用(Pass by Pointer)
传值调用
定义:函数接收参数的副本,函数内对参数的修改不会影响原始数据。
语法:
1 | void function_name(int a) { |
示例:
1 |
|
输出:
1 | Inside function: 100 |
解释:changeValue
函数修改的是num
的副本,原始变量value
保持不变。
传引用调用
定义:函数接收参数的引用,函数内对参数的修改会影响原始数据。
语法:
1 | void function_name(int &a) { |
示例:
1 |
|
输出:
1 | Inside function: 100 |
解释:changeValue
函数通过引用修改了原始变量value
的值。
传指针调用
定义:函数接收指向参数的指针,函数内通过指针可以修改原始数据。
语法:
1 | void function_name(int *a) { |
示例:
1 |
|
输出:
1 | Inside function: 100 |
解释:changeValue
函数通过指针修改了原始变量value
的值。
选择合适的传递方式
- 传值调用:适用于不需要修改原始数据且数据量较小的情况。
- 传引用调用:适用于需要修改原始数据或传递大型数据结构以提高效率。
- 传指针调用:类似传引用调用,但更灵活,可用于传递
nullptr
或指向动态分配的内存。
5. 返回值
函数可以通过return
语句将结果返回给调用者。返回值的类型可以是基本数据类型、引用、指针、对象等。
5.1 返回基本数据类型
示例:
1 |
|
输出:
1 | Sum: 7 |
5.2 返回引用
注意:返回引用需要确保引用的对象在返回后依然有效(避免悬垂引用)。
示例:
1 |
|
输出:
1 | Max: 20 |
解释:getMax
函数返回较大的变量的引用,修改max
实际上修改了y
。
5.3 返回指针
示例:
1 |
|
输出:
1 | 0 2 4 6 8 |
解释:allocateArray
函数动态分配一个数组并返回指向数组的指针。调用者需负责释放内存。
5.4 返回对象
示例:
1 |
|
输出:
1 | Name: Alice, Age: 25 |
解释:createPerson
函数返回一个Person
对象。现代编译器通过返回值优化(RVO)减少对象拷贝,提高效率。
6. 函数重载
定义
函数重载允许在同一个作用域内定义多个名称相同但参数列表不同的函数。编译器通过参数列表的不同来区分调用哪个函数。
规则
- 函数名相同。
- 参数列表(类型、数量或顺序)不同。
- 返回类型不参与重载的区分。
示例
1 |
|
输出:
1 | add(2, 3) = 5 |
注意事项
- 仅返回类型不同的重载是非法的。
- 默认参数可能会与重载产生冲突,使用时需谨慎。
非法示例:
1 | double add(int a, int b) { |
7. 默认参数
定义
函数参数可以指定默认值,调用函数时可以省略这些参数,默认值将被使用。
规则
- 默认参数从右到左设置,不能部分设置。
- 函数声明和定义中默认参数只需在声明中指定。
示例
1 |
|
输出:
1 | Name: Bob, Age: 25, City: New York |
注意事项
- 默认参数必须从右端开始,不能跳过中间参数。
- 如果同时使用默认参数和重载,可能会产生歧义,需谨慎设计。
8. 内联函数
定义
内联函数通过在函数前加inline
关键字,建议编译器将函数代码嵌入到调用处,减少函数调用的开销。
使用场景
适用于函数体积小、调用频繁的函数,如访问器(getter)和修改器(setter)等。
示例
1 |
|
输出:
1 | Square of 5: 25 |
优点
- 减少函数调用的开销(如栈操作)。
- 可能提高程序性能。
缺点
- 使得代码体积增大,可能影响缓存性能。
- 编译器可能忽略内联请求,特别是对于复杂函数。
注意事项
- 编译器对
inline
关键字的处理是建议性质,最终是否内联由编译器决定。 - 过度使用内联函数可能导致代码膨胀。
9. 递归函数
定义
递归函数是指在函数体内调用自身的函数。递归通常用于解决可以分解为相似子问题的问题,如阶乘、斐波那契数列、树的遍历等。
基本结构
递归函数通常包含两个部分:
- 基准情形(Base Case):直接返回结果,避免无限递归。
- 递归情形(Recursive Case):将问题分解为更小的子问题并调用自身。
示例:计算阶乘
1 |
|
输出:
1 | 5! = 120 |
示例:斐波那契数列
1 |
|
输出:
1 | Fibonacci(10) = 55 |
注意事项
- 基准情形:必须正确设置,避免无限递归导致栈溢出(Stack Overflow)。
- 效率问题:一些递归实现可能效率低下(如斐波那契数列),可以通过“记忆化”或改用迭代方法优化。
- 堆栈深度:递归深度过大可能导致栈溢出,需避免深度递归。
递归优化:尾递归
尾递归是指递归调用在函数的最后一步,可以被编译器优化为循环,减少堆栈消耗。
示例:尾递归阶乘
1 |
|
输出:
1 | 5! = 120 |
解释:factorialHelper
函数的递归调用是函数的最后一步,编译器可以将其优化为迭代,减少堆栈消耗。
10. Lambda表达式
定义
Lambda表达式是C++11引入的匿名函数,便于在需要函数对象的地方快速定义和使用函数。它允许定义内联的、小型的可调用对象,无需单独定义函数。
语法
1 | [ capture_list ] ( parameter_list ) -> return_type { |
示例
1 |
|
输出:
1 | 1 2 3 4 5 |
组件说明
- 捕获列表(Capture List):指定如何访问外部变量。
[ ]
:不捕获任何外部变量。[&]
:按引用捕获所有外部变量。[=]
:按值捕获所有外部变量。[x, &y]
:按值捕获x
,按引用捕获y
。
- 参数列表(Parameter List):类似普通函数的参数列表,可以省略类型(C++14及以上支持自动类型推断)。
- 返回类型(Return Type):可指定返回类型,也可省略,编译器自动推断。
- 函数体(Function Body):Lambda的具体实现。
高级示例:捕获并排序
1 |
|
输出:
1 | Sorted data: 9 6 5 5 2 1 |
使用Lambda表达式与标准库
C++标准库中的许多算法(如std::for_each
、std::sort
、std::transform
等)常用Lambda表达式作为参数,以实现自定义的操作。
11. 函数指针与回调函数
函数指针
定义:指向函数的指针变量,保存函数的地址,可以通过指针调用函数。
声明与使用
1 |
|
输出:
1 | Hello from greet! |
函数指针作为参数
示例:
1 |
|
输出:
1 | Hi! |
回调函数
定义:通过函数指针传递的函数,通常用于在特定事件发生时执行自定义操作。
示例:基于函数指针的回调
1 |
|
输出:
1 | Before callback |
与Lambda表达式结合
函数指针也可以指向Lambda表达式,但仅限于不捕获外部变量的Lambda。
示例:
1 |
|
输出:
1 | Lambda callback! |
注意:捕获外部变量的Lambda无法转换为普通函数指针。
12. 总结与练习
课程总结
- 函数的基本概念:了解函数的作用、基本结构及使用方法。
- 函数声明与定义:掌握在头文件和源文件中分离声明与定义的方法。
- 参数传递机制:理解传值、传引用和传指针的区别及应用场景。
- 返回值:学习不同类型的返回值及其使用方法。
- 函数重载:掌握函数名相同但参数不同的重载机制。
- 默认参数:学习设定和使用函数的默认参数。
- 内联函数:了解内联函数的概念、优缺点及使用场景。
- 递归函数:理解递归的基本原理、编写方法及优化技巧。
- Lambda表达式:掌握定义和使用Lambda表达式的方法,及其在标准库中的应用。
- 函数指针与回调函数:了解函数指针的声明、使用以及如何实现回调机制。
13. 课后练习
1. 练习1
编写一个递归函数,计算斐波那契数列的第n项
问题描述
斐波那契数列是由0和1开始,后续的每一项都是前两项的和。数列如下:
1 | 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ... |
编写一个递归函数 fibonacci
,接受一个整数 n
,返回斐波那契数列的第 n
项。假设 fibonacci(0) = 0
,fibonacci(1) = 1
。
答案代码
1 |
|
输出
1 | Fibonacci(10) = 55 |
解释
- 函数
fibonacci
定义了两个基准情形:n == 0
返回0,n == 1
返回1。 - 对于
n > 1
,函数递归调用自身计算fibonacci(n - 1)
和fibonacci(n - 2)
,并返回它们的和。 - 在
main
函数中,计算并输出斐波那契数列的第10项,其值为55。
2. 练习2
使用Lambda表达式和std::sort
对一个字符串数组按长度排序
问题描述
给定一个字符串数组,使用Lambda表达式和std::sort
函数对数组中的字符串按照其长度进行排序。
答案
1 |
|
输出
1 | Fruits sorted by length: |
解释
- 定义了一个包含多个水果名称的字符串向量
fruits
。 - 使用
std::sort
对fruits
进行排序,第三个参数是一个Lambda表达式,用于指定排序的规则。 - Lambda表达式接收两个字符串
a
和b
,比较它们的长度,以实现按长度升序排序。 - 排序完成后,输出排序后的水果名称,按长度从短到长排列。
3. 练习3
实现一个简易的事件系统,允许注册和触发回调函数
问题描述
构建一个简单的事件系统,允许用户注册多个回调函数(函数指针或Lambda表达式),并在特定事件触发时调用这些回调函数。
答案
1 |
|
输出
1 | Event triggered with data = 10. Executing callbacks... |
解释
- 定义了一个带参数的回调函数类型
std::function<void(int)>
,允许回调函数接受一个整数参数。 EventSystem
类的方法triggerEvent
接受一个整数data
,并将其作为参数传递给每个回调函数。- 在
main
函数中,注册了三个带不同处理逻辑的回调函数,并在触发事件时传递参数10
。 - 回调函数根据传入的
data
执行相应的操作,展示了回调函数的灵活性。