函数

3 阅读18分钟

 

一、函数的概念

1. 核心定义

C 语言中的函数(又称 “子程序”)是一段完成特定独立任务的代码片段,具有固定语法格式和调用规则。类比数学中的y=kx+b(给定 x 可求唯一 y),C 语言函数接收输入(参数),经内部逻辑处理后输出结果(返回值),或仅执行特定操作(无返回值)。

2. 核心价值

  • 模块化编程:将大型程序拆解为多个小函数,降低代码复杂度,便于维护和调试(如计算 “学生总成绩” 可拆分为 “输入成绩”“计算总分”“打印结果” 3 个函数)。
  • 代码复用:同一功能的函数可在程序中多次调用,无需重复编写(如printf函数可反复用于输出),提升开发效率。
  • 可读性提升:函数名按功能命名(如Add表示加法、is_leap_year表示判断闰年),使代码逻辑更清晰。

3. 函数分类

  • 库函数:由 C 语言标准库(ANSI C 规定)提供,编译器厂商实现的现成函数,直接调用即可(如printfscanfsqrt)。
  • 自定义函数:开发者根据实际需求编写的函数,灵活性强,是编程核心(如实现两个数的乘法、数组排序等)。
//计算f的下标 
	char arr[]="abcdef";  //[a b c d e f \0] 
						  //[0 1 2 3 4 5 ]	
	int right1= strlen(arr)-1;
	int right2= sizeof(arr)/sizeof(arr[0])-2;
	printf("%d\n",right1);
	printf("%d\n",right2);

二、库函数

1. 基础特性

  • 依赖标准库:C 语言仅规定语法规则,不提供库函数;库函数是编译器厂商按 ANSI 标准实现的通用功能集合。
  • 需包含头文件:库函数的声明(函数名、参数、返回值)存储在对应头文件中,使用前必须通过#include引入(如数学函数需#include <math.h>,输入输出函数需#include <stdio.h>)。

2. 常用库函数类别及头文件

功能类别代表函数对应头文件
输入输出printfscanf<stdio.h>
数学运算sqrt(平方根)、pow(幂运算)<math.h>
字符串操作strlen(长度)、strcpy(拷贝)<string.h>
日期时间time(获取时间)<time.h>

3. 库函数使用步骤(以sqrt为例)

(1)查询函数文档

通过官方工具了解函数细节:

  • 函数原型:double sqrt(double x)double为返回值类型,x为参数,需传入双精度浮点数)。
  • 功能:计算x的平方根(x≥0,否则返回 NaN)。
  • 头文件:<math.h>

(2)编写代码并调用

#include <stdio.h>   // 包含printf的头文件
#include <math.h>    // 包含sqrt的头文件
int main() {
    double d = 25.0;  // 传入sqrt的参数(double类型)
    double result = sqrt(d);  // 调用库函数,接收返回值
    printf("平方根:%lf\n", result);  // 输出结果:5.000000
    return 0;
}

​

(3)注意事项

  • 必须匹配参数类型:若传入int类型(如sqrt(25)),编译器会自动转换为double,但建议显式声明类型(如sqrt(25.0))。
  • 不包含头文件的后果:编译器无法识别函数,可能报 “未定义标识符” 警告(如未包含<math.h>时,sqrt可能被默认当作int类型函数,导致返回值错误)。

4. 库函数学习工具

三、自定义函数

1. 语法格式

​
ret_type fun_name(parameter_list) {
    // 函数体:完成任务的代码逻辑
    statement;
    return return_value;  // 与ret_type匹配(void类型可省略)
}

​

各部分说明:

  • ret_type(返回值类型):函数执行后返回结果的类型,如int(整型)、double(双精度浮点型);若无需返回值,用void(空类型)。
  • fun_name(函数名):遵循标识符命名规则(字母、数字、下划线组成,首字符非数字),需 “见名知义”(如Sub表示减法、SortArray表示数组排序)。
  • parameter_list(参数列表):函数接收的输入数据,格式为 “类型 1 变量名 1, 类型 2 变量名 2,...”;无参数时可写void(或省略)。
  • 函数体:用{}包裹的代码块,是函数的核心逻辑(如计算、循环、判断等)。
  • return语句:返回结果给调用者,执行后函数立即结束(后续代码不执行);void类型函数可写return;(或省略)。

