🔷 第六章 函数
📄 一句话概括

声明是"先打个招呼":只写函数头、末尾加一个分号,告诉编译器"有这么个函数、它怎么用",真正的函数体(定义)可以晚点再出场。声明专治"想先用、后写定义"的顺序问题——编译器读代码是自上而下一遍过的,用到之前它必须"认识"这个名字。

📝 本节和前面几节的关系

6.1.1 学的是把机器造出来(定义),这一节解决顺序问题。为什么会有顺序问题?回看 2.1.4:程序执行从 main 第一条语句开始,但编译器读文件是从第一行开始的。读到 add(3, 5) 那一刻,它必须已经知道"add 收两个 int、还一个 int"。声明就是把这份"用法说明"提前摆出来。

一、编译器为什么需要声明

编译器像一个从上往下读、且只读一遍的读者:

两种解法:要么把定义放在调用前面,要么先写一句声明。声明的好处是——函数体可以挪到文件后面(甚至别的文件里),main 照样能放在最前面读。

二、声明长什么样:定义砍掉函数体,留个分号

int add(int a, int b);        // 声明:函数头 + 分号(参数名可省)

int add(int a, int b) {       // 定义:函数头 + 函数体
    return a + b;
}

一句口诀记牢:分号 = 声明,花括号 = 定义。

三、实例 01:声明、定义和使用函数

#include <iostream>
using namespace std;

int add(int a, int b);                       // ① 声明:提前打招呼

int main() {
    cout << "3 + 5 = " << add(3, 5) << endl;         // ② 使用:放心调用
    cout << "10 + 20 = " << add(10, 20) << endl;
    return 0;
}

int add(int a, int b) {                      // ③ 定义:真身放在后面
    return a + b;
}

看到什么算成功:

3 + 5 = 8
10 + 20 = 30

必做实验(本节的关键一步):把第 4 行那句声明注释掉,重新编译——

error: 'add' was not declared in this scope

报错信息就是答案:not declared = 缺声明。记住这张脸,以后见到它就知道该补声明(或把定义往前挪)。

再试一次:把 int add(int a, int b) { ... } 整段定义剪切到 main 之前,声明也删掉——照样能编过。这证明:编译器要的是"用之前先认识",声明和定义都能满足它,只是声明更灵活。

四、声明与定义的分工

声明(原型) 定义
写什么 函数头 + ; 函数头 + { 语句 }
对编译器说 怎么用(名字/参数/返回类型) 怎么干(具体语句)
能出现几次 可以多次(内容必须一致) 有且只有一次
在内存里有位置吗 没有 有

一句话:声明是菜单,定义是后厨。顾客(main)看菜单就能点菜,菜在后厨现做——不用让顾客先进后厨看一眼才能点。

五、常见误区

  1. "声明和定义差不多,随便写哪个"——差一个分号和一对花括号,但作用完全不同:声明不产生代码,定义才产生代码。
  2. "定义也能写多次"——重复定义会报错(redefinition / multiple definition)。而声明写多次(内容一致)是允许的,这正是头文件能被到处 #include 的原因(1.3.1 讲过多文件工程,后面会重逢)。
  3. "声明里的参数名必须和定义一样"——不必,声明里参数名甚至可以全省略:int add(int, int);。参数名只在函数体里有用。
  4. "有声明兜底,函数体写错也会被揪出来"——不一定。编译器只按声明检查调用,函数体里的问题要等链接阶段或运行时才暴露(回看 1.3.1 的"编译 + 链接"两步)。

30 秒实验:把声明改成 double add(int a, int b);(故意和定义不一致),编译看报错——声明和定义必须口径一致,这就是原型的主要价值:它是编译器手中的"合同"。

自测三问(点开对答案) 1. 声明和定义在写法上差什么?——声明只有函数头 + 分号;定义是函数头 + 花括号里的函数体。 2. 为什么需要声明?——编译器自上而下只读一遍,调用前必须先"认识"这个名字(或在调用前就写定义)。 3. `'xxx' was not declared in this scope` 通常是什么原因?——调用前既无声明也无定义(漏写声明,或定义位置在调用之后)。

相关笔记