Day4 函数
不需要知道手机是怎么制造的,也能熟练地使用它;不需要了解软件的内部实现,也能正常运行它。
函数,就是编程世界里的"手机"和"软件"。
以"比较两个数大小"为例:如果没有函数,每次需要比较大小时,都得在main函数里重新写一遍逻辑;而有了函数,只需要把这段逻辑封装起来,之后无论在哪里需要比较大小,直接调用这个函数即可,不必关心它内部是怎么实现的。
这就是函数的核心价值:封装细节,暴露接口,让代码可以被复用。
二、函数声明与定义分离
在 C++ 中,函数有三个相关概念,职责各不相同:
- 函数声明:告诉编译器"这个函数存在",让编译器知道它的名称、参数和返回值类型
- 函数定义:真正实现函数的功能,包含具体的执行逻辑
- 函数调用:在需要的地方使用这个函数
三者的关系可以类比为:声明是"提前打招呼",定义是"真正做事",调用是"发出指令"。
下面用一段代码来展示:
#include <iostream> using namespace std; int add(int a, int b); // 声明:放在头部或 .h 文件中,告知编译器函数的存在 int main() { int result = add(3, 5); // 调用:直接使用,不需要关心内部实现 cout << "结果为:" << result << endl; return 0; } int add(int a, int b) { // 定义:具体实现加法逻辑 return a + b; }💡为什么要分离?在大型项目中,声明通常放在
.h头文件里,定义放在.cpp源文件里。这样其他文件只需引入头文件就能使用函数,而不必关心实现细节——和第一节的"手机"类比一脉相承。
三、参数的三种传递方式
值传递:将变量的值复制一份传给函数,函数操作的是副本,原变量不受影响。
引用传递:传递的是变量的别名,函数内部操作的就是原变量本身,修改立即生效。
指针传递:传递变量的内存地址,通过解引用*p间接修改原变量。效果类似引用传递,但语法更繁琐,C++ 中优先使用引用。
下面用swap(交换两数)这个经典场景来对比三种方式的实际效果:
#include <iostream> using namespace std; // ❌ 值传递:交换的只是副本,外部 a、b 不变 void swap_wrong(int a, int b) { int t = a; a = b; b = t; } // ✅ 引用传递:直接操作原变量,交换成功 void swap_right(int& a, int& b) { int t = a; a = b; b = t; } // 了解即可:指针传递,效果同引用,但写法繁琐 void swap_ptr(int* a, int* b) { int t = *a; *a = *b; *b = t; } int main() { int x = 3, y = 5; swap_wrong(x, y); cout << "值传递后:x=" << x << " y=" << y << endl; // x=3, y=5(未变) swap_right(x, y); cout << "引用传递后:x=" << x << " y=" << y << endl; // x=5, y=3(成功) return 0; }四、默认参数(了解即可)
讲两个要点,教材常漏的细节:
- 默认参数必须从右往左设置,不能跳着来
- 声明和定义分开时,默认参数只写在声明里,定义里不写(否则编译报错)
void greet(std::string name, std::string prefix = "你好,"); // 定义里不再写默认值 void greet(std::string name, std::string prefix) { std::cout << prefix << name << "\n"; }五、函数重载
C++ 允许定义多个同名函数,只要它们的参数类型或数量不同,编译器就能在调用时自动区分。这种机制叫做函数重载。
注意:返回值类型不同不能构成重载。
下面用print函数演示三个重载版本:
#include <iostream> #include <string> using namespace std; void print(int x) { cout << "整数:" << x << endl; } void print(double x) { cout << "浮点数:" << x << endl; } void print(string x) { cout << "字符串:" << x << endl; } int main() { print(42); // 调用 print(int) print(3.14); // 调用 print(double) print("hello"); // 调用 print(string) return 0; }🤔思考题:为什么返回值不能用于区分重载?
假设存在这样两个函数:
int func(); double func();当你写下
func();时,编译器该调用哪个?它无从判断——因为你没有使用返回值,或者返回值被赋给了一个可以隐式转换的变量。这种调用歧义是返回值无法参与重载决议的根本原因。
六、内联函数inline
普通函数调用时,程序需要跳转到函数地址、保存现场、执行、再返回。对于极短的函数,这个"来回跳转"的开销反而比函数本身的逻辑还贵。
inline的作用就是建议编译器:把函数体直接展开在调用处,省去跳转开销。
#include <iostream> using namespace std; inline int square(int x) { return x * x; } int main() { cout << square(4) << endl; // 编译器可能展开为:cout << 4 * 4 << endl; return 0; }调用square(4)时,编译器可以直接把代码替换成4 * 4,完全省掉函数调用的开销。
适用场景与注意事项:
inline适合逻辑极简的函数(1-3 行),如取绝对值、取最大值、简单的 getter- 函数体过长时,编译器通常会忽略
inline建议,仍按普通函数处理 - 开启
-O2优化后,编译器会自动判断并内联合适的函数,手动写inline更多是一种语义提示,告诉读者"这是一个轻量函数" inline函数通常定义在头文件中,因为每个调用它的编译单元都需要看到完整的函数体
⚠️ 坑1:值传递修改无效
初学者最常踩的坑。以为传进去改了,外面就变了——实则函数操作的是副本。
cpp
void swap_wrong(int a, int b) { int t = a; a = b; b = t; // 只交换了副本,外部毫无变化 } int main() { int x = 3, y = 5; swap_wrong(x, y); cout << x << " " << y << endl; // 仍然输出:3 5 }解决:改用引用传递int& a, int& b。
⚠️ 坑2:返回局部变量的引用
函数执行结束后,其栈帧会被销毁,局部变量随之消失。若返回该变量的引用,调用方拿到的是一个指向已释放内存的悬空引用,访问它是未定义行为——可能崩溃,也可能得到随机值,比崩溃更难排查。
cpp
int& danger() { int x = 42; return x; // ❌ x 在函数返回后已被销毁 } int main() { int& ref = danger(); cout << ref << endl; // 未定义行为,结果不可预期 }解决:返回值而非引用,或将变量声明为static(生命周期延长至程序结束),或使用堆上分配的对象。
⚠️ 坑3:默认参数重复声明
默认参数只能写一次。声明和定义分离时,默认参数写在声明处,定义处不再重复,否则编译器会报"重复指定默认参数"错误。
cpp
// ✅ 正确:默认参数写在声明处 void greet(string name, string msg = "你好"); // 声明(.h 文件) void greet(string name, string msg) { // 定义(.cpp 文件),不再写默认值 cout << msg << "," << name << endl; } // ❌ 错误:定义处重复写默认参数 void greet(string name, string msg = "你好") { // 编译报错 cout << msg << "," << name << endl; }八、实战练习
综合前面所学,实现一个简单计算器。要求包含四个运算函数,并通过引用参数处理除零异常,而不是直接返回错误值。
设计思路:
add/subtract/multiply直接返回结果divide用引用参数bool& ok返回是否成功,避免除以零崩溃main函数中逐一调用,演示正常与异常两种情况
cpp
#include <iostream> #include <string> using namespace std; double add(double a, double b) { return a + b; } double subtract(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } // ok 为引用参数:成功时置 true,除零时置 false double divide(double a, double b, bool& ok) { if (b == 0) { ok = false; return 0; } ok = true; return a / b; } int main() { double x = 10, y = 3; cout << "加法:" << x << " + " << y << " = " << add(x, y) << endl; cout << "减法:" << x << " - " << y << " = " << subtract(x, y) << endl; cout << "乘法:" << x << " * " << y << " = " << multiply(x, y) << endl; bool ok; double result = divide(x, y, ok); if (ok) { cout << "除法:" << x << " / " << y << " = " << result << endl; } // 演示除零情况 result = divide(x, 0, ok); if (!ok) { cout << "除法:" << x << " / 0 → 除零错误,操作取消" << endl; } return 0; }输出结果:
加法:10 + 3 = 13 减法:10 - 3 = 7 乘法:10 * 3 = 30 除法:10 / 3 = 3.33333 除法:10 / 0 → 除零错误,操作取消九、小结+明日预告
函数让代码有了结构,下一天给函数传更复杂的数据——数组和字符串。
