云计算百科
云计算领域专业知识百科平台

C语言从入门到进阶——第6讲:函数

文章目录

    • 1. 函数的概念
    • 2. 库函数
      • 2.1 标准库、库函数、头文件
      • 2.2 库函数的使用方法
        • 2.2.1 函数原型
        • 2.2.2 功能
        • 2.2.3 头文件包含
        • 2.2.4 实践
        • 2.2.5 库函数文档的一般格式
    • 3. 自定义函数
      • 3.1 函数的语法形式
      • 3.2 函数的举例
    • 4. 形参和实参
      • 4.1 实参
      • 4.2 形参
      • 4.3 实参和形参的关系
    • 5. return语句
    • 6. 数组做函数参数
    • 7. 嵌套调用和链式访问
      • 7.1 嵌套调用
      • 7.2 链式访问
    • 8. 函数的声明和定义
      • 8.1 单个文件
      • 8.2 多个文件
      • 8.3 static 和 extern
        • 8.3.1 作用域和生命周期
        • 8.3.2 static 和 extern 的用途
        • 8.3.3 static 修饰局部变量
        • 8.3.4 static 修饰全局变量
        • 8.3.5 static 修饰函数

1. 函数的概念

数学中我们见过函数(如一次函数 y=kx+b),给任意 x 可得到对应 y 值。

C语言中的函数(又称子程序),是完成某项特定任务的小段代码,有特殊写法和调用规则。 C语言程序由无数个函数组合而成,即一个大的计算任务可分解为若干个小函数完成,函数的复用性能提升了软件的开发效率。

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

  • 库函数
  • 自定义函数

2. 库函数

2.1 标准库、库函数、头文件

C语言标准仅规定语法规则,不提供库函数。

国际标准ANSI C定义了常用函数标准,即标准库。

编译器厂商根据标准库实现的函数称为库函数。我们之前学习的printf、scanf均为库函数,库函数无需程序员自行实现,直接学习使用即可,其质量和执行效率更有保障。

库函数按功能划分,声明在不同头文件中。

库函数相关头文件参考:https://zh.cppreference.com/w/c/header,涵盖数学、字符串、日期等各类功能,无需一次性全部掌握,可逐步学习。

2.2 库函数的使用方法

库函数的学习和查看工具丰富,常用的有:

  • C/C++官方链接:https://zh.cppreference.com/w/c/header
  • 中文版:https://cppreference.cn/w/
  • cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/

以sqrt函数为例,详细说明库函数的使用:

2.2.1 函数原型

double sqrt (double x);

  • sqrt:函数名
  • x:函数参数,需传递一个double类型的值
  • double:返回值类型,函数计算结果为double类型
2.2.2 功能

计算平方根,返回参数x的平方根。

2.2.3 头文件包含

库函数需包含对应头文件才能使用,sqrt函数的头文件为<math.h>。

2.2.4 实践

#include <stdio.h>
#include <math.h>

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

运行结果:

4.000000

