函数是 C++ 中组织代码、封装逻辑的基本单元。从简单的自由函数到泛型算法、函数对象与 Lambda,理解函数相关规则是掌握 C++ 的必经之路。本文从基础语法出发,逐步深入高阶用法,并提炼出可直接用于编程实践与面试回答的二级结论


一、函数基础

1.1 声明与定义

// 声明(函数原型)
int add(int a, int b);

// 定义
int add(int a, int b) {
    return a + b;
}

关键点

  • 声明可多次,定义只能一次(ODR 原则)。

  • 声明放在头文件中,定义放在源文件中,可分离编译。

  • 返回值类型不可省略(除 main 外,C++ 不允许隐式 int)。

二级结论 1
函数声明是接口契约,定义是实现细节。在设计库时,应尽量在头文件中只放声明,将定义隐藏于 .cpp 文件,以降低编译依赖。

1.2 参数传递基础

传递方式 语法 特点
值传递 void f(int x) 拷贝实参,函数内修改不影响原值;效率取决于拷贝开销
指针传递 void f(int *p) 传递地址,可修改所指对象;可为空;需检查
引用传递 void f(int &r) 别名,无拷贝,可修改实参;不可为空
常引用传递 void f(const int &r) 无拷贝且只读;可绑定右值

二级结论 2
对于只读大对象参数,应优先使用 const T&:既避免拷贝,又保护实参不被修改,且可接受右值。对于内置类型,值传递通常更优。

1.3 数组参数

void f(int arr[]);      // 等价于 void f(int* arr);
void f(int arr[10]);    // 10 被忽略,仍是指针
void f(int (&arr)[10]); // 引用,数组大小是类型的一部分

二级结论 3

  • 传递数组时,一维数组会退化为指针,丢失长度信息;可改用 std::arraystd::vector 或模板+引用数组保持长度。

  • 多维数组只有第一维可省略:void f(int arr[][5]) 等价于 void f(int (*arr)[5])


二、默认参数

void func(int a, int b = 10, int c = 20);
func(1);        // 1,10,20
func(1, 2);     // 1,2,20
func(1, 2, 3);  // 1,2,3

规则

  • 有默认值的参数必须从右向左连续指定。

  • 声明与定义只能在一处指定默认值(通常在声明处)。

  • 默认参数在编译时替换,不参与重载决议。

二级结论 4
默认参数不是函数重载的替代品。如果不同参数组合需要不同逻辑,应使用重载而非默认参数,避免语义混淆。


三、函数重载

void print(int x);
void print(double x);
void print(const std::string &s);

重载规则

  • 参数数量、类型或顺序不同(不包含返回值)。

  • 顶层 const 不区分重载(值参数):void f(int) 与 void f(const int) 视为相同。

  • 底层 const 可区分:void f(int*) 与 void f(const int*) 不同。

  • 引用限定可区分:void f(int&) 与 void f(const int&) 不同。

二级结论 5
重载时避免模糊调用(歧义)。当实参可匹配多个重载版本时,编译器按最佳匹配规则选择;若无法区分,则产生编译错误。设计 API 时,应保证每个调用上下文有唯一最佳候选。


四、内联函数

inline int max(int a, int b) { return a > b ? a : b; }
  • inline 是对编译器的建议,现代编译器会根据自身启发式决策。

  • 内联函数通常定义在头文件中(为可见),否则无法在调用点展开。

  • 适合小型、频繁调用的函数;递归函数不易内联。

二级结论 6

  • 内联不改变函数语义,是纯优化手段。不应为性能随意加 inline,应依赖编译器优化和链接时代码生成(LTO)。

  • 类内定义的成员函数隐式为内联。


五、函数返回

5.1 返回类型

返回类型 语义 注意事项
值 T 返回副本 可能触发拷贝/移动;常用于小对象或需安全独立副本
引用 T& 返回别名 不能返回局部变量引用;常用于链式调用、访问容器元素
常引用 const T& 只读别名 最安全,避免拷贝,常用于返回成员变量
指针 T* 返回地址 可为 nullptr,注意生命周期

5.2 返回值优化(RVO/NRVO)

std::string make_string() {
    return std::string("hello"); // RVO
}

std::string make_string() {
    std::string s("hello");
    return s;  // NRVO
}

