函数基础:从调用到返回,C++函数的完整面貌

函数这东西,说白了就是一段可以反复调用的代码块。我刚开始学C++时觉得函数没啥了不起,不就是把代码包起来嘛。后来在项目中写了几万行代码才明白——函数设计的好坏,直接决定了代码能不能维护、能不能复用。

今天咱们就把函数这块彻底讲透。从定义声明,到参数传递的三种方式,再到重载、默认参数、内联函数,一个不落。

C++ 函数知识体系 函数基础 定义 vs 声明 参数传递:值 / 指针 / 引用 返回值 函数重载 默认参数 inline 内联 核心原则:声明在前,定义在后;参数传递选引用,避免不必要拷贝 重载看参数列表,默认参数靠右放,内联适合小函数

1. 函数定义与声明:先打招呼,再干活

函数声明就是告诉编译器:「嘿,有个函数长这样,参数是这些,返回值是那个。」而函数定义才是真正的实现代码。

我个人习惯把声明放在头文件里,定义放在源文件里。这样别的文件#include头文件就能调用,不用看到实现细节。

// 声明(通常在 .h 文件)
int add(int a, int b);

// 定义(通常在 .cpp 文件)
int add(int a, int b) {
    return a + b;
}
注意:如果只定义不声明,而且定义在调用之后,编译器会报错。因为编译器从上往下读,遇到调用时还不知道这个函数存在。

我曾经在一个项目里把一个大函数的定义写在文件末尾,结果前面调用了二十多次,编译死活过不去。后来老老实实加了个声明在前面,一秒解决。

2. 参数传递:三种方式,各有千秋

参数传递是函数的核心机制。C++给了我们三种选择:值传递、指针传递、引用传递。你想想看,这三种方式本质区别在哪?

传递方式 是否拷贝 能否修改原值 适用场景
值传递 是(拷贝一份) 不能 小对象、不需要修改原值
指针传递 否(传地址) 能(通过指针修改) 需要修改原值、可选参数(可传nullptr)
引用传递 否(传别名) 需要修改原值、避免拷贝大对象

值传递

void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
    // 外面不受影响,因为x和y是副本
}

指针传递

void swap(int* x, int* y) {
    int temp = *x;
    *x = *y;
    *y = temp;
    // 外面受影响
}

引用传递

void swap(int& x, int& y) {
    int temp = x;
    x = y;
    y = temp;
    // 外面受影响,写法更自然
}
我的建议:能用引用就别用指针。引用语法更干净,而且不可能为空,少了很多空指针检查的麻烦。但如果你需要「这个参数可以不传」,那就用指针,传nullptr表示缺省。

我在项目中遇到过一个大坑——有个函数参数是个巨大的结构体,用了值传递。每次调用都要拷贝几十KB的数据,性能直接崩了。改成const引用后,速度提升了十几倍。嗯,这里要注意:大对象一定要用const引用传递,除非你确实需要一份副本。

3. 返回值:不只是return一个数

返回值是函数对外输出结果的方式。最基础的是return一个值,但C++还支持返回引用、返回指针。

// 返回值
int getValue() { return 42; }

// 返回引用(注意:不能返回局部变量的引用!)
int& getRef(int& x) { return x; }

// 返回指针
int* getPtr(int* p) { return p; }
致命陷阱:永远不要返回局部变量的引用或指针!局部变量在函数结束时就被销毁了,返回的引用或指针指向的是垃圾内存。我曾经在代码审查时看到过这种写法,当场就拦下来了——这种bug极难排查,运行时可能偶尔正常,偶尔崩溃。

4. 函数重载:同名不同参

函数重载允许你定义多个同名函数,只要参数列表不同就行。编译器会根据调用时传入的参数类型,自动匹配最合适的版本。

void print(int x) {
    cout << "整数: " << x << endl;
}

void print(double x) {
    cout << "浮点数: " << x << endl;
}

void print(const string& s) {
    cout << "字符串: " << s << endl;
}

// 调用
print(42);       // 匹配第一个
print(3.14);     // 匹配第二个
print("hello");  // 匹配第三个

重载的规则很简单:参数个数不同,或者参数类型不同。返回值类型不能作为重载的依据——光靠返回值不同,编译器不知道你调用的是哪个版本。

核心要点:重载只看参数列表(个数、类型、顺序),不看返回值。如果两个函数只有返回值不同,编译会报错。

5. 默认参数:少写点代码

默认参数让你在声明函数时给参数一个默认值。调用时如果没传这个参数,就使用默认值。

void showInfo(const string& name, int age = 18, const string& city = "北京") {
    cout << name << " " << age << " " << city << endl;
}

// 调用
showInfo("张三");              // 张三 18 北京
showInfo("李四", 25);          // 李四 25 北京
showInfo("王五", 30, "上海");  // 王五 30 上海
规则:默认参数必须从右往左连续排列。你不能写成 void f(int a = 1, int b),因为编译器不知道你调用 f(2) 时,这个2是给a还是给b。

我个人习惯把最常用的值设为默认参数。比如日志函数,默认级别是INFO,调用时如果不需要改级别,直接传消息就行,省事。

6. 内联函数(inline):用空间换时间

内联函数是C++提供的一种优化手段。在函数声明前加上inline关键字,编译器会尝试把函数调用替换成函数体代码,避免函数调用的开销(压栈、跳转、返回等)。

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

// 调用 max(3, 5) 时,编译器可能直接替换成 (3 > 5 ? 3 : 5)

但要注意,inline只是对编译器的建议,不是强制命令。如果函数体太大(比如超过几十行),编译器会忽略inline,按普通函数处理。

适用场景:内联适合那些体积小、调用频繁的函数。比如getter/setter、简单的比较函数。我一般在头文件里直接定义这些短小函数,加上inline,省得再写个源文件。

我曾经在一个性能敏感的系统里,把几个频繁调用的工具函数改成内联,整体性能提升了大约8%。虽然不多,但零成本啊——改个关键字就行。

总结一下:
  • 声明在前,定义在后,这是规矩
  • 参数传递:小对象值传递,大对象const引用传递,需要修改用引用
  • 返回值:别返回局部变量的引用或指针
  • 重载:参数不同就行,返回值不行
  • 默认参数:从右往左,连续排列
  • 内联:小函数用inline,大函数别用

函数是C++程序的基本积木。把这些基础打牢了,后面学类、模板、Lambda表达式都会轻松很多。说白了,函数设计得好,代码写起来就像搭积木一样顺畅。


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