第11章 函数进阶
第11章 函数进阶
- 如何定义和使用参数数量可变的函数
- 函数指针的原理与典型应用场景
- 内联函数如何提升程序运行效率
在第6章,我们学会了如何定义和调用一个参数固定的函数。但在实际项目中,你经常会遇到一些更灵活的需求。比如,写一个日志函数,有时想打印一条信息,有时想打印三条,参数数量不固定;或者,你需要根据用户的选择,动态调用不同的处理函数。本章,我们就来解决这些问题。
11.1 可变参数函数:处理不确定数量的输入
想象一下餐厅的点餐系统。顾客可以只点一份主食,也可以“主食+汤+沙拉+饮料”点一大堆。一个优秀的点餐函数,应该能处理这种数量不确定的订单。在C++中,可变参数函数就是这样的“点餐系统”。
技术定义
可变参数函数:允许函数接受数量可变的参数。它使用省略号...表示参数列表的结束,并通过一组宏来访问这些不确定的参数。
C++标准库提供了<cstdarg>头文件来支持可变参数。其核心是三个宏:va_start、va_arg和va_end。它们的工作原理就像一个指针,在参数列表中逐个移动并读取数据。
下面是一个计算任意数量整数之和的示例。注意,第一个固定参数count用于告知函数后面有几个可变参数,这是最常见的用法。
#include <iostream>
#include <cstdarg>
int sum(int count, ...) {
int total = 0;
va_list args; // 定义参数列表指针
va_start(args, count); // 初始化指针,指向第一个可变参数
for (int i = 0; i < count; ++i) {
total += va_arg(args, int); // 按int类型读取一个参数,指针后移
}
va_end(args); // 清理工作
return total;
}
int main() {
std::cout << sum(3, 10, 20, 30) << std::endl; // 输出 60
std::cout << sum(5, 1, 2, 3, 4, 5) << std::endl; // 输出 15
return 0;
}
常见误区
- 忘记传递参数数量信息:可变参数函数本身不知道有多少个参数,你必须通过第一个固定参数(如
count)或一个特殊的结束标志(如字符串中的‘\0’)来告知。我在一个日志模块中踩过这个坑,忘记传数量导致读到了垃圾内存,程序随机崩溃。 - 类型错配:
va_arg(args, int)假设下一个参数是int。如果你传了一个double,读取的结果将是错误的,且无法在编译期发现。C++11引入的类型安全可变参数模板(第31章内容)是更好的现代解决方案。 - 遗漏va_end:虽然在一些平台上不调用
va_end可能不会立即出错,但这不符合规范,可能导致资源泄漏或不可预知的行为。
11.2 函数指针:让函数成为“变量”
把函数想象成一台特定的机器(比如榨汁机、烤箱)。函数指针就是这台机器的“遥控器”。你不需要知道机器内部如何工作,只需要通过遥控器(指针)来启动它。这让你可以在运行时决定使用哪台机器。
技术定义
函数指针:一种指向函数入口地址的指针变量。通过它,可以间接调用函数,从而实现回调、函数表等动态行为。
声明一个函数指针需要模仿目标函数的签名。语法略显复杂,但记住模式:返回类型 (*指针变量名)(参数类型列表)。
| 函数声明 | 对应的函数指针声明 | 说明 |
|---|---|---|
int max(int a, int b); | int (*pFunc)(int, int); | 指向返回int,接受两个int参数的函数 |
void log(const char* msg); | void (*pLog)(const char*); | 指向无返回值,接受一个字符串参数的函数 |
double calculate(); | double (*pCalc)(); | 指向无参,返回double的函数 |
赋值和调用时,函数名本身就代表它的地址(和数组名类似),可以省略取地址符&。
#include <iostream>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
int main() {
// 声明一个函数指针p
int (*p)(int, int);
p = add; // 将p指向add函数,等价于 p = &add;
std::cout << p(3, 4) << std::endl; // 输出 7,等价于 (*p)(3, 4)
p = multiply; // 将p改为指向multiply函数
std::cout << p(3, 4) << std::endl; // 输出 12
return 0;
}
函数指针最经典的应用是回调函数和函数表。例如,一个排序算法(如qsort)需要你提供一个比较函数的指针,它就能根据你的规则排序任何数据。一个简单的计算器菜单,可以用函数指针数组来实现,避免冗长的if-else或switch链。
// 函数表应用示例片段
typedef int (*Operation)(int, int);
Operation opTable[] = {add, multiply, /* 可添加subtract, divide等 */};
// 用户选择操作0(加)或1(乘)...
int choice = 1;
int result = opTable[choice](5, 6); // 调用multiply(5, 6)
编者提示:函数指针的语法和概念是理解C++中更高级抽象(如第27章的Lambda表达式、第30章的设计模式中的策略模式)的重要基石。虽然现代C++提供了std::function等更安全的包装器,但理解底层指针原理依然至关重要。
11.3 内联函数:用空间换时间的优化
调用函数是有成本的:需要压栈参数、跳转指令、执行函数体、返回结果。对于像“求两个数最大值”这样极其简单的函数,调用开销可能比执行操作本身还大。这就像为了打印一页纸,你每次都从工位跑到遥远的打印机(函数调用),而不是在桌上放一台小型打印机(内联展开)。
技术定义
内联函数:使用inline关键字建议编译器将函数体直接插入到每个调用点,从而消除函数调用的开销。它是一种以增加代码体积为代价来提升运行速度的优化手段。
你只需要在函数声明或定义前加上inline关键字即可。注意,这只是一个建议,编译器最终决定是否内联。复杂的函数、递归函数或虚函数通常不会被内联。
// 头文件 math_utils.h
inline int max(int a, int b) {
return (a > b) ? a : b;
}
// 源文件 main.cpp
#include "math_utils.h"
int main() {
int x = 10, y = 20;
int m = max(x, y); // 编译器可能会将此展开为:int m = (x > y) ? x : y;
return 0;
}
适合内联的场景
- 函数体非常小(1-5行简单语句)。
- 在性能关键的循环中被频繁调用。
- “Getter/Setter”等访问成员变量的函数。
避免内联的场景
- 函数体复杂(包含循环、递归、大量逻辑)。
- 虚函数(运行时多态决定了无法在编译期确定代码位置)。
- 函数指针指向的函数(因为指针值在运行时才能确定)。
内联函数 vs. 带参数的宏:你可能在C语言中用过#define MAX(a,b) ((a)>(b)?(a):(b))来实现类似效果。但宏是文本替换,没有类型检查,且可能因参数多次求值导致副作用(如MAX(i++, j++))。内联函数是真正的函数,有类型安全和作用域,是现代C++中替代宏的首选。
工程经验
实际开发中,对于小型工具函数,我习惯直接写在类定义内部(头文件中)。在类内部定义的成员函数默认就是内联的,无需显式添加inline关键字。对于全局工具函数,将其定义在头文件中并加上inline,可以避免在多文件包含时产生“重复定义”的链接错误。
动手试一试
- 实现一个可变参数求平均值函数:仿照
sum函数,写一个average(int count, ...)函数,计算任意数量整数的平均值(返回double)。注意处理count为0的情况。 - 构建一个简单的命令分发器:定义三个函数:
void cmd_help(),void cmd_version(),void cmd_exit()。创建一个函数指针数组来存储它们。编写一个循环,根据用户输入的字符串(如“help”、“version”),查找并调用对应的函数。 - 测试内联效果:写一个简单的内联函数
square(int x)返回x的平方。在main函数中循环调用它100万次。尝试去掉inline关键字,在开启编译器优化(如g++的-O2)的情况下,比较两者运行时间(可用<chrono>库计时)。你会发现,对于这种微小函数,现代编译器即使没有inline关键字也可能自动内联。
检验你的理解
- 编写一个可变参数函数
printValues(const char* format, ...),其中format是一个简单格式字符串,如“%d %f %s”。你需要根据格式字符串中的字符(%d对应int,%f对应double,%s对应char*)来使用va_arg读取并打印相应类型的值。这个练习能深刻检验你对可变参数类型匹配的理解。 - 假设有一个函数指针
int (*callback)(int),它被用作一个数据处理库的回调。库函数会传入一个整型数据,期望回调函数返回处理后的结果。请设计一个场景,编写两个不同的回调函数(例如,一个将输入加倍,一个将输入平方),并通过函数指针传递给一个模拟的库函数void processData(int data, int (*cb)(int))。 - 为什么递归函数通常不会被编译器内联?请从内联展开的原理角度思考。
本章小结
- 可变参数函数通过
<cstdarg>中的宏实现,能处理参数数量不确定的情况,但需注意手动传递参数数量和类型安全的问题。 - 函数指针(如
int (*p)(int, int))允许将函数作为数据传递和存储,是实现回调机制和函数表(避免复杂分支判断)的核心技术。 - 内联函数(
inline关键字)建议编译器将函数体展开到调用处,以消除调用开销,适用于简单且频繁调用的函数,是类型安全的宏替代品。
微信扫码关注
抖音扫码关注