第7章 函数

函数的基本概念

函数是C++程序的基本组成单位,它是一组执行特定任务的语句集合:

1
2
3
4
返回类型 函数名(参数列表) {
// 函数体
return 返回值;
}

函数的组成部分:

  1. 返回类型:函数返回值的类型,可以是任何有效的C++类型,包括void(无返回值)
  2. 函数名:函数的标识符,遵循C++的命名规则
  3. 参数列表:函数接受的参数,每个参数由类型和名称组成,多个参数用逗号分隔
  4. 函数体:包含函数执行的语句,用大括号包围
  5. 返回语句:可选,用于返回函数值

函数声明和定义

函数声明

函数声明告诉编译器函数的存在及其签名(返回类型、函数名和参数列表):

1
2
3
4
// 函数声明
int add(int a, int b);
double calculateArea(double radius);
void printMessage();

函数声明的位置:

  1. 在函数使用前:在调用函数之前声明
  2. 在头文件中:将函数声明放在头文件中,在需要使用的地方包含头文件

函数定义

函数定义提供了函数的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数定义
int add(int a, int b) {
return a + b;
}

double calculateArea(double radius) {
const double PI = 3.1415926535;
return PI * radius * radius;
}

void printMessage() {
std::cout << "Hello, function!" << std::endl;
}

函数调用

函数调用是执行函数的过程:

1
2
3
4
5
6
7
8
// 函数调用
int sum = add(5, 3); // 调用add函数,参数为5和3,返回值赋给sum
std::cout << "Sum: " << sum << std::endl;

double area = calculateArea(2.5); // 调用calculateArea函数
std::cout << "Area: " << area << std::endl;

printMessage(); // 调用printMessage函数,无返回值

函数调用的过程:

  1. 参数传递:将实际参数传递给形式参数
  2. 执行函数体:执行函数体内的语句
  3. 返回值:将返回值传回调用点(如果有)

参数传递

值传递

值传递是将实际参数的值复制给形式参数:

1
2
3
4
5
6
7
8
9
10
11
void increment(int x) {
x++; // 只修改局部变量x
std::cout << "Inside function: " << x << std::endl;
}

int main() {
int a = 5;
increment(a); // 传递a的值
std::cout << "Outside function: " << a << std::endl; // a仍然是5
return 0;
}

引用传递

引用传递是将实际参数的引用传递给形式参数:

1
2
3
4
5
6
7
8
9
10
11
void increment(int& x) {
x++; // 修改原始变量
std::cout << "Inside function: " << x << std::endl;
}

int main() {
int a = 5;
increment(a); // 传递a的引用
std::cout << "Outside function: " << a << std::endl; // a变为6
return 0;
}

指针传递

指针传递是将实际参数的地址传递给形式参数:

1
2
3
4
5
6
7
8
9
10
11
void increment(int* x) {
(*x)++; // 通过指针修改原始变量
std::cout << "Inside function: " << *x << std::endl;
}

int main() {
int a = 5;
increment(&a); // 传递a的地址
std::cout << "Outside function: " << a << std::endl; // a变为6
return 0;
}

常量引用传递

常量引用传递用于避免复制大对象,同时防止修改原始对象:

1
2
3
4
5
6
7
8
9
10
void printLargeObject(const std::string& s) {
// s是常量引用,不能修改
std::cout << s << std::endl;
}

int main() {
std::string largeString = "Hello, world!"; // 大字符串
printLargeObject(largeString); // 传递常量引用,避免复制
return 0;
}

默认参数

默认参数是在函数声明中为参数指定默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数声明
void printMessage(std::string message = "Hello", int count = 1);

// 函数定义
void printMessage(std::string message, int count) {
for (int i = 0; i < count; i++) {
std::cout << message << std::endl;
}
}

// 函数调用
printMessage(); // 使用默认参数:message="Hello", count=1
printMessage("Hi"); // 使用默认参数count=1
printMessage("Hey", 3); // 不使用默认参数

默认参数的规则:

  1. 从右到左:默认参数必须从右到左连续设置
  2. 声明中指定:默认参数只在函数声明中指定,定义中不指定
  3. 同一作用域:默认参数在同一作用域中只能指定一次

