7.1 复习函数的基本知识

函数是 C++ 程序的基本构建块,它们允许我们将代码组织成可重用的、逻辑独立的单元。使用函数可以使程序更模块化、更易于理解和维护。本节将复习函数的基本概念:定义、原型和调用。

7.1.1 定义函数

函数定义 (Function Definition) 包含了函数的实际代码,它说明了函数做什么以及如何做。一个函数定义包括以下几个部分:

  1. 返回类型 (Return Type): 函数执行完毕后返回给调用者的值的类型。如果函数不返回任何值,则返回类型为 void
  2. 函数名 (Function Name): 用于调用函数的标识符。命名规则与变量名相同。
  3. 参数列表 (Parameter List): 位于函数名后的圆括号 () 中,用于接收传递给函数的值。参数之间用逗号分隔,每个参数都需要指定类型和名称。如果函数不接受任何参数,括号内可以为空或写 void
  4. 函数体 (Function Body): 位于花括号 {} 中,包含实现函数功能的 C++ 语句。

语法:

1
2
3
4
5
return_type function_name(parameter_list) {
// 函数体:包含执行任务的语句
// 如果 return_type 不是 void,则需要 return 语句返回值
return value; // (如果 return_type 不是 void)
}

示例:定义一个简单的函数

这个函数不接受参数,也不返回值 (void),只是打印一条消息。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

// 函数定义
void print_greeting() {
std::cout << "Hello from the function!" << std::endl;
}

int main() {
// 调用函数
print_greeting();
return 0;
}

示例:定义一个带参数并返回值的函数

这个函数接受两个整数作为参数,并返回它们的和。

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

// 函数定义
int add_numbers(int num1, int num2) {
int sum = num1 + num2;
return sum; // 返回计算结果
}

int main() {
int a = 5;
int b = 3;
// 调用函数并将返回值存储在 result 变量中
int result = add_numbers(a, b);
std::cout << "The sum of " << a << " and " << b << " is: " << result << std::endl; // 输出: The sum of 5 and 3 is: 8
return 0;
}

7.1.2 函数原型和函数调用

函数原型 (Function Prototype) 也称为函数声明 (Function Declaration),它告诉编译器函数的名称、返回类型以及参数列表(类型和顺序),但不包含函数体。原型通常放在 main() 函数之前或单独的头文件中。

为什么需要原型?

C++ 编译器在处理代码时需要“预先知道”函数的接口(它接受什么参数,返回什么类型),然后才能正确地处理对该函数的调用。如果函数定义出现在调用它的代码之后,编译器在遇到调用时就不知道该函数是否存在或如何调用它,从而导致编译错误。函数原型解决了这个问题。

语法:

1
return_type function_name(parameter_type_list); // 注意末尾的分号

参数名称在原型中是可选的,但写上通常能提高可读性。

函数调用 (Function Call) 是指在程序中执行一个函数。通过使用函数名,并在括号中提供所需的实际参数(称为**实参 (Arguments)**)来完成调用。

语法:

1
2
3
4
5
6
7
// 对于不返回值的函数
function_name(argument_list);

// 对于返回值的函数
variable = function_name(argument_list);
// 或者直接在表达式中使用
// std::cout << function_name(argument_list);

示例:使用函数原型

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

// 函数原型 (声明)
void display_message(const char* msg); // 声明函数接口
int multiply(int x, int y); // 声明函数接口

int main() {
// 函数调用
display_message("This is a message.");

int num1 = 6;
int num2 = 7;
int product = multiply(num1, num2); // 调用函数
std::cout << "The product of " << num1 << " and " << num2 << " is: " << product << std::endl; // 输出: 42

return 0;
}

// 函数定义 (实现) - 可以放在 main 之后,因为原型已经提供了信息
void display_message(const char* msg) {
std::cout << msg << std::endl;
}

// 函数定义 (实现)
int multiply(int x, int y) {
return x * y;
}

总结:

  • 函数定义: 提供了函数的完整实现(代码)。
  • 函数原型: 声明了函数的接口(名称、返回类型、参数类型),让编译器知道如何调用它,通常放在调用之前。
  • 函数调用: 通过函数名和实参来执行函数定义的代码。

函数是构建结构化和可维护 C++ 程序的核心工具。

7.2 函数参数和按值传递

函数参数是函数与调用它的代码之间传递信息的桥梁。当调用函数时,我们提供的值(实参)会被传递给函数定义中声明的变量(形参)。C++ 默认的参数传递方式是**按值传递 (Pass by Value)**。

形参 (Parameters): 在函数定义或函数原型中声明的变量,它们是函数内部使用的局部变量,用于接收调用时传入的值。

实参 (Arguments): 在函数调用时传递给函数的具体值或变量。

按值传递 (Pass by Value):

当使用按值传递时,函数会创建每个形参的副本。调用函数时提供的实参的值会被复制到这些新的形参变量中。函数内部对形参所做的任何修改都只影响这个副本,不会影响到函数调用中使用的原始实参。

示例:演示按值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

// 函数定义:接受一个 int 参数 (形参 n)
// 按值传递,n 是 value 的副本
void modify_value(int n) {
std::cout << "Inside function (before modification): n = " << n << std::endl;
n = n * 2; // 修改形参 n 的值
std::cout << "Inside function (after modification): n = " << n << std::endl;
}

int main() {
int value = 10; // 实参

std::cout << "Before calling function: value = " << value << std::endl;

// 调用函数,将 value 的值传递给形参 n
modify_value(value);

std::cout << "After calling function: value = " << value << std::endl; // value 的值并未改变

return 0;
}

输出:

1
2
3
4
Before calling function: value = 10
Inside function (before modification): n = 10
Inside function (after modification): n = 20
After calling function: value = 10

代码解释:

  1. main 函数中的变量 value 初始化为 10。
  2. 调用 modify_value(value) 时,value 的值 (10) 被复制给了 modify_value 函数的形参 n
  3. modify_value 函数内部,n 的值被修改为 20。但这仅仅修改了 n 这个局部副本
  4. 当函数执行完毕返回 main 后,main 函数中的原始变量 value 仍然是 10,没有受到函数内部修改的影响。

优点:

  • 安全性: 保护了原始数据不被函数意外修改。

缺点:

  • 效率: 对于大型数据结构(如复杂的类对象或结构体),复制整个对象可能消耗较多的时间和内存。在这种情况下,后续章节将介绍的按引用传递或按指针传递可能更高效。

7.2.1 多个参数

函数可以接受任意数量的参数。在函数定义和原型中,参数之间用逗号 , 分隔。调用函数时,提供的实参也必须用逗号分隔,并且数量、类型和顺序应与形参列表匹配。

示例:接受多个参数的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

// 函数原型:接受两个 double 和一个 char
void display_data(double length, double width, char unit) {
std::cout << "Length: " << length << " " << unit << std::endl;
std::cout << "Width: " << width << " " << unit << std::endl;
std::cout << "Area: " << length * width << " sq " << unit << std::endl;
}

int main() {
double len = 5.5;
double wid = 2.0;
char symbol = 'm';

// 调用函数,传递三个实参
display_data(len, wid, symbol);

return 0;
}

输出:

1
2
3
Length: 5.5 m
Width: 2 m
Area: 11 sq m

7.2.2 另外一个接受两个参数的函数

下面是另一个简单的例子,计算并返回两个整数中的较大值。

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

// 函数定义:接受两个 int 参数,返回较大的那个 int
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
// 或者使用三元运算符: return (a > b) ? a : b;
}

int main() {
int num1 = 15;
int num2 = 28;

// 调用函数,传递两个实参
int larger_value = max(num1, num2);

std::cout << "Between " << num1 << " and " << num2
<< ", the larger value is: " << larger_value << std::endl; // 输出: 28

// 也可以直接在输出语句中调用
std::cout << "The max of 100 and 99 is: " << max(100, 99) << std::endl; // 输出: 100

return 0;
}

