10、函数基础:函数定义、声明、参数传递(值传递)
函数这东西,说白了就是给一段代码起个名字。你想想看,如果每次要计算两个数的和,都得把加法逻辑写一遍,那代码得多臃肿?我刚开始学C++那会儿,就犯过这种傻——一个简单的求和逻辑,在程序里复制粘贴了七八次。后来改需求,要改成浮点数加法,改得我头皮发麻。嗯,从那以后,我再也不敢不写函数了。
10.1 函数是什么?为什么需要它?
函数就是一段独立的、可重复调用的代码块。它接收输入(参数),执行特定操作,然后返回结果(或者不返回)。
我个人习惯把函数想象成一个「黑盒子」:你往里扔东西,它帮你处理完,再吐出来。至于盒子里面怎么运作的,调用者不需要关心。这就是封装的思想。
函数的好处很明显:
- 代码复用:写一次,用N次
- 模块化:每个函数只做一件事
- 易维护:改逻辑只改一个地方
- 可读性强:函数名本身就是注释
核心原则:一个函数只做一件事,并且把它做好。如果你发现一个函数里做了三件不同的事,拆开它。
10.2 函数的定义与声明
很多新手搞不清「定义」和「声明」的区别。我打个比方:
- 声明:告诉编译器「嘿,有个叫add的函数,它长这样,但具体实现你待会儿再看」
- 定义:告诉编译器「好了,现在我把add的具体代码写给你」
函数定义的基本语法:
返回值类型 函数名(参数列表) {
// 函数体
// 执行逻辑
return 返回值;
}
举个例子:
// 函数定义
int add(int a, int b) {
int sum = a + b;
return sum;
}
而函数声明(也叫函数原型)长这样:
// 函数声明
int add(int a, int b);
声明可以省略参数名,只写类型:
int add(int, int); // 这样也行
我的习惯:我一般会在头文件里放声明,在源文件里放定义。这样别人想看接口,直接看头文件就行,不用翻实现细节。
10.3 参数传递:值传递
C++里参数传递有好几种方式,今天先讲最基础的一种——值传递。
值传递的意思是:调用函数时,把实参的值复制一份传给形参。函数内部操作的是副本,不影响原来的变量。
看代码:
#include <iostream>
using namespace std;
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
cout << "函数内部:x = " << x << ", y = " << y << endl;
}
int main() {
int a = 10, b = 20;
cout << "调用前:a = " << a << ", b = " << b << endl;
swap(a, b);
cout << "调用后:a = " << a << ", b = " << b << endl;
return 0;
}
输出结果:
调用前:a = 10, b = 20
函数内部:x = 20, y = 10
调用后:a = 10, b = 20
看到了吗?函数内部确实交换了,但外部的a和b纹丝不动。为什么?因为swap函数拿到的是a和b的复印件,原件还好好的。
我曾经踩过的坑:刚学C++时,我写了一个排序函数,传了数组进去,结果发现原数组没变。查了半天才发现,我传的是数组元素的值,不是数组本身。值传递不会修改实参,这是新手最容易犯的错误之一。
10.4 值传递的内存模型
理解值传递,关键要理解内存里发生了什么。我画了一张图帮你理清思路:
从图上可以看得很清楚:main函数里的a和b,跟swap函数里的x和y,是两套完全独立的内存空间。值传递的本质就是「复制」,函数内部再怎么折腾,也影响不到外面。
10.5 值传递的优缺点
| 优点 | 缺点 |
|---|---|
| 数据安全:函数无法意外修改实参 | 复制开销:如果参数是大型结构体或类,复制成本高 |
| 逻辑简单:容易理解,不容易出错 | 无法直接修改实参(有时这是缺点) |
| 适合基本数据类型(int、char、double等) | 不适合大对象(vector、string、自定义类等) |
我的建议:对于内置类型(int、float、char等),放心用值传递。对于自定义类型或STL容器,优先考虑const引用传递,这个我们后面会讲。
10.6 实战:写一个完整的函数
咱们来写一个实际点的例子——计算圆的面积:
#include <iostream>
using namespace std;
// 函数声明
double calculateArea(double radius);
int main() {
double r;
cout << "请输入圆的半径:";
cin >> r;
double area = calculateArea(r);
cout << "圆的面积是:" << area << endl;
return 0;
}
// 函数定义
double calculateArea(double radius) {
const double PI = 3.1415926;
return PI * radius * radius;
}
这个例子虽然简单,但包含了函数的所有要素:声明、定义、参数传递、返回值。注意看,radius是值传递,main函数里的r不会被修改——实际上我们也不想修改它,只是读取它的值来计算。
10.7 常见错误与避坑指南
我总结几个新手常犯的错误:
- 忘记声明就调用:如果函数定义在调用之后,必须提前声明。否则编译器会报错。
- 返回值类型不匹配:声明返回int,结果return了一个double,编译器会截断或报错。
- 以为值传递能修改实参:这个我前面已经强调过了,再犯就打手心。
- 参数顺序搞反:比如声明是void func(int a, double b),调用时写成func(3.14, 5),类型不匹配。
我曾经犯过的错:有一次我写了一个函数,参数是一个很大的结构体,我用了值传递。结果每次调用都要复制几百字节的数据,程序跑起来慢得像蜗牛。后来改成const引用传递,速度直接翻倍。记住:大对象别用值传递!
10.8 小结
函数是C++程序的基本组成单元。今天咱们重点讲了:
- 函数声明和定义的区别
- 值传递的机制——复制副本,不影响原变量
- 值传递的适用场景和注意事项
说白了,值传递就是「我给你一份复印件,你随便改,原件我留着」。简单、安全,但复制大文件时有点费劲。下一节我们会讲引用传递,那才是真正「动原件」的本事。
一句话记住:函数是代码的积木,值传递是安全的快递——只送复印件,不送原件。
公众号:蓝海资料掘金营,微信deep3321