c语言-函数讲解

一、函数的本质:什么是 C 语言中的函数?

数学中的函数我们并不陌生,比如一次函数y=kx+b,给定 x 就能得到对应的 y 值。C 语言中的函数(也称为子程序)延续了这一核心思想 ------一段完成特定任务的独立代码块,通过接收输入(参数)、执行逻辑、返回结果(可选),实现功能的封装与复用。

C 语言程序的本质的是函数的组合:一个复杂的任务可以拆解为多个小型函数,每个函数负责单一功能,最终通过函数间的配合完成整体需求。这种设计的优势在于:

  • 代码复用:避免重复编写相同逻辑,提升开发效率;
  • 结构清晰:每个函数职责明确,便于维护和调试;
  • 模块化开发:支持多人协作,各自负责不同函数模块。

C 语言中的函数主要分为两类:

  • 库函数:编译器厂商根据 ANSI C 标准实现的现成函数(如printfscanf),直接调用即可;
  • 自定义函数:开发者根据需求编写的函数,灵活性更高,是编程的核心重点。

二、库函数:站在巨人的肩膀上编程

库函数是 C 语言标准库提供的现成工具,覆盖了数学计算、字符串处理、输入输出等常见场景。使用库函数无需关心底层实现,只需掌握调用方法,就能大幅提升开发效率。

2.1 库函数的使用前提

库函数的声明都包含在对应的头文件中,因此使用前必须通过#include引入头文件,否则可能导致编译错误。例如:

  • 输入输出函数(printfscanf)需要引入<stdio.h>
  • 数学函数(sqrtpow)需要引入<math.h>
  • 字符串函数(strlenstrcpy)需要引入<string.h>

2.2 库函数的调用示例:计算平方根

sqrt函数(计算平方根)为例,讲解库函数的完整使用流程:

  1. 查看函数文档:明确函数原型、参数、返回值和头文件;
    • 函数原型:double sqrt(double x)
    • 功能:返回 x 的平方根(x 需为非负数);
    • 头文件:<math.h>
  2. 编写代码:
cpp 复制代码
#include <stdio.h>
#include <math.h>

int main() {
    double d = 16.0;
    double result = sqrt(d);  // 调用sqrt函数
    printf("平方根:%lf\n", result);  // 输出结果:4.000000
    return 0;
}

2.3 库函数的学习工具

无需死记硬背所有库函数,学会使用以下工具即可按需查询:

三、自定义函数:打造专属功能模块

库函数虽方便,但无法满足所有个性化需求,自定义函数才是 C 语言编程的核心。自定义函数的设计自由度极高,可根据实际需求灵活定义功能、参数和返回值。

3.1 自定义函数的语法结构

自定义函数的语法与库函数一致,核心由 4 部分组成:

cpp 复制代码
ret_type fun_name(parameter_list) {
    // 函数体:实现功能的代码逻辑
}
  • ret_type:返回值类型,指定函数执行后的返回结果类型(如intdouble);若无需返回值,用void表示;
  • fun_name:函数名,需遵循标识符命名规则,建议根据功能命名(如Add表示加法,isLeapYear表示判断闰年);
  • parameter_list:参数列表,即函数的输入(原材料),可无参数(写void)或多个参数(需指定类型和名称);
  • 函数体:用{}包裹的代码块,是函数功能的核心实现。

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

需求:编写一个函数,接收两个整型参数,返回它们的和。

cpp 复制代码
#include <stdio.h>

// 自定义加法函数
int Add(int x, int y) {
    return x + y;  // 直接返回表达式结果,简化代码
}

int main() {
    int a = 0, b = 0;
    printf("请输入两个整数:");
    scanf("%d %d", &a, &b);
    
    int sum = Add(a, b);  // 调用自定义函数
    printf("和为:%d\n", sum);
    
    return 0;
}

四、形参和实参:函数的输入传递机制

在函数调用过程中,参数分为实际参数(实参)形式参数(形参),两者既相关又独立。

在函数调用过程中,参数分为实际参数(实参)形式参数(形参),两者既相关又独立。

4.1 概念区分

  • 实参:调用函数时传递的具体值(如上述代码中Add(a, b)ab),是真实存在的数据,占用内存空间;
  • 形参:函数定义时括号中的参数(如Add(int x, int y)xy),仅在函数被调用时才分配内存,用于接收实参的值,函数执行结束后立即释放。