这个例子再次展示了如何定义和调用带有多个参数的函数,并且该函数还返回一个值。参数 ab 也是按值传递的。

7.3 函数和数组

将数组传递给函数是 C++ 中常见的操作,但其工作方式与传递普通变量(如 intdouble)有显著不同。理解这种差异对于正确使用数组作为函数参数至关重要。

与基本类型默认使用“按值传递”(创建副本)不同,当将数组传递给函数时,C++ 不会复制整个数组。相反,它传递的是数组第一个元素的内存地址。这意味着函数实际上接收的是一个指向数组起始位置的指针。

7.3.1 函数如何使用指针来处理数组

因为函数接收的是数组的地址(指针),所以它可以通过这个地址直接访问和修改原始数组的内容。这与按值传递完全不同,后者操作的是副本。

在函数定义中,接收数组参数有几种等效的语法:

1
2
3
4
5
6
7
8
// 语法 1: 使用指针表示法
void process_array(int* arr, int size);

// 语法 2: 使用带空括号的数组表示法
void process_array(int arr[], int size);

// 语法 3: 使用带指定大小的数组表示法 (大小会被忽略,不推荐)
// void process_array(int arr[10], int size); // 这里的 10 实际上没有作用

这三种语法在函数参数列表中是等效的,它们都告诉编译器 arr 是一个指向 int 的指针。最常用的是语法 1 和语法 2。

关键点: 无论使用哪种语法,函数都不知道数组的实际大小。因此,通常需要将数组的大小作为单独的参数传递给函数。

示例:函数修改数组元素

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

// 函数原型:接收一个 int 指针 (数组) 和大小
void double_elements(int arr[], int size) { // 或者 int* arr
std::cout << "Inside function: Modifying array elements..." << std::endl;
for (int i = 0; i < size; ++i) {
arr[i] *= 2; // 直接修改原始数组的元素
}
}

int main() {
int my_array[] = {1, 2, 3, 4, 5};
int array_size = sizeof(my_array) / sizeof(my_array[0]); // 计算数组大小

std::cout << "Before calling function: ";
for (int i = 0; i < array_size; ++i) {
std::cout << my_array[i] << " ";
}
std::cout << std::endl;

// 调用函数,传递数组名 (即地址) 和大小
double_elements(my_array, array_size);

std::cout << "After calling function: ";
for (int i = 0; i < array_size; ++i) {
std::cout << my_array[i] << " "; // 数组内容已被修改
}
std::cout << std::endl;

return 0;
}

输出:

1
2
3
Before calling function: 1 2 3 4 5
Inside function: Modifying array elements...
After calling function: 2 4 6 8 10

代码解释:

  1. main 函数定义了一个数组 my_array
  2. 调用 double_elements(my_array, array_size) 时,my_array (代表数组首元素的地址) 被传递给函数的 arr 参数,array_size 被传递给 size 参数。
  3. 函数内部通过指针 arr 访问并修改了 main 函数中定义的 my_array 的元素。
  4. 函数返回后,main 函数中的 my_array 的内容确实发生了改变。

7.3.2 将数组作为参数意味着什么

将数组名传递给函数时,会发生所谓的“数组退化”(Array Decay)。数组名会“退化”成指向其第一个元素的指针。这就是为什么函数参数 int arr[]int* arr 是等价的。

Implications:

  1. 效率: 不需要复制整个数组,传递地址非常快,尤其是对于大数组。
  2. 修改能力: 函数可以直接修改调用者提供的原始数组。这既是优点(允许函数“返回”修改后的数组)也是缺点(可能意外修改数据)。
  3. 丢失大小信息: 函数本身无法知道数组的大小。必须显式传递大小信息。

7.3.3 更多数组函数示例

示例 1: 计算数组元素总和

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

// 函数:计算数组元素的总和
int sum_array(const int arr[], int size) { // 使用 const 防止意外修改
int total = 0;
for (int i = 0; i < size; ++i) {
total += arr[i];
}
return total;
}

int main() {
int data[] = {10, 20, 30, 40};
int size = sizeof(data) / sizeof(data[0]);
int sum = sum_array(data, size);
std::cout << "Sum of array elements: " << sum << std::endl; // 输出: 100
return 0;
}

示例 2: 查找数组中的最大值

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
#include <iostream>
#include <limits> // 为了使用 INT_MIN

// 函数:查找数组中的最大值
int find_max(const int* arr, int size) { // 使用指针表示法和 const
if (size <= 0) {
std::cerr << "Error: Array size must be positive." << std::endl;
return std::numeric_limits<int>::min(); // 返回可能的最小值作为错误指示
}
int max_val = arr[0];
for (int i = 1; i < size; ++i) {
if (arr[i] > max_val) {
max_val = arr[i];
}
}
return max_val;
}

int main() {
int scores[] = {88, 95, 72, 100, 91};
int count = sizeof(scores) / sizeof(scores[0]);
int highest_score = find_max(scores, count);
std::cout << "Highest score: " << highest_score << std::endl; // 输出: 100
return 0;
}

7.3.4 使用数组区间的函数

除了传递数组首地址和大小之外,另一种常见且更灵活的方法是传递指向数组开始结束之后位置的指针(或迭代器,STL 中常用)。这定义了一个处理范围 [begin, end)(包含 begin,不包含 end)。

示例:使用指针区间求和

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

// 函数:计算从 begin 到 end (不含 end) 的元素和
int sum_range(const int* begin, const int* end) {
int total = 0;
// 循环直到当前指针达到 end 指针
for (const int* ptr = begin; ptr != end; ++ptr) {
total += *ptr; // 解引用指针获取元素值
}
return total;
}

int main() {
int data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int size = sizeof(data) / sizeof(data[0]);

// 计算整个数组的和
int total_sum = sum_range(data, data + size); // data + size 指向数组末尾之后的位置
std::cout << "Total sum: " << total_sum << std::endl; // 输出: 55

// 计算数组一部分的和 (例如,索引 2 到 6,即元素 3, 4, 5, 6, 7)
int partial_sum = sum_range(data + 2, data + 7); // data+2 指向第3个元素, data+7 指向第8个元素 (区间终点)
std::cout << "Partial sum (index 2 to 6): " << partial_sum << std::endl; // 输出: 25

return 0;
}

这种方法在 C++ 标准库算法中非常常用。

7.3.5 指针和 const

如前面的示例所示,如果函数不应该修改传入的数组,应该在函数参数中使用 const 关键字。这是一种重要的编程实践,可以提高代码的安全性和清晰度。