可变参数

C++11引入了可变参数模板,可以接受任意数量的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 可变参数模板
void print() {
std::cout << std::endl;
}

template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // 递归调用
}

// 函数调用
print(1, 2.5, "Hello", true);

返回值

基本返回类型

函数可以返回任何有效的C++类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int getInt() {
return 42;
}

double getDouble() {
return 3.14;
}

std::string getString() {
return "Hello";
}

bool getBool() {
return true;
}

无返回值

使用void作为返回类型表示函数不返回值:

1
2
3
4
5
void printHello() {
std::cout << "Hello" << std::endl;
// 可以有return语句,但不能带值
return;
}

返回引用

函数可以返回引用,用于避免复制大对象:

1
2
3
4
5
6
7
8
9
10
11
12
// 返回引用
int& getLargest(int& a, int& b) {
return (a > b) ? a : b;
}

int main() {
int x = 10, y = 20;
int& largest = getLargest(x, y); // 获取引用
largest = 30; // 修改原始变量
std::cout << "x: " << x << ", y: " << y << std::endl; // x: 10, y: 30
return 0;
}

返回指针

函数可以返回指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 返回指针
int* createArray(int size) {
int* arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i;
}
return arr;
}

int main() {
int* arr = createArray(5);
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
delete[] arr; // 释放内存
return 0;
}

返回对象

函数可以返回类的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
public:
Point(int x, int y) : x(x), y(y) {}
int x, y;
};

Point createPoint(int x, int y) {
return Point(x, y); // 返回临时对象
}

int main() {
Point p = createPoint(10, 20);
std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
return 0;
}

函数重载

函数重载是指在同一作用域中定义多个同名函数,它们的参数列表不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 函数重载
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {
return a + b;
}

std::string add(const std::string& a, const std::string& b) {
return a + b;
}

// 函数调用
int sum1 = add(1, 2); // 调用int版本
int sum2 = add(1.5, 2.5); // 调用double版本
std::string sum3 = add("Hello", " World"); // 调用string版本

函数重载的规则

  1. 参数列表不同:参数的数量、类型或顺序不同
  2. 返回类型不同:仅返回类型不同不能重载函数
  3. const修饰符:成员函数的const修饰符不同可以重载
  4. 引用修饰符:成员函数的引用修饰符(&和&&)不同可以重载
  5. 参数的cv限定符:参数的const和volatile限定符不同可以重载

函数重载的解析过程

  1. 名称查找:找到所有同名函数
  2. 可行函数筛选:筛选出参数数量匹配的函数
  3. 最佳匹配选择:根据参数类型转换规则选择最佳匹配
  4. 歧义处理:如果有多个最佳匹配,编译错误

函数重载与默认参数的交互

1
2
3
4
5
6
7
8
9
10
11
// 注意:默认参数可能导致函数重载歧义
void print(int x, int y = 0) {
std::cout << "Ints: " << x << ", " << y << std::endl;
}

void print(double x) {
std::cout << "Double: " << x << std::endl;
}

// 调用
print(5); // 歧义:print(int, int) 或 print(double)

函数重载的最佳实践

  1. 语义一致:重载函数应该具有相似的语义
  2. 避免歧义:避免可能导致歧义的重载
  3. 参数类型差异明显:确保参数类型差异足够明显
  4. 考虑模板:对于多种类型的相似操作,考虑使用函数模板

内联函数

内联函数是将函数体直接嵌入到调用点,减少函数调用的开销:

1
2
3
4
5
6
7
8
9
10
// 内联函数
inline int max(int a, int b) {
return (a > b) ? a : b;
}

int main() {
int result = max(10, 20); // 编译器可能会将max函数体直接嵌入此处
std::cout << "Max: " << result << std::endl;
return 0;
}

内联函数的工作机制

  1. 编译期处理:编译器在编译期将内联函数的调用替换为函数体
  2. 代码展开:函数体直接展开到调用点,避免函数调用的开销
  3. 无函数调用栈:不需要创建函数调用栈,减少栈空间使用
  4. 编译期优化:编译器可以对展开后的代码进行更有效的优化

