C语言基础:函数(一)
在 C 语言中,函数是模块化编程的核心,它不仅能避免代码重复,还能让程序结构更清晰、更易维护。本文将从函数的基本概念出发,深入探讨函数的定义、调用、变量特性、内存分配及递归应用,并结合实战案例解析常见问题,帮助你全面掌握 C 语言函数的精髓。
目录
一、函数的基本要素:定义、调用与声明
1. 函数的定义:实现具体功能
函数的定义是其 “实现”,包含函数类型、函数名、形式参数(形参)和函数体。其基本形式如下:
函数类型 函数名(数据类型1 形参1, 数据类型2 形参2, ...) {
函数体;
return 返回值;
}
- 函数类型:指定函数返回值的类型(如
int、void、char等),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类型,如intchar等 ),必须返回匹配类型的值,保证返回值一致性。
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 步核心逻辑,体现了 “分而治之” 的思想。
六、常见问题与避坑指南
- 函数声明与定义不匹配:声明的参数类型 / 个数与定义不一致,会导致编译错误。
- 递归深度过深:递归次数过多(如
n=10000的阶乘)会导致栈溢出(Stack Overflow),需改用迭代。 - 全局变量滥用:全局变量可被任意修改,易引发 Bug,优先使用局部变量和函数参数传递。
- static 修饰的函数:
static函数仅在当前文件可见,避免跨文件调用
更多推荐



所有评论(0)