2-2 函数

0 阅读7分钟

2-2 函数

⏱ 本节预计用时:40分钟

本节意义:函数是把大程序拆成小模块的工具。井字棋的 initBoard()、printBoard()、checkWin() 都是独立的函数——各做一个零件,拼起来就是一台机器。

本节目标及要点:掌握函数的定义、声明、调用,理解值传递和数组传递的本质区别,会用函数组织井字棋逻辑。

1 100行全堆在main里 vs 拆成4个函数

先给你看两张图——它们做的是同一件事,但写法完全不同。

写法A:全部堆在main里

main() {
    初始化棋盘9个格子
    打印棋盘
    显示提示文字
    读取玩家输入
    检查输入是否合法
    把棋子放到对应位置
    检查横线是否连成一条
    检查竖线是否连成一条
    检查对角线是否连成一条
    如果赢了就结束
    如果没赢就换人
    再打印棋盘
    再读取输入
    ... 中间省略80行 ...
}

这读起来就像一份没有分段的合同——你每次改一个地方,都得从上到下扫一遍才能确定改哪。

写法B:拆成4个函数

initBoard(board);      // 清空棋盘
printBoard(board);     // 打印当前状态
checkWin(board, 'X');  // 判断X是否赢了
getPlayerMove();       // 获取玩家输入

每个函数只负责一件事,main 变成了"总指挥"(main也是一个函数,叫主函数)——它不亲自干活,只安排别人干活。

如果你写的函数名里出现了"并且"两个字,就该拆成两个函数。

  • printBoard()——只打印,不判断,不修改 ✓
  • checkWin()——只判断,不打印,不结束游戏 ✓
  • checkWinAndPrint() ❌——又判断又打印,拆!

这个原则叫单一职责(Single Responsibility)。它的好处:

  1. 查错容易——出了bug,你知道去哪个函数里找
  2. 复用方便——checkWin 可以在多个地方调用,不用重复写判断逻辑
  3. 测试简单——你可以单独测试 checkWin("X赢了没有?"),不用启动整个游戏

你会发现:写得好的程序,函数都很短,名字都很好读。main 函数读起来像流程图——三步走、五步走,一目了然。

2 函数的结构

一个函数由四部分组成,缺一不可(void函数缺 return 除外):

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

举个例子:

int add(int a, int b) {
    int result = a + b;
    return result;
}
  • 返回类型 int——告诉编译器"我算完会还你一个整数"
  • 函数名 add——你给它的名字,以后用这个名字调用它
  • 参数 (int a, int b)——你要给它的原材料,这里需要两个整数
  • 函数体 { ... }——它干活的步骤
  • return——把结果交还给调用方

调用的时候:

int sum = add(3, 5);   // sum 变成 8
printf("%d\n", add(10, 20));  // 直接打印 30

一句话记结构:返回类型告诉别人"我给你什么",参数告诉别人"你给我什么",函数体是"我怎么干",return是"干完了,拿去吧"。

完整示例:

#include <stdio.h>

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

int main() {
    int sum = add(3, 5);   // sum 变成 8
    printf("%d\n", add(10, 20));  // 直接打印 30
    return 0;
}

运行结果:

2-1 函数例子

3 void 返回类型

不是所有函数都要返回东西。打印棋盘只需要做动作,不需要"还"什么东西回来:

void printBoard(char board[]) { //[]里写数字没用,讲指针的时候会说明原因。
    printf("\n"); 
    printf(" %c | %c | %c \n", board[0], board[1], board[2]);
    printf("---+---+---\n");
    printf(" %c | %c | %c \n", board[3], board[4], board[5]);
    printf("---+---+---\n");
    printf(" %c | %c | %c \n", board[6], board[7], board[8]);
    printf("\n");
}

2-2 void返回类型

  • void 的意思是"空的"——我不返还任何东西
  • void函数里可以写 return;(直接返回,不带值),也可以什么都不写,到了 } 自动返回
  • void函数不能放在赋值语句右边:int x = printBoard(board); ——它没有返回值给你

什么时候用void? 当函数只做"动作"不做"计算"时——打印、初始化、清空、显示菜单,这些都是void。

4 函数声明——目录先告诉你书名在哪页

C语言编译器有个"倔脾气":编译时从上往下读,读到函数调用的时候,必须已经见过这个函数——要么见过它的完整定义,要么见过它的"预告"。

