火龙信奥
  • 分享
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

第四章 C++ 函数专题讲义

作者: 作者的头像   huolong , 时间:2026-08-20 18:58:20 , 所有人可见, 阅读  37

第四章 C++ 函数专题讲义

本讲义系统地梳理了 C++ 中函数的基本概念、参数传递机制、生命周期安全、现代 C++ 高级特性(如 Lambda 表达式、可调用对象等),旨在帮助你构建完整的函数知识体系。


1. 函数基础

函数是 C++ 程序的基本构建模块,用于将复杂的逻辑拆分为可复用的代码块。

1.1 函数的定义与声明

在 C++ 中,函数必须先声明后调用。

  • 定义(Definition):提供函数的具体实现。
  • 声明(Declaration / 原型):告知编译器函数的名称、返回类型及参数列表。通常放在头文件或调用处的上方。
#include <iostream>

// 1. 函数声明(Forward Declaration)
double add(double a, double b);

int main() {
    // 2. 函数调用
    double result = add(3.5, 4.5);
    std::cout << "Result: " << result << std::endl;
    return 0;
}

// 3. 函数定义
double add(double a, double b) {
    return a + b;
}

1.2 返回值类型

  • 普通类型:如 int, double, 自定义结构体等。
  • void 类型:不返回任何值。若要提前结束函数,可直接使用 return;。
  • 注意:不能直接对函数名赋值。

2. 参数传递机制

C++ 支持多种参数传递方式。选择合适的传递方式对程序性能和正确性至关重要。

2.1 值传递 (Pass by Value)

将实参的副本复制给形参。函数内对形参的修改不会影响外部实参。 * 适用场景:内置基本类型(如 int, double),占用内存小的对象。 * 缺点:对于大型对象(如 std::vector、大结构体),复制会带来显著的性能开销。

void increment(int x) {
    x++; // 仅修改了副本
}

2.2 指针传递 (Pass by Pointer)

将实参的地址复制给形参指针。可以通过解引用(*)修改外部实参的值。 * 特点:传递的是地址(通常为 8 字节),效率高。 * 注意:指针可能为 nullptr,在函数内使用前通常需要进行空指针检查。

void increment(int* p) {
    if (p != nullptr) {
        (*p)++; // 修改了指针所指向的值
    }
}

2.3 引用传递 (Pass by Reference)

形参是实参的别名,不发生任何复制。对形参的修改会直接反映在实参上。 * 优点:既拥有指针的高效率(无复制开销),又避免了繁琐的解引用操作,且无需担心空指针问题。 * 局限性:无法直接传入字面量常数(如 5)或临时表达式。

void increment(int& r) {
    r++; // 直接修改外部实参
}

2.4 常量引用传递 (Pass by Const Reference)

通过 const Type& 传递参数。 * 优点:既避免了复制开销,又保护了数据不被函数内部意外修改,并且允许传入字面量常数和临时表达式。 * 适用场景:标准库容器、复杂类对象等大对象的“只读”传递。

// 既能避免大对象复制,又能防止内部修改,且支持传入常量
void printVector(const std::vector<int>& vec) {
    for (int x : vec) {
        std::cout << x << " ";
    }
}

参数传递方式对比

传递方式 是否发生复制 能否修改实参 是否支持传入常量/字面量 推荐使用场景
值传递 是 否 是 基础类型(int, char, double 等)
指针传递 否(仅复制地址) 是 否(需取地址 &) 需要兼容 C 语言接口,或允许参数为 nullptr
引用传递 否 是 否 需要修改实参的大对象
常量引用传递 否 否 是 不需要修改的复杂类对象、标准库容器

3. 返回值与生命周期安全

3.1 悬挂指针与悬挂引用 (Dangling Pointer / Reference)

当函数返回指针或引用时,绝对不能返回指向函数内部局部变量的指针或引用。局部变量在函数生命周期结束时会被销毁,返回它们的地址会导致未定义行为(Undefined Behavior)。

❌ 错误示例:

int& getLocalReference() {
    int val = 42;
    return val; // 警告:返回了局部变量 val 的引用!
} // val 占用的内存在此处被销毁

3.2 安全的解决方案

  1. 值返回:直接返回值,依靠编译器优化(RVO/NRVO)可以避免复制开销。
  2. 静态局部变量:使用 static 延长生命周期。
  3. 全局变量或堆内存(需注意内存释放)。
// 正确做法:利用 static 使变量在程序运行期间一直存在
const int& getStaticReference() {
    static int val = 42;
    return val; 
}

4. 函数的高级特性

4.1 函数重载 (Function Overloading)

在同一作用域内,允许存在多个同名但参数列表不同(参数个数、类型或顺序不同)的函数。 * 注意:仅返回值类型不同不能构成重载。

int add(int a, int b);
double add(double a, double b); // OK,重载
// void add(int a, int b);       // 错误,仅返回值类型不同,无法重载

4.2 默认参数 (Default Arguments)

可以为函数的参数指定默认值。如果调用时未传入该参数,则使用默认值。 * 规则:默认参数必须从右向左连续排列。

// 正确:默认参数在最右侧
void displayInfo(std::string name, int age = 18, double score = 100.0);

// 错误:默认参数后面出现了无默认值的参数
// void displayInfo(std::string name = "User", int age); 