1
2
3
4
5
6
7
8
9
// 这个函数承诺不会修改 arr 指向的数组内容
void print_array(const int arr[], int size) {
std::cout << "Array elements: ";
for (int i = 0; i < size; ++i) {
// arr[i] = 0; // 错误!编译器会阻止修改 const 数据
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}

使用 const 有两个主要好处:

  1. 防止意外修改: 编译器会检查并阻止函数内部对 const 参数的修改尝试。
  2. 表明意图: 向函数的调用者表明该函数不会改变传入的数组,使得函数接口更清晰。
  3. 接受更广泛的参数: const 参数的函数可以接受 const 数组和非 const 数组作为实参,而非 const 参数的函数只能接受非 const 数组。

总结来说,将数组传递给函数是通过传递指向其首元素的指针来实现的。这使得函数能够访问和(如果未使用 const)修改原始数组,但也要求调用者通常需要额外传递数组的大小或使用指针区间来界定操作范围。

7.4 函数和二维数组

将二维数组传递给函数比传递一维数组稍微复杂一些。与一维数组类似,二维数组名在传递时也会“退化”成指向其第一个元素的指针。但二维数组的第一个元素本身是一个一维数组。因此,传递的是指向一维数组的指针。

为了让函数能够正确地计算元素在内存中的位置,编译器需要知道除第一维(行数)之外的所有其他维度的大小(列数,以及更高维度的相应大小)。

关键点: 在函数参数中声明二维数组时,必须指定除第一维之外的所有维度的大小。第一维的大小是可选的(通常省略)。

语法:

假设有一个二维数组 int data[3][4];

函数原型或定义可以这样写:

1
2
3
4
5
6
7
8
9
// 语法 1: 指定列数
void process_2d_array(int arr[][4], int rows); // 必须指定列数 4

// 语法 2: 使用指向数组的指针 (更精确地反映底层机制)
// arr 是一个指针,指向一个包含 4 个 int 的数组
void process_2d_array(int (*arr)[4], int rows);

// 语法 3: 可以包含第一维,但通常省略
// void process_2d_array(int arr[3][4], int rows); // 这里的 3 实际上会被忽略

为什么必须指定列数?

考虑二维数组 arr[rows][cols] 在内存中是线性存储的。要访问元素 arr[i][j],编译器需要计算其内存地址,公式通常类似于:基地址 + (i * cols + j) * sizeof(元素类型)。可以看到,计算地址需要知道 cols(列数)的值。如果函数不知道列数,就无法正确地进行指针运算来定位元素。

示例:处理二维数组的函数

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

const int COLS = 4; // 使用常量定义列数,方便维护

// 函数原型:计算二维数组所有元素的和
// 参数:二维数组 (必须指定列数 COLS),行数
int sum_2d_array(int arr[][COLS], int rows);

// 函数原型:打印二维数组
void print_2d_array(const int arr[][COLS], int rows); // 使用 const 防止修改

int main() {
int data[3][COLS] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int num_rows = 3;

std::cout << "Original 2D Array:" << std::endl;
print_2d_array(data, num_rows);

int total_sum = sum_2d_array(data, num_rows);
std::cout << "\nSum of all elements: " << total_sum << std::endl; // 输出: 78

return 0;
}

// 函数定义:计算二维数组所有元素的和
int sum_2d_array(int arr[][COLS], int rows) {
int sum = 0;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < COLS; ++j) {
sum += arr[i][j];
}
}
return sum;
}

// 函数定义:打印二维数组
void print_2d_array(const int arr[][COLS], int rows) {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < COLS; ++j) {
std::cout << arr[i][j] << "\t"; // 使用制表符分隔
}
std::cout << std::endl; // 每行结束后换行
}
}

代码解释:

  1. 我们定义了一个全局常量 COLS 来表示数组的列数。这使得在函数原型和定义中指定列数更加方便和一致。
  2. sum_2d_arrayprint_2d_array 函数的第一个参数都声明为 int arr[][COLS]const int arr[][COLS],明确指定了列的大小。
  3. main 函数中定义了一个 3x4 的二维数组 data
  4. 调用函数时,传递数组名 data(它代表指向第一个包含 COLSint 的一维数组的指针)和行数 num_rows
  5. 函数内部可以使用标准的 arr[i][j] 语法来访问数组元素,因为编译器知道列数 COLS,可以正确计算每个元素的地址。

总结:

  • 将二维(或更高维)数组传递给函数时,必须在函数参数中指定除第一维之外的所有维度的大小。
  • 这是因为函数需要这些维度信息来进行正确的指针运算以访问数组元素。
  • 通常将数组维度(尤其是除第一维外的维度)定义为常量,以提高代码的可读性和可维护性。
  • 与一维数组一样,函数操作的是原始数组,而不是副本(除非使用了 const,否则函数可以修改原始数组)。

7.5 函数和 C-风格字符串

C-风格字符串本质上是字符数组 (char[]),其末尾有一个特殊的空字符 (\0) 来标记字符串的结束。因此,将 C-风格字符串传递给函数遵循与传递普通数组相同的规则:传递的是指向字符串第一个字符的指针 (char*)。

函数不需要单独的参数来指定字符串的长度,因为它可以遍历字符序列直到遇到空字符 \0 来确定字符串的结束。

7.5.1 将 C-风格字符串作为参数的函数

当函数接收 C-风格字符串作为参数时,通常使用 char*const char* 类型。如果函数不打算修改字符串内容,强烈建议使用 const char*,这可以防止意外修改,并允许函数接受字符串字面值(它们是常量)和 const 字符数组作为参数。

示例 1: 计算 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
#include <iostream>

// 函数原型:计算 C 风格字符串的长度
// 使用 const char* 因为我们不修改字符串
unsigned int string_length(const char* str) {
unsigned int length = 0;
// 循环直到遇到空字符 '\0'
while (*str != '\0') {
length++;
str++; // 移动指针到下一个字符
}
return length;
}

int main() {
char greeting[] = "Hello";
const char* message = "World!";

std::cout << "Length of \"" << greeting << "\": " << string_length(greeting) << std::endl; // 输出: 5
std::cout << "Length of \"" << message << "\": " << string_length(message) << std::endl; // 输出: 6
std::cout << "Length of \"C++\": " << string_length("C++") << std::endl; // 输出: 3

return 0;
}

示例 2: 打印 C-风格字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

// 函数:打印 C 风格字符串
void print_string(const char* str) {
// 可以直接使用 cout,它知道如何处理 char* 直到遇到 '\0'
std::cout << str;
// 或者手动遍历
// while (*str != '\0') {
// std::cout << *str;
// str++;
// }
}

int main() {
char name[] = "Alice";
std::cout << "Name: ";
print_string(name);
std::cout << std::endl;

return 0;
}

重要注意事项:

  • 空字符终止: 处理 C-风格字符串的函数依赖于空字符 \0 来确定结束。如果传递的字符数组没有正确地以 \0 结尾,函数可能会读取超出数组边界的内存,导致未定义行为(通常是程序崩溃或数据损坏)。
  • 缓冲区溢出: 如果函数需要修改传入的字符串或将数据写入字符数组缓冲区(例如 strcpy, strcat 的自定义版本),必须确保操作不会超出缓冲区的分配大小,否则会发生缓冲区溢出,这是一个严重的安全漏洞。通常需要传递缓冲区的大小作为额外参数。
  • const 正确性: 明确使用 const char* 来表示函数不会修改输入字符串。

7.5.2 返回 C-风格字符串的函数

让函数返回一个 C-风格字符串(即 char*)比传递它要复杂得多,并且充满了潜在的陷阱。主要问题在于字符串数据存储在哪里以及其生命周期。

常见的错误方式 (危险!):

  1. 返回指向局部变量的指针:
    1
    2
    3
    4
    5
    6
    // !!! 错误且危险的示例 !!!
    char* create_temp_string() {
    char temp[] = "Temporary";
    return temp; // 错误!temp 是局部数组,函数返回后内存被释放
    // 返回的指针将指向无效内存 (悬挂指针)
    }
    调用 create_temp_string() 后得到的指针是无效的,解引用它会导致未定义行为。