2. 自定义函数示例(3 个典型场景)

(1)有参数、有返回值(加法函数)

#include <stdio.h>
// 函数定义:计算两个int类型数的和
int Add(int x, int y) {  // x、y为形参,接收实参的值
    int z = x + y;       // 函数体:计算逻辑
    return z;            // 返回结果z(类型与ret_type一致)
}
// 简化写法(省略临时变量z)
// int Add(int x, int y) { return x + y; }

int main() {
    int a = 10, b = 20;
    // 函数调用:将a、b作为实参传入,接收返回值
    int sum = Add(a, b);  
    printf("和:%d\n", sum);  // 输出:30
    return 0;
}

​

(2)有参数、无返回值(数组置零函数)

#include <stdio.h>
// 函数定义:将数组所有元素置为0(无需返回值,用void)
void SetZero(int arr[], int sz) {  // arr为数组参数,sz为数组长度
    for (int i = 0; i < sz; i++) {
        arr[i] = 0;  // 函数体:修改数组元素
    }
    // 无return语句(或写return;)
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int sz = sizeof(arr) / sizeof(arr[0]);  // 计算数组长度
    SetZero(arr, sz);  // 函数调用:传入数组名和长度
    // 打印数组(验证结果)
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);  // 输出:0 0 0 0 0
    }
    return 0;
}

​

(3)无参数、有返回值(获取随机数函数)

#include <stdio.h>
#include <time.h>  // 包含time函数的头文件
// 函数定义:生成1-100的随机数(无参数)
int GetRandom() {
    srand((unsigned int)time(NULL));  // 设置随机数种子
    return rand() % 100 + 1;  // 返回1-100的随机数
}

int main() {
    int num = GetRandom();  // 函数调用:无实参传入
    printf("随机数:%d\n", num);  // 输出:1-100的随机数
    return 0;
}

​

3. 自定义函数设计原则

  • 单一职责:一个函数只完成一个核心任务(如 “计算加法” 和 “打印结果” 拆分两个函数)。
  • 参数清晰:明确参数的类型、含义和个数,避免过多参数(超过 3 个可考虑用结构体封装)。
  • 返回值明确:若函数有计算结果,必须返回;若仅执行操作(如修改数组),用void

四、形参和实参

1. 定义与区别

