C++学习:六个月从基础到就业——C++基础语法回顾:函数定义与调用
本文是我C++学习之旅系列的第四篇技术文章,主要回顾C++中的函数定义与调用,包括函数重载、默认参数、内联函数和Lambda表达式等。查看完整系列目录了解更多内容。
引言
函数是C++程序的基本构建块,它们允许我们将代码分解成逻辑单元,提高代码的可重用性和可维护性。无论是计算值、执行操作还是组织代码,函数都扮演着不可或缺的角色。本文将详细介绍C++中函数的定义与调用,从基础概念到高级特性,并提供实用的编程技巧和最佳实践。
函数基础
函数的定义
C++中的函数定义包括返回类型、函数名、参数列表和函数体:
返回类型 函数名(参数列表) {
// 函数体
return 返回值; // 如果函数有返回值
}
示例:
int add(int a, int b) {
return a + b;
}
函数定义的各个部分:
-
返回类型:函数执行后返回的值的数据类型。可以是任何有效的C++类型,包括基本类型、用户定义类型,或者
void
(表示不返回值)。 -
函数名:标识符,遵循C++命名规则。应该选择有意义的名称,反映函数的功能。
-
参数列表:函数接收的输入数据。每个参数包括类型和名称,多个参数用逗号分隔。参数列表可以为空。
-
函数体:包含在花括号内的代码块,定义了函数的具体行为。
-
返回语句:使用
return
关键字返回值(除非函数返回类型为void
)。
函数声明与定义
函数声明(也称为函数原型)告诉编译器函数的签名,而不提供实现细节:
返回类型 函数名(参数类型列表);
示例:
int add(int, int); // 函数声明
函数声明和定义的区别:
- 声明只包含函数签名,以分号结束,没有函数体
- 定义包含完整的函数实现
- 一个函数可以有多个声明,但只能有一个定义
函数声明通常放在头文件中,而定义放在源文件中,这种分离有助于:
- 改善编译时间(修改实现时只需重新编译相关源文件)
- 支持分离编译(不同的编译单元可以独立编译)
- 隐藏实现细节(头文件提供接口,源文件提供实现)
函数调用
函数调用是通过函数名和参数列表来执行函数:
函数名(参数列表);
示例:
int result = add(5, 3); // 调用add函数,返回值存储在result中
函数调用流程:
- 程序流转到函数代码处
- 传递参数
- 执行函数体
- 返回到调用点,可能带有返回值
void函数
如果函数不需要返回值,可以使用void
返回类型:
void printMessage(const std::string& message) {
std::cout << message << std::endl;
// 不需要return语句,或者可以使用 return; 提前返回
}
参数传递
C++提供了多种向函数传递参数的方式,每种方式都有其适用场景。
值传递
最简单的参数传递方式是值传递,函数接收参数的副本:
void modifyValue(int x) {
x = 100; // 修改的是局部副本,不影响原始值
}
int main() {
int num = 5;
modifyValue(num);
std::cout << num << std::endl; // 输出5,原值未变
return 0;
}
值传递的特点:
- 函数接收参数的副本
- 对参数的修改不影响调用者的原始值
- 适合小型数据类型
- 对大型对象可能导致性能问题(复制开销)
引用传递
引用传递允许函数直接操作传入的原始值:
void modifyValue(int& x) {
x = 100; // 直接修改原始值
}
int main() {
int num = 5;
modifyValue(num);
std::cout << num << std::endl; // 输出100,原值被修改
return 0;
}
引用传递的特点:
- 函数接收参数的引用(别名)
- 对参数的修改会影响调用者的原始值
- 避免了复制大型对象的开销
- 可能导致意外的副作用,如果不小心修改了参数
常量引用传递
常量引用结合了值传递和引用传递的优点:
void displayValue(const int& x) {
std::cout << x << std::endl;
// x = 100; // 错误:不能修改常量引用
}
常量引用传递的特点:
- 避免复制大型对象
- 防止函数修改参数
- 适用于传递大型只读参数
指针传递
指针传递与引用传递类似,允许函数通过指针间接修改原始值:
void modifyValue(int* ptr) {
if (ptr) {
// 检查空指针
*ptr = 100; // 通过解引用修改指针指向的值
}
}
int main() {
int num = 5;
modifyValue(&num);
std::cout << num << std::endl; // 输出100,原值被修改
return 0;
}
指针传递的特点:
- 函数接收内存地址
- 可以传递
nullptr
- 需要额外的空指针检查
- 允许函数修改调用者的原始值
指针vs引用的选择:
- 当参数可能为空时,使用指针
- 当参数一定存在且不会重新指向其他对象时,使用引用
- 引用通常提供更清晰的语法,指针提供更明确的意图表达
数组传递
数组作为参数时会退化为指针:
void processArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2; // 修改原始数组元素
}
}
// 等效的指针版本
void processArray(int* arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] *= 2;
}
}
int main() {
int numbers[5] = {
1, 2, 3, 4, 5};
processArray(numbers, 5);
// numbers现在是{2, 4, 6, 8, 10}
return 0;
}
数组传递的特点:
- 数组名作为参数时退化为指向首元素的指针
- 函数内无法知道数组的实际大小,需要额外参数指定
- 原始数组的元素可以被修改
现代C++中处理数组的更好方法:
- 使用标准容器如
std::vector
或std::array
- 使用
std::span
(C++20)
// 使用std::vector
void processVector(std::vector<int>& vec) {
for (int& value : vec) {
value *= 2;
}
}
// 使用std::array
void processArray(std::array<int, 5>& arr) {
for (int& value : arr) {
value *= 2;
}
}
// C++20: 使用std::span
void processSpan(std::span<int> values) {
for (int& value : values) {
value *= 2;
}
}
函数重载
C++允许同一作用域中的多个函数使用相同的函数名,只要它们的参数列表不同。这种特性称为函数重载。
重载基础
重载函数必须在以下至少一个方面不同:
- 参数数量
- 参数类型
- 参数顺序
示例:
// 重载的print函数
void print(int value) {
std::cout << "整数: " << value << std::endl;
}
void print(double value) {
std::cout << "浮点数: " << value << std::endl;
}
void print(const std::string& value) {
std::cout << "字符串: " << value << std::endl;
}
void print(int value, double rate) {
std::cout << "整数: " << value << ", 比率: " << rate << std::endl;
}
int main() {
print(42); // 调用 print(int)
print(3.14); // 调用 print(double)
print("Hello"); // 调用 print(const std::string&)
print(100, 0.5); // 调用 print(int, double)
return 0;
}
注意:函数返回类型不同但参数列表相同的函数不构成重载。
重载解析
编译器通过比较函数调用中的参数类型与每个重载函数的参数类型来选择最匹配的函数:
void display(int x) {
std::cout << "Int: " << x << std::endl; }
void display(double x) {
std::cout << "Double: " << x << std::endl; }
int main() {
display(5); // 调用 display(int)
display(5.0); // 调用 display(double)
display(5.0f); // 调用 display(double),float被提升为double
return 0;
}
重载解析过程:
- 寻找完全匹配的函数
- 寻找可通过简单类型转换匹配的函数
- 寻找可通过标准类型转换匹配的函数
- 寻找可通过用户定义的类型转换匹配的函数
如果有多个函数同样匹配,则调用是不明确的,编译器会报错。
重载与默认参数
函数重载与默认参数结合使用时要小心,以避免不明确的调用:
// 有潜在问题的代码
void process(int x) {
/* ... */ }
void process(int x, int y = 0) {
/* ... */ }
int main() {
process(10); // 不明确:调用哪个process?
return 0;
}
重载与作用域
函数重载只能在同一作用域内进行:
void display(int x) {
std::cout << "全局: " << x << std::endl; }
namespace Util {
void display(int x) {
std::cout << "Util: " << x << std::endl; }
void example() {
display(5); // 调用 Util::display,不是重载
}
}
默认参数
C++允许为函数参数指定默认值,在调用函数时如果未提供该参数,将使用默认值:
void setConfiguration(int timeout = 30, bool debug = false, const std::string& mode = "normal") {
std::cout << "超时: " << timeout << ", 调试: " << debug << ", 模式: " << mode << std::endl;
}
int main() {
setConfiguration(); // 使用所有默认值
setConfiguration(60); // 自定义timeout,其他使用默认值
setConfiguration(60, true); // 自定义timeout和debug,mode使用默认值
setConfiguration(60, true, "safe"); // 自定义所有参数
return 0;
}
默认参数的规则:
- 默认参数必须从右到左指定(如果一个参数有默认值,则它右侧的所有参数也必须有默认值)
- 函数调用时,参数从左到右提供,不能跳过参数
- 默认参数通常在函数声明而非定义中指定
- 默认参数的值在编译时确定,不能是运行时表达式
- 同一参数的默认值不能在多个声明中重复指定
常见的默认参数使用场景:
- 配置选项,大多数调用使用默认配置
- 函数的可选行为
- 向后兼容性,添加新参数但不破坏现有代码
内联函数
内联函数是对编译器的一种建议,请求将函数调用替换为函数体,以减少函数调用开销:
inline int square(int x) {
return x * x;
}
int main() {
int result = square(5); // 编译器可能将其替换为 int result = 5 * 5;
return 0;
}
内联函数的特点:
- 使用
inline
关键字标记 - 只是对编译器的建议,编译器可以忽略
- 适用于小型、频繁调用的函数
- 可能增加程序体积(代码被复制到每个调用点)
- 减少函数调用开销(无需保存/恢复寄存器,跳转等)
现代C++中,编译器通常会自动决定是否内联函数,即使没有inline
关键字。从C++17开始,inline
关键字还有另一个作用:允许函数在多个翻译单元中定义。
类成员函数的隐式内联
在类定义内实现的成员函数自动成为内联候选:
class Point {
public:
// 隐式内联
double getX() const {
return x; }
double getY() const {
return y; }
// 显式内联,在类外定义
void setX(double newX);
void setY(double newY);
private:
double x;
double y;
};
// 类外定义内联成员函数
inline void Point::setX(double newX) {
x = newX;
}
inline void Point::setY(double newY) {
y = newY;
}
函数指针
函数指针是指向函数的指针,它允许将函数作为参数传递或存储以便稍后调用:
// 函数指针语法
返回类型 (*指针名)(参数类型列表);
示例:
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// 声明一个函数指针
int (*operation)(int, int);
// 指向add函数
operation = add;
std::cout << "结果: " << operation(5, 3) << std::endl; // 输出8
// 指向subtract函数
operation = subtract;
std::cout << "结果: " << operation(5, 3) << std::endl; // 输出2
return 0;
}
函数指针作为参数
函数指针常用于策略模式和回调函数:
// 接受函数指针作为参数
void processArray(int arr[], int size, int (*processor)(int)) {
for (int i = 0; i < size; i++) {
arr[i] = processor(arr[i]);
}
}
// 可以传递的处理函数
int double_value(int x) {
return x * 2; }
int square_value(int x) {
return x * x; }
int main() {
int numbers[5] = {
1, 2, 3, 4, 5};
// 使用double_value函数处理数组
processArray(numbers, 5, double_value);
// 使用square_value函数处理数组
processArray(numbers, 5, square_value);
return 0;
}
使用typedef和using简化函数指针
函数指针的语法可能复杂,可以使用typedef
或using
(C++11)简化:
// 使用typedef
typedef int (*Operation)(int, int);
// 使用using (C++11)
using Operation = int (*)(int, int);
// 简化后的使用
Operation op = add;
std::cout << op(5, 3) << std::endl;
现代C++中的函数对象
现代C++提供了更多灵活的方式处理函数对象:
#include <functional>
void processValue(int value, const std::function<int(int)>& processor) {
std::cout << "处理结果: " << processor(value) << std::endl;
}
int main() {
// 使用普通函数
processValue(5, double_value);
// 使用lambda表达式
processValue(5, [](int x) {
return x * 3; });
// 使用函数对象
struct Triple {
int operator()(int x) const {
return x * 3; }
};
processValue(5, Triple());
return 0;
}
Lambda表达式
C++11引入了Lambda表达式,允许在代码中直接定义匿名函数:
// Lambda语法
[capture](parameters) -> return_type {
body }
其中:
capture
:捕获列表,指定如何访问外部变量parameters
:参数列表(可选)return_type
:返回类型(可选,通常由编译器推导)body
:函数体
基本Lambda示例
auto add = [](int a, int b) {
return a + b; };
std::cout << add(5, 3) << std::endl; // 输出8
// 带有返回类型的Lambda
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};
std::cout << divide(10, 2) << std::endl; // 输出5
捕获外部变量
Lambda可以捕获外部作用域的变量:
int multiplier = 3;
// 值捕获
auto triple = [multiplier](int x) {
return x * multiplier; };
std::cout << triple(5) << std::endl; // 输出15
multiplier = 4; // 不影响已捕获的值
std::cout << triple(5) << std::endl; // 仍然输出15
// 引用捕获
auto quadruple = [&multiplier](int x) {
return x * multiplier; };
std::cout << quadruple(5) << std::endl; // 输出20
multiplier = 5; // 影响引用捕获
std::cout << quadruple(5) << std::endl; // 输出25
// 混合捕获
auto mixed = [multiplier, &quadruple](int x) {
return quadruple(x) + multiplier; };
// 捕获所有变量(值捕获)
auto captureAll = [=](int x) {
return x * multiplier; };
// 捕获所有变量(引用捕获)
auto captureAllRef = [&](int x) {
return x * multiplier; };
// 捕获this
auto captureThis = [this](int x) {
return x * this->value; };
可变Lambda
默认情况下,Lambda通过值捕获的变量在Lambda内是只读的。使用mutable
关键字可以修改值捕获的变量:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; };
std::cout << increment() << std::endl; // 输出1
std::cout << increment() << std::endl; // 输出2
std::cout << counter << std::endl; // 输出0,原始值未变
Lambda与标准库算法
Lambda表达式与标准库算法结合使用非常强大:
#include <algorithm>
#include <vector>
std::vector<int> numbers = {
5, 3, 1, 4, 2};
// 使用Lambda排序
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b; });
// 使用Lambda查找
auto it = std::find_if(numbers.begin(), numbers.end(), [](int x) {
return x > 3; });
// 使用Lambda转换
std::transform(numbers.begin(), numbers.end(), numbers.begin(), [](int x) {
return x * 2; });
// 使用Lambda累积
int sum = std::accumulate(numbers.begin(), numbers.end(), 0, [](int total, int x) {
return total + x; });
递归函数
递归函数是调用自身的函数。它通常用于解决可以分解为子问题的问题:
unsigned long long factorial(int n) {
// 基本情况
if (n <= 1) return 1;
// 递归情况
return n * factorial(n - 1);
}
int main() {
std::cout << factorial(5) << std::endl; // 输出120 (5*4*3*2*1)
return 0;
}
递归函数的结构:
- 基本情况(基线条件):递归终止的条件
- 递归情况:将问题分解并递归调用
递归函数的优势:
- 代码简洁,容易理解
- 自然地表达分治算法
- 适合树状结构和递归定义的问题
递归函数的缺点:
- 可能导致栈溢出(如果递归层次太深)
- 通常比迭代解决方案效率低
- 可能导致重复计算(可以用记忆化技术解决)
尾递归
尾递归是递归的一种特殊形式,递归调用是函数执行的最后一个操作:
unsigned long long factorialTail(int n, unsigned long long acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
尾递归的优势:
- 某些编译器可以优化尾递归,将其转换为迭代形式
- 避免栈溢出(如果编译器支持尾递归优化)
函数模板
函数模板允许我们定义一个通用函数,可用于不同的数据类型:
// 函数模板定义
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
std::cout << max(10, 20) << std::endl; // 使用int类型
std::cout << max(3.14, 2.72) << std::endl; // 使用double类型
std::cout << max("abc", "xyz") << std::endl; // 使用const char*类型
return 0;
}
函数模板的特点:
- 通过替换模板参数生成具体的函数实例
- 编译器根据调用时的参数类型自动推导模板参数
- 支持多个模板参数
- 可以有默认模板参数(C++11)
显式指定模板参数
有时需要显式指定模板参数,尤其是当编译器无法推导或推导结果不符合预期时:
template <typename ReturnType, typename InputType>
ReturnType convert(InputType value) {
return static_cast<ReturnType>(value);
}
int main() {
double pi = 3.14159;
// 显式指定返回类型为int
int pi_int = convert<int>(pi);
// 完全指定模板参数
int another = convert<int, double>(pi);
return 0;
}
模板特化
模板特化允许为特定类型提供自定义实现:
// 通用模板
template <typename T>
void process(T value) {
std::cout << "通用处理: " << value << std::endl;
}
// 特化模板(针对int类型)
template <>
void process<int>(int value) {
std::cout << "整数特殊处理: " << value << std::endl;
}
int main() {
process(3.14); // 使用通用模板
process(42); // 使用int特化模板
return 0;
}
函数对象(函数符)
函数对象(或函数符)是可以像函数一样被调用的对象,它们是重载了operator()
的类:
class Adder {
public:
Adder(int base) : base_(base) {
}
int operator()(int value) const {
return base_ + value;
}
private:
int base_;
};
int main() {
Adder add5(5);
std::cout << add5(10) << std::endl; // 输出15
// 创建临时函数对象并调用
std::cout << Adder(3)(10) << std::endl; // 输出13
return 0;
}
函数对象的优势:
- 可以保持状态(成员变量)
- 可以有多个重载的
operator()
- 比函数指针更有效(编译器可以内联)
- 可用于泛型编程(如STL算法)
变长参数函数
C++提供了几种定义接受可变数量参数的函数的方法。
C风格的可变参数
使用<cstdarg>
头文件中的宏:
#include <cstdarg>
double average(int count, ...) {
va_list args;
va_start(args, count);
double sum = 0;
for (int i = 0; i < count; i++) {
sum += va_arg(args, double);
}
va_end(args);
return sum / count;
}
int main() {
std::cout << average(3, 2.5, 3.5, 4.5) << std::endl; // 输出3.5
return 0;
}
C风格可变参数的缺点:
- 类型不安全(没有类型检查)
- 容易出错
- 需要提供参数数量或终止标记
可变参数模板(C++11)
现代C++提供了类型安全的可变参数模板:
// 递归终止函数
template <typename T>
T sum(T value) {
return value;
}
// 可变参数模板
template <typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 输出15
std::cout << sum(1.1, 2.2, 3.3) << std::endl; // 输出6.6
return 0;
}
C++17引入了折叠表达式,进一步简化了可变参数模板:
template <typename... Args>
auto sum(Args... args) {
return (... + args); // 折叠表达式
}
可变参数模板的优势:
- 类型安全
- 编译时检查
- 高效(没有运行时开销)
最佳实践与编程技巧
1. 函数设计原则
- 单一职责原则:一个函数应该只有一个明确的目的
- 短小精悍:函数应该相对短小,易于理解
- 合适的抽象级别:函数应该在一个一致的抽象级别上操作
- 明确的接口:参数和返回值应该有明确的含义
- 尽量减少副作用:函数的行为应该可预测,避免修改全局状态
2. 参数传递指南
- 对小型对象(如基本类型),使用值传递
- 对大型对象,使用const引用传递(只读)或非const引用传递(可修改)
- 当参数可能为nullptr时,使用指针
- 对于接收和传递对象所有权的场景,使用智能指针或移动语义
3. 命名约定
- 函数名应该表明函数的作用(动词或动词短语)
- 参数名应该表明参数的用途
- 遵循项目或团队的命名约定(驼峰、下划线等)
- 避免缩写,除非它们是通用的或已在项目中定义
4. 错误处理策略
- 选择一致的错误处理策略(返回错误码、抛出异常等)
- 对于可能失败的函数,明确说明错误处理方式
- 对于无法恢复的错误,考虑使用异常
- 对于预期的错误状态,考虑返回std::optional或std::expected(C++23)
5. 文档和注释
- 为公共API编写清晰的文档,说明功能、参数、返回值和异常
- 注释复杂的算法或非显而易见的实现细节
- 使用工具如Doxygen生成API文档
实际应用案例
自定义排序比较器
#include <algorithm>
#include <vector>
#include <string>
// 使用普通函数
bool compareByLength(const std::string& a, const std::string& b) {
return a.length() < b.length();
}
// 使用函数对象
struct LengthComparer {
bool operator()(const std::string& a, const std::string& b) const {
return a.length() < b.length();
}
};
int main() {
std::vector<std::string> words = {
"apple", "banana", "cherry", "date", "elderberry"};
// 使用普通函数
std::sort(words.begin(), words.end(), compareByLength);
// 使用函数对象
std::sort(words.begin(), words.end(), LengthComparer());
// 使用Lambda表达式
std::sort(words.begin(), words.end(), [](const std::string& a, const std::string& b) {
return a.length() < b.length();
});
return 0;
}
简单事件系统
#include <functional>
#include <vector>
#include <string>
class EventSystem {
public:
// 定义回调函数类型
using EventCallback = std::function<void(const std::string&)>;
// 注册事件监听器
void addEventListener(const std::string& eventName, EventCallback callback) {
listeners_[eventName].push_back(callback);
}
// 触发事件
void dispatchEvent(const std::string& eventName, const std::string& data) {
if (listeners_.find(eventName) == listeners_.end()) return;
for (const auto& callback : listeners_[eventName]) {
callback(data);
}
}
private:
std::unordered_map<std::string, std::vector<EventCallback>> listeners_;
};
int main() {
EventSystem events;
// 添加事件监听器
events.addEventListener("message", [](const std::string& data) {
std::cout << "收到消息: " << data << std::endl;
});
events.addEventListener("error", [](const std::string& data) {
std::cerr << "错误: " << data << std::endl;
});
// 触发事件
events.dispatchEvent("message", "Hello, World!");
events.dispatchEvent("error", "Connection lost!");
return 0;
}
使用函数模板实现通用算法
#include <vector>
#include <iostream>
// 通用查找算法
template <typename Container, typename Predicate>
auto find_if(const Container& container, Predicate pred) {
for (const auto& item : container) {
if (pred(item)) {
return &item;
}
}
return static_cast<const typename Container::value_type*>(nullptr);
}
int main() {
std::vector<int> numbers = {
1, 3, 5, 7, 9};
// 查找大于5的第一个数
auto result = find_if(numbers, [](int n) {
return n > 5; });
if (result) {
std::cout << "找到: " << *result << std::endl;
} else {
std::cout << "未找到" << std::endl;
}
return 0;
}
总结
函数是C++程序设计的基本构建块,掌握函数的定义与调用对于编写高效、可维护的代码至关重要。本文回顾了C++中的各种函数相关特性,包括基本函数定义、参数传递、函数重载、默认参数、内联函数、函数指针、Lambda表达式、递归函数和函数模板等。
通过理解这些特性并遵循最佳实践,您可以设计出更加灵活、高效和可维护的代码。随着C++语言的发展,函数相关的特性也在不断演化和丰富,为程序员提供了越来越强大的工具。
在下一篇文章中,我们将探讨C++中的数组与字符串,包括C风格数组、字符串以及现代C++中的std::array、std::vector和std::string等容器。
参考资料
- Bjarne Stroustrup. The C++ Programming Language (4th Edition)
- Scott Meyers. Effective Modern C++
- cppreference.com - 函数
- C++ Core Guidelines - 函数
这是我C++学习之旅系列的第四篇技术文章。查看完整系列目录了解更多内容。