7.1 复习函数的基本知识 函数是 C++ 程序的基本构建块,它们允许我们将代码组织成可重用的、逻辑独立的单元。使用函数可以使程序更模块化、更易于理解和维护。本节将复习函数的基本概念:定义、原型和调用。
7.1.1 定义函数 函数定义 (Function Definition) 包含了函数的实际代码,它说明了函数做什么以及如何做。一个函数定义包括以下几个部分:
返回类型 (Return Type): 函数执行完毕后返回给调用者的值的类型。如果函数不返回任何值,则返回类型为 void
。
函数名 (Function Name): 用于调用函数的标识符。命名规则与变量名相同。
参数列表 (Parameter List): 位于函数名后的圆括号 ()
中,用于接收传递给函数的值。参数之间用逗号分隔,每个参数都需要指定类型和名称。如果函数不接受任何参数,括号内可以为空或写 void
。
函数体 (Function Body): 位于花括号 {}
中,包含实现函数功能的 C++ 语句。
语法:
1 2 3 4 5 return_type function_name (parameter_list) { return value; }
示例:定义一个简单的函数
这个函数不接受参数,也不返回值 (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 ; int result = add_numbers(a, b); std ::cout << "The sum of " << a << " and " << b << " is: " << result << std ::endl ; 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);
示例:使用函数原型
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 ; return 0 ; } 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> void modify_value (int n) { std ::cout << "Inside function (before modification): n = " << n << std ::endl ; n = n * 2 ; std ::cout << "Inside function (after modification): n = " << n << std ::endl ; } int main () { int value = 10 ; std ::cout << "Before calling function: value = " << value << std ::endl ; modify_value(value); std ::cout << "After calling function: value = " << value << std ::endl ; 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
代码解释:
main
函数中的变量 value
初始化为 10。
调用 modify_value(value)
时,value
的值 (10) 被复制 给了 modify_value
函数的形参 n
。
在 modify_value
函数内部,n
的值被修改为 20。但这仅仅修改了 n
这个局部副本 。
当函数执行完毕返回 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> 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 max (int a, int b) { if (a > b) { return a; } else { return 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 ; std ::cout << "The max of 100 and 99 is: " << max(100 , 99 ) << std ::endl ; return 0 ; }
这个例子再次展示了如何定义和调用带有多个参数的函数,并且该函数还返回一个值。参数 a
和 b
也是按值传递的。
7.3 函数和数组 将数组传递给函数是 C++ 中常见的操作,但其工作方式与传递普通变量(如 int
或 double
)有显著不同。理解这种差异对于正确使用数组作为函数参数至关重要。
与基本类型默认使用“按值传递”(创建副本)不同,当将数组传递给函数时,C++ 不会 复制整个数组。相反,它传递的是数组第一个元素的内存地址 。这意味着函数实际上接收的是一个指向数组起始位置的指针。
7.3.1 函数如何使用指针来处理数组 因为函数接收的是数组的地址(指针),所以它可以通过这个地址直接访问和修改原始数组的内容。这与按值传递完全不同,后者操作的是副本。
在函数定义中,接收数组参数有几种等效的语法:
1 2 3 4 5 6 7 8 void process_array (int * arr, int size) ;void process_array (int arr[], int size) ;
这三种语法在函数参数列表中是等效的,它们都告诉编译器 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> void double_elements (int arr[], int size) { 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
代码解释:
main
函数定义了一个数组 my_array
。
调用 double_elements(my_array, array_size)
时,my_array
(代表数组首元素的地址) 被传递给函数的 arr
参数,array_size
被传递给 size
参数。
函数内部通过指针 arr
访问并修改了 main
函数中定义的 my_array
的元素。
函数返回后,main
函数中的 my_array
的内容确实发生了改变。
7.3.2 将数组作为参数意味着什么 将数组名传递给函数时,会发生所谓的“数组退化”(Array Decay)。数组名会“退化”成指向其第一个元素的指针。这就是为什么函数参数 int arr[]
和 int* arr
是等价的。
Implications:
效率: 不需要复制整个数组,传递地址非常快,尤其是对于大数组。
修改能力: 函数可以直接修改调用者提供的原始数组。这既是优点(允许函数“返回”修改后的数组)也是缺点(可能意外修改数据)。
丢失大小信息: 函数本身无法知道数组的大小。必须显式传递大小信息。
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) { 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 ; 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) { 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 ; 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> int sum_range (const int * begin, const int * end) { int total = 0 ; 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); std ::cout << "Total sum: " << total_sum << std ::endl ; int partial_sum = sum_range(data + 2 , data + 7 ); std ::cout << "Partial sum (index 2 to 6): " << partial_sum << std ::endl ; return 0 ; }
这种方法在 C++ 标准库算法中非常常用。
7.3.5 指针和 const 如前面的示例所示,如果函数不应该修改传入的数组,应该在函数参数中使用 const
关键字。这是一种重要的编程实践,可以提高代码的安全性和清晰度。
1 2 3 4 5 6 7 8 9 void print_array (const int arr[], int size) { std ::cout << "Array elements: " ; for (int i = 0 ; i < size; ++i) { std ::cout << arr[i] << " " ; } std ::cout << std ::endl ; }
使用 const
有两个主要好处:
防止意外修改: 编译器会检查并阻止函数内部对 const
参数的修改尝试。
表明意图: 向函数的调用者表明该函数不会改变传入的数组,使得函数接口更清晰。
接受更广泛的参数: const
参数的函数可以接受 const
数组和非 const
数组作为实参,而非 const
参数的函数只能接受非 const
数组。
总结来说,将数组传递给函数是通过传递指向其首元素的指针来实现的。这使得函数能够访问和(如果未使用 const
)修改原始数组,但也要求调用者通常需要额外传递数组的大小或使用指针区间来界定操作范围。
7.4 函数和二维数组 将二维数组传递给函数比传递一维数组稍微复杂一些。与一维数组类似,二维数组名在传递时也会“退化”成指向其第一个元素的指针。但二维数组的第一个元素本身是一个一维数组 。因此,传递的是指向一维数组的指针。
为了让函数能够正确地计算元素在内存中的位置,编译器需要知道除第一维(行数)之外的所有其他维度的大小(列数,以及更高维度的相应大小)。
关键点: 在函数参数中声明二维数组时,必须 指定除第一维之外的所有维度的大小。第一维的大小是可选的(通常省略)。
语法:
假设有一个二维数组 int data[3][4];
函数原型或定义可以这样写:
1 2 3 4 5 6 7 8 9 void process_2d_array (int arr[][4 ], int rows) ; void process_2d_array (int (*arr)[4 ], int rows) ;
为什么必须指定列数?
考虑二维数组 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 ; int sum_2d_array (int arr[][COLS], int rows) ;void print_2d_array (const int arr[][COLS], int rows) ; 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 ; 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 ; } }
代码解释:
我们定义了一个全局常量 COLS
来表示数组的列数。这使得在函数原型和定义中指定列数更加方便和一致。
sum_2d_array
和 print_2d_array
函数的第一个参数都声明为 int arr[][COLS]
或 const int arr[][COLS]
,明确指定了列的大小。
main
函数中定义了一个 3x4 的二维数组 data
。
调用函数时,传递数组名 data
(它代表指向第一个包含 COLS
个 int
的一维数组的指针)和行数 num_rows
。
函数内部可以使用标准的 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> unsigned int string_length (const char * str) { unsigned int length = 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 ; std ::cout << "Length of \"" << message << "\": " << string_length(message) << std ::endl ; std ::cout << "Length of \"C++\": " << string_length("C++" ) << std ::endl ; 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> void print_string (const char * str) { std ::cout << 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 2 3 4 5 6 char * create_temp_string () { char temp[] = "Temporary" ; return temp; }
调用 create_temp_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> const char * get_static_message () { static char message[] = "Static Message" ; return message; } int main () { const char * msg1 = get_static_message(); std ::cout << "Msg1: " << msg1 << std ::endl ; const char * msg2 = get_static_message(); std ::cout << "Msg2: " << msg2 << std ::endl ; return 0 ; }
缺点: 返回的指针指向的内存在后续调用中可能被覆盖(如果函数修改静态变量的话),并且这种方法不是线程安全的。
返回指向动态分配内存的指针 (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 ]; 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[]
来释放返回的指针所指向的内存,否则会导致内存泄漏。这种责任转移很容易出错。
传递由调用者分配的缓冲区 (推荐方式): 这是最安全、最常用的方法。函数接受一个指向调用者提供的缓冲区的指针和该缓冲区的大小,然后将结果字符串写入该缓冲区。
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) { int written = snprintf (buffer, buffer_size, "Hello, %s!" , name); 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 ; } 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; }; void display_point (Point p) { 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 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; } int main () { Point pt_main = {3.0 , 4.0 }; std ::cout << "Before calling display_point: (" << pt_main.x << ", " << pt_main.y << ")" << std ::endl ; 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 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 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; } 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); std ::cout << std ::endl ; return 0 ; }
这个例子再次展示了按值传递结构(p1
, p2
是副本)和按值返回结构(midpoint
的副本被返回)。
7.6.3 传递结构的地址 为了避免复制整个结构的开销,特别是当结构很大时,或者当需要函数能够修改原始结构时,可以传递结构的地址 (即指向结构的指针)而不是结构本身。
方法:
函数参数: 声明为指向结构类型的指针 (struct_type*
)。
函数调用: 使用地址运算符 &
获取结构变量的地址传递给函数。
访问成员: 在函数内部,需要使用间接成员访问运算符 ->
(箭头运算符) 来访问指针指向的结构的成员。或者,先解引用指针 *ptr
,然后再使用点运算符 .
,即 (*ptr).member
。ptr->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; }; 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; } 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; } 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 ; double area = calculate_area(&box); std ::cout << "Area: " << area << std ::endl ; scale_rectangle(&box, 2.0 ); std ::cout << "Scaled Box: " ; print_rectangle(&box); std ::cout << std ::endl ; area = calculate_area(&box); std ::cout << "New Area: " << area << std ::endl ; 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 #include <iostream> struct Circle { double radius; };double circle_area_ref (const Circle& c) { return 3.14159 * c.radius * c.radius; } void scale_circle_ref (Circle& c, double factor) { 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 ; 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 头文件 void display_string_value (std ::string str) { std ::cout << "Inside function (value): \"" << str << "\"" << std ::endl ; 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 ; 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> void modify_string_ref (std ::string & str_ref) { std ::cout << "Inside function (reference): \"" << str_ref << "\"" << std ::endl ; 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 ; 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> void display_string_const_ref (const std ::string & str_cref) { std ::cout << "Inside function (const reference): \"" << str_cref << "\"" << std ::endl ; 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 ; 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; } int main () { std ::string user_name = "Alice" ; std ::string greeting = create_greeting(user_name); std ::cout << greeting << std ::endl ; 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 using FiveDoubles = std ::array <double , 5 >;double sum_array_value (FiveDoubles arr) { std ::cout << "Inside function (value): Modifying copy..." << std ::endl ; arr[0 ] = 1000.0 ; double sum = 0.0 ; for (double x : arr) { sum += x; } 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 ; 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 >;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 << " " ; 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 >;void print_array_const_ref (const FiveDoubles& arr_cref) { std ::cout << "Inside function (const reference): Array elements are: " ; 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); 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 >;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; } int main () { ThreeInts powers_of_5 = create_powers(5 ); std ::cout << "Powers of 5: " ; for (int val : powers_of_5) { std ::cout << val << " " ; } 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) 是一种编程技巧,其中函数直接或间接地调用自身来解决问题。递归函数将一个大问题分解为一个或多个与原问题相似但规模更小的子问题,直到问题规模小到可以直接解决(称为基线条件 或基本情况 )。
递归函数通常包含两个关键部分:
基线条件 (Base Case): 一个或多个停止递归的条件。当满足基线条件时,函数不再调用自身,而是返回一个确定的值或执行一个简单的操作。没有基线条件会导致无限递归,最终耗尽内存(栈溢出)。
递归步骤 (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> void countdown (int n) { if (n <= 0 ) { std ::cout << "Blastoff!" << std ::endl ; } else { std ::cout << n << "..." << std ::endl ; countdown(n - 1 ); } } int main () { countdown(5 ); return 0 ; }
输出:
1 2 3 4 5 6 5... 4... 3... 2... 1... Blastoff!
工作原理 (调用栈):
main
调用 countdown(5)
。
countdown(5)
打印 “5…”,然后调用 countdown(4)
。
countdown(4)
打印 “4…”,然后调用 countdown(3)
。
… 这个过程继续 …
countdown(1)
打印 “1…”,然后调用 countdown(0)
。
countdown(0)
满足基线条件 (n <= 0
),打印 “Blastoff!” 并返回。
countdown(1)
返回。
countdown(2)
返回。
… 依次回溯 …
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> unsigned long long factorial (int n) { if (n == 0 ) { return 1 ; } else { return n * factorial(n - 1 ); } } int main () { int num = 5 ; std ::cout << num << "! = " << factorial(num) << std ::endl ; num = 0 ; std ::cout << num << "! = " << factorial(num) << std ::endl ; 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> unsigned long long fibonacci (int n) { if (n <= 0 ) { return 0 ; } else if (n == 1 ) { return 1 ; } else { return fibonacci(n - 1 ) + fibonacci(n - 2 ); } } int main () { int term = 10 ; std ::cout << "Fibonacci(" << term << ") = " << fibonacci(term) << std ::endl ; term = 6 ; std ::cout << "Fibonacci(" << term << ") = " << fibonacci(term) << std ::endl ; 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 int (*func_ptr)(int , int );void (*process)(const char *);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; process = print_message;
使用函数指针调用函数:
可以通过函数指针来调用它所指向的函数,语法与直接调用函数类似。
1 2 3 4 5 6 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> int add (int x, int y) { return x + y; } int subtract (int x, int y) { return x - y; } void display_result (int result) { std ::cout << "The result is: " << result << std ::endl ; } int main () { int (*operation)(int , int ); void (*show)(int ); operation = add; std ::cout << "Using 'add' function via pointer:" << std ::endl ; int sum = operation(10 , 5 ); std ::cout << "10 + 5 = " << sum << std ::endl ; operation = subtract; std ::cout << "\nUsing 'subtract' function via pointer:" << std ::endl ; int diff = operation(10 , 5 ); std ::cout << "10 - 5 = " << diff << std ::endl ; show = display_result; std ::cout << "\nDisplaying difference using 'show' pointer:" << std ::endl ; show(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); std ::cout << "\nChecking for positive numbers:" << std ::endl ; print_numbers_if(data, 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 bool (*CheckFunction) (int ) ; 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); print_if(10 , checker, printer); 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 CheckFunction = bool (*)(int ); using DisplayFunction = void (*)(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); print_if(10 , checker, printer); return 0 ; }
使用类型别名可以显著提高涉及函数指针的代码的可读性。
总结:
函数指针是 C++ 中一个强大的特性,它允许将函数视为数据进行传递和存储。虽然语法可能初看起来有些复杂,但通过 typedef
或 using
可以简化。理解函数指针对于掌握回调机制、某些设计模式以及与 C 库交互非常重要。在现代 C++ 中,函数对象(Functors)和 Lambda 表达式(将在后续章节介绍)提供了更灵活、有时更方便的替代方案,但函数指针仍然有其用武之地。
7.11 总结 本章深入探讨了函数这一 C++ 编程的基本模块,涵盖了函数定义、调用、参数传递机制以及如何将函数与各种数据类型(数组、字符串、结构、对象)结合使用。
主要内容回顾:
函数基础: 复习了函数的定义(返回类型、名称、参数列表、函数体)、函数原型(声明函数接口以供编译器使用)和函数调用(执行函数代码)。
参数传递:
按值传递 (Pass by Value): C++ 的默认方式,适用于基本类型、结构和类对象。函数操作的是实参的副本,不影响原始数据,但可能因复制大型对象而效率低下。
数组传递: C 风格数组传递时会退化为指向首元素的指针,函数直接操作原始数组,效率高但丢失大小信息,需额外传递大小或使用指针区间。const
可用于保护数组内容。
二维数组传递: 必须在函数参数中指定除第一维之外的所有维度的大小。
结构传递: 默认按值传递。为提高效率或允许修改,可传递结构指针 (struct_type*
),使用 ->
访问成员,或使用引用(第 8 章内容)。
std::string
和 std::array
对象传递: 默认按值传递,但通常推荐按常量引用 (const T&
) 传递以获得效率和安全性,或按引用 (T&
) 传递以允许修改。
函数与特定类型:
C-风格字符串: 作为 char*
传递,依赖 \0
结束符。返回 C 风格字符串比较复杂,推荐让调用者提供缓冲区。
std::string
对象: 使用 const std::string&
传递是常用方式。返回 std::string
通常因 RVO/NRVO 而高效。
std::array
对象: 行为类似结构,大小是类型的一部分。推荐使用 const std::array<T, N>&
传递。
递归: 函数调用自身来解决问题。需要明确的基线条件 来停止递归,以及使问题规模缩小的递归步骤 。递归可以使某些问题的代码简洁,但可能效率低或导致栈溢出。
函数指针: 指向函数地址的指针变量。允许将函数作为参数传递、在运行时选择函数等。声明时需匹配函数签名(返回类型和参数类型)。typedef
或 using
可简化其声明。
通过本章的学习,我们掌握了如何有效地定义和使用函数来构建模块化、可重用和可维护的 C++ 程序,并了解了不同数据类型在函数参数传递中的行为和最佳实践。