目录

 

引言:理解函数的基本概念

第一部分:库函数——C语言的标准工具

1.1 标准库和头文件

1.2 库函数使用方法:以sqrt为例

1.2.1 sqrt函数功能

1.2.2 必须包含头文件

1.2.3 库函数文档格式

第二部分:自定义函数——创造自己的工具

2.1 函数的语法形式

2.2 函数实例:加法函数

第三部分:形参和实参——理解参数传递

3.1 实参(Actual Parameter)

3.2 形参(Formal Parameter)

3.3 实参和形参的关系

第四部分:return语句——函数的出口

4.1 return语句的注意事项

第五部分:数组作为函数参数

5.1 数组参数传递的基础

5.2 数组传参的重要规则

第六部分:嵌套调用和链式访问

6.1 嵌套调用:函数间的协作

6.2 链式访问:简洁的代码风格

6.3 有趣的链式访问示例

第七部分:函数的声明和定义

7.1 单个文件中的函数声明

7.2 多文件组织

第八部分:static和extern关键字

8.1 作用域和生命周期回顾

8.2 static修饰局部变量

8.3 static修饰全局变量

8.4 static修饰函数

总结:函数的核心要点

1. 函数的基本概念

2. 函数定义和调用

3. 数组作为函数参数

4. 作用域和存储期

5. 工程化实践

6. 编程技巧


 

引言:理解函数的基本概念

函数是C语言程序的基本构建单元,就像乐高积木一样,通过函数的组合可以构建出复杂的程序。在C语言中,函数可以理解为完成特定任务的一小段代码,具有特殊的写法和调用方法。

函数的重要性

  • 模块化:将大任务分解为小函数

  • 复用性:一次编写,多次调用

  • 可维护性:便于调试和修改代码

C语言中主要有两类函数:库函数自定义函数


第一部分:库函数——C语言的标准工具

1.1 标准库和头文件

C语言标准规定了一些常用函数的标准,这些函数的实现由编译器厂商提供,组成了标准库。我们之前学习的 printfscanf 就是库函数。

库函数特点

  • 已经实现好的函数

  • 质量有保证

  • 提升开发效率

  • 需要包含对应的头文件

库函数相关头文件参考:https://zh.cppreference.com/w/c/header

1.2 库函数使用方法:以sqrt为例

学习库函数时,需要了解以下信息:

// sqrt函数原型
double sqrt(double x);
// sqrt 是函数名
// x 是函数的参数,需要传递一个double类型的值
// double 是返回值类型 - 函数计算结果

1.2.1 sqrt函数功能

  • 计算平方根(Compute square root)

  • 返回x的平方根(Returns the square root of x)

1.2.2 必须包含头文件

#include <stdio.h>
#include <math.h>  // sqrt函数需要这个头文件

int main()
{
    double d = 16.0;
    double r = sqrt(d);
    printf("%lf\n", r);  // 输出:4.000000
    return 0;
}

1.2.3 库函数文档格式

  1. 函数原型:函数声明

  2. 函数功能介绍:功能描述

  3. 参数和返回类型说明:详细说明

  4. 代码举例:使用示例

  5. 代码输出:预期结果

  6. 相关知识链接:进一步学习


第二部分:自定义函数——创造自己的工具

  了解了库函数,我们的关注度应该聚焦在自定义函数上,⾃定义函数其实更加重要,也能给程序员写代码更多的创造性。

2.1 函数的语法形式

ret_type fun_name(形式参数)
{
    // 函数体
}

函数组成要素

  • ret_type:函数返回类型

  • fun_name:函数名

  • 形式参数:函数的输入

  • 函数体:实现功能的代码

函数比喻

2.2 函数实例:加法函数

需求:写一个加法函数,完成2个整型变量的加法操作

#include <stdio.h>

// 函数定义
int Add(int x, int y)
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 0;
    int b = 0;
    
    // 输入
    scanf("%d %d", &a, &b);
    
    // 调用加法函数
    int r = Add(a, b);  // a和b是实参
    
    // 输出
    printf("%d\n", r);
    
    return 0;
}

简化版本

int Add(int x, int y)
{
    return x + y;  // 直接返回表达式结果
}

第三部分:形参和实参——理解参数传递

3.1 实参(Actual Parameter)

在上面的Add函数示例中:

int r = Add(a, b);  // 这里的a和b是实参

实参是实际传递给函数的参数。

3.2 形参(Formal Parameter)

int Add(int x, int y)  // 这里的x和y是形参
{
    return x + y;
}

形参是函数定义中的参数,只有当函数被调用时才会创建并分配内存,同时利用完后会直接销毁。

3.3 实参和形参的关系

重要结论:形参是实参的一份临时拷贝

通过调试可以观察到:

#include <stdio.h>
int Add(int x, int y)  // x和y是形参
{
    int z = 0;
    z = x + y;
    return z;
}

