第六章:函数基础——让代码学会“分工合作”

写代码写到一定规模,你就会发现:把所有逻辑塞进一个 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;
}

调用时,实参会被拷贝给形参。这里有个细节:如果只声明不定义,链接时会报错。我遇到过有人把声明当定义用,结果链接器一脸懵。

小技巧: 我习惯把函数声明放在头文件里,定义放在 .cpp 里。这样别人看头文件就知道你的接口,不用翻实现。

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(); 没赋值给任何变量,该调用哪个?

经验之谈: 重载时尽量让参数类型差异明显,避免隐式转换导致的歧义。我曾在项目里因为 intlong 的重载,导致调用时走了错误的版本,排查了半天。

知识体系图

下面这张图帮你理清本章的核心脉络:

函数基础 定义、声明、调用 参数传递 返回值 函数重载 值传递 指针传递 引用传递 不要返回局部变量 参数不同,返回值无关 核心:分工明确,接口清晰,避免陷阱

这张图把函数基础拆成了四个大块:定义/声明/调用、参数传递、返回值、重载。参数传递又细分为值、指针、引用三种方式。返回值特别强调了“不要返回局部变量”这个经典坑。重载的核心规则是参数列表必须不同,返回值类型不能作为区分依据。

好了,这一章就到这里。函数这东西,写多了自然就熟了。但记住:好的函数就像好的工具,功能单一、接口清晰、用起来顺手。下次写函数前,先想想这个函数到底该干什么,参数怎么传最合理,返回值怎么设计最安全。养成习惯后,你的代码会越来越干净。


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