可行的(但各有缺点)方式:

  1. 返回指向静态局部变量的指针:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>

    // 使用静态局部变量
    const char* get_static_message() {
    static char message[] = "Static Message";
    // message 在程序整个生命周期内存在,但只有一个实例
    return message;
    }

    int main() {
    const char* msg1 = get_static_message();
    std::cout << "Msg1: " << msg1 << std::endl; // 输出: Static Message

    // 如果函数被再次调用,它会返回指向 *同一个* 静态内存的指针
    // 如果函数内部修改了静态变量,所有之前的指针都会看到变化
    // 这种方式不是线程安全的
    const char* msg2 = get_static_message();
    std::cout << "Msg2: " << msg2 << std::endl; // 输出: Static Message
    // msg1 和 msg2 指向同一块内存

    return 0;
    }

    缺点: 返回的指针指向的内存在后续调用中可能被覆盖(如果函数修改静态变量的话),并且这种方法不是线程安全的。

  2. 返回指向动态分配内存的指针 (new):

    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
    #include <iostream>
    #include <cstring> // 为了 strcpy

    // 返回动态分配的内存
    char* create_dynamic_string(const char* initial_value) {
    char* dynamic_str = new char[strlen(initial_value) + 1]; // 分配内存 (+1 for '\0')
    strcpy(dynamic_str, initial_value); // 复制内容
    return dynamic_str; // 返回指向新分配内存的指针
    }

    int main() {
    char* str1 = create_dynamic_string("Dynamic Data");
    std::cout << "Dynamic String 1: " << str1 << std::endl;

    // !!! 重要:调用者必须负责释放内存 !!!
    delete[] str1;
    str1 = nullptr; // 好习惯:释放后置空指针

    char* str2 = create_dynamic_string("More Data");
    std::cout << "Dynamic String 2: " << str2 << std::endl;
    delete[] str2;
    str2 = nullptr;

    return 0;
    }

    缺点: 调用者必须记住使用 delete[] 来释放返回的指针所指向的内存,否则会导致内存泄漏。这种责任转移很容易出错。

  3. 传递由调用者分配的缓冲区 (推荐方式):
    这是最安全、最常用的方法。函数接受一个指向调用者提供的缓冲区的指针和该缓冲区的大小,然后将结果字符串写入该缓冲区。

    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
    #include <iostream>
    #include <cstring> // 为了 strncpy

    // 函数将结果写入调用者提供的缓冲区
    // 返回值可以指示成功/失败或写入的字符数
    bool format_greeting(char buffer[], size_t buffer_size, const char* name) {
    // 使用 snprintf 或其他安全函数来防止缓冲区溢出
    int written = snprintf(buffer, buffer_size, "Hello, %s!", name);

    // 检查是否成功且未截断 (snprintf 返回值特性)
    if (written > 0 && written < buffer_size) {
    return true; // 成功
    } else {
    // 可能缓冲区太小或发生错误
    if (buffer_size > 0) buffer[0] = '\0'; // 确保缓冲区为空字符串
    return false; // 失败
    }
    }

    int main() {
    const size_t BUF_SIZE = 50;
    char my_buffer[BUF_SIZE];

    if (format_greeting(my_buffer, BUF_SIZE, "Alice")) {
    std::cout << "Formatted Greeting: " << my_buffer << std::endl; // 输出: Hello, Alice!
    } else {
    std::cerr << "Failed to format greeting (buffer too small?)" << std::endl;
    }

    if (format_greeting(my_buffer, 10, "Bob The Builder")) { // 尝试用小缓冲区
    std::cout << "Formatted Greeting: " << my_buffer << std::endl;
    } else {
    std::cerr << "Failed to format greeting (buffer too small?)" << std::endl; // 这将被打印
    std::cout << "Buffer content after fail: \"" << my_buffer << "\"" << std::endl; // 输出: "" (因为函数清空了)
    }

    return 0;
    }

    优点: 内存管理由调用者负责,函数本身不分配内存,避免了内存泄漏和悬挂指针的风险。函数接口清晰地表明了其对缓冲区的需求。

总结:

  • 将 C-风格字符串作为参数传递给函数时,传递的是 char*,函数依赖 \0 确定结束,使用 const char* 防止意外修改。
  • 让函数返回 C-风格字符串 (char*) 比较棘手。返回指向局部变量的指针是错误的。返回静态变量指针有局限性。返回动态分配内存 (new) 要求调用者管理内存 (delete[])。
  • 最安全、最推荐的方式是让调用者提供缓冲区,函数将结果写入该缓冲区,并通常传递缓冲区大小以防止溢出。
  • 在现代 C++ 中,通常更推荐使用 std::string 类来处理字符串,因为它会自动管理内存,避免了许多与 C-风格字符串相关的陷阱。

7.6 函数和结构

结构 (struct) 是一种用户定义的复合类型,可以将不同类型的数据项组合成一个单一的实体。与基本数据类型一样,结构也可以作为参数传递给函数,并且函数也可以返回结构类型的值。

7.6.1 传递和返回结构

默认情况下,结构与基本数据类型(如 int, double)一样,是按值传递 (Pass by Value) 给函数的。这意味着当将一个结构变量作为实参传递给函数时,函数会创建该结构的一个完整副本(形参),并在函数内部操作这个副本。对副本成员的任何修改都不会影响原始结构变量。

同样,函数也可以声明一个结构类型作为其返回类型。当函数返回一个结构时,它会创建一个该结构的临时副本,并将其返回给调用者。

示例:按值传递和返回结构

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
#include <iostream>
#include <string> // 使用 string 类成员

// 定义一个简单的结构
struct Point {
double x;
double y;
};

// 函数原型:按值接收 Point 结构,并打印其坐标
void display_point(Point p) { // p 是 pt_main 的副本
std::cout << "Displaying Point (inside function): (" << p.x << ", " << p.y << ")" << std::endl;
// 修改副本的值,不会影响原始结构
p.x = 100.0;
std::cout << "Modified copy inside function: (" << p.x << ", " << p.y << ")" << std::endl;
}

// 函数原型:创建一个新的 Point 结构并按值返回
Point create_point(double x_val, double y_val) {
Point new_p;
new_p.x = x_val;
new_p.y = y_val;
std::cout << "Creating Point (" << new_p.x << ", " << new_p.y << ") inside create_point." << std::endl;
return new_p; // 返回 Point 结构的副本
}

int main() {
Point pt_main = {3.0, 4.0};

std::cout << "Before calling display_point: (" << pt_main.x << ", " << pt_main.y << ")" << std::endl;
// 按值传递 pt_main
display_point(pt_main);
std::cout << "After calling display_point: (" << pt_main.x << ", " << pt_main.y << ")" << std::endl; // 原始结构未改变

std::cout << "\nCalling create_point..." << std::endl;
// 接收函数返回的 Point 结构副本
Point pt_returned = create_point(5.5, -1.2);
std::cout << "Returned Point in main: (" << pt_returned.x << ", " << pt_returned.y << ")" << std::endl;

return 0;
}

输出:

1
2
3
4
5
6
7
8
Before calling display_point: (3, 4)
Displaying Point (inside function): (3, 4)
Modified copy inside function: (100, 4)
After calling display_point: (3, 4)

Calling create_point...
Creating Point (5.5, -1.2) inside create_point.
Returned Point in main: (5.5, -1.2)

按值传递的优缺点:

  • 优点: 保护原始数据不被函数修改,概念简单。
  • 缺点: 对于包含大量数据成员的结构,复制整个结构可能效率低下,消耗时间和内存。

7.6.2 另一个处理结构的函数示例

假设我们需要一个函数来计算两个点之间的中点。

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

struct Point {
double x;
double y;
};

// 函数:计算两个 Point 的中点,并返回一个新的 Point
Point find_midpoint(Point p1, Point p2) {
Point midpoint;
midpoint.x = (p1.x + p2.x) / 2.0;
midpoint.y = (p1.y + p2.y) / 2.0;
return midpoint;
}

// 辅助函数:打印 Point (按值传递)
void print_point(Point p) {
std::cout << "(" << p.x << ", " << p.y << ")";
}

int main() {
Point start_point = {1.0, 1.0};
Point end_point = {5.0, 7.0};

std::cout << "Start point: ";
print_point(start_point);
std::cout << std::endl;

std::cout << "End point: ";
print_point(end_point);
std::cout << std::endl;

// 调用函数计算中点
Point mid = find_midpoint(start_point, end_point);

std::cout << "Midpoint: ";
print_point(mid); // 输出: (3, 4)
std::cout << std::endl;

return 0;
}

这个例子再次展示了按值传递结构(p1, p2 是副本)和按值返回结构(midpoint 的副本被返回)。

7.6.3 传递结构的地址

为了避免复制整个结构的开销,特别是当结构很大时,或者当需要函数能够修改原始结构时,可以传递结构的地址(即指向结构的指针)而不是结构本身。

方法:

  1. 函数参数: 声明为指向结构类型的指针 (struct_type*)。
  2. 函数调用: 使用地址运算符 & 获取结构变量的地址传递给函数。
  3. 访问成员: 在函数内部,需要使用间接成员访问运算符 -> (箭头运算符) 来访问指针指向的结构的成员。或者,先解引用指针 *ptr,然后再使用点运算符 .,即 (*ptr).memberptr->member(*ptr).member 的简洁写法。

