第六章:函数基础——让代码学会“分工合作”
写代码写到一定规模,你就会发现:把所有逻辑塞进一个 main() 里,简直就是灾难。我早期带项目时见过一个同事,一个函数写了八百行,调试的时候自己都找不到北。说白了,函数就是帮你把大问题拆成小问题,每个小问题各自解决,最后拼起来。
这一章,我们聊聊 C++ 里函数那些事。定义、声明、调用、参数传递、返回值、重载——这些都是基本功,但细节里藏着不少坑。
6.1 函数的定义、声明与调用
函数定义,就是告诉编译器“这个函数长什么样,要干什么”。一个典型的函数定义长这样:
// 函数定义
int add(int a, int b) {
return a + b;
}
而函数声明,就是提前打个招呼:“嘿,编译器,后面会有个叫 add 的函数,它接受两个 int,返回 int。” 声明可以放在头文件里,也可以放在调用之前。
// 函数声明
int add(int a, int b);
int main() {
int result = add(3, 5); // 函数调用
return 0;
}
// 函数定义(可以放在后面)
int add(int a, int b) {
return a + b;
}
调用时,实参会被拷贝给形参。这里有个细节:如果只声明不定义,链接时会报错。我遇到过有人把声明当定义用,结果链接器一脸懵。
6.2 参数传递:值、指针、引用
参数传递是新手最容易踩坑的地方。三种方式,各有各的脾气。
6.2.1 值传递
值传递就是把实参的值复制一份给形参。函数内部改的是副本,不影响原变量。
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 10, b = 20;
swap(a, b);
// a 还是 10,b 还是 20
return 0;
}
你看,swap 函数白忙活了。我当年刚学 C 语言时就犯过这个错,调试了半天才发现值传递不改变实参。
6.2.2 指针传递
指针传递,传的是变量的地址。函数通过地址直接操作原变量。
void swap(int* x, int* y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int a = 10, b = 20;
swap(&a, &b);
// a 变成 20,b 变成 10
return 0;
}
指针传递的好处是能改原值,坏处是容易写出空指针解引用。我曾经在项目里因为没检查指针是否为 nullptr,导致线上程序崩溃,被运维大哥骂了一顿。
6.2.3 引用传递
引用传递是 C++ 的特色。它像指针一样能改原值,但语法更自然。
void swap(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 10, b = 20;
swap(a, b);
// a 变成 20,b 变成 10
return 0;
}
引用没有空引用的概念,所以比指针安全。我个人更推荐用引用,除非你需要处理“不指向任何对象”的情况(比如可选参数)。
| 传递方式 | 能否修改原值 | 是否拷贝 | 安全性 |
|---|---|---|---|
| 值传递 | 否 | 是 | 高 |
| 指针传递 | 是 | 否(传地址) | 中(需检查空指针) |
| 引用传递 | 是 | 否(传别名) | 高 |
6.3 返回值
函数可以返回值,也可以不返回(void)。返回值可以是基本类型、对象、指针、引用。
int getValue() {
return 42;
}
std::string getName() {
return "Alice";
}
int* getPointer() {
static int value = 100;
return &value;
}
这里有个坑:不要返回局部变量的指针或引用。局部变量在函数结束后就销毁了,返回的指针会变成野指针。
我见过一个同事,返回了局部数组的指针,结果程序偶尔正常偶尔崩溃,查了两天才找到原因。嗯,从那以后我写返回语句时都会多看一眼变量的生命周期。
6.4 函数重载
函数重载,就是同一个函数名,不同的参数列表。编译器根据实参的类型和数量,自动选择匹配的函数。
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;
}
int main() {
print(42); // 调用第一个
print(3.14); // 调用第二个
print("Hello"); // 调用第三个
return 0;
}
重载的好处是让接口更统一。比如你写一个绘图库,draw 函数可以画圆、画矩形、画线,参数不同但名字相同,调用者用起来很舒服。
但要注意:返回值类型不能作为重载的依据。下面这样是错的:
// 错误:只有返回值不同,不能重载
int getValue();
double getValue();
为什么?因为调用时可能不接收返回值,编译器就不知道选哪个了。你想想看,如果写 getValue(); 没赋值给任何变量,该调用哪个?
int 和 long 的重载,导致调用时走了错误的版本,排查了半天。
知识体系图
下面这张图帮你理清本章的核心脉络:
这张图把函数基础拆成了四个大块:定义/声明/调用、参数传递、返回值、重载。参数传递又细分为值、指针、引用三种方式。返回值特别强调了“不要返回局部变量”这个经典坑。重载的核心规则是参数列表必须不同,返回值类型不能作为区分依据。
好了,这一章就到这里。函数这东西,写多了自然就熟了。但记住:好的函数就像好的工具,功能单一、接口清晰、用起来顺手。下次写函数前,先想想这个函数到底该干什么,参数怎么传最合理,返回值怎么设计最安全。养成习惯后,你的代码会越来越干净。