← 返回文章列表

C 语言(二)|函数:声明、调用与模块化设计

详细介绍C语言函数的定义、参数、返回值、嵌套调用、函数声明、static与extern、数组传参等核心知识点。

版权声明:本文为作者原创,版权归作者所有。如需转载或引用,请联系作者获得授权,并在使用时注明出处。

一、函数的基础认知

1.1 函数的定义

函数是编程中对一组实现特定功能的代码逻辑的封装与命名,是可复用的"代码模块"。

核心特点:可接收输入(参数,可选)、执行专属逻辑、返回输出(返回值,可选);定义后可在程序任意位置多次调用,无需重复编写代码,同时让程序结构更清晰、易维护。

核心总结:

  • 函数是封装特定功能的可复用代码块,有专属唯一名称
  • 核心逻辑:传参(输入)→ 执行操作 → 返回结果(输出),输入和输出均可省略
  • 核心价值:减少代码冗余,提升程序可读性、可维护性和可调试性

对比示例:重复代码实现求和 VS 函数封装实现求和

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main() {
    int sum = 0;
    int a, b, c, d;
    printf("请输入第1个整数:"); scanf("%d", &a); sum += a;
    printf("请输入第2个整数:"); scanf("%d", &b); sum += b;
    printf("请输入第3个整数:"); scanf("%d", &c); sum += c;
    printf("请输入第4个整数:"); scanf("%d", &d); sum += d;
    printf("所有数的和为:%d\n", sum);
    return 0;
}

那如果用函数呢?

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int add(int x, int y) {
    return x + y;
}

int main() {
    int sum = 0;
    int a, b, c, d;
    printf("请输入4个整数(空格分隔):");
    scanf("%d %d %d %d", &a, &b, &c, &d);
    sum = add(add(add(a, b), c), d);
    printf("所有数的和为:%d\n", sum);
    return 0;
}

1.2 函数与main()的核心关系

main()是C程序的唯一入口,普通函数是程序的功能单元,二者为「主调-被调」关系,核心两点:

  1. 程序运行时,操作系统仅会先找到并执行main(),执行流程从main()第一行开始,到main()执行结束(return或正常走完)为止
  2. 普通函数不会主动执行,必须在main()内部(或main()调用的其他函数内部)被"调用",才能参与程序执行

通俗比喻:main()是厂长办公室(启动流程、分配任务),普通函数是各个车间(负责具体功能),车间不会自行开工,需等待厂长办公室的指令(调用),所有任务完成后回到厂长办公室,程序结束。

1.3 函数的分类

C语言函数按来源分为两类,是后续学习的基础分类:

  • 库函数:C语言标准库提供的现成函数,无需自己定义,直接调用即可(需引入对应头文件),如printf()、strlen()
  • 自定义函数:开发者根据需求自行定义的函数,实现专属功能,如上述的add()函数

二、自定义函数核心语法

自定义函数是实现个性化功能的核心,固定格式为「函数头 + {函数体}」,二者缺一不可。

2.1 函数头组成

函数头是函数的"标识信息",必须按顺序书写:返回值类型 + 函数名 + (形参列表)

  • 返回值类型:指定函数执行后返回数据的类型(如int、float),无返回值则写void
  • 函数名:函数的唯一标识,调用函数时通过函数名触发
  • 形参列表:函数的输入参数,无参数则写空括号(),多个参数用逗号分隔

2.2 函数体要求

必须用大括号{}包裹,即使只有一行代码,也建议保留大括号(规范写法);内部实现函数的具体专属功能,建议单一功能原则(一个函数只做一件事),语句不宜过多;可在函数体内定义局部变量、执行业务语句、书写return语句。

2.3 函数名命名规范

由字母、数字、下划线组成,首字符不能是数字;不能与C语言关键字(如int、if、for)重名,不与库函数名重复;见名知意(如求和用add/sum,求平方用square),小写为主,多个单词用下划线分隔(如sum_square)。

标准示例:

int sum_square(int a, int b) {
    int s1 = a * a;
    int s2 = b * b;
    return s1 + s2;
}

三、函数的参数和返回值

参数是函数的"输入",返回值是函数的"输出",二者是函数与主调程序交互的核心。

3.1 函数的参数

参数分为形参和实参,传参方式为大一核心重点——值传递。

形参(形式参数)

  • 定义:在函数头中声明的参数,如add(int x, int y)中的x、y
  • 特点:属于函数的局部变量,仅在函数体内有效;函数调用时才分配内存,调用结束后内存立即释放;多个形参需分别指定类型(如int x, int y,不能写int x, y)

实参(实际参数)

  • 定义:调用函数时传递给形参的具体值,如add(1, 2)中的1、2
  • 要求:实参的类型、个数、顺序必须与形参一一对应
  • 可以是:常量、变量、合法表达式(如add(a+b, 3*4))

3.2 值传递的核心特性

重要结论:在函数调用时,实参将自己的值"复制"一份传递给形参,形参是独立的新变量,在函数内部修改形参不会影响实参本身。

#include <stdio.h>

void change(int n) {
    n = 100; // 修改形参n
}

int main() {
    int a = 10;
    change(a); // 实参a传递给形参n
    printf("a = %d", a); // 输出a=10,形参修改不影响实参
    return 0;
}

3.3 函数的返回值

return语句的核心作用

  1. 终止函数调用,强制返回到主调函数继续执行
  2. 将函数结果(值)带回到主调函数
  3. 一个函数可有多条return语句,但仅会执行第一条满足条件的return
int getMax(int a, int b) {
    if (a > b)
        return a; // 第一条满足的return执行,后续代码不运行
    return b;
}

无返回值的函数

void printMsg() {
    printf("Hello C\n");
    return; // 可省略,仅写return;,不能跟值
}

四、函数的调用

4.1 基本语法

// 有参函数:函数名 + (实参列表),多个实参用逗号分隔
函数名(实参1, 实参2, ...);
// 无参函数:函数名 + 空括号,括号不能省略
函数名();

4.2 调用类型

  • 普通调用:主调函数直接调用被调函数,执行完被调函数后,返回主调函数的调用位置继续执行后续代码
  • 嵌套调用:一个函数内部调用另一个函数,C语言允许多层嵌套调用

4.3 不同返回值类型的调用方式

有返回值函数(非void)

  • 赋值调用:将函数返回值赋值给同类型变量(最常用)
int res = add(1, 2); // add返回3,赋值给res
  • 表达式调用:将函数调用作为表达式的一部分,直接使用返回值
printf("和为:%d", add(1, 2)); // add返回值直接作为printf的参数

无返回值函数(void)

  • 仅有一种调用方式:直接调用,单独作为一条语句,无需赋值
printMsg(); // 直接调用,执行打印功能

五、函数的嵌套调用 & 链式访问

5.1 嵌套调用

核心定义

在一个函数的执行过程中调用另一个函数,执行完被调函数的全部代码后,再回到原函数的调用位置继续执行后续代码。核心禁忌:禁止函数嵌套定义(如在main()内定义add()),只能在函数外定义、函数内调用。

示例1:基础嵌套调用

#include <stdio.h>

// 被调用函数:求一个数的平方
int square(int n) {
    return n * n;
}

// 主调函数:求两数平方和,嵌套调用square
int sumSquare(int a, int b) {
    int s1 = square(a);
    int s2 = square(b);
    return s1 + s2;
}

int main() {
    int res = sumSquare(3, 4); // main调用sumSquare
    printf("3²+4²=%d", res); // 输出25
    return 0;
}

示例2:多层嵌套调用

#include <stdio.h>

// 步骤1:求两数之和
int add(int a, int b) {
    return a + b;
}

// 步骤2:求和的2倍,嵌套调用add
int doubleNum(int a, int b) {
    return add(a, b) * 2;
}

// 步骤3:求最终结果,嵌套调用doubleNum
int getResult(int x, int y, int z) {
    return doubleNum(x, y) - z;
}

