在 C 语言中,函数是模块化编程的核心,它不仅能避免代码重复,还能让程序结构更清晰、更易维护。本文将从函数的基本概念出发,深入探讨函数的定义、调用、变量特性、内存分配及递归应用,并结合实战案例解析常见问题,帮助你全面掌握 C 语言函数的精髓。

目录

一、函数的基本要素:定义、调用与声明

1. 函数的定义:实现具体功能

2. 函数的调用:使用已定义的函数

3. 函数的声明:告诉编译器函数的存在

二、变量的作用域与生存周期

1. 局部变量

2. 全局变量

实战分析:变量初始化的坑

实战分析:void 的用法

1. 函数返回值控制

2. 函数参数限定

3. 变量声明限制

4. 函数调用规范

三、存储类型:决定变量的存放位置

1. auto:默认的局部变量

2. register:寄存器中的 “快变量”

3. extern:跨文件的全局变量声明

4. static:静态变量的 “双重身份”

四、程序运行时的内存分配

五、递归函数:自己调用自己

1. 递归的三要素

2. 实例 1:阶乘计算

3. 实例 2:汉诺塔问题

六、常见问题与避坑指南


一、函数的基本要素:定义、调用与声明

1. 函数的定义:实现具体功能

函数的定义是其 “实现”,包含函数类型、函数名、形式参数(形参)和函数体。其基本形式如下:

函数类型 函数名(数据类型1 形参1, 数据类型2 形参2, ...) {
    函数体;
    return 返回值;
}
  • 函数类型:指定函数返回值的类型(如intvoidchar等),void表示无返回值。
  • 函数名:命名规则与变量一致(字母、数字、下划线组成,不与关键字重名)。
  • 形参:函数接收的输入,定义时必须指定类型,仅在函数体内有效。

例如,一个实现两数相加的函数:

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

2. 函数的调用:使用已定义的函数

调用函数时,需传入实际参数(实参),实参个数和类型需与形参匹配。调用形式为:

函数名(实参1, 实参2, ...);
  • 实参可以是常量、变量或表达式,函数调用的结果为返回值,类型与函数类型一致。
  • 实参与形参类型不一致时,会将实参类型转换为形参类型。
  • 主调函数(主动调用者)与被调函数(被调用者)的关系:例如main函数调用add,则main是主调函数,add是被调函数。

3. 函数的声明:告诉编译器函数的存在

若被调函数定义在主调函数之后,需在主调函数前声明函数,以告知编译器函数的参数和返回值信息。声明形式为:

函数类型 函数名(数据类型1 形参1, 数据类型2 形参2, ...);

例如,上述add函数的声明可写为:

int add(int x, int y);
  • 声明时形参名可省略,只需保留类型:int add(int, int);

二、变量的作用域与生存周期

变量的特性由作用域(可访问的范围)和生存周期(内存存在的时间)决定,主要分为局部变量和全局变量。

1. 局部变量

  • 定义:在函数内部或代码块(如for循环)中定义的变量。
  • 作用域:仅在定义它的函数或代码块内有效。
  • 生存周期:从定义处开辟内存,离开作用域后内存被回收。

例如,for循环内的变量a

for (int i = 0; i < 3; i++) {
    int a = 0; // 每次循环重新定义,离开循环后销毁
    a++;
}

2. 全局变量

  • 定义:在所有函数外部定义的变量。
  • 作用域:整个工程,所有函数均可访问。
  • 生存周期:程序编译时分配内存,程序结束后回收。

例如:

int global_var; // 全局变量,整个程序可见

实战分析:变量初始化的坑

1.不同存储类型的变量表现出不同的初始化行为:

int c; // 全局变量,默认初始化为0
int f = 0; // 全局变量,显式初始化为0