二级结论 7

  • C++17 起返回值优化为强制要求(纯右值传递不发生拷贝/移动)。

  • 编写返回局部对象的函数时,直接 return 该对象,不要尝试 std::move,否则会抑制 RVO。

  • 函数按值返回且类型匹配的局部对象,在现代 C++ 中几乎零开销。


六、函数指针

using FuncPtr = int (*)(int, int);   // 等价于 typedef int (*FuncPtr)(int, int);
int add(int a, int b) { return a + b; }
FuncPtr fp = add;
int result = fp(3, 4);   // 调用
  • 函数指针可以指向静态函数、全局函数或类的静态成员函数。

  • 不能直接指向非静态成员函数(因含有隐含 this 参数)。

二级结论 8

  • 为增强可读性,推荐用 using 定义函数指针类型。

  • 函数指针是 C 风格的“回调”机制,C++ 中更推荐使用 std::function 或模板。


七、函数对象(仿函数)

struct Compare {
    bool operator()(int a, int b) const {
        return a < b;
    }
};

Compare cmp;
bool less = cmp(3, 5); // 像函数一样调用
  • 函数对象可以拥有状态(成员变量)。

  • 相比函数指针,编译器更容易内联。

  • 常与泛型算法搭配。

二级结论 9

  • 在需要自定义比较或操作的状态保持时,优先用函数对象而非函数指针。

  • std::sort 等算法接受可调用对象,函数对象性能常优于函数指针。


八、Lambda 表达式(C++11+)

auto f = [](int x) { return x * 2; };
int y = f(10); // 20

捕获方式

捕获 语义
[] 不捕获外部变量
[=] 以值捕获所有外部变量
[&] 以引用捕获所有外部变量
[a, &b] 混合捕获:a 按值,b 按引用
[this] 捕获当前对象指针(可访问成员)
[*this] C++17 捕获当前对象副本

二级结论 10

  • Lambda 本质上生成了一个匿名函数对象的类,捕获变量成为其成员。

  • 按值捕获的变量默认不可修改(相当于函数对象的 const 成员)。若需修改,加 mutable 关键字。

  • 泛型 Lambda:[](auto x) { ... } 生成模板调用运算符。

二级结论 11

  • 避免以引用捕获的方式让 Lambda 超出被引用变量的生命周期(悬垂引用)。

  • 在异步或回调场景,推荐按值捕获或使用 shared_ptr 延长生命周期。


九、std::function:通用函数包装器

#include <functional>
std::function<int(int, int)> func;
func = add;                    // 函数指针
func = [](int a, int b) { return a * b; }; // Lambda
func = std::multiplies<int>(); // 函数对象
  • std::function 能存储任何可调用实体,只要签名兼容。

  • 有轻微运行时开销(类型擦除),不适合极端性能场景。

二级结论 12

  • API 需要接受用户自定义可调用对象时,首选模板;次选 std::function(当需要运行时绑定或类型统一时)。

  • 在高性能循环中,避免直接传递 std::function,改用模板或硬编码调用。


十、成员函数与 const

class MyClass {
    int data;
public:
    int get() const { return data; }   // const 成员函数
    void set(int x) { data = x; }
};

要点

  • const 成员函数的 this 是 const MyClass*,不能修改非 mutable 成员。

  • const 对象只能调用 const 成员函数。

  • 可重载:const 与 non-const 版本可共存,根据对象是否是常量自动选择。

二级结论 13

  • 如果成员函数不修改对象状态,应声明为 const。这不仅是语义承诺,更影响接口在 const 上下文中的可用性。

  • 减少代码重复的惯用法:在 non-const 版本中调用 const 版本,结合 const_cast

const T& get() const { ... }
T& get() {
    return const_cast<T&>(static_cast<const MyClass&>(*this).get());
}

十一、虚函数与多态

class Base {
public:
    virtual void show() const { ... }
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void show() const override { ... }
};
  • virtual 启用动态绑定,通过基类指针/引用调用时根据实际类型派发。

  • override 关键字确保确实重写了基类虚函数。

  • final 可禁止进一步重写。

  • 虚函数表(vtable)是常见实现方式,每个多态类有 vtable,对象包含 vptr。

