C语言函数知识全解析

一、前言

函数是C语言程序设计的核心基石之一,合理运用函数能让代码结构清晰、复用性高、逻辑简洁。本文围绕C语言函数,从概念到实践,结合案例、内存分析等,深入讲解函数知识体系,助你扎实掌握函数用法。

二、函数的概念

函数是一段可重复使用的代码块,它接收输入(参数),经过内部逻辑处理,输出结果(返回值,可选 )。打个比方,函数就像工厂:你给工厂送原材料(参数),工厂加工后产出产品(返回值),比如计算两数之和的函数,输入两个数,输出相加结果 。

三、库函数

(一)标准库和头文件

C语言标准库提供大量现成函数(如printfscanfstrcpy 等),使用时需包含对应头文件。头文件像“说明书”,告诉编译器函数的声明(名字、参数、返回值类型)。例如:

  • printf输出,要包含 ,因为 printf 的声明在这个头文件里;
  • 处理字符串复制 strcpy ,需包含

(二)库函数的使用方法——以 strlen 为例

#include 
#include 
int main() {
    char str[] = "Hello";
    // strlen 计算字符串长度(不包含 '\0')
    size_t len = strlen(str); 
    printf("字符串长度:%zu\n", len); 
    return 0;
}

关键点

  • 先包含头文件 ,让编译器知道 strlen 怎么用;
  • strlen 接收字符串参数,返回无符号整数(size_t 类型 ),表示字符串有效字符数量。

(三)库函数文档的一般格式

库函数文档通常包含:

  • 功能描述:清晰说明函数能做什么,比如 strlen 是计算字符串长度;
  • 函数原型:像 size_t strlen(const char *s); ,明确返回值类型、函数名、参数及参数类型;
  • 参数说明:解释每个参数的含义、要求,比如 strlens 是要计算长度的字符串指针;
  • 返回值说明:告知返回结果代表什么,strlen 返回字符串有效字符数;
  • 示例代码:演示函数怎么用,帮你快速理解实践。

四、自定义函数

(一)函数的语法形式

自定义函数的基本语法:

返回值类型 函数名(参数列表) {
    // 函数体:实现功能的代码
    // 可包含变量定义、逻辑运算、return 语句(按需)
}

示例:实现两数相加的自定义函数

// 返回值类型 int,函数名 Add,参数是两个 int 类型的 x、y
int Add(int x, int y) { 
    return x + y; // 函数体,返回两数之和
}

(二)函数的举例——判断闰年函数

#include 
// 判断是否为闰年的函数
_Bool is_leap_year(int y) { 
    if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0)) {
        return 1; // 是闰年返回真(1)
    }
    return 0; // 不是返回假(0)
}
int main() {
    int year = 2024;
    if (is_leap_year(year)) {
        printf("%d 年是闰年\n", year);
    } else {
        printf("%d 年不是闰年\n", year);
    }
    return 0;
}

分析

  • 自定义函数 is_leap_year ,接收年份参数 y ,通过闰年规则判断,返回布尔值;
  • 主函数调用它,根据返回值输出结果,体现函数“封装逻辑,复用调用”的特点。

五、形参和实参

(一)实参

实参调用函数时,传递给函数的实际数据,可以是常量、变量、表达式等。比如:

int result = Add(3, 5); 

这里调用 Add 函数时,35 就是实参,是真正传给函数参与运算的值。

(二)形参

形参是函数定义时,括号里的参数,它是“形式上”的参数,用于接收实参的值 。例如:

int Add(int x, int y) { 
    // x、y 是形参,定义函数时用来“占位”
    return x + y; 
}

内存角度分析
形参只有在函数被调用时,才会在栈区(内存区域)分配空间,函数执行结束,形参占用的内存会释放。实参传递给形参,相当于把实参的值“拷贝”给形参

关键点

  • 形参和实参是不同内存空间
  • 形参是实参的临时拷贝,函数内修改形参,不会影响实参的值 。比如:
void Swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // 这里修改的是形参 a、b,实参不会变
}
int main() {
    int x = 1, y = 2;
    Swap(x, y);
    // x、y 值还是 1、2,因为形参修改没影响实参
    printf("x=%d, y=%d\n", x, y); 
    return 0;
}

