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 = 10 地址: 0x100 b = 20 地址: 0x104 复制值 swap函数栈帧 x = 10 (副本) 地址: 0x200 y = 20 (副本) 地址: 0x204 交换后: x = 20 y = 10 注意:swap函数修改的是副本,main函数中的a和b不受影响

从图上可以看得很清楚: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 常见错误与避坑指南

我总结几个新手常犯的错误:

  1. 忘记声明就调用:如果函数定义在调用之后,必须提前声明。否则编译器会报错。
  2. 返回值类型不匹配:声明返回int,结果return了一个double,编译器会截断或报错。
  3. 以为值传递能修改实参:这个我前面已经强调过了,再犯就打手心。
  4. 参数顺序搞反:比如声明是void func(int a, double b),调用时写成func(3.14, 5),类型不匹配。

我曾经犯过的错:有一次我写了一个函数,参数是一个很大的结构体,我用了值传递。结果每次调用都要复制几百字节的数据,程序跑起来慢得像蜗牛。后来改成const引用传递,速度直接翻倍。记住:大对象别用值传递!

10.8 小结

函数是C++程序的基本组成单元。今天咱们重点讲了:

  • 函数声明和定义的区别
  • 值传递的机制——复制副本,不影响原变量
  • 值传递的适用场景和注意事项

说白了,值传递就是「我给你一份复印件,你随便改,原件我留着」。简单、安全,但复制大文件时有点费劲。下一节我们会讲引用传递,那才是真正「动原件」的本事。

一句话记住:函数是代码的积木,值传递是安全的快递——只送复印件,不送原件。


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