搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第11章 函数进阶

第11章 函数进阶

入门 👤 已掌握函数基础,希望写出更灵活、高效代码的开发者 🔧 处理不确定数量的数据、实现回调机制、优化函数调用
本章你将学到
  • 如何定义和使用参数数量可变的函数
  • 函数指针的原理与典型应用场景
  • 内联函数如何提升程序运行效率
核心概念可变参数函数指针

在第6章,我们学会了如何定义和调用一个参数固定的函数。但在实际项目中,你经常会遇到一些更灵活的需求。比如,写一个日志函数,有时想打印一条信息,有时想打印三条,参数数量不固定;或者,你需要根据用户的选择,动态调用不同的处理函数。本章,我们就来解决这些问题。

11.1 可变参数函数:处理不确定数量的输入

想象一下餐厅的点餐系统。顾客可以只点一份主食,也可以“主食+汤+沙拉+饮料”点一大堆。一个优秀的点餐函数,应该能处理这种数量不确定的订单。在C++中,可变参数函数就是这样的“点餐系统”。

技术定义

可变参数函数:允许函数接受数量可变的参数。它使用省略号...表示参数列表的结束,并通过一组宏来访问这些不确定的参数。

C++标准库提供了<cstdarg>头文件来支持可变参数。其核心是三个宏:va_start、va_arg和va_end。它们的工作原理就像一个指针,在参数列表中逐个移动并读取数据。

固定参数
→
va_start
→
参数1
→
参数2
→
...
↓↓↓
(初始化指针)
va_arg va_arg va_end

下面是一个计算任意数量整数之和的示例。注意,第一个固定参数count用于告知函数后面有几个可变参数,这是最常见的用法。

#include <iostream>
#include <cstdarg>

int sum(int count, ...) {
    int total = 0;
    va_list args;        // 定义参数列表指针
    va_start(args, count); // 初始化指针,指向第一个可变参数

    for (int i = 0; i < count; ++i) {
        total += va_arg(args, int); // 按int类型读取一个参数,指针后移
    }

    va_end(args); // 清理工作
    return total;
}

int main() {
    std::cout << sum(3, 10, 20, 30) << std::endl; // 输出 60
    std::cout << sum(5, 1, 2, 3, 4, 5) << std::endl; // 输出 15
    return 0;
}

常见误区

  • 忘记传递参数数量信息:可变参数函数本身不知道有多少个参数,你必须通过第一个固定参数(如count)或一个特殊的结束标志(如字符串中的‘\0’)来告知。我在一个日志模块中踩过这个坑,忘记传数量导致读到了垃圾内存,程序随机崩溃。
  • 类型错配:va_arg(args, int)假设下一个参数是int。如果你传了一个double,读取的结果将是错误的,且无法在编译期发现。C++11引入的类型安全可变参数模板(第31章内容)是更好的现代解决方案。
  • 遗漏va_end:虽然在一些平台上不调用va_end可能不会立即出错,但这不符合规范,可能导致资源泄漏或不可预知的行为。

11.2 函数指针:让函数成为“变量”

把函数想象成一台特定的机器(比如榨汁机、烤箱)。函数指针就是这台机器的“遥控器”。你不需要知道机器内部如何工作,只需要通过遥控器(指针)来启动它。这让你可以在运行时决定使用哪台机器。

技术定义

函数指针:一种指向函数入口地址的指针变量。通过它,可以间接调用函数,从而实现回调、函数表等动态行为。

声明一个函数指针需要模仿目标函数的签名。语法略显复杂,但记住模式:返回类型 (*指针变量名)(参数类型列表)。

函数声明对应的函数指针声明说明
int max(int a, int b);int (*pFunc)(int, int);指向返回int,接受两个int参数的函数
void log(const char* msg);void (*pLog)(const char*);指向无返回值,接受一个字符串参数的函数
double calculate();double (*pCalc)();指向无参,返回double的函数

赋值和调用时,函数名本身就代表它的地址(和数组名类似),可以省略取地址符&。

#include <iostream>
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

int main() {
    // 声明一个函数指针p
    int (*p)(int, int);

    p = add; // 将p指向add函数,等价于 p = &add;
    std::cout << p(3, 4) << std::endl; // 输出 7,等价于 (*p)(3, 4)

    p = multiply; // 将p改为指向multiply函数
    std::cout << p(3, 4) << std::endl; // 输出 12

    return 0;
}

函数指针最经典的应用是回调函数和函数表。例如,一个排序算法(如qsort)需要你提供一个比较函数的指针,它就能根据你的规则排序任何数据。一个简单的计算器菜单,可以用函数指针数组来实现,避免冗长的if-else或switch链。