六、return 语句

(一)核心规则

  1. 返回值多样return 后可以跟数值、表达式。表达式会先计算,再返回结果。例如:
int Add(int x, int y) {
    // 先算 x + y,再返回结果
    return x + y; 
}
  1. 无返回值场景:函数返回类型是 void 时,return 可以单独用(也可省略,函数执行到末尾自动返回 ),用于提前结束函数。比如:
void PrintInfo() {
    printf("执行到这\n");
    // 提前返回,后面代码不执行
    return; 
    printf("不会执行到这\n");
}
  1. 类型匹配(隐式转换)return 返回的值会隐式转换为函数声明的返回值类型。例如:
// 函数返回 int 类型
int Func() { 
    // 返回 float 类型 3.14,会被转成 int(丢失小数部分,变成 3 )
    return 3.14; 
}
  1. 终止函数执行return 执行后,函数立即结束,后面代码不再运行。比如:
int Max(int a, int b) {
    if (a > b) {
        // a 大,返回 a,函数结束
        return a; 
    }
    // a 不大,返回 b
    return b; 
}
  1. 分支覆盖(易错点!):函数里有 if 等分支时,要保证所有分支都有 return 返回,否则编译器报错。比如:
// 错误示例:如果 a == b,没有 return 返回,编译器报错
int Compare(int a, int b) { 
    if (a > b) {
        return 1;
    } else if (a < b) {
        return -1;
    }
    // 这里少了 a == b 的 return 处理
}
// 正确写法,补充 a == b 的返回
int Compare(int a, int b) { 
    if (a > b) {
        return 1;
    } else if (a < b) {
        return -1;
    }
    return 0; 
}

七、数组做函数参数

(一)传递方式——地址传递(本质)

数组传参时,实际传递的是数组首元素的地址,函数形参可以写成以下形式(本质一样 ):