2.2.5 库函数文档的一般格式
  • 函数原型
  • 函数功能介绍
  • 参数和返回类型说明
  • 代码举例
  • 代码输出
  • 相关知识链接
  • 3. 自定义函数

    自定义函数能给程序员更多创造性,是编程中更为重要的部分。

    3.1 函数的语法形式

    自定义函数与库函数形式一致,语法如下:

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

    • ret_type:函数返回类型,若无需返回值则为void
    • fun_name:函数名,需根据功能起有意义的名称
    • 形式参数:可以无参数(void),有参数需说明参数个数、每个参数的类型、形参的名字
    • 函数体:用{}括起,是实现函数功能的核心代码

    可将函数类比为小型加工厂:输入原材料(形式参数),经加工(函数体代码),产出产品(返回值)。 在这里插入图片描述

    3.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);
    // 输出结果
    printf("%d\\n", r);
    return 0;
    }

    简化版本

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

    • 函数的参数部分要交代清楚:参数个数、每个参数的类型、形参的名字。
    • 函数的参数、返回类型、函数名可根据实际需求灵活设计。

    4. 形参和实参

    在函数使用过程中,参数分为实际参数(实参)和形式参数(形参)。

    4.1 实参

    调用函数时传递给函数的参数称为实参(实际参数)。例如上述加法函数中,main函数里调用Add(a, b)时,a和b就是实参,是真实传递给函数的值。

    4.2 形参

    定义函数时,函数名后括号中的参数称为形参(形式参数)。例如加法函数Add(int x, int y)中的x和y,仅在函数被调用时才会申请内存空间(即形参实例化),用于存放实参传递的值;若函数未被调用,形参不会真实存在。

    4.3 实参和形参的关系

    实参是传递给形参的值,但二者是独立的内存空间。 在这里插入图片描述 通过调试该代码可以看到,形参x和y确实得到了实参a和b的值,但是x和y的地址和a和b的地址是不同的,即形参是实参的一份临时拷贝。

    5. return语句

    return语句是函数中重要的关键字,使用注意事项如下:

  • return后可跟数值或表达式,若为表达式则先执行表达式,再返回结果。
  • return后可无内容(直接写return;),适用于返回类型为void的函数。
  • return语句执行后,函数立即返回,后续代码不再执行。
  • 返回值与函数返回类型不一致时,系统会自动隐式转换为函数返回类型。
  • 若函数包含if等分支语句,需保证每种分支情况都有return返回,否则会编译错误。
  • 函数未写返回类型时,编译器默认返回类型为int。
  • 函数声明了返回类型,但未使用return返回值,其返回值是未知的。
  • 6. 数组做函数参数

    当在函数中需要操作数组时,要将数组作为参数传递给函数。

    数组传参的重点:

  • 形参个数需与实参匹配;
  • 实参为数组时,形参可写成数组形式;
  • 一维数组形参的大小可省略;
  • 二维数组形参的行可省略,但列不能省略;
  • 数组传参时,形参不会创建新数组,形参操作的是实参对应的原数组。
  • 示例:写一个函数将一个整型数组的内容全部置为-1,再写一个函数打印数组的内容。

    #include <stdio.h>

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

    // 函数print_arr:打印数组内容
    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);
    print_arr(arr, sz);
    return 0;
    }

    7. 嵌套调用和链式访问

    7.1 嵌套调用

    函数之间的互相调用称为嵌套调用,就像乐高零件配合搭建出复杂模型,函数通过嵌套调用能实现大型程序的功能。

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

    #include <stdio.h>

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

    // 函数get_days_of_month:计算某月的天数
    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];
    // 嵌套调用is_leap_year函数
    if (is_leap_year(y) && m == 2)
    day += 1; // 闰年2月加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;
    }

    注意:函数不能嵌套定义,只能嵌套调用。

    7.2 链式访问

    将一个函数的返回值作为另一个函数的参数,像链条一样串起函数,称为链式访问。

    示例1:简化字符串长度打印

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

    int main()
    {
    // 链式访问:strlen的返回值作为printf的参数
    printf("%zu\\n", strlen("abcdef"));
    return 0;
    }

    示例2:多层printf链式调用

    #include <stdio.h>

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

  • printf函数的返回值是打印在屏幕上的字符个数。
  • 最内层printf("%d", 43):打印"43"(2个字符),返回2;
  • 中层printf("%d", 2):打印"2"(1个字符),返回1;
  • 最外层printf("%d", 1):打印"1",最终输出"4321"。
  • 8. 函数的声明和定义

    8.1 单个文件

    一般我们在使用函数的时候,将函数定义写在函数调用之前。 示例:写一个函数判断一年是否是闰年。

    #include <stido.h>

    // 函数定义
    // 函数is_leap_year:判断一年是不是闰年
    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);
    if(r == 1)
    printf("闰年\\n");
    else
    printf("非闰年\\n");
    return 0;
    }

    这种场景下编译没有出现问题。

    那如果我们希望将函数定义放在函数调用后,如下:

    #include <stdio.h>

    int main()
    {
    int y = 0;
    scanf("%d", &y);
    // 函数调用
    int r = is_leap_year(y);
    if(r == 1)
    printf("闰年\\n");
    else
    printf("非闰年\\n");
    return 0;
    }

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

    代码在VS2022上编译,会出现下面的警告信息。 在这里插入图片描述 这是因为C语言编译器对源代码进行编译的时候,从第1行往下扫描的,当遇到第8行is_leap_year函数调用的时候,并没有发现前面有is_leap_year的函数定义。

    解决方法:在调用函数之前声明函数。函数声明需交代清楚返回类型、函数名和参数类型,语法为ret_type fun_name(参数类型 参数名),参数名可省略。

    #include <stdio.h>

    // 函数声明
    int is_leap_year(int y); //参数名y可省略:int is_leap_year(int)

    int main()
    {
    int y = 0;
    scanf("%d", &y);
    // 函数调用
    int r = is_leap_year(y);
    if(r == 1)
    printf("闰年\\n");
    else
    printf("非闰年\\n");
    return 0;
    }

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

    规则:函数调用需满足“先声明后使用”,函数定义本身也是一种特殊声明,因此定义在调用之前可无需额外声明。

    8.2 多个文件

    企业开发中,代码常按功能拆分到多个文件,一般规则如下:

    • 头文件(.h):存放函数声明、类型声明;
    • 源文件(.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);
    return 0;
    }

    运行结果: 在这里插入图片描述

    8.3 static 和 extern

    8.3.1 作用域和生命周期

    作用域:变量/函数的可用代码范围。

  • 局部变量的作用域是变量所在的局部范围。
  • 全局变量的作用域是整个工程(项目)。
  • 生命周期:变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。

  • 局部变量的生命周期是:进入作用域变量创建→生命周期开始,出作用域→生命周期结束。
  • 全局变量的生命周期是:整个程序的生命周期。
  • 8.3.2 static 和 extern 的用途

    static和extern都是C语言中的关键字。

    static 可以用来:

    • 修饰局部变量
    • 修饰全局变量
    • 修饰函数。

    extern 是用来声明外部符号的,如果一个全局的符号在A文件中定义的,在B文件中想使用,就可以使用 extern 进行声明,然后使用。

    8.3.3 static 修饰局部变量

    对比代码1和代码2,理解 static 修饰局部变量的意义。

    代码1(无static):

    #include <stdio.h>
    void test()
    {
    int i = 0;
    i++;
    printf("%d ", i);
    }
    int main()
    {
    int i = 0;
    for(i=0; i<5; i++)
    {
    test();
    }
    return 0;
    }

    输出结果:

    1 1 1 1 1

    代码2(有static):

    #include <stdio.h>
    void test()
    {
    // static修饰局部变量
    static int i = 0;
    i++;
    printf("%d ", i);
    }
    int main()
    {
    int i = 0;
    for(i=0; i<5; i++)
    {
    test();
    }
    return 0;
    }

    输出结果:

    1 2 3 4 5

    代码1test函数中的局部变量i是每次进入test函数会创建变量(生命周期开始,申请内存)并赋值为0,然后++,再打印,出函数的时候销毁变量(生命周期结束,收回内存)。 代码2test函数中的i创建好后,被static修饰后存储在静态区,生命周期与程序一致,出函数的时候是不会销毁的,重新进入函数也就不会重新创建变量,直接上次累积的数值继续计算。 结论:static修饰局部变量改变了变量的生命周期(从“局部作用域”变为“程序运行期间”),本质是改变了变量的存储类型(从内存的“栈区”存储到了“静态区”),存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样了,只有程序结束,变量才销毁,内存才回收。但作用域不变的。 在这里插入图片描述 使用建议:若变量出函数后需保留值,下次进入函数继续使用,可使用static修饰。

    8.3.4 static 修饰全局变量

    对比代码1和代码2,理解 static 修饰全局变量的意义。

    代码1(无static):

    add.c

    int g_val = 20; // 定义全局变量

    test.c

    #include <stdio.h>
    extern int g_val; // 声明外部全局变量
    int main()
    {
    printf("%d\\n", g_val);
    return 0;
    }

    输出结果:

    20

    代码2(有static):

    add.c

    static int g_val = 20; // static修饰全局变量

    test.c

    #include <stdio.h>
    extern int g_val; // 声明外部全局变量
    int main()
    {
    printf("%d\\n", g_val);
    return 0;
    }

    编译结果: 在这里插入图片描述

    代码1正常运行,全局变量默认具有“外部链接属性”,只要适当声明就可跨文件使用。 代码2编译报错,因为全局变量变为了“内部链接属性”,仅文件内部可访问。 结论:static修饰全局变量改变了变量的访问权限(从“可跨文件可访问”变为“仅文件内部可访问”),本质是改变了变量的连接属性(从“外部链接属性”变为“内部链接属性”),变量变为 “内部链接属性”就只能在自己所在的源文件内部使用了,其他源文件即使声明了也无法正常使用。

    使用建议:如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用 static 修饰。

    8.3.5 static 修饰函数

    对比代码1和代码2,理解 static 修饰函数的意义。 代码1(无static): 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));
    return 0;
    }

    输出结果:

    5

    代码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;
    }

    编译结果: 在这里插入图片描述

    代码1正常运行,函数默认具有“外部链接属性”,只要适当声明就可跨文件使用。 代码2编译报错,因为函数 变为了“内部链接属性”,仅文件内部可访问。 结论:static修饰函数后,其“外部链接属性”变为“内部链接属性”,只能在本源文件内使用,其他文件无法调用。 使用建议:若函数仅需在所在源文件内部使用,不想被其他文件调用,可使用static修饰。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C语言从入门到进阶——第6讲:函数
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!