// 函数表应用示例片段
typedef int (*Operation)(int, int);
Operation opTable[] = {add, multiply, /* 可添加subtract, divide等 */};

// 用户选择操作0(加)或1(乘)...
int choice = 1;
int result = opTable[choice](5, 6); // 调用multiply(5, 6)

编者提示:函数指针的语法和概念是理解C++中更高级抽象(如第27章的Lambda表达式、第30章的设计模式中的策略模式)的重要基石。虽然现代C++提供了std::function等更安全的包装器,但理解底层指针原理依然至关重要。

11.3 内联函数:用空间换时间的优化

调用函数是有成本的:需要压栈参数、跳转指令、执行函数体、返回结果。对于像“求两个数最大值”这样极其简单的函数,调用开销可能比执行操作本身还大。这就像为了打印一页纸,你每次都从工位跑到遥远的打印机(函数调用),而不是在桌上放一台小型打印机(内联展开)。

技术定义

内联函数:使用inline关键字建议编译器将函数体直接插入到每个调用点,从而消除函数调用的开销。它是一种以增加代码体积为代价来提升运行速度的优化手段。

你只需要在函数声明或定义前加上inline关键字即可。注意,这只是一个建议,编译器最终决定是否内联。复杂的函数、递归函数或虚函数通常不会被内联。

// 头文件 math_utils.h
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

// 源文件 main.cpp
#include "math_utils.h"
int main() {
    int x = 10, y = 20;
    int m = max(x, y); // 编译器可能会将此展开为:int m = (x > y) ? x : y;
    return 0;
}

适合内联的场景

  • 函数体非常小(1-5行简单语句)。
  • 在性能关键的循环中被频繁调用。
  • “Getter/Setter”等访问成员变量的函数。

避免内联的场景

  • 函数体复杂(包含循环、递归、大量逻辑)。
  • 虚函数(运行时多态决定了无法在编译期确定代码位置)。
  • 函数指针指向的函数(因为指针值在运行时才能确定)。

内联函数 vs. 带参数的宏:你可能在C语言中用过#define MAX(a,b) ((a)>(b)?(a):(b))来实现类似效果。但宏是文本替换,没有类型检查,且可能因参数多次求值导致副作用(如MAX(i++, j++))。内联函数是真正的函数,有类型安全和作用域,是现代C++中替代宏的首选。

工程经验

实际开发中,对于小型工具函数,我习惯直接写在类定义内部(头文件中)。在类内部定义的成员函数默认就是内联的,无需显式添加inline关键字。对于全局工具函数,将其定义在头文件中并加上inline,可以避免在多文件包含时产生“重复定义”的链接错误。

动手试一试

  1. 实现一个可变参数求平均值函数:仿照sum函数,写一个average(int count, ...)函数,计算任意数量整数的平均值(返回double)。注意处理count为0的情况。
  2. 构建一个简单的命令分发器:定义三个函数:void cmd_help(), void cmd_version(), void cmd_exit()。创建一个函数指针数组来存储它们。编写一个循环,根据用户输入的字符串(如“help”、“version”),查找并调用对应的函数。
  3. 测试内联效果:写一个简单的内联函数square(int x)返回x的平方。在main函数中循环调用它100万次。尝试去掉inline关键字,在开启编译器优化(如g++的-O2)的情况下,比较两者运行时间(可用<chrono>库计时)。你会发现,对于这种微小函数,现代编译器即使没有inline关键字也可能自动内联。

检验你的理解

  1. 编写一个可变参数函数printValues(const char* format, ...),其中format是一个简单格式字符串,如“%d %f %s”。你需要根据格式字符串中的字符(%d对应int,%f对应double,%s对应char*)来使用va_arg读取并打印相应类型的值。这个练习能深刻检验你对可变参数类型匹配的理解。
  2. 假设有一个函数指针int (*callback)(int),它被用作一个数据处理库的回调。库函数会传入一个整型数据,期望回调函数返回处理后的结果。请设计一个场景,编写两个不同的回调函数(例如,一个将输入加倍,一个将输入平方),并通过函数指针传递给一个模拟的库函数void processData(int data, int (*cb)(int))。
  3. 为什么递归函数通常不会被编译器内联?请从内联展开的原理角度思考。

本章小结

  • 可变参数函数通过<cstdarg>中的宏实现,能处理参数数量不确定的情况,但需注意手动传递参数数量和类型安全的问题。
  • 函数指针(如int (*p)(int, int))允许将函数作为数据传递和存储,是实现回调机制和函数表(避免复杂分支判断)的核心技术。
  • 内联函数(inline关键字)建议编译器将函数体展开到调用处,以消除调用开销,适用于简单且频繁调用的函数,是类型安全的宏替代品。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注