// 写法 1:数组形式(常用,直观)
void PrintArray(int arr[], int size) { 
// 写法 2:指针形式,等价于写法 1
void PrintArray(int *arr, int size) { 
    for (int i = 0; i < size; i++) {
        // 通过地址访问数组元素
        printf("%d ", arr[i]); 
    }
}
int main() {
    int arr[] = {1, 2, 3};
    // 传递数组名(首元素地址)和数组长度
    PrintArray(arr, 3); 
    return 0;
}

(二)易错点:数组长度丢失

函数形参里的数组,无法通过 sizeof 获取原数组长度!因为传递的是地址,形参 arr[] 退化成指针,sizeof(arr) 得到的是指针大小(如 4 或 8 字节,取决于系统)。所以必须手动传数组长度(像上面 size 参数 )。

八、嵌套调用和链式访问

(一)嵌套调用

函数里调用另一个函数,就是嵌套调用。比如:

// 计算 a + b
int Add(int a, int b) { 
    return a + b;
}
// 计算 a + b + c,嵌套调用 Add
int AddThree(int a, int b, int c) { 
    // 先调 Add 算 a+b,结果再和 c 相加
    return Add(Add(a, b), c); 
}
int main() {
    int sum = AddThree(1, 2, 3);
    // 结果是 6(1+2=3,3+3=6 )
    printf("sum=%d\n", sum); 
    return 0;
}

执行流程AddThree 调用 AddAdd 执行完返回结果,AddThree 继续用返回值运算,最后返回给 main

(二)链式访问

把一个函数的返回值,作为另一个函数的参数,像链条一样连接调用。例如:

#include 
#include 
int main() {
    char str[] = "Hello";
    // 链式调用:strlen 返回字符串长度,作为 printf 的参数
    printf("字符串长度:%d\n", strlen(str)); 
    return 0;
}

执行顺序:先执行 strlen(str) 得到长度,再把这个长度传给 printf 输出。再看一个有趣的嵌套 printf 例子:

#include 
int main() {
    // 从最内层开始执行
    printf("%d", printf("%d", printf("%d", 43)));
    return 0; 
}

执行拆解

  1. 最内层 printf("%d", 43) :输出 43 ,返回值是输出字符的个数(2个字符:‘4’和’3’ )
  2. 中间层 printf("%d", 2) :输出 2 ,返回值是输出字符个数(1个字符:‘2’ )
  3. 最外层 printf("%d", 1) :输出 1
    所以最终控制台输出 4321 ,这就是链式调用的嵌套执行逻辑,很考验对函数返回值的理解!

九、函数的声明和定义

(一)单个文件场景(简单情况)

函数定义(实现逻辑)和调用在同一个 .c 文件时,直接写函数定义即可。例如:

#include 
// 函数定义(声明 + 实现一体)
int Add(int x, int y) { 
    return x + y;
}
int main() {
    // 直接调用,没问题
    int sum = Add(1, 2); 
    printf("sum=%d\n", sum);
    return 0;
}

(二)多个文件场景(工程化必备)

实际项目中,代码会拆分到多个 .c 文件,这时候需要函数声明函数定义分离:

  1. 函数定义:写在一个 .c 文件(如 func.c ),包含函数的具体实现:
// func.c
int Add(int x, int y) {
    return x + y;
}
  1. 函数声明:写在头文件(如 func.h ),告诉其他文件“函数长什么样”:
// func.h
// 声明 Add 函数,分号结尾
int Add(int x, int y); 
  1. 其他文件调用:在要调用的 .c 文件(如 main.c )里,包含头文件,就能用函数:
// main.c
#include 
// 包含声明,让编译器知道 Add 怎么用
#include "func.h" 
int main() {
    int sum = Add(3, 4);
    printf("sum=%d\n", sum);
    return 0;
}

编译链接

  • 编译时,main.c 包含 func.h ,知道 Add 的声明;
  • 链接时,编译器找到 func.cAdd 的定义,把调用和实现关联起来。

(三)staticextern(作用域控制)

1. static 修饰局部变量(静态局部变量)
void Test() {
    // 静态局部变量,只会初始化一次,函数结束后内存不释放
    static int num = 0; 
    num++;
    printf("num=%d\n", num);
}
int main() {
    Test(); // 输出 num=1
    Test(); // 输出 num=2(因为 num 没被释放,继续自增)
    Test(); // 输出 num=3
    return 0;
}

特点

  • 只在函数内有效(作用域局部);
  • 生命周期和程序一致(程序运行时分配内存,结束才释放 );
  • 第一次调用函数时初始化,之后调用不再初始化,保留之前的值。
2. static 修饰全局变量
// file1.c
// 静态全局变量,作用域限制在 file1.c 文件内
static int g_num = 10; 
// file2.c
// 想访问 file1.c 的 g_num?不行!因为 static 限制了作用域
// 编译报错:未定义的标识符
extern int g_num; 

作用:用 static 修饰全局变量,能隐藏变量,让它只能在当前 .c 文件里被访问,其他文件看不到,避免全局变量冲突。

3. static 修饰函数
// func.c
// 静态函数,只能在 func.c 文件内被调用
static int Add(int x, int y) { 
    return x + y;
}
// main.c
// 想调用 func.c 的 Add?不行!static 限制了作用域
extern int Add(int x, int y); 

效果static 修饰函数后,函数只能在定义的 .c 文件内使用,其他文件无法调用,用于封装内部逻辑,减少对外暴露。

4. extern 关键字(跨文件访问)

extern 用来声明外部变量/函数(定义在其他文件里 ),告诉编译器:“这个变量/函数在别的地方定义,你先记着,链接时找它”。
示例(跨文件访问全局变量):

// file1.c
// 定义全局变量 g_data
int g_data = 20; 
// file2.c
#include 
// 声明外部全局变量 g_data,告诉编译器它在其他文件定义
extern int g_data; 
int main() {
    // 可以访问到 file1.c 的 g_data,输出 20
    printf("g_data=%d\n", g_data); 
    return 0;
}

注意extern 用于“声明”,不是“定义”。变量/函数的定义只能有一次,声明可以多次。

十、总结

函数是C语言编程的核心工具,掌握以下要点,就能灵活运用函数:

  1. 清晰区分形参、实参,理解参数传递的值拷贝特性;
  2. 正确使用 return ,避免分支遗漏、类型不匹配等错误;
  3. 合理运用库函数,记得包含对应头文件;
  4. 自定义函数时,做好声明和定义分离(工程化必备 );
  5. 利用 staticextern 控制变量

你可能感兴趣的:(C语言函数知识全解析)