内联函数的特点

  1. 关键字:使用inline关键字声明
  2. 编译器决定:inline只是建议,编译器可以根据情况忽略
  3. 定义在头文件:内联函数通常定义在头文件中,便于多个编译单元使用
  4. 链接期处理:内联函数具有内部链接,避免多个编译单元的重复定义

内联函数的优缺点

优点

  1. 减少函数调用开销:避免函数调用的栈操作、参数传递等开销
  2. 提高执行速度:对于频繁调用的小函数,性能提升明显
  3. 编译器优化:展开后的代码可以进行更有效的优化
  4. 避免函数指针歧义:内联函数可以避免函数指针导致的优化障碍

缺点

  1. 增加代码大小:函数体展开会增加目标代码大小
  2. 编译时间增加:更多的代码需要编译,增加编译时间
  3. 调试困难:内联函数在调试时可能难以设置断点
  4. 不适合大函数:大函数展开会导致代码膨胀,反而降低性能

内联函数的适用场景

  1. 频繁调用的小函数:如数学运算、访问器方法等
  2. 性能关键路径:在性能关键的代码路径中使用
  3. 类的成员函数:类的小型成员函数,特别是访问器和修改器
  4. 模板函数:模板函数默认是内联的

内联函数的最佳实践

  1. 只内联小函数:函数体不超过10-15行
  2. 避免递归:递归函数不适合内联
  3. 避免复杂控制流:包含循环、switch等复杂控制流的函数不适合内联
  4. 不要强制内联:让编译器决定是否内联,过度使用inline可能适得其反
  5. 在头文件中定义:内联函数必须在使用它的每个编译单元中可见,因此通常在头文件中定义

递归函数

递归函数是调用自身的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 递归函数:计算阶乘
int factorial(int n) {
if (n <= 1) {
return 1; // 基线条件
}
return n * factorial(n - 1); // 递归调用
}

// 递归函数:计算斐波那契数列
int fibonacci(int n) {
if (n <= 1) {
return n; // 基线条件
}
return fibonacci(n - 1) + fibonacci(n - 2); // 递归调用
}

递归函数的特点:

  1. 基线条件:递归必须有一个终止条件
  2. 递归调用:函数调用自身
  3. 栈溢出:递归深度过大可能导致栈溢出
  4. 性能:递归可能比迭代慢,因为有函数调用开销

函数指针

函数指针是指向函数的指针变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 函数定义
int add(int a, int b) {
return a + b;
}

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

// 函数指针类型
int (*operation)(int, int);

int main() {
// 赋值
operation = add;
std::cout << "Add: " << operation(5, 3) << std::endl;

operation = subtract;
std::cout << "Subtract: " << operation(5, 3) << std::endl;

return 0;
}

函数指针的应用:

  1. 回调函数:将函数作为参数传递
  2. 函数表:使用函数指针数组实现函数表
  3. 策略模式:在运行时选择不同的算法

lambda 表达式(C++11+)

lambda表达式是C++11引入的匿名函数:

C++20 lambda表达式改进

C++20对lambda表达式进行了多项改进,包括模板lambda、consteval lambda等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 模板lambda(C++20+)
auto add = []<typename T>(T a, T b) { return a + b; };
std::cout << "Add int: " << add(5, 3) << std::endl;
std::cout << "Add double: " << add(2.5, 3.5) << std::endl;

// consteval lambda(C++20+)
auto compileTimeAdd = []<typename T>(T a, T b) consteval { return a + b; };
constexpr int result = compileTimeAdd(5, 3); // 编译时计算

// 带模板参数列表的lambda
auto maxValue = []<typename T>(T a, T b) { return a > b ? a : b; };

// 泛型lambda的约束(使用concepts)
#include <concepts>
auto addNumbers = []<std::integral T>(T a, T b) { return a + b; };

编译期函数:constexpr和consteval

constexpr函数(C++11+)

constexpr函数是C++11引入的,可以在编译期计算的函数:

1
2
3
4
5
6
7
8
9
10
11
// constexpr函数(C++11+)
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}

// 编译期计算
constexpr int result1 = factorial(5); // 编译期计算

// 运行期计算
int n = 5;
int result2 = factorial(n); // 运行期计算

constexpr函数的发展

  • C++11:引入constexpr函数,限制较多(只能有一个return语句)
  • C++14:放宽限制,允许多个return语句、局部变量等
  • C++17:进一步放宽限制,允许if/switch语句、循环等
  • C++20:支持constexpr lambda、constexpr虚函数等

constexpr函数的规则

  1. 参数和返回类型:必须是字面量类型
  2. 函数体:C++11中限制较多,C++14+中可以使用更多语言特性
  3. 调用:只能调用其他constexpr函数
  4. 编译期计算:当参数是编译期常量时,函数在编译期执行

constexpr函数的适用场景

  1. 数学计算:编译期计算数学常量和函数
  2. 数组大小:计算编译期数组大小
  3. 模板参数:作为模板的非类型参数
  4. 常量表达式:在需要常量表达式的地方使用

consteval函数(C++20+)

consteval函数是C++20引入的强制编译时计算函数,确保函数在编译期执行:

1
2
3
4
5
6
7
8
9
10
11
// consteval函数
consteval int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}

// 编译期计算
constexpr int result = factorial(5); // 正确,编译期计算

// 运行期计算会报错
// int n = 5;
// int result = factorial(n); // 错误,n是运行期变量

consteval函数的特点

  1. 强制编译期执行:必须在编译期计算,否则编译错误
  2. 返回值:总是编译期常量
  3. 参数:必须是编译期常量表达式
  4. 与constexpr的区别:constexpr可以在运行期执行,consteval必须在编译期执行

constinit变量(C++20+)

constinit变量是C++20引入的常量初始化变量,确保变量在编译期初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 全局变量,在编译期初始化
constinit int global_value = 42;

// 静态变量,在编译期初始化
void function() {
static constinit int static_value = 100;
}

// 注意:constinit只保证初始化在编译期,不保证变量是const
constinit int counter = 0;
void increment() {
counter++; // 正确,counter不是const
}

constinit变量的特点

  1. 编译期初始化:变量在编译期完成初始化
  2. 静态存储期:只能用于静态存储期的变量
  3. 非const:变量本身可以是非常量
  4. 与constexpr的区别:constexpr变量是常量,constinit变量可以是变量

编译期函数的最佳实践

  1. 优先使用constexpr:对于既可以在编译期又可以在运行期执行的函数
  2. 使用consteval:对于必须在编译期执行的函数
  3. 合理使用constinit:对于需要编译期初始化但运行期修改的变量
  4. 注意编译时间:复杂的编译期计算可能增加编译时间
  5. 测试编译期执行:确保函数在编译期正确执行

C++20新特性:协程

C++20引入了协程(Coroutines),用于简化异步编程:

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
#include <coroutine>
#include <iostream>
#include <future>

// 简单的协程返回类型
struct Task {
struct promise_type {
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};

std::coroutine_handle<promise_type> handle;
};

// 协程函数
Task simpleCoroutine() {
std::cout << "Coroutine started" << std::endl;
co_return;
}

int main() {
simpleCoroutine();
std::cout << "Main function" << std::endl;
return 0;
}

// lambda表达式
auto add = [](int a, int b) { return a + b; };
std::cout << “Add: “ << add(5, 3) << std::endl;

// 带捕获的lambda
int x = 10;
auto addX = [x](int a) { return a + x; };
std::cout << “Add X: “ << addX(5) << std::endl;

// 引用捕获
auto addXRef = [&x](int a) { return a + x; };

// 捕获所有变量
auto addAll = [=](int a) { return a + x; };

// 可变lambda
auto increment = x mutable { return ++x; };

lambda表达式的语法:

1
2
3
[capture](parameters) mutable -> return_type {
// 函数体
}

函数的存储类别

外部函数

默认情况下,函数是外部的,可以在其他文件中使用:

1
2
3
4
5
6
7
8
9
10
11
12
// file1.cpp
extern int add(int a, int b); // 声明外部函数