示例:按指针传递结构

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 <iostream>

struct Rectangle {
double width;
double height;
};

// 函数原型:接收指向 Rectangle 的指针,计算面积
// 使用 const 表示函数不会通过指针修改结构内容
double calculate_area(const Rectangle* rect_ptr) {
// 检查空指针是一种好的防御性编程习惯
if (rect_ptr == nullptr) {
std::cerr << "Error: Null pointer passed to calculate_area." << std::endl;
return 0.0;
}
// 使用箭头运算符 -> 访问成员
return rect_ptr->width * rect_ptr->height;
// 或者使用 (*rect_ptr).width * (*rect_ptr).height
}

// 函数原型:接收指向 Rectangle 的指针,并修改其尺寸 (放大)
void scale_rectangle(Rectangle* rect_ptr, double factor) {
if (rect_ptr == nullptr || factor <= 0) {
std::cerr << "Error: Invalid arguments for scale_rectangle." << std::endl;
return;
}
rect_ptr->width *= factor; // 修改原始结构
rect_ptr->height *= factor; // 修改原始结构
}

// 辅助函数:打印 Rectangle (按指针传递,使用 const)
void print_rectangle(const Rectangle* rect_ptr) {
if (rect_ptr == nullptr) return;
std::cout << "Rectangle [Width=" << rect_ptr->width << ", Height=" << rect_ptr->height << "]";
}


int main() {
Rectangle box = {10.0, 5.0};

std::cout << "Original Box: ";
print_rectangle(&box);
std::cout << std::endl;

// 传递 box 的地址给 calculate_area
double area = calculate_area(&box);
std::cout << "Area: " << area << std::endl; // 输出: 50

// 传递 box 的地址给 scale_rectangle 以修改它
scale_rectangle(&box, 2.0); // 放大两倍

std::cout << "Scaled Box: ";
print_rectangle(&box); // 打印修改后的原始 box
std::cout << std::endl; // 输出: Rectangle [Width=20, Height=10]

// 重新计算面积
area = calculate_area(&box);
std::cout << "New Area: " << area << std::endl; // 输出: 200

return 0;
}

按指针传递的优缺点:

  • 优点:
    • 效率高,只传递地址,不复制整个结构。
    • 允许函数修改原始结构数据。
  • 缺点:
    • 语法稍复杂(需要使用 & 获取地址,使用 ->(*). 访问成员)。
    • 可能意外修改原始数据(除非使用 const)。
    • 需要处理空指针的可能性。

按引用传递 (Pass by Reference):

C++ 还提供了另一种避免复制并允许修改原始数据的方式:按引用传递。这将在第 8 章详细介绍。按引用传递通常比按指针传递更简洁、更安全(因为它通常不涉及空引用的概念)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 示例:按引用传递 (将在第 8 章详细讲解)
#include <iostream>

struct Circle { double radius; };

// 参数是 Circle 的引用 (别名)
double circle_area_ref(const Circle& c) { // 使用 const 引用避免复制且不修改
return 3.14159 * c.radius * c.radius;
}

void scale_circle_ref(Circle& c, double factor) { // 使用非 const 引用允许修改
if (factor > 0) {
c.radius *= factor; // 直接用 . 访问成员,修改原始对象
}
}

int main() {
Circle circ = {5.0};
std::cout << "Area: " << circle_area_ref(circ) << std::endl;
scale_circle_ref(circ, 3.0);
std::cout << "New Radius: " << circ.radius << std::endl; // 输出: 15
std::cout << "New Area: " << circle_area_ref(circ) << std::endl;
return 0;
}

总结:

  • 结构默认按值传递给函数(创建副本)。
  • 函数可以按值返回结构(返回副本)。
  • 为提高效率或允许修改原始结构,可以传递结构的地址(指针 struct_type*),使用 -> 访问成员。
  • 使用 const 配合指针(或引用)可以防止函数意外修改结构。
  • 按引用传递 (struct_type&) 是另一种常用的高效传递方式,通常更受欢迎。

7.7 函数和 string 对象

C++ 标准库提供的 std::string 类是处理字符串的现代、更安全、更方便的方式,它与 C-风格字符串(字符数组)有很大不同。将 std::string 对象传递给函数或从函数返回它们,其行为更像结构体,但也受益于 C++ 的引用特性。

传递 std::string 对象

与结构类似,std::string 对象默认也是按值传递 (Pass by Value)**。这意味着当将一个 string 对象传递给函数时,会创建该对象的一个副本**。

示例:按值传递 std::string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string> // 包含 string 头文件

// 函数:按值接收 string 对象
void display_string_value(std::string str) { // str 是 message 的副本
std::cout << "Inside function (value): \"" << str << "\"" << std::endl;
// 修改副本,不影响原始 string
str[0] = 'J';
std::cout << "Modified copy inside function: \"" << str << "\"" << std::endl;
}

int main() {
std::string message = "Hello";

std::cout << "Before call: \"" << message << "\"" << std::endl;
display_string_value(message);
std::cout << "After call: \"" << message << "\"" << std::endl; // 原始 string 未改变

return 0;
}

输出:

1
2
3
4
Before call: "Hello"
Inside function (value): "Hello"
Modified copy inside function: "Jello"
After call: "Hello"

按值传递 std::string 的问题:

虽然按值传递可以保护原始数据,但 std::string 对象可能存储很长的字符串。每次调用函数都复制整个字符串(包括其内部可能动态分配的内存)可能会导致显著的性能开销。

按引用传递 std::string

为了避免复制开销并允许函数修改原始 string 对象,可以使用**按引用传递 (Pass by Reference)**。

示例:按引用传递 std::string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>

// 函数:按引用接收 string 对象
void modify_string_ref(std::string& str_ref) { // str_ref 是 message 的别名
std::cout << "Inside function (reference): \"" << str_ref << "\"" << std::endl;
// 修改引用,会直接修改原始 string
str_ref += " World";
std::cout << "Modified original via reference: \"" << str_ref << "\"" << std::endl;
}

int main() {
std::string message = "Hello";

std::cout << "Before call: \"" << message << "\"" << std::endl;
modify_string_ref(message); // 传递引用
std::cout << "After call: \"" << message << "\"" << std::endl; // 原始 string 已被修改

return 0;
}

输出:

1
2
3
4
Before call: "Hello"
Inside function (reference): "Hello"
Modified original via reference: "Hello World"
After call: "Hello World"

按常量引用传递 std::string (推荐方式)

如果函数需要读取 string 的内容但不需要修改它,最佳实践是使用**按常量引用传递 (Pass by Constant Reference)**。这提供了按引用传递的效率(避免复制),同时具有按值传递的安全性(防止函数修改原始数据)。

示例:按常量引用传递 std::string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <string>

// 函数:按常量引用接收 string 对象
void display_string_const_ref(const std::string& str_cref) { // str_cref 是 message 的常量别名
std::cout << "Inside function (const reference): \"" << str_cref << "\"" << std::endl;
// str_cref[0] = 'J'; // 错误!不能通过常量引用修改对象
std::cout << "String length: " << str_cref.length() << std::endl;
}

int main() {
std::string message = "Hello C++";

std::cout << "Before call: \"" << message << "\"" << std::endl;
display_string_const_ref(message); // 传递常量引用
std::cout << "After call: \"" << message << "\"" << std::endl; // 原始 string 未改变

// 也可以传递字符串字面值,它们会自动转换为临时的 string 对象
display_string_const_ref("Temporary String");

return 0;
}

输出:

1
2
3
4
5
6
Before call: "Hello C++"
Inside function (const reference): "Hello C++"
String length: 9
After call: "Hello C++"
Inside function (const reference): "Temporary String"
String length: 16

