C语言中的工具箱——函数

欢迎来到白刘的领域   Miracle_86.-CSDN博客

系列专栏  C语言知识

先赞后看,已成习惯

   创作不易,多多支持!

这里是白刘,函数想必大家都不陌生,在数学中,我们学到了很多函数,像y=kx+b这种一次函数,y=ax²+bx+c这种二次函数,对数函数,指数函数,三角函数......在C语言中同样也存在着函数,此函数非彼函数。

我们可以将C语言中的函数,看作程序员在编程时的工具箱,假如说这个地方我需要拧螺丝,那我就需要螺丝刀,这个地方需要拧螺母,我就需要扳手。而在C语言中,我们要完成一个项目,可以将其看作一个玩具或者机器,而每一处需求都是一个小零件,这个时候我们就需要工具箱来把这一处处给拼好。


目录

一、函数的概念

二、库函数

1.标准库和头文件

2.库函数的使用方法

3.如何学习库函数

三、自定义函数

1.自定义函数的基本形式

2.举个栗子

3.形参和实参

4.return

 5.数组作为函数的参数

四、嵌套调用与链式访问

1.函数的嵌套

2.链式访问

五、函数的声明和定义

1.单个文件

2.多个文件

3.作用域和生命周期

4.static&extern

static对局部变量的修饰

static修饰全局变量

static修饰函数


一、函数的概念

C语言中的函数,有个名字叫做:子程序,其实叫子程序比叫函数容易理解。因为C语言的函数就是为了完成某个特定的任务的一小段代码。而这段小代码,我们有特殊的写法和调用方法。

C语言的程序是由无数个小的函数组合而成的,就像玩具或机器由无数个零件组成的。也可以说是:一个大的问题由好几个小问题组成(类似于算法中的分治)。如果这个函数,不光能完成这个任务,还能复用,那么就大大提升了开发效率。

而在C语言中,我们见到的函数分为两种:库函数和自定义函数。

二、库函数

1.标准库和头文件

C语⾔标准中规定了C语⾔的各种语法规则,C语⾔并不提供库函数;C语⾔的国际标准ANSI C规定了⼀些常⽤的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。

很好理解,之前我们学过的printf以及scanf,这俩就属于<stdio.h>这个头文件里的一个库函数。库函数是不需要自己再加工的了,是之前的一些人已经写好的,我们直接用就好了。

关于如何学习库函数,这里给大家两个网址:C 标准库头文件 - cppreference.comicon-default.png?t=N7T8https://zh.cppreference.com/w/c/headerC library - C++ Reference (cplusplus.com)icon-default.png?t=N7T8https://legacy.cplusplus.com/reference/clibrary/

里面有关于时间,关于字符串等等的一系列库函数。不必一次性学会,逐个击破,来日方长。

2.库函数的使用方法

首先,先确定功能,比如我们要计算平方根,那我们可以用到sqrt函数。

然后要包含对应头文件,这点很重要,就像printf和scanf,没有头文件是不可以的。

接下来看代码实现:

#include <stdio.h>
#include <math.h>
int main()
{
 double d = 16.0;
 double r = sqrt(d);
 printf("%lf\n", r);
 return 0;
}

结果如下:

3.如何学习库函数

有好多同学点开那个网址,一看全是英文,就提前打了退堂鼓。其实这样做是不好的,我们要敢于尝试翻译,这玩意毕竟是从西方那边传过来的,我们也是向他们学习,那有什么新的技术,肯定是以他们的文字来写的,我们如果想学习,就免不了翻译,并且这个一点也不难,现在还有很强大的翻译软件。

简单说一下每个库函数文档的格式,总共可以分为六点:

1.函数原型.2.函数功能介绍.3.参数和返回类型说明.4.代码举例.5.代码输出.6.相关知识链接.

三、自定义函数

既然库函数都可以被写出来,那我们是不是也可以自己写一些函数呢,一切皆有可能。C语言中支持我们写自定义函数,这就给大大提高了编程的可创造性与可能性。其实编程它也算一种创新,多敲敲代码脑子会更灵活的(头发也可能变少)。

1.自定义函数的基本形式

ret_type fun_name(形式参数)
{

}

ret_type是返回类型,⽤来表⽰函数计算结果的类型,有时候返回类型可以是 void,表⽰什么都不返回

后面那个name是函数名,为了⽅便使⽤函数;就像⼈的名字⼀样,有了名字⽅便称呼,函数有了名字⽅便调⽤,所以函数名尽量要根据函数的功能起的有意义。

小括号里用来装形参,函数的参数也可以是 void ,明确表⽰函数没有参数。如果有参数,要交代清楚参数的类型和名字,以及参数个数。

中括号括起来的部分称为函数体,函数体就是完成计算的过程。