int main() {
    printf("结果:%d", getResult(1,2,3)); // 输出3
    return 0;
}

5.2 链式访问

核心定义

把一个函数的返回值,直接作为另一个函数的参数来使用,无需用临时变量接收返回值,让代码更简洁,像"链条"一样把函数串起来。

示例1:基础链式访问

#include <stdio.h>

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

int main() {
    // add(1,2)返回3,直接作为第二个add的第一个参数
    int res = add(add(1, 2), 3);
    printf("1+2+3=%d", res); // 输出6
    return 0;
}

示例2:经典库函数链式访问

#include <stdio.h>
#include <string.h>

int main() {
    // strlen("hello")返回5,直接作为printf的%d参数
    printf("字符串长度:%d", strlen("hello")); // 输出5
    return 0;
}

5.3 避坑注意点

  • 嵌套调用:禁止嵌套定义,多层嵌套不要过多(否则代码可读性差、调试困难)
  • 链式访问:严格保证返回值类型与形参类型一致
  • 二者可混用:实际开发中,嵌套调用里可以用链式访问,链式访问的底层也可能是嵌套调用

六、函数的声明与定义

6.1 声明的必要性

C语言编译器从上到下扫描编译源代码,若函数调用在前,定义在后,编译器遇到调用时未找到函数定义,会报"隐式声明"警告或直接报错。解决方法:在函数调用之前,先对函数进行声明。

6.2 函数声明的语法

// 写法1(推荐,清晰):保留形参名和类型
返回值类型 函数名(形参类型1 形参名1, 形参类型2 形参名2, ...);
// 写法2(简洁):仅保留形参类型,省略形参名
返回值类型 函数名(形参类型1, 形参类型2, ...);

// 示例:add函数的两种声明方式
int add(int a, int b); // 写法1
int add(int, int);     // 写法2

6.3 声明与定义的位置示例

示例1:单文件场景(调用在前,定义在后)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

// 函数声明:调用前声明
int add(int a, int b);

int main() {
    int res = add(1, 2); // 函数调用(在前)
    printf("和为:%d", res);
    return 0;
}

// 函数定义:实现功能(在后)
int add(int a, int b) {
    return a + b;
}

示例2:多文件场景(企业标准)

add.h(头文件:仅声明)

int add(int a, int b); // 函数声明

add.c(源文件:仅实现)

#include "add.h"
int add(int a, int b) {
    return a + b;
}

main.c(主文件:调用函数)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "add.h"

int main() {
    printf("3+4=%d", add(3,4)); // 输出7
    return 0;
}

七、static与extern(跨文件作用域控制)

7.1 extern:实现跨文件访问

核心作用:声明外部符号(全局变量、函数),告诉编译器该变量/函数在其他.c文件定义。

示例:extern修饰全局变量

add.h

extern int g_num; // 声明:g_num在其他.c文件定义
int add(int a, int b);

add.c

#include "add.h"
int g_num = 100; // 唯一定义:分配内存并初始化
int add(int a, int b) {
    return a + b;
}

main.c

#include <stdio.h>
#include "add.h"

int main() {
    printf("跨文件访问g_num:%d\n", g_num); // 输出100
    g_num = 200;
    printf("修改后g_num:%d\n", g_num); // 输出200
    return 0;
}

7.2 static:实现多文件封装隔离

核心作用:限制符号的作用域为当前.c文件,成为当前文件的"私有财产",其他文件无法访问。

示例1:static修饰全局变量

#include "add.h"
static int g_num = 100; // 仅add.c可用,其他文件无法访问
int add(int a, int b) {
    return a + b;
}

示例2:static修饰函数

#include "add.h"
int g_num = 100;
static int add(int a, int b) { // 仅add.c可用,其他文件无法调用
    return a + b;
}

八、数组传参

8.1 一维数组传参

合法写法

// 写法1(推荐):省略数组长度
void printArr(int arr[], int len);
// 写法2:写原数组长度
void printArr(int arr[5], int len);