总结传递方式:

  • 按值 (std::string str): 创建副本,安全但可能低效。
  • 按引用 (std::string& str): 不创建副本,高效,允许修改原始对象。
  • 按常量引用 (const std::string& str): 不创建副本,高效,不允许修改原始对象。这是将字符串传递给函数进行只读访问的最常用和推荐的方式。

返回 std::string 对象

函数也可以返回 std::string 对象。通常直接按值返回即可。

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

// 函数:创建一个问候语字符串并返回
std::string create_greeting(const std::string& name) {
std::string result = "Hello, " + name + "!";
return result; // 返回 string 对象
}

int main() {
std::string user_name = "Alice";
std::string greeting = create_greeting(user_name);

std::cout << greeting << std::endl; // 输出: Hello, Alice!

return 0;
}

虽然看起来这里也涉及复制(返回 result 的副本),但现代 C++ 编译器通常会应用返回值优化 (RVO) 或**命名返回值优化 (NRVO)**。这些优化可以避免在返回 string(或其他对象)时进行实际的复制,使得按值返回 std::string 非常高效。

与 C-风格字符串的比较:

使用 std::string 对象与函数交互比使用 C-风格字符串 (char*) 简单得多:

  • 不需要担心空字符 \0
  • 不需要手动管理内存(new/delete[])。
  • 不需要单独传递大小(string 对象知道自己的大小)。
  • 按引用(尤其是常量引用)传递避免了复制开销,同时保持了代码的清晰和安全。
  • 返回值优化使得按值返回 string 通常很高效。

因此,在现代 C++ 中,强烈推荐使用 std::string 而不是 C-风格字符串来处理文本数据。

7.8 函数与 array 对象

C++11 引入了 std::array 模板类(在 <array> 头文件中定义),它提供了一种更安全、更方便的方式来表示固定大小的数组。与 C 风格数组会“退化”成指针不同,std::array 对象表现得更像普通的类对象(类似于结构体)。

关键特性:

  • std::array 封装了一个固定大小的 C 风格数组。
  • 其大小是类型信息的一部分(例如 std::array<int, 5>std::array<int, 10> 是不同的类型)。
  • 它提供了成员函数(如 size(), at(), front(), back())和对迭代器的支持。
  • 它不会自动退化为指针。

传递 std::array 对象

由于 std::array 表现得像一个对象,它默认是按值传递 (Pass by Value) 给函数的。这意味着当将一个 array 对象传递给函数时,会创建该对象的完整副本

示例:按值传递 std::array

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 <iostream>
#include <array> // 包含 array 头文件
#include <numeric> // 为了 std::accumulate

// 定义一个包含 5 个 double 的 array 类型别名
using FiveDoubles = std::array<double, 5>;

// 函数:按值接收 array 对象,计算总和
// arr 是 data 的副本
double sum_array_value(FiveDoubles arr) {
std::cout << "Inside function (value): Modifying copy..." << std::endl;
arr[0] = 1000.0; // 修改副本,不影响原始 array
double sum = 0.0;
for (double x : arr) {
sum += x;
}
// 或者使用 std::accumulate(arr.begin(), arr.end(), 0.0);
return sum;
}

int main() {
FiveDoubles data = {1.1, 2.2, 3.3, 4.4, 5.5};

std::cout << "Before call, data[0] = " << data[0] << std::endl;
double total = sum_array_value(data);
std::cout << "After call, data[0] = " << data[0] << std::endl; // 原始 array 未改变
std::cout << "Sum calculated by value: " << total << std::endl;

return 0;
}

输出:

1
2
3
4
Before call, data[0] = 1.1
Inside function (value): Modifying copy...
After call, data[0] = 1.1
Sum calculated by value: 1015.4

按值传递 std::array 的问题:

std::string 和大型结构体类似,如果 std::array 很大,按值传递会复制整个数组内容,导致性能开销。

按引用传递 std::array

为了避免复制开销并允许函数修改原始 array 对象,可以使用**按引用传递 (Pass by Reference)**。

示例:按引用传递 std::array

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>
#include <array>

using FiveDoubles = std::array<double, 5>;

// 函数:按引用接收 array 对象,并将所有元素乘以因子
// arr_ref 是 data 的别名
void scale_array_ref(FiveDoubles& arr_ref, double factor) {
std::cout << "Inside function (reference): Scaling original array..." << std::endl;
for (double& x : arr_ref) { // 使用引用访问元素以修改它们
x *= factor;
}
}

int main() {
FiveDoubles data = {1.0, 2.0, 3.0, 4.0, 5.0};

std::cout << "Before call: ";
for (double x : data) std::cout << x << " ";
std::cout << std::endl;

scale_array_ref(data, 10.0); // 传递引用

std::cout << "After call: ";
for (double x : data) std::cout << x << " "; // 原始 array 已被修改
std::cout << std::endl;

return 0;
}

输出:

1
2
3
Before call: 1 2 3 4 5
Inside function (reference): Scaling original array...
After call: 10 20 30 40 50

按常量引用传递 std::array (推荐方式)

如果函数只需要读取 array 的内容而不需要修改它,最佳实践是使用**按常量引用传递 (Pass by Constant Reference)**。这提供了效率(避免复制)和安全性(防止修改)。

示例:按常量引用传递 std::array

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
#include <iostream>
#include <array>

using FiveDoubles = std::array<double, 5>;

// 函数:按常量引用接收 array 对象,并打印它
// arr_cref 是 data 的常量别名
void print_array_const_ref(const FiveDoubles& arr_cref) {
std::cout << "Inside function (const reference): Array elements are: ";
// arr_cref[0] = 0.0; // 错误!不能通过常量引用修改
for (double x : arr_cref) {
std::cout << x << " ";
}
std::cout << "(Size: " << arr_cref.size() << ")" << std::endl;
}

int main() {
FiveDoubles data = {1.1, 2.2, 3.3, 4.4, 5.5};

print_array_const_ref(data); // 传递常量引用

// 原始 array 未改变
std::cout << "Back in main, data[0] = " << data[0] << std::endl;

return 0;
}

输出:

1
2
Inside function (const reference): Array elements are: 1.1 2.2 3.3 4.4 5.5 (Size: 5)
Back in main, data[0] = 1.1

总结传递方式:

  • 按值 (std::array<T, N> arr): 创建副本,安全但可能低效。
  • 按引用 (std::array<T, N>& arr): 不创建副本,高效,允许修改。
  • 按常量引用 (const std::array<T, N>& arr): 不创建副本,高效,不允许修改。这是将 array 传递给函数进行只读访问的最常用和推荐的方式。

返回 std::array 对象

函数也可以返回 std::array 对象,通常按值返回。与 std::string 类似,编译器通常会应用 RVO/NRVO 来优化掉返回时的复制操作。

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
#include <iostream>
#include <array>
#include <cmath> // for std::pow

using ThreeInts = std::array<int, 3>;

// 函数:创建一个包含 x, x^2, x^3 的 array 并返回
ThreeInts create_powers(int x) {
ThreeInts result;
result[0] = x;
result[1] = static_cast<int>(std::pow(x, 2));
result[2] = static_cast<int>(std::pow(x, 3));
return result; // 返回 array 对象 (通常会被优化)
}

int main() {
ThreeInts powers_of_5 = create_powers(5);

std::cout << "Powers of 5: ";
for (int val : powers_of_5) {
std::cout << val << " "; // 输出: 5 25 125
}
std::cout << std::endl;

return 0;
}

与 C 风格数组的比较:

使用 std::array 与函数交互比使用 C 风格数组更优越:

  • 大小是类型的一部分: 函数签名明确指定了期望的数组大小,提高了类型安全。例如,不能将 std::array<int, 5> 传递给期望 std::array<int, 10> 的函数。
  • 无指针退化: std::array 不会退化为指针,避免了相关的混淆和错误。
  • 传递方式明确: 像普通对象一样按值、按引用或按常量引用传递,语义清晰。
  • 接口更丰富: 可以直接在函数内部使用 size(), at() 等成员函数。

