乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

默认参数与内联函数

定义与作用

默认参数允许在函数声明中为参数指定默认值,调用时可以省略该实参:

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) 编译错误从右向左
头文件函数不写 inlinemultiple definition 链接错误头文件函数加 inline 或 constexpr
默认参数值在头文件中是编译期常量修改默认值需重新编译所有翻译单元默认值遵循 ODR
虚函数使用默认参数默认参数是静态绑定的(取基类版本)避免在虚函数中使用默认参数

常见面试问题

  1. 默认参数的规则是什么?为什么必须从右向左?——调用时从右向左匹配,不指定默认值的参数必须所有右边参数都不指定默认值。如不限制,调用 f(1) 无法判断跳过了中间哪个参数。

  2. inline 关键字在 C++ 中的真正作用是什么?——两重作用:1) 建议编译器内联展开;2) 允许函数在多个翻译单元中有相同定义(ODR 例外)。现代 C++ 中后者才是主要用途。

  3. inline 函数一定会被内联吗?什么情况下不会被内联?——不一定。递归函数、通过函数指针调用、函数体过大、虚函数等情况通常不会被内联。现代编译器自主决定。

  4. 虚函数可以 inline 吗?有意义吗?——可以写 inline,但通过基类指针的多态调用无法内联(编译期不知道实际类型)。直接通过对象调用时可能被内联。

  5. 为什么默认参数在虚函数中是危险的?——默认参数是静态绑定的(编译期确定,取基类声明中的值),而虚函数调用是动态绑定的——造成了不一致。

小结

  • 默认参数从右向左设置,仅在声明中指定,不在定义中重复
  • 头文件中的非模板函数必须标记 inline 以避免 ODR 违规
  • constexpr 函数隐式 inline
  • inline 在现代 C++ 中主要用于解决 ODR 问题,而非性能
  • 虚函数避免使用默认参数(静态绑定与动态绑定的冲突)
上一页
函数重载
下一页
Lambda 表达式