八:《初学C语言》— 函数的基本概念

简介: 【8月更文挑战第3天】本篇文章详细讲解了库函数与自定义函数的区别、函数的嵌套调用及链式访问、函数的声明和定义、static和extern等基础知识

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.comhttps://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文件再也没有办法使用这个变量了)

目录
相关文章
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
7天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
22 6
|
27天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
34 10
|
20天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。
|
25天前
|
机器学习/深度学习 C语言
【c语言】一篇文章搞懂函数递归
本文详细介绍了函数递归的概念、思想及其限制条件,并通过求阶乘、打印整数每一位和求斐波那契数等实例,展示了递归的应用。递归的核心在于将大问题分解为小问题,但需注意递归可能导致效率低下和栈溢出的问题。文章最后总结了递归的优缺点,提醒读者在实际编程中合理使用递归。
53 7
|
25天前
|
存储 编译器 程序员
【c语言】函数
本文介绍了C语言中函数的基本概念,包括库函数和自定义函数的定义、使用及示例。库函数如`printf`和`scanf`,通过包含相应的头文件即可使用。自定义函数需指定返回类型、函数名、形式参数等。文中还探讨了函数的调用、形参与实参的区别、return语句的用法、函数嵌套调用、链式访问以及static关键字对变量和函数的影响,强调了static如何改变变量的生命周期和作用域,以及函数的可见性。
29 4
|
1月前
|
存储 编译器 C语言
C语言函数的定义与函数的声明的区别
C语言中,函数的定义包含函数的实现,即具体执行的代码块;而函数的声明仅描述函数的名称、返回类型和参数列表,用于告知编译器函数的存在,但不包含实现细节。声明通常放在头文件中,定义则在源文件中。
|
23天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
19 0
|
23天前
|
C语言
【c语言】qsort函数及泛型冒泡排序的模拟实现
本文介绍了C语言中的`qsort`函数及其背后的回调函数概念。`qsort`函数用于对任意类型的数据进行排序,其核心在于通过函数指针调用用户自定义的比较函数。文章还详细讲解了如何实现一个泛型冒泡排序,包括比较函数、交换函数和排序函数的编写,并展示了完整的代码示例。最后,通过实际运行验证了排序的正确性,展示了泛型编程的优势。
19 0
|
26天前
|
算法 C语言
factorial函数c语言
C语言中实现阶乘函数提供了直接循环和递归两种思路,各有优劣。循环实现更适用于大规模数值,避免了栈溢出风险;而递归实现则在代码简洁度上占优,但需警惕深度递归带来的潜在问题。在实际开发中,根据具体需求与环境选择合适的实现方式至关重要。
25 0