因此,在需要固定大小数组的场景下,std::array 通常是比 C 风格数组更好的选择,尤其是在函数参数和返回值中使用时。

7.9 递归

递归 (Recursion) 是一种编程技巧,其中函数直接或间接地调用自身来解决问题。递归函数将一个大问题分解为一个或多个与原问题相似但规模更小的子问题,直到问题规模小到可以直接解决(称为基线条件基本情况)。

递归函数通常包含两个关键部分:

  1. 基线条件 (Base Case): 一个或多个停止递归的条件。当满足基线条件时,函数不再调用自身,而是返回一个确定的值或执行一个简单的操作。没有基线条件会导致无限递归,最终耗尽内存(栈溢出)。
  2. 递归步骤 (Recursive Step): 函数调用自身,但通常使用修改后的参数,使得问题规模向基线条件靠近。

7.9.1 包含一个递归调用的递归

这是最简单的递归形式,函数在每次执行时最多调用自身一次。

示例:使用递归进行倒计时

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

// 递归函数:从 n 倒数到 1
void countdown(int n) {
// 基线条件:当 n 小于等于 0 时,停止递归
if (n <= 0) {
std::cout << "Blastoff!" << std::endl;
} else {
// 递归步骤:打印当前数字,然后调用自身处理 n-1
std::cout << n << "..." << std::endl;
countdown(n - 1); // 函数调用自身,问题规模减小 (n -> n-1)
}
}

int main() {
countdown(5);
return 0;
}

输出:

1
2
3
4
5
6
5...
4...
3...
2...
1...
Blastoff!

工作原理 (调用栈):

  1. main 调用 countdown(5)
  2. countdown(5) 打印 “5…”,然后调用 countdown(4)
  3. countdown(4) 打印 “4…”,然后调用 countdown(3)
  4. … 这个过程继续 …
  5. countdown(1) 打印 “1…”,然后调用 countdown(0)
  6. countdown(0) 满足基线条件 (n <= 0),打印 “Blastoff!” 并返回。
  7. countdown(1) 返回。
  8. countdown(2) 返回。
  9. … 依次回溯 …
  10. countdown(5) 返回到 main

每次函数调用都会在称为“调用栈”的内存区域中创建一个新的记录(栈帧),用于存储函数的局部变量和返回地址。当函数返回时,其栈帧被移除。

示例:使用递归计算阶乘

阶乘 n! 定义为 n * (n-1) * ... * 1,并且 0! = 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

// 递归函数:计算 n 的阶乘
unsigned long long factorial(int n) {
// 基线条件:0! = 1
if (n == 0) {
return 1;
}
// 递归步骤:n! = n * (n-1)!
else {
return n * factorial(n - 1); // 函数调用自身
}
}

int main() {
int num = 5;
std::cout << num << "! = " << factorial(num) << std::endl; // 输出: 5! = 120
num = 0;
std::cout << num << "! = " << factorial(num) << std::endl; // 输出: 0! = 1
return 0;
}

7.9.2 包含多个递归调用的递归

在这种形式中,函数在一次执行中可能会调用自身多次。这通常用于解决可以分解为多个相同类型子问题的问题,例如树的遍历或某些数学序列的计算。

示例:使用递归计算斐波那契数列

斐波那契数列定义如下:F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2) for n > 1

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

// 递归函数:计算第 n 个斐波那契数
unsigned long long fibonacci(int n) {
// 基线条件
if (n <= 0) {
return 0;
} else if (n == 1) {
return 1;
}
// 递归步骤:F(n) = F(n-1) + F(n-2)
else {
// 函数调用自身两次
return fibonacci(n - 1) + fibonacci(n - 2);
}
}

int main() {
int term = 10;
std::cout << "Fibonacci(" << term << ") = " << fibonacci(term) << std::endl; // 输出: Fibonacci(10) = 55

term = 6;
std::cout << "Fibonacci(" << term << ") = " << fibonacci(term) << std::endl; // 输出: Fibonacci(6) = 8

return 0;
}

工作原理和潜在问题:

计算 fibonacci(5) 的过程大致如下:

1
2
3
4
5
6
7
8
9
10
fibonacci(5)
-> fibonacci(4) + fibonacci(3)
-> (fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))
-> ((fibonacci(2) + fibonacci(1)) + (fibonacci(1) + fibonacci(0))) + ((fibonacci(1) + fibonacci(0)) + 1)
-> (((fibonacci(1) + fibonacci(0)) + 1) + (1 + 0)) + ((1 + 0) + 1)
-> (((1 + 0) + 1) + (1 + 0)) + ((1 + 0) + 1)
-> ((1 + 1) + 1) + (1 + 1)
-> (2 + 1) + 2
-> 3 + 2
-> 5

(注意:实际计算 fibonacci(10) 会涉及更多层调用)

这种包含多个递归调用的实现方式(如此处的斐波那契)虽然直观地反映了数学定义,但效率可能非常低。例如,在计算 fibonacci(5) 时,fibonacci(3) 被计算了两次,fibonacci(2) 被计算了三次。随着 n 的增大,重复计算的次数呈指数级增长。

对于这类问题,迭代(使用循环)或其他优化技术(如记忆化,即存储已计算的结果)通常是更高效的解决方案。

递归的优缺点:

  • 优点:
    • 对于某些问题(如树遍历、分治算法),递归可以提供非常自然、简洁和易于理解的解决方案。
    • 代码可以更接近问题的数学或逻辑描述。
  • 缺点:
    • 可能效率低下,特别是当存在大量重复计算或深度递归时。
    • 每次函数调用都有开销(创建栈帧),可能导致性能问题。
    • 深度递归可能耗尽调用栈空间,导致栈溢出错误。
    • 调试递归函数可能比调试迭代函数更困难。

在选择使用递归还是迭代时,需要权衡代码的清晰度、简洁性与潜在的性能和内存消耗。

7.10 函数指针

就像变量有地址,函数也有地址。函数指针 (Function Pointer) 就是一个指向函数内存地址的指针变量。通过函数指针,我们可以像调用普通函数一样调用它所指向的函数。函数指针的主要用途包括:

  • 将函数作为参数传递给其他函数(例如,实现回调机制或策略模式)。
  • 在运行时决定调用哪个函数。
  • 构建函数表或调度表。

7.10.1 函数指针的基础知识

声明函数指针:

声明函数指针时,必须指定它所指向的函数的返回类型参数列表类型。这确保了类型安全,即函数指针只能指向具有匹配签名的函数。

语法:

1
return_type (*pointer_name)(parameter_type_list);
  • return_type: 函数指针指向的函数的返回类型。
  • pointer_name: 函数指针变量的名称。
  • parameter_type_list: 函数指针指向的函数的参数类型列表,用逗号分隔。
  • (*pointer_name): 括号是必需的,它表明 pointer_name 是一个指针。如果没有括号,return_type *pointer_name(parameter_type_list); 会被解释为一个返回 return_type* 类型的函数声明。

示例声明:

1
2
3
4
5
6
7
8
9
10
11
// 声明一个名为 func_ptr 的函数指针
// 它指向一个接受两个 int 参数并返回 int 的函数
int (*func_ptr)(int, int);

// 声明一个名为 process 的函数指针
// 它指向一个接受 const char* 参数且无返回值 (void) 的函数
void (*process)(const char*);

// 声明一个名为 compare 的函数指针
// 它指向一个接受两个 double 参数并返回 bool 的函数
bool (*compare)(double, double);

初始化函数指针:

可以将函数的名称(不带括号)直接赋给具有匹配签名的函数指针。函数名本身就代表了函数的地址。

1
2
3
4
5
6
7
8
int add(int a, int b) { return a + b; }
void print_message(const char* msg) { std::cout << msg << std::endl; }

// 初始化 func_ptr 指向 add 函数
func_ptr = add; // 或者 func_ptr = &add; (& 是可选的)