二级结论 14

  • 若类可能被继承,基类析构函数应声明为 virtual 或 protected(非 virtual 析构会阻止正确释放派生资源)。

  • 构造函数不能是虚函数(对象尚未构造),析构函数可以。

  • 默认参数在虚函数中使用需谨慎:默认参数是静态绑定,会取基类定义的默认值,而不是动态类型中的默认值。


十二、模板函数(泛型函数)

template<typename T>
T max(T a, T b) { return a > b ? a : b; }

特性

  • 编译时实例化,生成多个具体版本。

  • 支持显式指定模板参数:max<int>(3, 5)

  • 模板参数推导:可从函数实参自动推导。

  • 可重载:非模板函数优先匹配,模板版本仅在没有更佳匹配时使用。

二级结论 15

  • 模板函数定义通常放在头文件中,因为编译器需要看到完整定义才能实例化。

  • 模板 + 完美转发是实现泛型库的基础(见下文)。


十三、完美转发与可变参数模板

template<typename F, typename... Args>
auto call(F&& f, Args&&... args) -> decltype(f(std::forward<Args>(args)...)) {
    return f(std::forward<Args>(args)...);
}

要点

  • std::forward<Args>(args) 保留参数的左值/右值性质。

  • 引用折叠规则使万能引用 T&& 能绑定左值或右值。

  • 可变参数模板 Args... 允许任意数量参数。

二级结论 16

  • 完美转发是编写工厂函数、包装函数、委托构造函数等的核心技术。

  • 结合 decltype 和尾置返回类型,可精确保留原始函数调用的返回类型。


十四、移动语义与函数

void sink(std::string&& s) {
    std::string local = std::move(s);
}
  • 右值引用 T&& 绑定即将销毁的对象,允许移动构造/赋值,避免深拷贝。

  • 对于函数参数,T&& 可作为“万能引用”,配合完美转发传递。

  • 对于局部变量,如果确定不再使用,可使用 std::move 转为右值,启用移动。

二级结论 17

  • 不要返回局部对象的右值引用(引用悬垂)。

  • 设计按值返回的函数时,直接返回局部对象即可,不要 std::move,以免抑制 RVO。

  • 接受按值参数的函数,若需保留副本,可用 std::move 将其移入存储,减少一次拷贝。


十五、constexpr 函数(编译期函数)

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f5 = factorial(5); // 编译期计算
  • constexpr 函数可能在编译期执行,也可在运行期执行,取决于调用上下文。

  • C++14 起允许 constexpr 函数中有局部变量和循环,C++17 进一步放宽限制。

  • 与 const 不同:const 只读,constexpr 强调编译期求值可能性。

二级结论 18

  • 尽量将可在编译期确定的函数标记为 constexpr,提高性能并支持常量表达式上下文使用。

  • 注意:constexpr 函数在运行期调用时,其行为与普通函数一致,无额外约束。


十六、二级结论速查总表

编号 结论
1 头文件放声明,源文件放定义,降低编译依赖
2 大对象只读参数使用 const T&:避免拷贝、保护实参、接受右值
3 数组参数会退化为指针;保持长度信息用 std::array 或引用数组
4 默认参数替代重载可能导致语义模糊,不同逻辑应用重载
5 设计重载时确保唯一最佳匹配,避免歧义
6 内联由编译器决策,类内定义成员函数隐式内联
7 返回局部对象不要 std::move;RVO 在 C++17 强制
8 回调优先用模板或 std::function 而非函数指针
9 需要状态的函数对象优于函数指针
10 Lambda 按值捕获变量不可修改,加 mutable 可改
11 避免引用捕获在异步中产生悬垂引用
12 运行时绑定用 std::function,性能敏感用模板
13 不修改对象的成员函数声明为 const,可减少重复用 const_cast
14 基类析构应为虚函数;虚函数默认参数静态绑定
15 模板定义放头文件;非模板比模板优先匹配
16 完美转发保留值类别,是泛型库的基础
17 不要返回局部右值引用,按值返回勿用 std::move
18 编译期可行函数标记为 constexpr,同时支持编译期和运行期调用

以上为 C++ 函数从基础到高阶的全景式总结。扎实掌握这些规则与二级结论,既是编写高质量 C++ 代码的保证,也是技术面试中脱颖而出的关键

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