int main()
{
    int a = 10;
    int b = 20;
    int r = Add(a, b);  // a和b是实参
    
    // 调试观察:
    // &a 和 &x 的地址不同
    // &b 和 &y 的地址不同
    // 证明它们在不同的内存空间
    
    return 0;
}
关键点:
  • 形参和实参各自是独立的内存空间

  • 函数调用时,实参的值会拷贝给形参

  • 修改形参不会影响实参(值传递)


第四部分:return语句——函数的出口

4.1 return语句的注意事项

  1. 返回数值或表达式

return x + y;  // 先计算表达式,再返回结果
  1. void函数的return

void PrintMessage()
{
    printf("Hello\n");
    return;  // 可以省略
}
  1. 类型转换

int Function()
{
    return 3.14;  // 自动转换为int类型,返回3
}
  1. 函数立即结束

int FindValue(int arr[], int size, int target)
{
    for(int i = 0; i < size; i++)
    {
        if(arr[i] == target)
            return i;  // 找到立即返回,后面代码不执行
    }
    return -1;  // 未找到
}
  1. 分支语句必须有return

int Absolute(int x)
{
    if(x >= 0)
        return x;     // 分支1
    else
        return -x;    // 分支2
    // 所有分支都有return
}

第五部分:数组作为函数参数

5.1 数组参数传递的基础

#include <stdio.h>

// 设置数组所有元素为-1
void set_arr(int arr[], int sz)
{
    int i = 0;
    for(i = 0; i < sz; i++)
    {
        arr[i] = -1;
    }
}

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

int main()
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    
    set_arr(arr, sz);    // 设置数组内容为-1
    print_arr(arr, sz);  // 打印数组内容
    
    return 0;
}

5.2 数组传参的重要规则

  1. 函数形参和实参个数必须匹配

  2. 形参可以写成数组形式

void func(int arr[])  // 可以
void func(int *arr)   // 也可以,等价
  1. 一维数组形参可以省略大小

void func(int arr[])   // 正确
void func(int arr[10]) // 正确,但10会被忽略
  1. 二维数组形参,行可以省略,列不能省略

void func(int arr[][5])   // 正确
void func(int arr[3][5])  // 正确,但3会被忽略
void func(int arr[][])    // 错误!列不能省略
  1. 数组传参的本质

  • 不会创建新的数组

  • 形参操作的数组就是实参的数组

  • 传递的是数组首元素的地址


第六部分:嵌套调用和链式访问

6.1 嵌套调用:函数间的协作

示例:计算某年某月的天数

#include <stdio.h>

// 判断闰年
int is_leap_year(int y)
{
    if(((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
        return 1;
    else
        return 0;
}

// 获取月份天数
int get_days_of_month(int y, int m)
{
    int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    int day = days[m];
    
    if(is_leap_year(y) && m == 2)  // 嵌套调用
        day += 1;
    
    return day;
}

int main()
{
    int y = 0;
    int m = 0;
    scanf("%d %d", &y, &m);
    
    int d = get_days_of_month(y, m);  // 调用函数
    printf("%d\n", d);
    
    return 0;
}

函数调用关系

  • main 调用 scanfprintfget_days_of_month

  • get_days_of_month 调用 is_leap_year

注意:函数可以嵌套调用,但不能嵌套定义

6.2 链式访问:简洁的代码风格

链式访问是将一个函数的返回值作为另一个函数的参数。

传统方式

#include <stdio.h>
#include <string.h>

int main()
{
    int len = strlen("abcdef");  // 1. 获取长度
    printf("%d\n", len);         // 2. 打印长度
    return 0;
}

链式访问

#include <stdio.h>
#include <string.h>

int main()
{
    printf("%d\n", strlen("abcdef"));  // 一步完成
    return 0;
}

6.3 有趣的链式访问示例

#include <stdio.h>

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));
    return 0;
}

输出结果:4321

解析过程

  1. printf("%d", 43) 打印"43",返回2(打印了2个字符)

  2. printf("%d", 2) 打印"2",返回1(打印了1个字符)

  3. printf("%d", 1) 打印"1"

  4. 最终屏幕显示:4321

关键printf函数返回打印在屏幕上的字符个数


第七部分:函数的声明和定义

7.1 单个文件中的函数声明

情况1:函数定义在调用之前(无需声明)

#include <stdio.h>