2.举个栗子

比如说我们来写个加法函数 

int main()
{
 int a = 0;
 int b = 0;
 //输⼊
 scanf("%d %d", &a, &b);
 //调⽤加法函数,完成a和b的相加
 //求和的结果放在r中
 //to do
 
 //输出
 printf("%d\n", r);
 return 0;
}

由于a、b都是整形,所以我们也应该返回一个整形,所以我们函数返回类型为int,然后名字取一个Add,形参我们应该给两个数,传上去两个数,函数那边就得接收到两个数,这个形参就是为了接收实参的,所以可以int a,int b,给两个参数,然后函数体里开始计算,可以直接返回a+b,也可以设置一个新的变量r,将a+b的值赋给r,返回r。来看代码:

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

int main()
{
 int a = 0;
 int b = 0;
 //输⼊
 scanf("%d %d", &a, &b);
 //调⽤加法函数,完成a和b的相加
 //求和的结果放在r中
 int r = Add(a, b);
 //输出
 printf("%d\n", r);
 return 0;
}

3.形参和实参

通过测试我们可以看到形参和实参的地址是不一样的,但是值是一样的。我们可以简单理解为:形参是实参的一份临时拷贝。正常我们要用到函数时,传过去的参数叫实际参数,简称实参,而这边有东西过去了,我函数得去接收吧,得有地方接收吧,这就有了形式参数,简称形参。

4.return

使用函数时经常会用到return这个关键字,接下来我们简单介绍一下return。

return后面既可以是表达式也可以是数值,如果是数值就直接返回数值,如果是表达式则先计算表达式,再返回结果。

return后面可以什么都没有,适用于void类型,当然void类型可以不写return,也可以写成return; 

当return返回的值与返回类型不一致时,会自动转换为返回类型。 

return一旦执行,函数就立刻返回,后边代码不再执行。

如果在函数中用到if等分支语句,确保每次都应有return,否则可能导致编译错误。 

 5.数组作为函数的参数

我们在写函数的时候,很难避免用数组作为函数的参数,那么这个时候,我们应该如何操作呢?比如:我想传入一个数组,将它全部初始化为0,然后再打印出来,我想用函数实现,应该怎么办呢?其实并不难操作,仔细想一下,大致操作应该如下:

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

 这里set_arr用来初始化,print_arr用来打印,如果我们想进行操作,首先得传一个数组,然后避免不了的要对数组进行操作,为了便于操作,我们可以将数组元素个数计算出来然后传给函数。因为这个过程避免不了遍历(循环),我得知道次数,才能更有效地操作。

#include <stdio.h>
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;
}

 之后就很容易写出函数啦,代码如下:

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");
}

这里简单总结一下几个比较重要的地方:

1.形参和实参个数要匹配

2.实参是数组,形参也可以写成数组形式。如果形参为一维数组,数组大小可省略;如果为而且数组,行可以省略,列不可以省略。

3.数组传参,形参不会创建新数组。

4.形参与实参操作的数组为同一个数组。

四、嵌套调用与链式访问

1.函数的嵌套

函数的嵌套其实是特别巧妙和富有创造力的一个过程,每个函数就好比一块积木,嵌套后拼出精美的玩具。嵌套其实听起来很复杂,实际上一点也不简单,其实说难不难说简单不简单,我们在最初学的main函数,还记得吗,它也是函数,当我们在里面写上printf和scanf时,这就构成了函数嵌套。

还是来举个例子,比如我想写一个判断某年某月有多少天的函数,我可以写两个函数:

is_leap_year():用来判断是否为闰年(因为闰年的二月份为29天)

get_days_of_month():一个用来计算天数。

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里嵌套调用了scanf、printf、get_days_of_month

get_days_of_month嵌套调用了is_leap_year

2.链式访问

何为链式访问,这个东西听起来很高级,但实际上很简单。它就是将一个函数返回的值,作为另一个函数的参数,像链条似的串起来了,比如:

#include <stdio.h>
int main()
{
 int len = strlen("abcdef");//1.strlen求⼀个字符串的⻓度
 printf("%d\n", len);//2.打印⻓度 
 return 0;
}

正常我们用的printf,后面传的参数是一个变量,但是我们直接传strlen的返回值呢?

#include <stdio.h>
int main()
{
 printf("%d\n", strlen("abcdef"));//链式访问
 return 0;
}

 二者答案是一样的,这就叫链式访问,简单吧。

再来看一道有趣的例子,猜一下答案是什么:

#include <stdio.h>
int main()
{
  printf("%d", printf("%d", printf("%d", 43)));
  return 0;
}

这个代码的关键是要明白printf返回的是什么

int printf ( const char * format, ... );

它返回的是打印在屏幕上的个数。

