八:C语言-函数的基本概念

八:函数的基本概念

1.库函数与自定义函数

C语言中的函数就是一个完成某项特定的任务的一小段代码(子程序),而这段代码是有特殊的写法和调用方法

的。C语言的程序是由无数个小的函数组合而成的。同时,一个函数如果能完成某项特定任务的话,这个函数也是

可以复用的,大大提升了开发软件的效率。

而在C语言中一般会见到两类函数:

(1)库函数:

1.标准库和头文件:

在C语⾔标准中规定了C语⾔的各种语法规则,C语⾔并不提供库函数;但C语⾔的国际标准ANSIC规定了⼀些常用

的函数标准,被称为标准库。不同的编译器⼚商(比如微软;苹果等)便根据ANSIC提供的C语⾔标准给出了⼀系

列函数的实现。这些函数就被称为库函数。

在前⾯内容中学到的 printfscanf 等就是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要

学会就能直接使⽤。有了库函数,⼀些常见的功能就不需要程序员自己实现了,⼀定程度上提升了效率;同时库函

数的质量和执行效率上都更有保证。各种编译器的标准库中提供了⼀系列的库函数,这些库函数根据功能的划分,

都在不同的头文件中进行了声明。

注意:

C语言并不是去实现标准库,只是规定了这个标准。标准库是由不同的编译器厂商在编译器中去提供这些函数的具

体实现。正因为如此就出现了一个问题:函数的使用和功能是一样的,但是函数的具体的实现可能有所差异。

2.库函数的使用方法:

库函数的学习和查看⼯具很多,比如:

C/C++官⽅的链接:https://zh.cppreference.com/w/c/header

cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/

示例:看上面的文档然后使用sqrt()开方求值

sqrt()语法格式:

double sqrt (double x);

//sqrt:指的是函数名
//x:是函数的参数,表示调用sqrt()函数需要传递一个double类型的值
//double:是返回值的类型,表示函数计算的结果是double类型的值

代码实现:使用sqrt()将16.0开方

#include <math.h>
#include <stdio.h>
int main()
{
    double a = sqrt(16.0);
    printf("%lf\n",a);
    return 0;
}
(2)自定义函数:

**自定义函数的语法格式:**自己创造的函数

ret_type fun_name(形式参数) //函数头
{
    //函数体
}
  • ret_type:用来表示函数计算结果的类型,有时候返回值类型可以是void(表示什么都不返回)。
  • fun_name:函数名,自己定义(尽量根据函数的功能起的有意义)。
  • 形式参数:函数的参数也可以是void(明确表示函数没有参数);如果有参数,要交代清楚参数的类型和名字,以及参数个数。
  • {}:括起来的部分被称为函数体,函数体就是用来完成函数核心作用的。

注意:

函数的返回值类型只有两种:

  1. void:表示什么都不返回。
  2. 其它类型:intshortchar…等,想定义什么类型,就返回该类型的值就可以了。

示例:写一个加法函数,输入2个整型数据,并进行加法操作

#include <stdio.h>
int Add(int x,int y) //x和y只是形式上的参数,简称形参
{
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d",&a,&b);
    int c = Add(a,b); //a和b是实际参数,简称实参,是真实传递给函数的参数
    printf("%d\n",c);
    return 0;
}

注意:

如果只是定义了Add函数,而不去调用的话,Add函数的参数x和y只是形式上存在的,不会向内存申请空间,也就不是真实存在的,只是简单放在那里的装饰,没有实际的应用,所以叫做形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值从而向内存申请空间,这个过程就是形参的实例化

2.return语句

return语句使用的注意事项:

  • return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
  • return后边也可以什么都没有,直接写return; 这种写法适合函数返回类型是void的情况。
  • return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
  • return语句执行后,函数就彻底返回,后边的代码不再执⾏。
  • 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
3.数组做函数参数

示例:写一个函数将一个整型数组的内容全部置为-1,再写一个函数打印数组的内容

#include <stdio.h>
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]);
    }
}
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    //写一个函数将一个整型数组的内容全部置为-1
    set_arr(arr,sz); //数组传参传的是数组名
    //再写一个函数打印数组的内容
    print_arr(arr,sz);
    return 0;
}

