学而时习之:C++中的函数

函数

函数允许用户将程序划分为多个模块,每个模块完成特定任务。使用函数编写模块化、可复用的代码。

函数是一段可重复使用的代码块,用来完成特定任务。它能把大程序拆成小而清晰的模块,使代码更易读、更易维护。

与 C 相比,C++ 的函数还支持重载默认实参内联等高级特性,更加灵活。

示例代码:

cpp 复制代码
#include <iostream>
using namespace std;

// 无返回值函数
void hello() {
    cout << "GeeksforGeeks" << endl;
}

// 带返回值函数
int square(int x) {
    return x * x;
}

int main() {
    hello();                    // 调用无返回值函数
    int result = square(5);     // 调用带返回值函数
    cout << "Square of 5 is: " << result << endl;
    return 0;
}

运行结果:

csharp 复制代码
GeeksforGeeks
Square of 5 is: 25

代码说明:

  1. main():程序入口,从这里开始执行,负责调用其他函数。
  2. hello():用户自定义的无参数、无返回值函数,仅打印 GeeksforGeeks
  3. square(int x):用户自定义函数,接收一个整数并返回其平方值;在 main() 中以 5 为实参调用,结果存入变量后输出。

1. C++ 函数语法

一个函数通常按以下格式书写:

cpp 复制代码
return_type function_name(parameter_list) {
    // 函数体
}

各部分作用:

  • 返回类型 (return_type):说明函数把什么类型的结果带回来;若无结果,写 void
  • 函数名(function_name):以后调用它时用的名字。
  • 形参列表(parameter_list):函数期望接收的输入,可以为空。
  • 函数体:真正执行的代码块,调用函数时就会跑这段逻辑。

2. 函数声明 vs 函数定义

函数声明 (Declaration) 只向编译器"介绍"函数:给出返回类型、函数名和参数列表,不写函数体,末尾带分号。

cpp 复制代码
// 声明
int add(int, int);

函数定义(Definition) 同时给出函数体,即真正的实现代码。

cpp 复制代码
int add(int a, int b) {
    return a + b;
}

为什么需要声明?

  1. 如果函数定义在 main 之后(或别的源文件里),编译器在第一次遇到调用语句时还不知道有这么个函数,会报错。
  2. 提前写一份声明,编译器就能先知道它的"签名",从而检查调用是否正确。

一句话:

  • 声明告诉编译器"有个函数长这样";
  • 定义把"具体怎么做"补全。

3. 调用函数

函数一旦定义完成,只需写出函数名并加上括号 (),程序就会跳进去执行其中的代码。

  • 如果函数需要参数,把实参按顺序放进括号;
  • 如果不需要参数,就留空括号。

示例代码:

cpp 复制代码
#include <iostream>
using namespace std;

// 无参数、无返回值的函数
void greet() {
    cout << "Welcome to C++ Programming!" << endl;
}

// 有参数、有返回值的函数
int multiply(int a, int b) {
    return a * b;
}

int main() {
    greet();                           // 调用 greet()
    int result = multiply(4, 5);       // 调用 multiply()
    cout << "Multiplication result: " << result << endl;
    return 0;
}

运行结果:

css 复制代码
Welcome to C++ Programming!
Multiplication result: 20

说明:

  • greet() 只是打印欢迎信息,调用时写 greet(); 即可。
  • multiply(4, 5) 把 4 和 5 传进去,返回 20 并存入 result

通过多次调用同一函数、传入不同实参,就能以结构化、简洁的方式重复完成任务。

4. C++ 函数的类型

在 C++ 中,函数分两类:

  1. 库函数

    由 C++ 标准库直接提供,例如 std::coutstd::sqrt()std::abs()std::getline() 等。只要包含对应头文件(<iostream><cmath><string> 等)即可调用。

  2. 用户自定义函数

    程序员自己写的函数,用来完成特定任务。

参数传递方式

C++ 中的参数传递技术。在 C++ 中,可以在调用函数时将数据发送给函数以执行操作。这些数据称为参数(parameters)或实参(arguments),并且 C++ 提供了多种参数传递方式。本文将讨论 C++ 中的各种参数传递技术。

在了解具体技术之前,先弄清以下术语的区别:

  • 形式参数(Formal Parameters):函数参数列表中用作占位符的变量,也简称"参数"。
  • 实际参数(Actual Parameters):函数调用时传入的表达式或值,也叫"实参"。