int main() {
int sum = add(1, 2);
return 0;
}

// file2.cpp
int add(int a, int b) { // 定义外部函数
return a + b;
}

静态函数

静态函数只在定义它的文件中可见:

1
2
3
4
5
6
7
8
9
// 静态函数
static int helper() {
return 42;
}

int main() {
int result = helper(); // 可以在同一文件中调用
return 0;
}

主函数

主函数是C++程序的入口点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基本形式
int main() {
// 函数体
return 0;
}

// 带命令行参数的形式
int main(int argc, char* argv[]) {
// argc: 参数数量
// argv: 参数数组
for (int i = 0; i < argc; i++) {
std::cout << "Argument " << i << ": " << argv[i] << std::endl;
}
return 0;
}

主函数的特点:

  1. 返回类型:必须是int
  2. 参数:可选,可以带命令行参数
  3. 返回值:0表示成功,非0表示失败
  4. 唯一入口:每个C++程序只能有一个主函数

函数的最佳实践

1. 函数设计

  • 单一职责:每个函数只做一件事
  • 函数名清晰:函数名应该清晰地表达函数的功能
  • 参数数量:函数参数不宜过多,一般不超过5个
  • 参数顺序:将最常用的参数放在前面
  • 返回值明确:返回值应该明确表达函数的结果

2. 代码风格

  • 缩进:使用一致的缩进风格
  • 注释:为复杂函数添加注释,说明功能、参数和返回值
  • 命名规范:使用有意义的函数名和参数名
  • 空行:在函数定义之间添加空行

3. 性能考虑

  • 避免不必要的复制:对于大对象,使用引用或指针传递
  • 内联小函数:对于频繁调用的小函数,考虑使用内联
  • 避免深度递归:对于深度递归,考虑使用迭代
  • 函数开销:了解函数调用的开销,合理使用函数

4. 错误处理

  • 返回错误码:对于简单错误,返回错误码
  • 抛出异常:对于严重错误,抛出异常
  • 断言:对于逻辑错误,使用断言
  • 参数验证:在函数开始时验证参数的有效性

常见错误和陷阱

1. 函数声明和定义不匹配

1
2
3
4
5
6
7
8
// 错误:声明和定义不匹配
// 声明
int add(int a, int b);

// 定义
int add(int a, int b, int c) {
return a + b + c;
}

2. 忘记返回值

1
2
3
4
// 错误:忘记返回值
int getValue() {
// 没有return语句
}

3. 栈溢出

1
2
3
4
// 错误:无限递归导致栈溢出
int infiniteRecursion() {
return infiniteRecursion();
}

4. 参数传递错误

1
2
3
4
5
6
7
8
9
10
11
// 错误:值传递修改不了原始变量
void modify(int x) {
x = 100;
}

int main() {
int a = 10;
modify(a);
std::cout << a; // 输出10,不是100
return 0;
}

5. 函数重载歧义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误:函数重载歧义
void print(int x) {
std::cout << "Int: " << x << std::endl;
}

void print(double x) {
std::cout << "Double: " << x << std::endl;
}

int main() {
print(5); // 正确:调用int版本
print(5.5); // 正确:调用double版本
print(5.0f); // 错误:float可以转换为int或double,产生歧义
return 0;
}

小结

本章介绍了C++中函数的基本概念、声明和定义、参数传递、返回值、函数重载、内联函数、递归函数、函数指针和lambda表达式等内容。通过本章的学习,你应该能够:

  1. 掌握函数的声明和定义方法
  2. 理解不同的参数传递方式(值传递、引用传递、指针传递)
  3. 掌握函数重载的规则和应用
  4. 理解内联函数、递归函数和函数指针的使用
  5. 了解C++11引入的lambda表达式
  6. 遵循函数设计的最佳实践

函数是C++程序的基本组成单位,合理使用函数可以提高代码的可读性、可维护性和可重用性。在后续章节中,我们将学习数组、指针、类等更高级的C++特性,这些特性将与函数结合使用,帮助我们构建更复杂、更强大的程序。