4.3 内联函数 (Inline Functions)

对于代码量少、调用频繁的函数,可以使用 inline 关键字修饰。 * 机制:通知编译器在调用处将函数体直接展开,避免函数调用栈的开销(如压栈、跳转、出栈)。 * 现代编译器:inline 仅仅是向编译器提出的建议,现代编译器会根据实际情况自动决定是否内联。

// 温度转换:先乘后除以保证整型计算的精度
inline int FtoC(int f) { 
    return (f - 32) * 5 / 9; 
}

4.4 常量表达式函数 (constexpr) (C++11 新增)

使用 constexpr 修饰的函数如果传入的参数是编译期常量,则该函数会在编译期进行计算,进一步提升运行效率。

constexpr int square(int x) {
    return x * x;
}

int main() {
    constexpr int res = square(5); // 编译期直接计算出 25
    int arr[res];                 // 可以作为数组大小
}

5. 函数指针与可调用对象

5.1 函数指针 (Function Pointer)

函数在内存中也有地址,函数指针用于存放函数的入口地址。

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

int main() {
    // 声明一个函数指针 pFunc,指向“接收两个 int 并返回 int”的函数
    int (*pFunc)(int, int) = add;

    // 调用
    int res = pFunc(3, 4); // 等价于 (*pFunc)(3, 4)
}

5.2 std::function (C++11 包装器)

std::function 是 C++11 引入的通用多态函数包装器,可以存储、复制和调用任何可调用目标(普通函数、Lambda、函数对象等)。

#include <functional>

int multiply(int a, int b) { return a * b; }

int main() {
    std::function<int(int, int)> func = multiply;
    int res = func(3, 4); // 12
}

6. Lambda 表达式 (C++11 新增匿名函数)

Lambda 表达式提供了一种在需要的地方直接定义匿名函数对象的方法,非常适合与 STL 算法配合。

6.1 基本语法

[捕获列表](参数列表) mutable(可选) -> 返回类型(可选) { 函数体 }

6.2 常用捕获方式

捕获写法 解释
[] 不捕获任何外部变量。
[x] 按值捕获变量 x(复制一份,内部默认不可修改,除非加 mutable)。
[&x] 按引用捕获变量 x(可直接修改外部原变量)。
[=] 按值捕获所有使用到的外部局部变量。
[&] 按引用捕获所有使用到的外部局部变量。
[=, &x] 默认按值捕获,但 x 按引用捕获。
[&, x] 默认按引用捕获,但 x 按值捕获。
[this] 捕获当前类的 this 指针,允许在 Lambda 内访问数成员。

6.3 典型应用示例

示例 1:自定义排序 (STL std::sort)

#include <vector>
#include <algorithm>

std::vector<int> v = {3, 1, 4, 1, 5};

// 降序排序:通过 Lambda 表达式定义比较规则
std::sort(v.begin(), v.end(), [](int a, int b) { 
    return a > b; 
});

示例 2:按条件计数 (STL std::count_if)

#include <vector>
#include <algorithm>

int threshold = 3;
std::vector<int> v = {1, 2, 3, 4, 5};

// 捕获外部变量 threshold 
auto cnt = std::count_if(v.begin(), v.end(), [threshold](int x) {
    return x > threshold;
}); // cnt = 2

6.4 递归 Lambda

由于匿名函数没有固定的函数名,实现自我调用(递归)需要一些特殊技巧。

方案 A:C++14 通用泛型引用方案

利用 C++14 的泛型 Lambda,通过将自身作为参数传递来实现递归。

#include <vector>

std::vector<std::vector<int>> g; // 邻接表结构

// 使用 [&] 捕获外部邻接表 g
// 使用 auto&& self 接收 Lambda 自身
auto dfs = [&](auto&& self, int u, int fa) -> void {
    for (int v : g[u]) {
        if (v != fa) {
            self(self, v, u); // 递归调用时,必须将自身作为第一个参数传入
        }
    }
};

// 启动调用
dfs(dfs, 1, 0);

方案 B:C++23 显式对象形参(Deducing This)

C++23 引入了显式对象形参,允许在 Lambda 中直接使用 this 关键字指代当前 Lambda 闭包,让递归 Lambda 的写法变得非常自然。

// C++23 标准写法
auto dfs = [&](this auto&& self, int u, int fa) -> void {
    for (int v : g[u]) {
        if (v != fa) {
            self(v, u); // 像普通函数一样直接递归,无需重复传递 self
        }
    }
};

// 启动调用
dfs(1, 0);

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

关于火龙

  • 关于我们
  • 学员获奖
  • 预约试听
  • ACM课程
  • CSP课程
  • 学习指南

帮助中心

  • 用户协议
  • 打字练习
  • 在线画图
  • DevC++下载
  • CSP报名
  • GESP官网

推荐课程

  • C++零基础入门(可试看)
  • C++进阶提升
  • GESP考级辅导
  • GESP打卡
  • CSP-J/S打卡

公众号

火龙信奥公众号二维码

地址:义乌市北门街188号新天地商厦二楼2F 邮箱:wdlok305@126.com

© 2017-2026 义乌市睿码科技有限公司版权所有 浙ICP备2021013995号

火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码