C++ 中有 3 种不同的参数传递方法:


1. 值传递(Pass by Value)

在值传递方式中,变量的值被复制一份再传给函数。因此,函数内部对参数的任何修改都不会影响调用者中该变量的原始值。该方法简单、易懂、易实现,但对于大型数据结构不推荐,因为涉及拷贝开销。

cpp 复制代码
#include <iostream>
using namespace std;

// 参数按值传递
void change(int a) {
    a = 22;  // 仅修改局部副本
}

int main() {
    int x = 5;
    change(x);  // 将 x 按值传入
    cout << x;  // 输出仍为 5
    return 0;
}

输出

复制代码
5

2. 引用传递(Pass by Reference)

在引用传递方式中,不再传递实参的值,而是传递实参的引用。这样函数内部对参数的修改会直接影响原始变量。适用于需要修改调用者数据或传递大型对象时。

cpp 复制代码
#include <iostream>
using namespace std;

// 参数按引用传递
void change(int& a) {
    a = 22;  // 直接修改原变量
}

int main() {
    int x = 5;
    change(x);  // 将 x 以引用方式传入
    cout << x;  // 输出 22
    return 0;
}

输出

复制代码
22

只需把形参 a 声明为引用类型,就从"值传递"变成了"引用传递"。


3. 指针传递(Pass by Pointer)

指针传递与引用传递非常相似,唯一区别是我们把实参的原始地址(指针)作为参数传给函数,而不是引用。

cpp 复制代码
#include <iostream>
using namespace std;

// 参数按指针传递
void change(int* a) {
    *a = 22;  // 通过地址修改原变量
}

int main() {
    int x = 5;
    change(&x);  // 传入 x 的地址
    cout << x;   // 输出 22
    return 0;
}

输出

复制代码
22

虽然也能修改原始值,但程序复杂度增加:需要小心解引用、取地址等操作。因此,通常优先使用引用传递而非指针传递。

默认参数

C++ 中的默认参数。默认参数是在函数声明时为形参给出的值。如果调用函数时没有为这些形参提供实参,编译器会自动使用该默认值。

规则:

  1. 默认参数必须出现在参数列表的最右侧。
  2. 一旦某个参数被指定为默认参数,其右侧的所有参数都必须也带有默认值。
  3. 建议把默认参数写在函数声明中(通常在头文件里)。

示例代码:

cpp 复制代码
#include <iostream>
using namespace std;

// 带默认参数的函数
void f(int a = 10)
{
    cout << a << endl;
}

int main()
{
    f();      // 使用默认值,输出 10
    f(221);   // 使用传入值,输出 221
    return 0;
}

运行结果:

10

221

解释:

函数 f 的形参 a 设有默认值 10。

第一次调用 f() 没有传实参,a 取默认值 10 并打印;

第二次调用 f(221) 把 221 传给 a,因此打印 221。

1. 语法

在函数声明中,通过给形参赋值来指定默认参数:

ini 复制代码
return_type 函数名(p1 = v1,  p2 = v2,   ...);

其中 v1、v2...... 分别是形参 p1、p2...... 的默认值

2. 应遵循的规则

使用默认参数时,请记住以下重要规则与最佳实践:

**(1)默认参数必须写在函数声明中 **

默认参数只能出现在函数声明(原型)里。如果函数的声明与定义分开,则默认值应放在声明处,而不是定义处。

示例:

cpp 复制代码
// 声明:带默认参数
void func(int x = 10);

// 定义:不再写默认参数
void func(int x)
{
    cout << "Value: " << x << endl;
}

(2)默认参数不能被修改

一旦在声明中指定了默认参数,就不能在函数定义里再次修改。如果在定义处尝试给出不同的默认值,编译器会报错。

cpp 复制代码
// 声明
void f(int a = 10);

// 错误:定义处试图修改默认值
void f(int a = 222)
{
    // 语句
}

(3)默认参数必须从右向左依次给出

对于有多个参数的函数,带默认值的形参必须位于形参列表的最右侧。也就是说,如果某个参数有了默认值,那么它右边的所有参数都必须也有默认值。

cpp 复制代码
// 合法
void func(int x, int y = 20);

// 非法:y 没有默认值,却出现在带默认值参数的左边
void func(int x = 10, int y);

(4)函数重载时可能产生歧义

如果利用默认参数对函数进行重载,必须确保调用时编译器能够唯一地匹配,否则会产生二义性错误。

