5、函数(上):函数定义与调用、参数传递、返回值、函数重载

好,咱们今天聊聊函数。说实话,函数是C++里最基础也最核心的概念之一。你想想看,如果没有函数,所有代码都堆在main里,那得多恐怖?我刚开始学的时候,就写过几百行的main函数,调试起来简直想哭。后来才明白,把功能拆成函数,才是正经的编程思维。

5.1 函数定义与调用——先学会怎么“造”和“用”

函数定义,说白了就是给一段代码起个名字,以后想用就喊这个名字。语法其实很简单:

返回值类型 函数名(参数列表) {
    // 函数体
    return 返回值;
}

举个例子,咱们写一个计算两个整数和的函数:

int add(int a, int b) {
    int sum = a + b;
    return sum;
}

调用的时候更简单:

int result = add(3, 5);  // result 就是 8

嗯,这里要注意:C++要求函数要么在调用之前定义,要么提前声明。我见过不少新手把函数定义写在main后面,结果编译报错,一脸懵。其实加个声明就行:

// 声明
int add(int a, int b);

int main() {
    int result = add(3, 5);
    return 0;
}

// 定义
int add(int a, int b) {
    return a + b;
}
小技巧:我个人习惯把函数声明放在头文件里,定义放在.cpp文件里。这样代码结构清晰,别人看你的代码也舒服。

5.2 参数传递——值传递 vs 引用传递

这是很多初学者容易搞混的地方。我当年也踩过坑,咱们一次说清楚。

5.2.1 值传递

值传递,就是把实参的值复制一份给形参。函数里改形参,不影响实参。就像你复印了一份文件,在复印件上涂改,原件还是好好的。

void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
    // 这里改的是副本,外面的a和b不受影响
}

int main() {
    int a = 10, b = 20;
    swap(a, b);
    // a还是10,b还是20
    return 0;
}

为什么会这样?因为值传递时,函数内部操作的是实参的拷贝。我在项目中遇到过有人用值传递去交换两个大对象,结果性能惨不忍睹——每次调用都要复制整个对象,你说亏不亏?

5.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;
}
核心区别:值传递不改变实参,引用传递可以改变实参。如果你只是想读取数据,不想修改,建议用const引用——既高效又安全。

我曾经在维护一个老项目时,发现有人用值传递传了一个巨大的结构体,每次调用都卡半天。改成const引用后,性能直接翻倍。你想想看,传一个指针大小的引用,和传几百字节的副本,能一样吗?

5.3 返回值——函数给你的“回信”

返回值就是函数执行完后,给调用者返回一个结果。可以是基本类型、对象、指针,甚至引用。

int max(int a, int b) {
    return (a > b) ? a : b;
}

这里有个坑:不要返回局部变量的引用或指针。因为局部变量在函数结束后就被销毁了,返回它的引用或指针,拿到的是野指针,用起来就是未定义行为。

警告:我曾经在代码审查时看到有人写了这样的代码:
int& getValue() {
    int x = 10;
    return x;  // 危险!x是局部变量
}
这种代码编译可能不报错,但运行时随时可能崩溃。记住:永远不要返回局部变量的引用或指针。

那什么时候返回引用呢?比如你想让函数返回一个容器里的元素,方便外部修改:

int& getElement(std::vector<int>& vec, int index) {
    return vec[index];  // 返回引用,外部可以修改vec[index]
}

5.4 函数重载——同名不同“料”

函数重载,就是同一个函数名,可以有不同的参数列表。编译器会根据你传的参数类型和个数,自动选择调用哪个版本。

int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

int add(int a, int b, int c) {
    return a + b + c;
}

调用的时候:

add(1, 2);       // 调用 int 版本
add(1.5, 2.3);   // 调用 double 版本
add(1, 2, 3);    // 调用三个参数的版本

嗯,这里要注意:重载只看参数列表,不看返回值类型。你不能写两个同名函数,参数一样,只是返回值不同——编译器会报错,因为它不知道你调的是哪个。

避坑指南:我曾经在项目中看到有人写了这样的重载:
void print(int x);
void print(const char* str);
然后调用 print(nullptr),结果编译器报错,因为nullptr既可以匹配int(0),也可以匹配指针。这种二义性要尽量避免。我的建议是:重载时尽量让参数类型差异明显,别让编译器猜。

知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以对照着看,哪里还不清楚,就回头翻翻。

函数(上) 函数定义与调用 声明在前 定义在后 参数传递 值传递(副本) 引用传递(别名) 返回值 值返回 引用返回(慎用) 函数重载 参数不同 返回值无关

好了,函数的上半部分就讲到这里。记住:定义要清晰,传参要选对方式,返回值要小心局部变量,重载要避免二义性。这些基本功打扎实了,后面学函数指针、lambda表达式才会轻松。


公众号:蓝海资料掘金营,微信 deep3321