默认参数与内联函数
定义与作用
默认参数允许在函数声明中为参数指定默认值,调用时可以省略该实参:
void log(const std::string& msg, LogLevel level = LogLevel::Info);
log("系统启动"); // level 使用默认值 Info
log("严重错误", Error); // 显式指定 level
内联函数(inline)提示编译器在调用点展开函数体,避免函数调用开销。C++ 中 inline 更重要的作用是允许函数在多个翻译单元中定义(ODR 例外)。
核心原理
默认参数规则
inline 的底层含义
完整示例
示例一:配置可控的日志系统
场景说明:设计一个日志函数,通过默认参数实现灵活的输出控制。
#include <iostream>
#include <string>
#include <chrono>
#include <iomanip>
// 前向声明枚举和默认值
enum class LogLevel { Debug, Info, Warning, Error };
std::string level_str(LogLevel level) {
switch (level) {
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARN";
case LogLevel::Error: return "ERROR";
default: return "????";
}
}
// 默认参数:输出目标默认 stdout,级别默认 Info
void log(const std::string& msg,
LogLevel level = LogLevel::Info,
std::ostream& out = std::cout,
bool show_timestamp = false) {
if (show_timestamp) {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
out << std::put_time(std::localtime(&time), "%H:%M:%S") << " ";
}
out << "[" << level_str(level) << "] " << msg << std::endl;
}
int main() {
std::cout << "===== 默认参数演示 =====" << std::endl;
// 使用全部默认参数
log("系统启动");
// 只覆盖第一个默认参数
log("磁盘空间不足", LogLevel::Warning);
// 覆盖前两个默认参数
log("严重错误", LogLevel::Error, std::cerr);
// 指定时间戳(必须同时指定前面的参数)
log("数据库连接成功", LogLevel::Info, std::cout, true);
// 可以通过"具名参数"模式改善可读性(C++20 前不支持真正的具名参数)
// 使用 enum + 重载作为替代方案
std::cout << "\n===== 内联函数 =====" << std::endl;
return 0;
}
预期输出:
===== 默认参数演示 =====
[INFO] 系统启动
[WARN] 磁盘空间不足
[ERROR] 严重错误 (输出到 stderr)
14:30:22 [INFO] 数据库连接成功
===== 内联函数 =====
逐段分析:
- 默认参数必须从右向左设置——不能跳过中间的参数指定后面的默认值
- 如想在
show_timestamp设为true的同时保持其他默认值,必须逐个写出所有前导参数——这是默认参数的固有限制 - 解决方式:使用重载函数、Builder 模式,或 C++20 的 designated initializers 模拟具名参数
std::cerr和std::cout都是std::ostream的子类,参数类型是引用
示例二:inline 正确用法与误区
场景说明:演示头文件中 inline 的必要性,以及 inline 无法内联的情况。
math_utils.hpp(头文件)
#ifndef MATH_UTILS_HPP
#define MATH_UTILS_HPP
#include <cmath>
// inline 允许在多个翻译单元中各有一个定义
inline double degrees_to_radians(double degrees) {
return degrees * M_PI / 180.0;
}
// constexpr 函数隐式 inline
constexpr double square(double x) {
return x * x;
}
// 不写 inline 的话,被多个 .cpp 包含会链接错误
inline double hypotenuse(double a, double b) {
return std::sqrt(square(a) + square(b));
}
#endif
geometry.cpp(使用方)
#include "math_utils.hpp"
#include <iostream>
void print_circle_info(double radius) {
double area = M_PI * square(radius);
// 复杂计算中的内联函数被展开,无调用开销
double diag = hypotenuse(3.0, 4.0);
std::cout << "圆面积: " << area << std::endl;
std::cout << "斜边: " << diag << std::endl;
}
main.cpp
#include "math_utils.hpp"
#include <iostream>
inline int add(int a, int b) { return a + b; } // 源文件中的 inline
int main() {
std::cout << "sin(90°) = " << std::sin(degrees_to_radians(90)) << std::endl;
std::cout << "3² = " << square(3) << std::endl;
std::cout << "5² + 12² 的开方 = " << hypotenuse(5, 12) << std::endl;
// 无法内联的情况
// 1. 递归函数(通常)
// 2. 通过函数指针调用(必须先有地址)
double (*fp)(double) = degrees_to_radians;
std::cout << "\n通过函数指针调用: " << fp(180) << std::endl;
return 0;
}
预期输出:
sin(90°) = 1
3² = 9
5² + 12² 的开方 = 13
通过函数指针调用: 3.14159
逐段分析:
- 头文件中的
degrees_to_radians、square、hypotenuse都必须标记为inline——否则当多个.cpp包含此头文件时,链接器会看到多个定义而报错 constexpr函数隐式 inline,不需要显式写inline- 通过函数指针
fp调用时,编译器必须生成函数的实际地址,因此即使标记了inline,函数体仍然需要存在 - 源文件中的
inline(如add)阻止该函数被外部翻译单元引用(内部链接),但如果其他翻译单元也有同名函数不会冲突 - 现代编译器的
-O2优化级别下,标记inline只是"建议",优化器自己决定是否展开
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 默认参数在声明和定义中同时指定 | 编译错误 | 仅在声明中指定 |
| 默认参数从中间开始设 | void f(int a = 1, int b) 编译错误 | 从右向左 |
头文件函数不写 inline | multiple definition 链接错误 | 头文件函数加 inline 或 constexpr |
| 默认参数值在头文件中是编译期常量 | 修改默认值需重新编译所有翻译单元 | 默认值遵循 ODR |
| 虚函数使用默认参数 | 默认参数是静态绑定的(取基类版本) | 避免在虚函数中使用默认参数 |
常见面试问题
默认参数的规则是什么?为什么必须从右向左?——调用时从右向左匹配,不指定默认值的参数必须所有右边参数都不指定默认值。如不限制,调用
f(1)无法判断跳过了中间哪个参数。inline关键字在 C++ 中的真正作用是什么?——两重作用:1) 建议编译器内联展开;2) 允许函数在多个翻译单元中有相同定义(ODR 例外)。现代 C++ 中后者才是主要用途。inline函数一定会被内联吗?什么情况下不会被内联?——不一定。递归函数、通过函数指针调用、函数体过大、虚函数等情况通常不会被内联。现代编译器自主决定。虚函数可以
inline吗?有意义吗?——可以写inline,但通过基类指针的多态调用无法内联(编译期不知道实际类型)。直接通过对象调用时可能被内联。为什么默认参数在虚函数中是危险的?——默认参数是静态绑定的(编译期确定,取基类声明中的值),而虚函数调用是动态绑定的——造成了不一致。
小结
- 默认参数从右向左设置,仅在声明中指定,不在定义中重复
- 头文件中的非模板函数必须标记
inline以避免 ODR 违规 constexpr函数隐式inlineinline在现代 C++ 中主要用于解决 ODR 问题,而非性能- 虚函数避免使用默认参数(静态绑定与动态绑定的冲突)