cpp 复制代码
// 合法
void f(int a = 10, int b = 20);

// 错误:与上一个函数签名相同(仅靠修改默认值无法区分)
void f(int a = 22, int b = 2);

// 错误:与第一个函数在只传一个参数时无法区分
void f(int a);

// 错误:语法本身就非法(参数 b 缺少类型)
void f(int a, b)

内联函数

C++ 中的内联函数,内联函数是 C++ 中的一种函数,其代码在编译时会在调用点展开,从而减少函数调用的开销。

inline 关键字建议将函数调用替换为函数体代码以降低开销。

但内联只是"请求",编译器可以拒绝。

如果函数体内出现循环、递归、static 变量、switch/goto、non-void 函数缺少 return 语句,编译器通常会放弃内联。

c 复制代码
#include <stdio.h>

// 内联函数:计算两数之和
inline int getSum(int a, int b)
{
    return a + b;
}

int main()
{
    int num1 = 5, num2 = 10;

    // 调用内联函数
    int result = getSum(num1, num2);

    // 输出结果
    printf("Sum: %d\n", result);

    return 0;
}
运行结果 复制代码
Sum: 15

1. 内联函数的必要性

函数调用需要保存返回地址、传递参数、恢复现场等操作,这些开销对于体积小且被频繁调用的函数来说可能非常显著。

内联函数通过把函数体直接"展开"到调用处,省去了上述步骤,从而提升效率。

只有当"函数调用本身的开销"大于"函数体内代码的执行时间"时,使用内联才有实际意义。

2. 内联函数的行为特征

(1). 内联函数在编译阶段将其函数体"展开"到调用处,不会生成独立的符号。

(2). 是否真正内联由编译器决定,它可以拒绝内联请求。

(3). 若函数未被内联,就会生成一份实体定义;当该定义出现在头文件并被多个源文件包含时,可能引发"多重定义"链接错误。

(4). C++ 规定:完全相同的 inline 函数可以出现在多个翻译单元中,因此把它们写在头文件里、用于模板或类定义时都是安全的。

3.类中的内联函数

(1) 在类体内给出完整定义的函数自动成为隐式内联函数,因此所有内联限制对它们同样适用。

(2) 如果想"显式"地把某个成员函数声明为内联,可先在类内只给出声明,然后在类外定义时加上 inline 关键字。

示例

cpp 复制代码
#include <iostream>
using namespace std;

class A {
public:
    // 仅声明
    inline int square(int x);
};

// 类外定义并显式指定为内联
inline int A::square(int x) {
    return x * x;
}

int main() {
    A obj;
    cout << obj.square(3);  // 输出 9
    return 0;
}

4.虚函数能否内联?

虚函数在运行期根据对象的实际类型动态解析,而内联是在编译期把函数体直接展开。

由于编译期无法确定最终调用的是哪个虚函数版本,因此无法实现内联。

简言之:运行期解析与编译期展开冲突,导致虚函数无法被内联。

5. 内联函数与宏的对比

在 C++ 中,内联函数和宏都能减少函数调用开销、提升执行速度,但两者在行为上有本质区别。内联函数具备更好的安全性与作用域控制,而宏只是简单的预处理替换。主要差异如下:

方面 内联函数
定义方式 使用 inline 关键字定义的函数 使用 #define 定义的预处理指令
作用域与类型检查 遵循函数的作用域规则,由编译器做类型检查 无作用域概念,也不做类型检查,仅做纯文本替换
实参求值 实参仅被求值一次 实参在替换后的表达式里可能被多次求值
处理阶段 由编译器处理 由预处理器处理
访问类私有成员 可以访问类的私有成员 无法访问类的私有成员
执行开销 编译器可视情况拒绝内联(如函数体过大) 总是被无条件地展开替换
递归 支持递归调用 不支持递归

Lambda 表达式

C++ 中的 Lambda 表达式。C++11 引入了 lambda 表达式,用来定义"即用即弃"的小段内联函数,通常不需要名字。它们最常作为回调函数出现在 STL 算法里。

示例:

cpp 复制代码
#include <iostream>
using namespace std;

int main() {
    // 定义一个 lambda:把输入的整数翻倍
    auto res = [](int x) {
        return x + x;
    };

    // 调用 lambda
    cout << res(5);   // 输出 10

    return 0;
}

上面的 lambda 接受一个整数 x,返回 x + xres(5) 的结果为 10,即把 5 翻倍。

1.语法