// 函数定义
int is_leap_year(int y)
{
    if(((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
        return 1;
    else
        return 0;
}

int main()
{
    int y = 0;
    scanf("%d", &y);
    int r = is_leap_year(y);  // 直接调用
    // ...
    return 0;
}

情况2:函数定义在调用之后(需要声明)

#include <stdio.h>

// 函数声明
int is_leap_year(int y);  // 分号结束

int main()
{
    int y = 0;
    scanf("%d", &y);
    int r = is_leap_year(y);  // 调用
    // ...
    return 0;
}

// 函数定义
int is_leap_year(int y)
{
    if(((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
        return 1;
    else
        return 0;
}

函数声明的要点

  • 只需要函数名、返回类型、参数类型

  • 参数名可以省略:int is_leap_year(int);

  • 函数定义也是一种声明

  • 必须遵循"先声明后使用"原则

编译错误示例

这是因为编译器从第一行开始扫描,遇到函数调用时还没有看到函数定义。

7.2 多文件组织

在实际项目中,代码通常分布在多个文件中:

文件结构

text

add.c    - 函数定义
add.h    - 函数声明
test.c   - 主程序

add.h(头文件)

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

add.c(源文件)

// 函数的定义
int Add(int x, int y)
{
    return x + y;
}

test.c(主程序)

#include <stdio.h>
#include "add.h"  // 包含自定义头文件

int main()
{
    int a = 10;
    int b = 20;
    
    // 函数调用
    int c = Add(a, b);
    printf("%d\n", c);  // 输出:30
    
    return 0;
}

包含头文件的区别

  • #include <stdio.h>:系统头文件,用尖括号

  • #include "add.h":自定义头文件,用双引号


第八部分:static和extern关键字

8.1 作用域和生命周期回顾

作用域(scope):名字的可用性范围

  • 局部变量:所在局部范围

  • 全局变量:整个工程

生命周期:变量创建到销毁的时间段

  • 局部变量:进入作用域开始,出作用域结束

  • 全局变量:整个程序的生命周期

8.2 static修饰局部变量

代码1:普通局部变量

#include <stdio.h>
void test()
{
    int i = 0;     // 每次调用都重新创建和初始化
    i++;
    printf("%d ", i);
}

int main()
{
    for(int i = 0; i < 5; i++)
    {
        test();    // 输出:1 1 1 1 1
    }
    return 0;
}

代码2:static局部变量

#include <stdio.h>
void test()
{
    static int i = 0;  // 只初始化一次
    i++;
    printf("%d ", i);
}

int main()
{
    for(int i = 0; i < 5; i++)
    {
        test();    // 输出:1 2 3 4 5
    }
    return 0;
}

static修饰局部变量的特点

  • 改变生命周期,不改变作用域

  • 从栈区存储变为静态区存储

  • 程序结束才销毁

使用场景:函数调用后需要保留值,供下次调用继续使用。

8.3 static修饰全局变量

代码1:普通全局变量

// add.c
int g_val = 2018;  // 定义全局变量

// test.c
#include <stdio.h>
extern int g_val;  // 声明外部全局变量

int main()
{
    printf("%d\n", g_val);  // 输出:2018
    return 0;
}

代码2:static全局变量

// add.c
static int g_val = 2018;  // static修饰

// test.c
#include <stdio.h>
extern int g_val;  // 声明

int main()
{
    printf("%d\n", g_val);  // 链接错误!
    return 0;
}

结论

  • static修饰全局变量,使其只能在本源文件内使用

  • 默认全局变量具有外部链接属性

  • static使其变为内部链接属性

使用建议:如果全局变量只想在所在源文件内部使用,用static修饰。

8.4 static修饰函数

代码1:普通函数

// add.c
int Add(int x, int y)
{
    return x + y;
}

// test.c
#include <stdio.h>
extern int Add(int x, int y);  // 声明外部函数

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

代码2:static函数

// add.c
static int Add(int x, int y)  // static修饰
{
    return x + y;
}

// test.c
#include <stdio.h>
extern int Add(int x, int y);  // 声明

int main()
{
    printf("%d\n", Add(2, 3));  // 链接错误!
    return 0;
}

结论

  • static修饰函数与修饰全局变量类似

  • 使函数只能在本文件内部使用

  • 函数默认具有外部链接属性,static使其变为内部链接属性

使用建议:如果函数只想在所在的源文件内部使用,用static修饰。


总结:函数的核心要点

1. 函数的基本概念

  • 函数是完成特定任务的代码块

  • 分为库函数和自定义函数

2. 函数定义和调用

  • 语法:ret_type fun_name(参数) { 函数体 }

  • 实参传递给形参是值传递

  • 形参是实参的临时拷贝

3. 数组作为函数参数

  • 传递的是数组首元素地址

  • 形参数组和实参数组是同一个数组

  • 需要传递数组大小参数

4. 作用域和存储期

  • static修饰局部变量:改变生命周期

  • static修饰全局变量/函数:限制作用域

  • extern声明外部符号

5. 工程化实践

  • 多文件组织:.h声明,.c定义

  • 函数声明先于使用

  • 合理使用static实现模块封装

6. 编程技巧

  • 嵌套调用构建复杂逻辑

  • 链式访问简化代码

  • 合理设计函数参数和返回值

函数是C语言模块化编程的基础,掌握函数的使用和设计原则,是成为合格程序员的关键一步。从简单的加法函数到复杂的多文件项目组织,函数贯穿了整个C语言编程的实践过程。

 

Logo

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

更多推荐