这个"预告"就叫函数声明(也叫函数原型):

void initBoard(char board[]);    // 声明:告诉编译器"这个东西存在"
void printBoard(char board[]);   // 声明
int  checkWin(char board[], char symbol);  // 声明

int main() {
    char board[9];
    initBoard(board);   // ✓ 编译器知道 initBoard 存在
    printBoard(board);  // ✓ 编译通过
    return 0;
}

// 定义在main后面——没关系,前面已经声明过了
void initBoard(char board[]) {
    for (int i = 0; i < 9; i++) {
        board[i] = 'X';
    }
}

void printBoard(char board[]) {
    printf(" %c | %c | %c \n", board[0], board[1], board[2]);
}

2-3 函数声明

声明只写一行,末尾加分号:

int checkWin(char board[], char symbol);   // 声明——分号结尾

定义写完整,末尾没有分号(花括号代替了):

int checkWin(char board[], char symbol) {  // 定义——花括号开始
    // 具体逻辑
}

经验法则:如果所有函数都写在 main 前面,就不需要声明。一旦有函数定义在 main 后面,就必须在前面声明。实际项目里通常把所有声明放在文件开头,定义放在后面——这样 main 永远在第一眼能看到的位置。

5 值传递 vs 数组传递——函数的"底线"

这是新手最容易踩的坑,但原理极其简单。

5.1 普通变量:传的是"复印件"

当你把一个普通变量(int、char、float)传给函数时,函数拿到的是一份副本。你在函数里怎么改,原件纹丝不动。

void changeIt(int x) {
    x = 100;
    printf("函数内:x = %d\n", x);  // 100
}

int main() {
    int num = 10;
    changeIt(num);
    printf("函数外:num = %d\n", num);  // 还是 10!没变!
    return 0;
}

2-4 值传递

5.2 数组:传的是"原件地址"

数组不一样。当你把数组传给函数时,函数拿到的不是副本,而是原件的地址。函数按地址找到原件,直接在上面改。

void changeFirst(char arr[]) {
    arr[0] = 'Z';  // 这改的是原件!
}

int main() {
    char name[] = {'A', 'B', 'C'};
    changeFirst(name);
    printf("name[0] = %c\n", name[0]);  // Z!变了!
    return 0;
}

2-5 数组传递

5.3 为什么会有这个区别?

简单说:数组比较大,复制一整份太浪费内存和时间,所以C语言干脆传地址。普通变量小,复制一份无所谓。

这个区别非常重要——它意味着:

  • initBoard(board) 能清空棋盘,因为它直接操作原数组 ✓
  • checkWin(board, 'X') 只看不改,传地址也无所谓 ✓
  • 如果你写个函数想让 int 变量被修改,普通的参数传递做不到——这需要后面学的指针。

6 总结

知识点一句话
函数结构返回类型 + 函数名 + (参数) + { 函数体 }
void函数只做动作不返回,比如打印、初始化、清空
值传递普通变量传复印件,函数里改外面不变
数组传递传原件地址,函数里改外面跟着变
函数声明函数定义在main后面时,必须先声明

📌 四条牢记:

  1. 函数 = 返回类型 + 名字 + (参数) + { 函数体 }
  2. 普通变量传值,数组传地址(函数内改数组,外面会变)
  3. 函数定义在 main 后面时必须先在前面声明
  4. 一个函数做一件事——printBoard() 只打印,checkWin() 只判断

7 练习题

兄弟,我知道你可能累了,但现在还不是休息的时候,把这两道题做了,它能帮助你变得更强。

题目1:自定义max3(int a,int b,int c),返回三个数中最大值

#include <stdio.h>
int max(int x, int y) {
    return x > y ? x : y;
}
int max3(int a,int b,int c) {
    return max(max(a,b), c);
}

int main() {
    int a,b,c;
    scanf("%d%d%d", &a,&b,&c);
    printf("最大值:%d", max3(a,b,c));
    return 0;
}

2-6 函数题目1

题目2:函数sumArr(int arr[], int len),计算数组所有元素和

#include <stdio.h>
int sumArr(int arr[], int len) {
    int sum = 0;
    for(int i = 0; i < len; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int a[5] = {1,2,3,4,5};
    printf("总和:%d", sumArr(a, 5));
    return 0;
}

2-7 函数题目2