一共有三个printf,第一个打印第二个的返回值,第二个打印第三个的返回值,

第三个printf打印43,打印两个字符,返回值为2;

第二个printf打印2,打印一个字符,返回值为1;

第一个printf打印1。

所以结果最终打印为4321。

再来一道题,如果我这么写,阁下的答案又是多少呢?

#include <stdio.h>
int main()
{
  printf("%d", printf("%d ", printf("%d ", 43)));
  return 0;
}

空格也算字符,

第三个printf打印43(空格),打印三个字符,返回值3,;

第二个printf打印3(空格),打印两个字符,返回值2;

第一个printf打印2。

所以结果最终打印43 3 2(注意空格嗷,不是连在一起的4332)。

五、函数的声明和定义

1.单个文件

我们C语言老师问我们什么是函数的定义,什么是函数的调用。说实话我认为这个问题无比的愚蠢。我用最简单的说法介绍这两个是什么,函数的定义就是,你写函数,你写的那一堆东西,就叫函数的定义;而函数的调用,调用嘛,你得用上才能叫调用,当我用的时候就叫调用。话说各位,你们觉得这两个很难区分嘛?

#include <stido.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);//这一句叫调用
 if(r == 1)
 printf("闰年\n");
 else
 printf("⾮闰年\n");
 return 0;
}

但是如果我们将这个定义挪到后面,会发生什么呢?

在VS2022中,会报错:

 还记得嘛,我们之前说的那句心法“从上到下,依次执行”,C语言中由于编译器在前面没扫到is_leap_year所以报错了,这时候我们在前面加上一句声明即可。

int is_leap_year(int y);

 声明是告诉编译器,我接下来可能会用到这个函数。

函数的定义是一种特殊的声明,因此放在前面是没关系的。

2.多个文件

一般在企业中我们编程的时候,不会像平常练习的那样,把所有代码都放在一起,而是会创建多个文件来存放代码,比如说头文件(.h)可以来存放函数的声明、类型的声明,源文件(.c)可以存放函数的实现

eg:

Add.c

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

Add.h

//函数的声明
int Add(int x, int 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;
}

 运行结果如下:

补充一点,我们在引用头文件的时候,如果是引用的标准库里的就用一对尖括号,如果引用的是自己的头文件,就要用双引号。 

3.作用域和生命周期

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

简单来说,什么是作用域,就是可以作用的领域呗,超出这个领域,就不好使了,就这个意思。

局部变量的作用域为变量所在的局部范围。

全局变量的作用域为整个项目(工程)。

eg:

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

 这里i就为全局变量,可以全局使用;而j是局部变量,只能在for循环里使用。

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

局部变量的生命周期为,进作用域开始,出作用域结束。

全局变量的生命周期为,整个程序的生命周期,也就是说只要程序还在,它就还活着。 

4.static&extern

static,静态的,它可以用来修饰局部变量、全局变量以及函数。

extern,外部的意思,用来修饰外部声明变量。

4.1 static对局部变量的修饰

先来看两个代码:

代码1:

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

代码2:

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

区别是代码2在函数里多了个static修饰i。

代码1中test函数里的i执行完生命周期就结束了,所以会死循环。

代码2中有static修饰了一下,效果截然不同,static的作用是改变变量的生命周期,本质是改变了存储方式,本来局部变量将存放在栈区,但是被static存放到了静态区,和全局变量的存储是一样的。作用域没变,生命周期变了。

总结就是如果未来一个变量出了函数后,我们想保留其值,等下次再次进入函数使用,就可以用static修饰。

4.2 static修饰全局变量

如果上面的局部变量明白了,接下来的就更好懂了,比如说我现在有两个文件,A文件和B文件,A中有一个全局变量a,我想在B文件中使用,这个时候我们就可以使用extern修饰一下全局变量a。这样在B文件中就可以使用了 。

那如果我只想在A文件中使用呢,其它的我不让它用,这个时候我们就可以用static修饰,这个时候就可以只在A中使用。

如:

代码1:

add_1.c
int g_val = 2018 ;
test_1.c
# include <stdio.h>
extern int g_val;
int main ()
{
printf ( "%d\n" , g_val);
return 0 ;
}

 代码2:

add_2.c
static int g_val = 2018 ;
test_2.c
# include <stdio.h>
extern int g_val;
int main ()
{
printf ( "%d\n" , g_val);
return 0 ;
}

在代码1中是可以正常运行的,而在代码2中是会出现链接性错误的

总结:如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使⽤ static修饰。

4.3 static修饰函数

如果你看懂了static修饰全局变量,那修饰函数和修饰全局变量简直是一模一样。

————关于函数就先说这么些吧,任重而道远啊各位朋友们。

  • 53
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Miracle_86.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值