关键要求

函数内部无法计算出原数组的真实长度,因此传参时必须把数组长度作为第二个参数一起传给函数。

示例1:函数打印一维数组

#include <stdio.h>

void printArr(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]);
    }
}

int main() {
    int arr[5] = {1,2,3,4,5};
    int len = 5;
    printArr(arr, len); // 输出1 2 3 4 5
    return 0;
}

示例2:函数修改一维数组元素

#include <stdio.h>

void updateArr(int arr[], int len) {
    for (int i = 0; i < len; i++) {
        arr[i] *= 2;
    }
}

int main() {
    int arr[3] = {10,20,30};
    int len = 3;
    updateArr(arr, len);
    for (int i = 0; i < len; i++) {
        printf("%d ", arr[i]); // 输出20 40 60
    }
    return 0;
}

8.2 二维数组传参

固定写法

// 写法1(推荐):省略行数,指定列数
void print2DArr(int arr[][3], int row);
// 写法2:指定行数和列数
void print2DArr(int arr[2][3], int row);

核心规则:形参必须指定第二维(列数),列数必须与原数组一致。

示例:函数打印二维数组

#include <stdio.h>

void print2DArr(int arr[][3], int row) {
    for (int i = 0; i < row; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() {
    int arr[2][3] = {{1,2,3}, {4,5,6}};
    int row = 2;
    print2DArr(arr, row);
    return 0;
}

九、常见错误汇总

9.1 语法类错误

  • 函数体遗漏大括号{},或大括号不匹配
  • 形参声明格式错误(如int add(int x, y))
  • 函数名与C语言关键字重名
  • 函数嵌套定义

9.2 参数相关错误

  • 实参的类型、个数、顺序与形参不一致
  • 传参时漏写实参、多写实参
  • 试图在函数头中直接初始化形参(C语言不支持)

9.3 返回值相关错误

  • void类型函数书写return 数值;
  • return语句后的值类型与函数头声明的返回值类型不匹配
  • 有返回值的函数未书写return语句

9.4 函数调用相关错误

  • 未声明函数就直接调用
  • 调用函数时拼写错误函数名
  • 数组传参时未传长度/行数,导致数组遍历越界

十、核心总结

10.1 核心要素

  • 自定义函数的完整结构:函数头(返回值类型 + 函数名 + 形参列表)+ 函数体{}
  • 值传递的参数传递规则:形参是实参的拷贝,修改形参不影响原实参(与数组传参区分)
  • return语句与返回值的正确使用:终止函数 + 返回结果,void函数无返回值
  • 函数调用的语法规范:普通调用/嵌套调用的规则,有/无返回值的不同调用方式

10.2 核心考点

  • 能独立编写自定义函数实现简单功能(求和、求最大值、数组遍历/修改等)
  • 能正确区分并使用形参和实参,规避形实参不匹配的错误
  • 能灵活调用有返回值/无返回值函数,掌握嵌套调用和链式访问的基础用法
  • 熟记stdio.h/string.h常用库函数的使用方法,会引入对应头文件
  • 掌握一维/二维数组的传参写法,理解函数内操作原数组的核心特性
  • 了解static/extern的基本用法,掌握多文件开发的声明/定义规范

10.3 编程思想

  • 代码复用思想:一次定义,多次调用,避免重复编写相同代码,减少冗余
  • 模块化编程思想:单个函数实现单一功能,将复杂程序拆分为多个简单的功能模块(函数),让程序结构清晰、便于调试和维护

十一、寄语

今天关于C语言函数的讨论总结就结束了,让我们不忘初心,砥砺前行!

补充内容(拓展考点 + 企业级规范)

一、主文章多余示例迁移

1. 链式访问 - 多层链式示例

#include <stdio.h>
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int main() {
    // 先add(2,3)=5 → 再mul(5,4)=20 → 直接打印20
    printf("结果:%d", mul(add(2, 3), 4)); // 输出20
    return 0;
}

2. 一维数组传参 - 求和示例

#include <stdio.h>
// 功能:求数组和,返回int类型结果
int sumArr(int arr[], int len) {
    int sum = 0;
    for (int i = 0; i < len; i++) {
        sum += arr[i];
    }
    return sum;
}
int main() {
    int arr[4] = {1,3,5,7};
    int len = 4;
    int total = sumArr(arr, len); // 接收返回值
    printf("数组和:%d", total); // 输出16
    return 0;
}

二、break与return的核心区别

对比维度breakreturn
作用目标针对当前层循环/switch语句针对整个当前函数
执行后流程跳出当前循环/switch,函数继续执行后续代码直接终止当前函数,返回到主调函数的调用位置,函数内后续代码全不执行
返回值无返回值,仅做跳转可带值(非void函数必须带),void函数仅能写return;
嵌套场景仅跳出最近一层循环/switch,不影响外层无视任何嵌套(循环/函数),直接结束当前函数

示例对比:

#include <stdio.h>
void test_break() {
    for (int i = 0; i < 3; i++) {
        if (i == 1) break; // 跳出当前循环
        printf("i = %d\n", i);
    }
    printf("break后,函数继续执行\n"); // 会执行
}
void test_return() {
    for (int i = 0; i < 3; i++) {
        if (i == 1) return; // 直接终止函数
        printf("i = %d\n", i);
    }
    printf("return后,函数不会执行\n"); // 不会执行
}
int main() {
    test_break();
    test_return();
    return 0;
}

三、static的额外实用特性:修饰局部变量

核心特性:

  • 内存变化:局部变量从栈内存转到静态内存,函数调用结束后不会销毁,常驻内存
  • 初始化规则:仅初始化一次(第一次调用函数时执行),后续调用直接使用上一次的结果
  • 作用域不变:仍为函数局部,只能在函数内访问,外部无法使用

实用示例:统计函数调用次数

#include <stdio.h>
void count() {
    static int cnt = 0; // 静态局部变量,只初始化一次,常驻内存
    cnt++;
    printf("函数被调用%d次\n", cnt);
}
int main() {
    count(); // 第1次调用,输出1
    count(); // 第2次调用,输出2
    count(); // 第3次调用,输出3
    return 0;
}

四、企业级多文件开发补充规范

1. 头文件(.h)保护机制

#ifndef __ADD_H__
#define __ADD_H__

// 头文件核心内容:函数声明、extern变量声明、宏定义、类型声明
extern int g_num;
int add(int a, int b);

#endif

2. .h/.c文件严格分工

  • 头文件(.h):可放函数声明、extern变量声明、宏定义、类型声明;禁放函数实现、全局变量定义、static变量/函数声明
  • 源文件(.c):用#include "xxx.h"引用自定义头文件;不需要跨文件的全局变量/函数,一律加static封装

3. 全局变量使用原则

  • 全局变量尽量少用(易导致程序状态混乱)
  • 若必须使用,优先用static封装,通过接口函数实现跨文件的读写
  • 全局变量只能定义一次,放在唯一的.c文件中

4. static/extern核心避坑点

  • 禁止用extern定义变量:extern int g_num=10;是错误写法
  • static不能修饰函数参数
  • extern不能修饰局部变量
  • 不同.c文件的static全局变量/函数可重名

五、库函数快速参考

stdio.h常用库函数:

  • printf(const char *format, ...):格式化输出到控制台
  • scanf(const char *format, ...):格式化从控制台输入
  • getchar():从控制台读取单个字符
  • putchar(int c):向控制台输出单个字符
  • puts(const char *s):输出字符串,自动换行
  • fgets(char s, int size, FILE stream):安全的字符串输入

string.h常用库函数:

  • strlen(const char *s):计算字符串有效长度(不含结束符'\0'),返回unsigned int
  • strcpy(char dest, const char src):将字符串src拷贝到dest
  • strcmp(const char s1, const char s2):按ASCII码比较两个字符串
  • strcat(char dest, const char src):将字符串src拼接到dest末尾