类别定义特点示例(Add 函数)
实参(实际参数)调用函数时传递给函数的真实数据必须有确定值(变量、常量、表达式均可),占用独立内存Add(a, b)中的ab(或Add(10, 20)中的1020
形参(形式参数)函数定义时括号中的参数变量仅在函数调用时实例化(申请内存),接收实参的值;函数执行完后销毁Add(int x, int y)中的xy

2. 核心关系:临时拷贝

  • 形参是实参的临时拷贝:函数调用时,编译器为形参分配内存,将实参的值复制到形参中;形参和实参的内存地址完全不同,修改形参的值不会影响实参。
  • 验证示例:
#include <stdio.h>
void Swap(int x, int y) {  // x、y为形参
    int temp = x;
    x = y;
    y = temp;  修改的是形参x、y的值
}

int main() {
    int a = 10, b = 20;
    Swap(a, b);  // a、b为实参
    printf("a=%d, b=%d\n", a, b);  // 输出:a=10, b=20(实参未变)
    return 0;
}

​

  • 结论:上述代码无法实现交换功能,因为Swap函数修改的是形参xy(临时拷贝),实参ab的内存未被修改(若需修改实参,需用指针传递)。

3. 注意事项

  • 形参的类型必须与实参匹配:若实参是int类型,形参不能是double类型(否则会发生隐式类型转换,可能导致数据丢失)。
  • 形参仅在函数内部有效:函数执行结束后,形参的内存被释放,外部无法访问。

五、return 语句

1. 基本用法

  • 带返回值:return 表达式;(表达式结果需与函数返回类型匹配),示例:
int Max(int x, int y) {
    if (x > y)
        return x;  // 返回x的值,函数结束
    else
        return y;  // 返回y的值,函数结束
}

​

  • 无返回值:return;(仅用于void类型函数),示例:
void PrintHello() {
    printf("Hello World!\n");
    return;  // 可选,函数执行到此处结束
    printf("This line will not be executed\n");  // 不会执行
}

​

2. 关键规则

  • 函数返回类型与 return 值类型需一致:若不一致,编译器会自动隐式转换(如return 3.14用于int类型函数,会转换为3),可能导致错误,建议显式匹配。
  • 分支结构中必须保证所有路径有返回值:若函数有返回类型(非void),且包含if-elseswitch等分支,需确保每个分支都有return语句,否则编译报错。
// 错误示例(缺少else分支的return)
int GetSign(int x) {
    if (x > 0)
        return 1;
    else if (x == 0)
        return 0;
    // 若x<0,无return语句,编译报错
}
// 正确示例
int GetSign(int x) {
    if (x > 0)
        return 1;
    else if (x == 0)
        return 0;
    else
        return -1;  // 补充else分支的return
}

​

  • return 语句执行后函数立即结束:后续代码无论是否在循环、判断中,均不再执行。

六、数组做函数参数

1. 数组传参的本质

  • 数组传参时,传递的是数组首元素的地址,而非整个数组的拷贝。
  • 形参接收的是地址,因此形参操作的数组与实参数组是同一个数组(修改形参数组元素会直接修改实参数组)。
  • 关键结论:形参无法通过sizeof获取数组的真实长度(sizeof(arr)得到的是地址的大小,而非数组总字节数),因此必须单独传递数组长度作为参数。

2. 形参的合法写法

实参形式形参合法写法说明
一维数组int arr[]int arr[]int arr[10](10 可省略)或 int* arr(指针形式)一维数组形参的大小可省略,编译器会忽略括号中的数字
二维数组int arr[3][4]int arr[][4](列数 4 不可省略)或 int (*arr)[4](指针形式)二维数组形参的行数可省略,但列数必须明确(否则编译器无法计算元素地址)

3. 示例:数组打印与修改

#include <stdio.h>
// 形参:数组arr(接收首元素地址),长度sz(单独传递)
void PrintArray(int arr[], int sz) {
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);  // 操作实参数组
    }
    printf("\n");
}