4.2 核心关系:形参是实参的临时拷贝

关键结论:形参和实参占用不同的内存空间,形参只是实参的一份临时拷贝

这意味着:函数内部修改形参的值,不会影响实参。例如:

cpp 复制代码
#include <stdio.h>

void Swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;  交换的是形参x和y的值
}

int main() {
    int a = 3, b = 5;
    printf("交换前:a=%d, b=%d\n", a, b);  // 输出:3 5
    Swap(a, b);
    printf("交换后:a=%d, b=%d\n", a, b);  // 输出:3 5(实参未变)
    return 0;
}

五、return 语句:函数的返回机制

return语句是函数返回结果的核心,其使用有明确的规则和注意事项:

  1. 返回值与函数类型匹配:return后的值类型需与函数声明的ret_type一致,若不一致,系统会自动隐式转换(可能导致精度丢失或错误);
  2. 终止函数执行:return语句执行后,函数立即结束,后续代码不再执行
cpp 复制代码
int Add(int x, int y) {
    return x + y;
    printf("这行代码不会执行");  //  unreachable code
}

六、数组做函数参数:批量数据的传递

当需要向函数传递批量数据时,数组是常用选择,但数组传参有特殊规则,需重点掌握。

6.1 数组传参的核心规则

  • 形参可写为数组形式(int arr[]),也可写为指针形式(int* arr),本质是传递数组首元素的地址;
  • 一维数组形参的大小可省略(如int arr[]),二维数组形参的行可省略,但列不能省略(如int arr[][3]);
  • 函数内部操作的是原数组:数组传参时,形参并未创建新数组,而是直接操作实参数组的内存空间,修改形参数组会影响原数组。

6.2 示例:数组初始化与打印

需求:编写两个函数,分别将数组所有元素置为 - 1,以及打印数组内容。

cpp 复制代码
#include <stdio.h>

// 将数组所有元素置为-1
void SetArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        arr[i] = -1;  // 操作原数组
    }
}

// 打印数组
void PrintArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);  // 计算数组长度
    
    printf("修改前:");
    PrintArray(arr, size);  // 输出:1 2 3 4 5
    
    SetArray(arr, size);
    
    printf("修改后:");
    PrintArray(arr, size);  // 输出:-1 -1 -1 -1 -1
    
    return 0;
}

七、函数的调用技巧:嵌套调用与链式访问

函数的强大之处不仅在于独立功能的实现,更在于函数间的灵活配合,主要体现为嵌套调用和链式访问。

7.1 嵌套调用:函数间的层级协作

嵌套调用指一个函数内部调用另一个函数,如同乐高积木的拼接,是构建复杂程序的核心方式。

示例:计算某年某月的天数(需判断是否为闰年)

cpp 复制代码
#include <stdio.h>