// 初始化 process 指向 print_message 函数
process = print_message;

使用函数指针调用函数:

可以通过函数指针来调用它所指向的函数,语法与直接调用函数类似。

1
2
3
4
5
6
int result = func_ptr(5, 3); // 调用 add(5, 3),result 将是 8
process("Hello via pointer!"); // 调用 print_message("Hello via pointer!")

// 也可以使用显式解引用语法 (较少见)
// int result = (*func_ptr)(5, 3);
// (*process)("Hello via pointer!");

7.10.2 函数指针示例

下面是一个完整的示例,演示如何声明、初始化和使用函数指针。

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

// 目标函数 1
int add(int x, int y) {
return x + y;
}

// 目标函数 2
int subtract(int x, int y) {
return x - y;
}

// 目标函数 3
void display_result(int result) {
std::cout << "The result is: " << result << std::endl;
}

int main() {
// 声明一个指向接受两个 int 并返回 int 的函数的指针
int (*operation)(int, int);

// 声明一个指向接受一个 int 且无返回值的函数的指针
void (*show)(int);

// 将 operation 指向 add 函数
operation = add;
std::cout << "Using 'add' function via pointer:" << std::endl;
int sum = operation(10, 5); // 调用 add(10, 5)
std::cout << "10 + 5 = " << sum << std::endl; // 输出: 15

// 将 operation 指向 subtract 函数
operation = subtract;
std::cout << "\nUsing 'subtract' function via pointer:" << std::endl;
int diff = operation(10, 5); // 调用 subtract(10, 5)
std::cout << "10 - 5 = " << diff << std::endl; // 输出: 5

// 将 show 指向 display_result 函数
show = display_result;
std::cout << "\nDisplaying difference using 'show' pointer:" << std::endl;
show(diff); // 调用 display_result(diff)

return 0;
}

7.10.3 深入探讨函数指针

函数指针作为函数参数:

一个常见的用途是将函数指针作为参数传递给另一个函数。这允许调用函数根据传入的函数指针来定制其行为。

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

// 函数:检查一个数字是否为偶数
bool is_even(int n) {
return n % 2 == 0;
}

// 函数:检查一个数字是否为正数
bool is_positive(int n) {
return n > 0;
}

// 函数:打印满足特定条件的数字
// 参数:一个整数向量,一个函数指针 (指向检查条件的函数)
void print_numbers_if(const std::vector<int>& numbers, bool (*check)(int)) {
std::cout << "Numbers satisfying the condition: ";
for (int num : numbers) {
if (check(num)) { // 使用传入的函数指针调用检查函数
std::cout << num << " ";
}
}
std::cout << std::endl;
}

int main() {
std::vector<int> data = {1, -2, 3, 4, -5, 6};

std::cout << "Checking for even numbers:" << std::endl;
print_numbers_if(data, is_even); // 传递 is_even 函数的地址

std::cout << "\nChecking for positive numbers:" << std::endl;
print_numbers_if(data, is_positive); // 传递 is_positive 函数的地址

return 0;
}

输出:

1
2
3
4
Checking for even numbers:
Numbers satisfying the condition: -2 4 6
Checking for positive numbers:
Numbers satisfying the condition: 1 3 4 6

在这个例子中,print_numbers_if 函数的行为由传递给它的 check 函数指针决定。

7.10.4 使用 typedef 或 using 进行简化

函数指针的声明语法可能比较冗长和复杂。可以使用 typedef (传统方式) 或 using (C++11 及以后推荐) 来创建函数指针类型的别名,使代码更清晰。

使用 typedef:

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

// 使用 typedef 定义函数指针类型别名
typedef bool (*CheckFunction)(int); // CheckFunction 是指向 (int) -> bool 函数的指针类型
typedef void (*DisplayFunction)(const char*);

void print_if(int val, CheckFunction check, DisplayFunction display) {
if (check(val)) {
display("Condition met!");
} else {
display("Condition not met.");
}
}

bool is_negative(int n) { return n < 0; }
void show_message(const char* msg) { std::cout << msg << std::endl; }

int main() {
CheckFunction checker = is_negative; // 使用别名声明和初始化
DisplayFunction printer = show_message;

print_if(-5, checker, printer); // 输出: Condition met!
print_if(10, checker, printer); // 输出: Condition not met.

return 0;
}

使用 using (C++11):

using 提供了更直观、更一致的别名语法。

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

// 使用 using 定义函数指针类型别名 (推荐)
using CheckFunction = bool (*)(int); // 指向 (int) -> bool 函数的指针类型
using DisplayFunction = void (*)(const char*);

// ... (函数 print_if, is_negative, show_message 和 main 函数同上) ...

void print_if(int val, CheckFunction check, DisplayFunction display) {
if (check(val)) {
display("Condition met!");
} else {
display("Condition not met.");
}
}

bool is_negative(int n) { return n < 0; }
void show_message(const char* msg) { std::cout << msg << std::endl; }

int main() {
CheckFunction checker = is_negative;
DisplayFunction printer = show_message;

print_if(-5, checker, printer);
print_if(10, checker, printer);

return 0;
}

使用类型别名可以显著提高涉及函数指针的代码的可读性。

总结:

函数指针是 C++ 中一个强大的特性,它允许将函数视为数据进行传递和存储。虽然语法可能初看起来有些复杂,但通过 typedefusing 可以简化。理解函数指针对于掌握回调机制、某些设计模式以及与 C 库交互非常重要。在现代 C++ 中,函数对象(Functors)和 Lambda 表达式(将在后续章节介绍)提供了更灵活、有时更方便的替代方案,但函数指针仍然有其用武之地。

7.11 总结

本章深入探讨了函数这一 C++ 编程的基本模块,涵盖了函数定义、调用、参数传递机制以及如何将函数与各种数据类型(数组、字符串、结构、对象)结合使用。

主要内容回顾:

  1. 函数基础: 复习了函数的定义(返回类型、名称、参数列表、函数体)、函数原型(声明函数接口以供编译器使用)和函数调用(执行函数代码)。

  2. 参数传递:

    • 按值传递 (Pass by Value): C++ 的默认方式,适用于基本类型、结构和类对象。函数操作的是实参的副本,不影响原始数据,但可能因复制大型对象而效率低下。
    • 数组传递: C 风格数组传递时会退化为指向首元素的指针,函数直接操作原始数组,效率高但丢失大小信息,需额外传递大小或使用指针区间。const 可用于保护数组内容。
    • 二维数组传递: 必须在函数参数中指定除第一维之外的所有维度的大小。
    • 结构传递: 默认按值传递。为提高效率或允许修改,可传递结构指针 (struct_type*),使用 -> 访问成员,或使用引用(第 8 章内容)。
    • std::stringstd::array 对象传递: 默认按值传递,但通常推荐按常量引用 (const T&) 传递以获得效率和安全性,或按引用 (T&) 传递以允许修改。
  3. 函数与特定类型:

    • C-风格字符串: 作为 char* 传递,依赖 \0 结束符。返回 C 风格字符串比较复杂,推荐让调用者提供缓冲区。
    • std::string 对象: 使用 const std::string& 传递是常用方式。返回 std::string 通常因 RVO/NRVO 而高效。
    • std::array 对象: 行为类似结构,大小是类型的一部分。推荐使用 const std::array<T, N>& 传递。
  4. 递归: 函数调用自身来解决问题。需要明确的基线条件来停止递归,以及使问题规模缩小的递归步骤。递归可以使某些问题的代码简洁,但可能效率低或导致栈溢出。

  5. 函数指针: 指向函数地址的指针变量。允许将函数作为参数传递、在运行时选择函数等。声明时需匹配函数签名(返回类型和参数类型)。typedefusing 可简化其声明。

通过本章的学习,我们掌握了如何有效地定义和使用函数来构建模块化、可重用和可维护的 C++ 程序,并了解了不同数据类型在函数参数传递中的行为和最佳实践。

评论