void ModifyArray(int arr[], int sz) {
    for (int i = 0; i < sz; i++) {
        arr[i] *= 2;  // 修改实参数组元素(翻倍)
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int sz = sizeof(arr) / sizeof(arr[0]);  // 计算真实长度(5)
    
    PrintArray(arr, sz);  // 输出:1 2 3 4 5
    ModifyArray(arr, sz); // 修改数组
    PrintArray(arr, sz);  // 输出:2 4 6 8 10(实参数组已被修改)
    
    return 0;
}

​

4. 常见错误

  • 错误 1:形参写int arr[],但未传递长度,用sizeof(arr)/sizeof(arr[0])计算长度(结果错误)。
  • 错误 2:二维数组形参写int arr[][](省略列数),编译报错(需明确列数,如int arr[][4])。

七、嵌套调用和链式访问

1. 嵌套调用(函数调用函数)

  • 定义:一个函数内部调用另一个函数,形成嵌套关系(类似乐高积木拼接),但函数不能嵌套定义(即不能在一个函数内部定义另一个函数)。
  • 示例:计算某年某月的天数(调用闰年判断函数)
	//计算某年某月有多少天 
	// 子函数1:判断是否为闰年(返回1=闰年,0=非闰年)
	int is_leap_year(int y) {
	    return ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0));
	}
	
	// 子函数2:计算某月天数(调用is_leap_year函数)
	int get_days(int y, int m) {
	    int days[] = {0,31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	    //            0,1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10, 11, 12
	    if (m == 2 && is_leap_year(y))  // 嵌套调用is_leap_year
	        days[2] += 1;  // 闰年2月29天
	    return days[m];
	}
	
    int year = 2024, month = 2;
    int day_count = get_days(year, month);  // 调用get_days函数
    printf("%d年%d月有%d天\n", year, month, day_count);  // 输出:2024年2月有29天
    return 0;

  • 调用关系:mainget_daysis_leap_year(层层嵌套,执行完子函数后返回上一层)。

2. 链式访问(函数返回值作为另一个函数的参数)

  • 定义:将一个函数的返回值直接作为另一个函数的参数,形成 “链条” 式调用。
  • 示例 1:简化字符串长度打印(strlen返回值作为printf参数)
#include <stdio.h>
#include <string.h>
int main() {
    // 链式访问:strlen的返回值(字符串长度)作为printf的参数
    printf("字符串长度:%d\n", strlen("hello world"));  // 输出:11
    return 0;
}

​

  • 示例 2:多层链式访问(printf嵌套调用)
#include <stdio.h>
int main() {
    // printf返回值:打印的字符个数(如printf("43")返回2printf("%d\n", printf("%d", printf("%d", 43)));  // 输出:4321
    printf("%d \n", printf("%d ", printf("%d ", 43)));  // 输出:43 3 2
    return 0;
}

​

  • 解析:

    1. 最内层printf("%d", 43):打印 “43”(2 个字符),返回 2。
    2. 中层printf("%d", 2):打印 “2”(1 个字符),返回 1。
    3. 最外层printf("%d", 1):打印 “1”,最终输出 “4321”。

八、函数的声明和定义

1. 基本概念

  • 函数定义:函数的完整实现(包含函数体),是函数的 “具体实现”,整个程序中只能定义一次。示例:int Add(int x, int y) { return x + y; }
  • 函数声明:告知编译器函数的 “存在”(函数名、返回类型、参数类型),无需函数体,可声明多次(通常在头文件中)。示例:int Add(int x, int y);(参数名可省略,如int Add(int, int);

2. 声明的必要性:解决 “先调用后定义” 的问题

C 语言编译器按从上到下的顺序扫描代码,若函数调用在定义之前,编译器会因 “未识别函数” 报错(或警告)。

  • 错误示例(先调用后定义):
  • 编译警告:warning C4013: "Add"未定义: 假设外部返回int(编译器无法确认函数类型,可能导致错误)。
  • 解决方法:在调用前添加函数声明:
#include <stdio.h>
int main() {
    int sum = Add(10, 20);  // 调用Add函数(此时未定义)
    printf("sum=%d\n", sum);
    return 0;
}
// 函数定义在调用之后
int Add(int x, int y) {
    return x + y;
}

​

#include <stdio.h>
// 函数声明(告知编译器Add函数的信息)
int Add(int x, int y);  // 或int Add(int, int);

int main() {
    int sum = Add(10, 20);  // 编译器已识别函数,正常调用
    printf("sum=%d\n", sum);
    return 0;
}
// 函数定义
int Add(int x, int y) {
    return x + y;
}

​

3. 多文件编程:声明与定义分离(企业级规范,方便多人协同)

当程序代码较多时,通常将函数声明放在头文件(.h) 中,函数定义放在源文件(.c) 中,实现模块化管理。

  • 示例结构:

    plaintext

项目文件夹/
├─ add.h    (函数声明)
├─ add.c    (函数定义)
└─ test.c   (主函数,调用函数)

  • 文件内容:

    1. add.h(头文件,存放声明):
​
​
#pragma once  // 防止头文件重复包含
// 函数声明
int Add(int x, int y);

​

​

    1. add.c(源文件,存放定义):
#include "add.h"  // 包含头文件(确保声明与定义一致)
// 函数定义
int Add(int x, int y) {
    return x + y;
}

​

    1. test.c(主文件,调用函数):
#include <stdio.h>
#include "add.h"  // 包含头文件(获取函数声明)
int main() {
    int sum = Add(10, 20);
    printf("sum=%d\n", sum);  // 输出:30
    return 0;
}

​

  • 核心优势:

    • 代码分离:声明(接口)与实现分离,修改函数实现(add.c)无需修改调用者(test.c)。
    • 复用性:多个源文件可通过包含头文件(#include "add.h")调用Add函数。
    • 避免重复定义:头文件用#pragma once(或 #ifndef 宏)防止重复包含,避免编译错误。

九、static 和 extern 关键字(函数与变量的作用域控制)

1. 基础概念:作用域与生命周期

  • 作用域:变量 / 函数的 “可用范围”(局部变量→局部范围,全局变量 / 函数→整个工程)。
  • 生命周期:变量从 “创建(申请内存)” 到 “销毁(释放内存)” 的时间段(局部变量→进入作用域创建,出作用域销毁;全局变量→程序启动创建,程序结束销毁)。

2. static 关键字的 3 种用法

(1)修饰局部变量:延长生命周期

  • 未修饰的局部变量:存储在栈区,生命周期为 “进入函数→退出函数”(每次调用重新初始化)。
  • static 修饰的局部变量:存储在静态区,生命周期与程序一致(仅初始化一次,退出函数不销毁,下次调用沿用上次的值)。
  • 示例:
#include <stdio.h>
void Test() {
    static int i = 0;  // static修饰局部变量,仅初始化一次
    i++;
    printf("i=%d ", i);
}

int main() {
    for (int j = 0; j < 5; j++) {
        Test();  // 调用5次Test函数
    }
    // 输出:i=1 i=2 i=3 i=4 i=5(i的值累加,未重新初始化)
    return 0;
}

​

  • 注意:static 不改变局部变量的作用域(仍仅在函数内部可用),仅改变生命周期。

(2)修饰全局变量:限制作用域

  • 未修饰的全局变量:具有外部链接属性,可在整个工程的多个源文件中使用(需用extern声明)。

  • static 修饰的全局变量:具有内部链接属性,仅在当前源文件中可用,其他源文件无法访问(即使extern声明也报错)。

  • 示例(多文件):

    1. file1.c(定义 static 全局变量):
static int g_val = 100;  // static修饰全局变量

    1. file2.c(尝试访问 g_val):
#include <stdio.h>
extern int g_val;  // 声明外部全局变量
int main() {
    printf("%d\n", g_val);  // 编译报错:无法解析的外部符号g_val
    return 0;
}

​

  • 用途:防止全局变量被其他文件意外修改,提高代码安全性。

(3)修饰函数:限制作用域

  • 未修饰的函数:具有外部链接属性,可在整个工程的多个源文件中使用(需用extern声明)。

  • static 修饰的函数:具有内部链接属性,仅在当前源文件中可用,其他源文件无法调用。

  • 示例(多文件):

    1. add.c(定义 static 函数):
static int Add(int x, int y) {  // static修饰函数
    return x + y;
}

​

    1. test.c(尝试调用 Add 函数):
#include <stdio.h>
extern int Add(int x, int y);  // 声明外部函数
int main() {
    int sum = Add(10, 20);  // 编译报错:无法解析的外部符号Add
    printf("sum=%d\n", sum);
    return 0;
}

​

  • 用途:隐藏内部函数(仅在当前文件使用),避免函数名冲突(多个文件可定义同名 static 函数)。

3. extern 关键字:声明外部符号

  • 作用:声明 “在其他源文件中定义的全局变量或函数”,告知编译器 “该符号存在,无需报错,链接时会找到”。

  • 示例(多文件调用全局变量):

    1. file1.c(定义全局变量):
​
int g_val = 100;  // 未修饰的全局变量(外部链接属性)

    1. file2.c(调用 g_val):
#include <stdio.h>
extern int g_val;  // 声明外部全局变量(无需定义,仅告知编译器)
int main() {
    printf("%d\n", g_val);  // 输出:100(链接时找到file1.c中的g_val)
    return 0;
}

​

  • 注意:extern仅用于 “声明”,不能用于 “定义”(如extern int g_val = 100;是错误的,会被当作定义,可能导致重复定义错误)。

十、常见问题与注意事项

  1. 函数名冲突:不同文件中的非 static 函数名不能重复(否则链接报错),建议按功能命名(如Add_intAdd_double区分不同类型的加法函数)。
  2. 数组传参遗漏长度:形参无法获取数组真实长度,必须单独传递长度参数,否则循环可能越界。
  3. return 语句缺失:非 void 类型函数的所有分支必须有 return 语句,否则编译报错。
  4. 形参修改实参:形参是临时拷贝,修改形参无法影响实参,需修改实参需用指针传递(后续章节讲解)。
  5. 头文件重复包含:头文件需添加#pragma once#ifndef宏,防止重复包含导致的重复声明错误。