// 判断是否为闰年:是返回1,否返回0
int IsLeapYear(int year) {
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

// 计算某月天数:调用IsLeapYear函数
int GetDays(int year, int month) {
    int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = days[month];
    if (IsLeapYear(year) && month == 2) {
        day += 1;  // 闰年2月多1天
    }
    return day;
}

int main() {
    int year = 0, month = 0;
    printf("请输入年和月(如2024 2):");
    scanf("%d %d", &year, &month);
    
    int result = GetDays(year, month);
    printf("%d年%d月有%d天\n", year, month, result);
    
    return 0;
}

7.2 链式访问:函数返回值的连续使用

链式访问指将一个函数的返回值作为另一个函数的参数,如同链条一样串联函数调用。

示例 2:多层链式访问(趣味题)

cpp 复制代码
#include <stdio.h>

int main() {
    // printf返回值为打印的字符个数
    printf("%d", printf("%d", printf("%d", 43)));  // 输出:4321
    return 0;
}

解析:

  • 最内层printf("%d", 43):打印43(2 个字符),返回 2;
  • 中间层printf("%d", 2):打印2(1 个字符),返回 1;
  • 最外层printf("%d", 1):打印1,最终输出4321

八、函数的声明与定义:多文件编程基础

在实际开发中,代码量较大时需拆分到多个文件,这就需要理解函数的声明与定义的区别,以及staticextern关键字的作用。

8.1 声明与定义的区别

  • 函数定义:完整的函数实现(包含函数体),用于明确函数的功能逻辑,一个函数只能定义一次;
  • 函数声明:告诉编译器函数的名称、返回类型和参数列表(无需函数体),用于让编译器识别函数,可声明多次。

8.2 单文件场景:声明的作用

若函数定义在调用之后,需先声明函数,否则编译器会报错。例如:

cpp 复制代码
#include <stdio.h>

// 函数声明(参数名可省略,仅需类型)
int IsLeapYear(int);

int main() {
    int year = 2024;
    if (IsLeapYear(year)) {  // 调用前已声明,编译器可识别
        printf("%d是闰年\n", year);
    }
    return 0;
}

// 函数定义(在调用之后)
int IsLeapYear(int year) {
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

8.3 多文件场景:头文件与源文件分离

企业开发中,通常将函数声明放在.h头文件中,函数实现放在.c源文件中,结构如下:

  • add.h(头文件):函数声明;
  • add.c(源文件):函数定义;
  • test.c(主文件):调用函数。

示例:

  1. add.h(声明)
cpp 复制代码
#ifndef ADD_H
#define ADD_H

int Add(int x, int y);  // 函数声明

#endif

add.c(定义):

cpp 复制代码
#include "add.h"

int Add(int x, int y) {
    return x + y;  // 函数实现
}

test.c(调用):

cpp 复制代码
#include <stdio.h>
#include "add.h"  // 包含头文件,获取函数声明

int main() {
    int sum = Add(3, 5);
    printf("sum = %d\n", sum);  // 输出:8
    return 0;
}

8.4 static 与 extern:控制函数 / 变量的作用域

  • extern:用于声明外部符号(函数或全局变量),允许在当前文件中使用其他文件定义的符号;
  • static:用于限制符号的作用域,使其仅在当前文件中可用。
(1)static 修饰局部变量

改变局部变量的生命周期:局部变量默认存储在栈区,函数执行结束后销毁;被static修饰后存储在静态区,生命周期与程序一致(程序结束后才销毁),但作用域仍为函数内部。

示例:

cpp 复制代码
#include <stdio.h>

void Test() {
    static int i = 0;  // static修饰局部变量
    i++;
    printf("%d ", i);
}

int main() {
    for (int j = 0; j < 5; j++) {
        Test();  // 输出:1 2 3 4 5(i的值累加)
    }
    return 0;
}
(2)static 修饰全局变量 / 函数

限制作用域:全局变量和函数默认具有外部链接属性(可被其他文件使用),被static修饰后变为内部链接属性(仅当前文件可用)。

示例:若add.c中定义static int g_val = 10,则test.c中无法通过extern int g_val使用该变量;同理,static int Add(...)定义的函数,也无法被其他文件调用。

九、总结

函数是 C 语言的核心模块化工具,掌握以下关键点,就能灵活运用函数编写高效、清晰的代码:

  1. 理解函数的本质:封装功能、实现复用;
  2. 库函数:学会查询文档,按需引入头文件;
  3. 自定义函数:掌握语法结构,根据需求设计参数和返回值;
  4. 形参实参:明确 "临时拷贝" 特性,避免踩坑;
  5. 数组传参:本质是传递首元素地址,需手动传递数组长度;
  6. 调用技巧:嵌套调用实现复杂逻辑,链式访问简化代码;
  7. 声明与定义:支持多文件编程,staticextern控制作用域。
相关推荐
癫狂的兔子2 小时前
【BUG】【Python】【Spider】Compound class names are not allowed.
开发语言·python·bug
css趣多多2 小时前
动态路由,路由重置,常量路由,$ref,表单验证流程
开发语言·javascript·ecmascript
秋深枫叶红2 小时前
嵌入式C语言阶段复习——循环语句和分支语句
c语言·开发语言
还在忙碌的吴小二2 小时前
Go-View 数据可视化大屏使用手册
开发语言·后端·信息可视化·golang
u0109272712 小时前
C++中的模板方法模式
开发语言·c++·算法
lly2024062 小时前
C++ 测验
开发语言
山上三树2 小时前
详细介绍读写锁
开发语言·c++·spring
jghhh012 小时前
基于MATLAB的协同过滤推荐算法实现
开发语言·matlab·推荐算法
比特森林探险记2 小时前
后端开发者快速入门react
开发语言·前端·javascript