int main(void) {
    int i = 0; // 局部变量,显式初始化
    int d = 0; // 局部变量,显式初始化
    int e; // 局部变量,未初始化,值为随机值
    for (i = 0; i < 3; i++) {
        int a = 0; // 局部变量,每次循环重新初始化
        static int b = 0; // 静态局部变量,仅第一次初始化
        a++; b++; c++; d++; e++; f++;
        printf("a=%d, b=%d, c=%d, d=%d, e=%d, f=%d\n", a, b, c, d, e, f);
    }
    return 0;
}
  • 全局变量(c、f):未显式初始化时自动为 0,安全可靠。
  • 静态局部变量(b):仅第一次循环初始化,后续保留上次值(输出结果中 b 最终为 3)。
  • 未初始化局部变量(e):值为栈区残留的随机值,直接使用可能导致不可预测结果(如输出异常数值),需坚决避免。

2.局部变量与全局变量重名问题:

#include <stdio.h>


// 全局变量
int a = 0; 


int main(void) {
  
    // 局部变量,屏蔽全局 a
    int a = 0; 

   
    return 0;
}

在 C 语言中,若局部变量和全局变量重名(如代码里全局 int a = 0; ,局部又定义 int a = 0; ),编译器处理时,在局部作用域内会忽略全局变量,优先使用局部变量。

这是作用域规则决定的,局部作用域的变量 “屏蔽” 了全局同名变量,以此区分不同作用域的变量使用,让程序变量管理更灵活,但也需注意避免因重名导致逻辑混淆 。

实战分析:void 的用法

在 C 语言里,void 是一个重要的类型说明符,主要用于函数相关场景,规则如下:

1. 函数返回值控制

  • 当函数无需返回值时,用 void 修饰函数返回值类型,如 void func(...) 。这类函数内部若用 return ,只能是 return; (无返回值形式 ),不能返回具体数据,像 void IsLeepYear(...) { return 1; } 就因违反规则报错 。
  • 若函数有返回值(非 void 类型,如 int char 等 ),必须返回匹配类型的值,保证返回值一致性。

2. 函数参数限定

函数无参数时,需用 int func(void) 形式声明(void 不可省略 ),明确告知编译器函数不接收参数,规范函数接口。

3. 变量声明限制

void 不能用于变量声明,比如 void year; 是非法的,void 本质是用于函数场景的类型说明,和变量类型(如 int float )有明显区分。

4. 函数调用规范

调用 void 函数时,禁止使用其 “返回值”(实际不存在返回值 ),只能单纯调用函数执行过程,如 void printMsg() { printf("Hello"); } ,调用时直接写 printMsg(); ,不能写 int res = printMsg(); 。

三、存储类型:决定变量的存放位置

C 语言通过存储类型指定变量的存储位置,影响其生存周期和访问特性,主要有 4 种:

1. auto:默认的局部变量

  • 特性:自动存储在栈区,仅在作用域内有效,离开后内存回收。
  • 使用:局部变量默认省略auto,如int a;等价于auto int a;

2. register:寄存器中的 “快变量”

  • 特性:建议编译器将变量存放在 CPU 寄存器中(若寄存器不足则存栈区),适用于频繁访问的变量(如循环计数器)。
  • 注意:寄存器无地址,不能用&取地址。

3. extern:跨文件的全局变量声明

对外声明

  • 特性:声明一个在其他文件中定义的全局变量,用于多文件共享变量。
  • 示例:若file1.c定义int g_var;file2.c可通过extern int g_var;使用该变量。

4. static:静态变量的 “双重身份”

对外隐藏

  • 局部静态变量:在函数内定义,用static修饰,存储在数据区,生存周期延长至程序结束(如上述示例中的b)。
  • 全局静态变量:仅在当前文件可见,避免跨文件命名冲突。

四、程序运行时的内存分配

程序运行时,内存被划分为多个区域,各区域存放不同类型的数据:

区域存储内容特性
文本段(.text)函数代码、指令只读,防止意外修改
数据区(.data/.bss)全局变量、静态变量编译时分配,程序结束后回收,默认初始化 0
栈区(.stack)局部变量(auto)、函数参数自动分配 / 回收,空间有限(默认 8M)
堆区(.heap)动态分配的内存(malloc)需手动申请 / 释放,大小灵活