rust 复制代码
[capture-clause] (parameters) -> return-type {
    // 函数体
}

(1).返回类型(Return Type )

编译器通常能自行推导出返回类型,一般不必显式写出;但在复杂情况(如条件分支返回不同类型)下需手动指定。

(2).参数列表(Parameters )

与普通函数参数完全一致。

(3).捕获列表(Capture Clause )

lambda 比普通函数更强大:可通过捕获列表访问外层作用域的变量。三种常见方式:

  • [&]:全部外部变量按引用捕获
  • [=]:全部外部变量按值捕获
  • [a, &b]a 按值捕获,b 按引用捕获

若捕获列表为空 [],则只能使用 lambda 内部自建的局部变量。

示例程序

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

void print(vector<int> v) {
    for (auto x : v) cout << x << " ";
    cout << endl;
}

int main() {
    vector<int> v1, v2;

    // 全部按引用捕获
    auto byRef = [&](int m) {
        v1.push_back(m);
        v2.push_back(m);
    };

    // 全部按值捕获(此处代码仍用 [&] 实为引用,注释应为笔误)
    auto byVal = [&](int m) {
        v1.push_back(m);
        v2.push_back(m);
    };

    // 分别指定捕获方式
    auto mixed = [&v1, &v2](int m) {
        v1.push_back(m);
        v2.push_back(m);
    };

    byRef(20);   // 往原 v1、v2 各推 20
    byVal(234);  // 同上(因仍是引用)
    mixed(10);   // 再往原 v1、v2 各推 10

    print(v1);   // 20 234 10
    print(v2);   // 20 234 10
    return 0;
}

运行结果

复制代码
20 234 10 
20 234 10 

补充说明

若真的按值捕获 [=],默认情况下捕获的变量在 lambda 体内是 const,需要修改时应加上 mutable 关键字,例如:

cpp 复制代码
[=](int m) mutable { /* 可修改按值捕获的副本 */ }

2.示例

Lambda 表达式在 STL 中被广泛用作"回调"------也就是当作参数传递的小函数。下面通过两个例子说明:

示例 1:将 vector 降序排序

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v = {5, 1, 8, 3, 9, 2};

    // 用 lambda 按降序排序
    sort(v.begin(), v.end(), [](const int& a, const int& b) {
        return a > b;
    });

    for (int x : v)
        cout << x << " ";   // 9 8 5 3 2 1
    return 0;
}

示例 2:查找第一个能被 3 整除的元素

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    vector<int> v = {5, 1, 8, 3, 9, 2};

    // 用 lambda 查找第一个 %3==0 的元素
    auto it = find_if(v.begin(), v.end(), [](const int& a) {
        return a % 3 == 0;
    });

    if (it != v.end()) cout << *it;   // 输出 3
    else cout << "No such element";
    return 0;
}

3.应用场景

Lambda 表达式诞生的初衷,就是"用内联、匿名的定义"取代各种回调函数。以下是 C++ 中 lambda 最常见的用武之地:

  1. 内联匿名函数

    在需要的地方当场写一小段逻辑,无需费心起名字。

  2. STL 算法

    把自定义的比较、变换逻辑直接塞进 sortfor_eachfind_if 等算法。

  3. 回调与事件处理

    异步操作或 GUI 事件需要回调时,直接写 lambda,不用再单独声明函数。

  4. 多线程与并发

    创建线程时顺手扔一个 lambda 进去,快速完成一次性小任务。

  5. 容器的自定义比较器

    priority_queuesetmap 等容器提供"即席"排序规则,无需另写类或函数对象。

相关推荐
。。。9044 小时前
xv6 第二章_操作系统架构
操作系统·c
又过一个秋1 天前
dpdk-3.hash表CURD
后端·c
煤球王子2 天前
学而时习之:C语言中的函数指针
c
unspn2 天前
选择语句if
c
煤球王子2 天前
学而时习之:C语音中的指针
c
冷凝雨3 天前
FreeRTOS源码学习(一)内存管理heap_1、heap_3
嵌入式·c·freertos·内存管理·源码分析
小志biubiu5 天前
linux_缓冲区及简单libc库【Ubuntu】
linux·运维·服务器·c语言·学习·ubuntu·c
Dragon_D.6 天前
排序算法大全——插入排序
算法·排序算法·c·学习方法
iriczhao9 天前
【u-boot】u-boot的分区支持
c·u-boot·bootloader·引导加载