示例:定义一个二维数组,并写一个函数进行打印操作

#include <stdio.h>
void print_arr(int arr[][5],int r,int c)
{
    int i = 0;
    for(i=0;i<r;i++)
    {
        int j = 0;
        for(j=0;j<c;j++)
        {
            printf("%d ",arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
    print_arr(arr,3,5);
    return 0;
}

关于数组传参的一些注意事项:

  • 函数的形参要和函数的实参个数匹配
  • 当函数的实参是数组的时候,形参也可以写成数组形式的
  • 形参如果是一维数组,那么传参的时候一维数组的大小可以省略不写
  • 形参如果是二维数组,行可以省略不写,但列不可以省略
  • 数组传参,形参是不会创建新的数组的
  • 形参操作的数组和实参的数组是同一个数组
  • 形参和实参的名字是可以相同的,因为它们都在不同的内存空间里
4.嵌套调用和链式访问
(1)函数的嵌套调用

嵌套调用就是函数之间的互相调用,也正是因为函数之间有效的互相调用,最后可以写出来一个相对大型的程序

示例:利用嵌套函数计算某年某月有多少天

需求:

  1. 根据年份来判断是否是闰年
  2. 确定是否是闰年后,再根据月来计算这个月的天数
#include <stdio.h>
int is_year(int y)  //判断y是否是闰年,如果是返回1;如果不是返回0
{
    if(y % 4 == 0 && y % 100 != 0 || (y % 400 == 0))
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

int get_month(int y,int m) //获取某年某月的天数
{
    int days[13] = {0, 31,28,31,30,31,30,31,31,30,31,30,31};
    int d = days[m];
    if(is_year(y) && m == 2)
    {
        d+=1;
    }
    return d;
}

int main()
{
    int y = 0; //年
    int m = 0; //月
    scanf("%d %d",&y,&m);
    int d = get_month(y,m);
    printf("%d\n",d);
    return 0;
}

注意: 函数是可以嵌套调用的;但是函数不可以嵌套定义。每一个函数都是平等的,不能把一个函数写在另一个函数的里面。

(2)链式访问

链式访问就是将一个函数的返回值作为另一个函数的参数,像链条一样把函数串起来

示例1:利用strlen()函数求字符串的函数并打印

#include <stdio.h>
#include <string.h>
int main()
{
    size_t len = strlen("abc");
    printf("%zd\n",len);
    return 0;
}

示例2:将示例1用链式访问的方式去写

#include <stdio.h>
#include <string.h>
int main()
{
    printf("%zd\n",strlen("abc")); //把strlen()函数的返回值作为printf()函数的参数,像链条一样将函数串起来,这种写法就是链式访问
    return 0;
}
5.函数的声明和定义
(1)声明,定义及调用

示例1:判断某一年是否是闰年

#include <stdio.h>

//函数的定义
int is_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);
    if (is_year(y)) //对函数的调用
    {
        printf("%d 是闰年\n",y);
    }
    else
    {
        printf("%d 不是闰年\n",y);
    }
    return 0;
}

示例2:判断某一年是否是闰年

#include <stdio.h>

int main()
{
    int y = 0; 
    scanf("%d",&y);
    if (is_year(y)) //对函数的调用
    {
        printf("%d 是闰年\n",y);
    }
    else
    {
        printf("%d 不是闰年\n",y);
    }
    return 0;
}

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

示例3:判断某一年是否是闰年

#include <stdio.h>

//函数的声明
int is_year(int y);
    
int main()
{
    int y = 0; 
    scanf("%d",&y);
    if (is_year(y)) //对函数的调用
    {
        printf("%d 是闰年\n",y);
    }
    else
    {
        printf("%d 不是闰年\n",y);
    }
    return 0;
}

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

注意:

运行完上面的3个示例,会发现一个问题,在运行第2个示例的时候出现了一个警告(函数未定义),这是因为将函数的定义放在了函数的调用的后面。

因为编译器是从上往下运行的,编译器在扫描代码的时候在int()函数中会出现找不到没有见过is_year()这个函数的情况,这个时候就会出现一个警告 – 函数未定义,因为在int()函数前没有见过is_year()这个函数(参考示例2)。所以为了不出现这个警告,函数的调用必须要满足先定义后调用这个原则(参考示例1)。如果真的想把函数的定义放在后面,也可以在前面进行一个函数的声明,这样的话也不会出现这个警告(参考示例3)

因为函数的定义也是一种特殊的声明,所以如果把函数的定义放在调用之前也是不会出现警告的

(2)多文件编写

一般在企业中写代码的时候,代码的数量往往会比较多。所以是不会将所有的代码都放在一个文件中的。一般都会根据程序的功能,将代码拆分放在多个文件中(函数的声明;类型的声明放在头文件(xxx.h)中,函数的实现是放在源文件(xxx.c)中

示例:利用多文件完成两个数相加的操作

// Add.c
// 函数的实现
int Add(int x,int y)
{
    return (x + y);
}
// Add.h
// 函数的声明
int Add(int x,int y);
// 主函数所在的文件
#include <stdio.h>
#include "Add.h" //注意:在包含我们自己创建的头文件的时候要用"",而在包含库里面的函数的时候要用<>
int main()
{
    int a = 0;
    int b = 0;
    scanf("%d %d",&a,&b);
    int c = Add(a,b);
    printf("%d\n",c);
    return 0;
}

注意:

  • 在写头文件和源文件的时候,最好保证名字相同
  • 一个头文件中是可以包含多个函数的声明的
  • 在包含我们自己创建的头文件的时候要用英文状态下的双引号来包裹文件名
  • 把一个文件分成多文件编写有助于代码的隐藏

扩展:静态库(使用VS编译器隐藏代码)

  • 鼠标右键点击你的项目文件名(注意:这里点击的是包含了你的头文件和源文件的项目文件,不是单个的.c或者.h文件) --> 点击属性 --> 选择配置类型 --> 选择静态库(.lib) --> 点击应用
  • 待运行结束后,找到你的项目文件路径点击x64文件 --> 点击Debug文件然后找到以.lib为结尾的文件
  • 这个文件里放置的是二进制的代码,也就是说这个文件是加密的
  • 想要运行这个.lib文件,需要把这个文件所对应的头文件(xxx.h)添加到项目中来,然后在你主函数所在的文件头部输入下面这行代码即可使用(这行代码相当于导入了静态库,也就可以使用这个静态库了)这样做是为了不泄露自己的源代码
#pragma comment(lib,"xxx.lib") //xxx是.lib文件的文件名
6.staticextern

staticextern都是C语言中的关键字

static可以用来:

  • 修饰全局变量(在大括号外部定义的变量,可以应用于整个工程)
  • 修饰局部变量(在大括号内部定义的变量,只能应用于该括号内)
  • 修饰函数

extern是用来声明外部符号的

(1)作用域和生命周期

作用域: 是程序设计概念,通常来说,一段代码中所用到的名字并不总是有效(可用)的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

简单来说就是一个变量可以在哪个范围使用,而那个范围就是它的作用域

如:

  1. 局部变量的作用域是变量所在的局部范围
  2. 而全局变量的作用域是整个工程(甚至在其它的文件中也可以使用)

示例:在其它的文件中使用全局变量

//test_1.c

int g = 2024;
//test_2.c

extern int g; //extern使用来声明外部符号的
#include <stdio.h>
void ceshi()
{
    printf("在其它函数中使用 %d\n",g);
}
int main()
{
    printf("在主函数中使用 %d\n",g);
    return 0;
}

生命周期: 指的是变量的创建(申请内存)到变量的销毁(系统收回内存)之间的一个时间段

如:

  1. 局部变量的生命周期是从进入作用域开始,出作用域生命周期结束
  2. 全局变量的生命周期是整个程序的生命周期
(2)用static修饰局部变量

示例1:

#include <stdio.h>

void test()
{
    int a = 0;
    a++;
    printf("%d",a);
}
int main()
{
    int i = 0;
    for(i=0;i<5;i++)
    {
        test();
    }
    return 0;
}

示例2:

#include <stdio.h>

void test()
{
    static int a = 0;
    a++;
    printf("%d",a);
}
int main()
{
    int i = 0;
    for(i=0;i<5;i++)
    {
        test();
    }
    return 0;
}

对比示例1和示例2两段代码会发现:

  • 示例1中的test()函数的局部变量a是每次进入test()函数中都会先创建变量(生命周期的开始)并赋值为0,然后++,再打印,出函数的时候变量会释放内存(生命周期结束)。
  • 而示例2,从输出结果来看,变量a的值有累加的效果,这说明在test()函数中的a创建好后,出函数的时候变量a是不会销毁的,也就是变量a重新进入函数也就不会重新创建变量,接着上次累积的数值继续计算。

结论: static修饰局部变量改变了变量的生命周期,生命周期的改变本质上是改变了变量的存储类型。本来一个局部变量是存储在内存的栈区的,但是被static修饰后存储到了静态区,存储在静态区的变量和全局变量是一样的(生命周期和程序的生命周期一致)只有程序结束,变量才会销毁,内存才会被系统回收。

注意: 变量的作用域是不会发生任何改变的

使用: 当一个变量出了函数以后,还想让它保留值等到下次进入函数后继续使用。就可以使用static修饰

(3)用static修饰全局变量

示例1:

// a1文件.c
int a = 10; //全局变量具有外部链接属性(可以跨文件使用,前提是要进行合理的声明)
//a2文件.c
#include <stdio.h>
extern int a; //声明其它文件中的变量
int main()
{
    printf("%d\n",a);
    return 0;
}

注意: extern是用来声明外部符号的,如果一个全局的符号在A文件中被定义,但在B文件中想要去使用这个符号,就可以使用extern进行声明,然后使用这个符号

示例2:

// b1文件.c
static int b = 20; //全局变量具有外部链接属性
//b2文件.c
#include <stdio.h>
extern int b; //声明其它文件中的变量
int main()
{
    printf("%d\n",a);
    return 0;
}

对比示例1和示例2两段代码会发现:

示例1中的代码正常运行,但示例2在编译代码的时候会出现报错这是因为当static修饰全局变量b后,变量b的外部链接属性就变成了内部链接属性(只能在当前.c文件中使用,其它的.c文件再也没有办法使用这个变量了)

结论: 当一个全局变量被static修饰后,使得这个全局变量只能在当前的.c文件中使用,不能在其它的.c文件中使用。其本质原因是因为全局变量默认是具有外部链接属性的;在外部的文件中想要使用它,只要进行正确的声明就可以使用;但当全局变量被static修饰后,外部链接属性就变成了内部链接属性,使得该全局变量只能在当前.c文件中使用,其它的源文件即使声明了,也是没有办法正常使用的。

使用: 如果一个全局变量,只想在当前.c文件中使用,不想被其它的文件发现,就可以使用static修饰

(4)用static修饰函数

示例1:

//函数.c文件
int Add(int x,int y) //函数默认是具有外部链接属性的
{
    return (x+y);
}
//源.c文件
#include <stdio.h>
extern int Add(int x,int y);
int main()
{
    int a = 10;
    int b = 20;
    int add = Add(a+b);
    printf("%d\n",add);
    return 0;
}

示例2:

//函数s.c文件
static int Add(int x,int y) //函数默认是具有外部链接属性的
{
    return (x+y);
}
//源s.c文件
#include <stdio.h>
extern int Add(int x,int y);
int main()
{
    int a = 10;
    int b = 20;
    int add = Add(a+b);
    printf("%d\n",add);
    return 0;
}

对比示例1和示例2两段代码会发现(结论与全局变量一样,不写了):

示例1中的代码正常运行,但示例2在编译代码的时候出现报错,这是因为当static修饰函数Add后,函数Add的外部链接属性就变成了内部链接属性(只能在当前.c文件中使用,其它的.c文件再也没有办法使用这个变量了)

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温轻舟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值