例如:main函数存放在文本段,auto变量a在栈区,static变量b在数据区。

#栈区:

  • 栈区空间有上限的,默认(8M),不要定义太大空间
  • 栈区是操作系统管理区域,频繁被申请释放,所以未经初始化值随机值
  • 代码执行到变量定义时开辟空间(栈空间),代码执行超过变量作用域回收空间(栈空间)

#数据区:

1.特点: 

  • 存放全局变量和静态变量,未经初始化时会初始化为0值
  • 程序编译时分配空间
  • 程序结束时回收空间

2. 数据区区域划分:

已初始化全局变量/静态变量区域(.data)

  •         存放初始化的全局变量和静态变量

未初始化全局变量/静态变量区域(.bss)

  •         存放未初始化的全局变量和静态变量
  •         在程序运行时会对.bss端初始化为0值

字符串常量区(.rodata)

  •         区域中的内容不能修改,修改的话会导致段错误(eg: "hello world")

#static关键字的作用:
1. 延长变量的生存周期,局部变量超过作用域被回收,但用static修饰,会在程序结束时回收空

2. static修饰变量,将变量存放在数据区中,未经初始化时值为0值
3. static限定全局变量作用域只能在本文件中使用
4. static防止全局变量或者全局函数重名

五、递归函数:自己调用自己

递归函数是指在函数体内调用自身的函数,适用于解决具有 “重复子问题” 的场景(如阶乘、汉诺塔),但需注意结束条件调用深度

1. 递归的三要素

  • 结束条件:避免无限递归(如阶乘中n=1时返回 1)。
  • 递推关系:将问题分解为更小的子问题(如n! = n * (n-1)!)。
  • 返回值:子问题的结果需传递给父问题。

2. 实例 1:阶乘计算

int factorial(int n) {
    if (n == 1) { // 结束条件
        return 1;
    } else {
        return n * factorial(n - 1); // 递推关系
    }
}
  • 调用factorial(5)时,过程为:5*4! → 5*4*3! → ... → 5*4*3*2*1 = 120

3. 实例 2:汉诺塔问题

汉诺塔是经典的递归案例,目标是将n个盘子从 A 塔移到 C 塔,中间用 B 塔辅助,要求大盘不能压小盘。

#include <stdio.h>

int HanNuoTa(int n, char src, char tmp, char dst)
{	
	if (1 == n) // 结束条件:1个盘子直接移
	{
		printf("%c -> %c\n", src, dst);
	}
	else 
	{
		//1.将n-1层盘子由源塔,利用目的塔,移动到中间塔
         // 步骤1:n-1个盘子从A移到B(用C辅助)
		HanNuoTa(n-1, src, dst, tmp);

		//2.将最后一个盘子由源塔挪动到目的塔
        // 步骤2:第n个盘子从A移到C
		printf("%c -> %c\n", src, dst);
	
		//3.将n-1层盘子由中间塔,利用源塔,移动到目的塔
        // 步骤3:n-1个盘子从B移到C(用A辅助)
		HanNuoTa(n-1, tmp, src, dst);
	}

	return 0;
}

int main(void)
{
	int n = 0;
	
	printf("请输入盘子个数:\n");
	scanf("%d", &n);

	HanNuoTa(n, 'A', 'B', 'C');
	
	return 0;
}
  • n=3时,输出移动步骤为:
    A -> C
    A -> B
    C -> B
    A -> C
    B -> A
    B -> C
    A -> C
    

    递归通过分解问题,将复杂操作简化为 3 步核心逻辑,体现了 “分而治之” 的思想。

六、常见问题与避坑指南

  1. 函数声明与定义不匹配:声明的参数类型 / 个数与定义不一致,会导致编译错误。
  2. 递归深度过深:递归次数过多(如n=10000的阶乘)会导致栈溢出(Stack Overflow),需改用迭代。
  3. 全局变量滥用:全局变量可被任意修改,易引发 Bug,优先使用局部变量和函数参数传递。
  4. static 修饰的函数static函数仅在当前文件可见,避免跨文件调用
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