在上一篇文章中,我们介绍了数组的基本知识,这篇文章则主要是介绍函数相关的基本知识,听到函数你是否是会想起什么?第一想法是不是数学中的函数?哈哈如果是这样,看来数学给我们留下了深刻的印象啊!!不要担心,今天的函数不是数学中那个函数。今天要讲的函数和C语言挂钩,接下来,让我们掀开它神秘的面纱!!
1.函数的概念
在数学中,y=kx+b是最常见一类函数,1个x对应1个y的值,一一对应。
在C语言中,函数就相当于一个子程序,而这个子程序帮我们负责解决对应的问题,所以函数就是解决相应的问题的一段代码。在我们每一次写的程序,都是由无数个小函数堆凑起来的。面对复杂的代码,我们可以利用函数,将其拆分成无数个小板块,这样敲出来的即通俗简介还便于理解。所以函数对我们解决复杂的问题!
函数分为两大类:
1)库函数
2)自定义函数
2.库函数
2.1 标准库和头文件
C语言规定了各种语法的规则,但是C语言并没有提供库函数。那么库函数是由谁提供的呢?C语言国际标准ANSIC规定了一些常用的函数的标准,被称为标准库,那不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。这些函数就被称为库函数。
我们经常敲得代码,譬如:scanf、printf等等,这类函数就是我们所说的库函数,其功能的实现是由我们的编译器去实现。库函数不仅提高了敲代码的效率,而且保证了我们的代码的质量。
库函数中含有不同的函数,每一个函数的功能都不一样,我们使用每一种函数都必须要包含相应的头文件。如果不包含正确的头文件,编译器会报错!
如果想要拓展知识或者了解相关函数头文件,可以去c plus plus官网。
C 标准库头文件 - cppreference.comhttps://zh.cppreference.com/w/c/header
C plus plus官网地址:cplusplus.comhttps://cplusplus.com/
2.2 库函数的使用方法
我们既然知道了库函数,那么我们该如何取使用库函数官网呢?举个简单栗子吧~
sqrt------计算平方根
库函数从其基本结构、函数功能、参数、返回类型对函数进行了讲解,后面还有相关例子。不妨实际操作一下sqrt函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <math.h>//sqrt的头文件
int main()
{
double num=0;
scanf("%lf",&num);
double ret=sqrt(num);
printf("%lf\n",ret);
return 0;
}
总结一下库函数的格式:
1).函数原型
2). 函数功能介绍
3). 参数和返回类型说明
4). 代码举例
5).相关知识链接
3.自定义函数
在解决一些实际问题时,利用标准库的函数可能解决不了,这个时候我们就想要自己写一个函数,来解决问题,自己写一个是完全没有问题,但是我们该怎样去写呢?要注意哪些问题呢?
3.1 自定义函数的基本结构
ret_type name(形式参数)
{
}
ret_type-----函数的返回值类型
函数的返回类型可以是int、char、double等等,如果不想要返回值可以用void ;
name-----函数名
创建的函数名可以是多样的,一般我们给函数命名是根据函数功能定义,譬如定义加法,通常命名为Add,诸如此类!
括号括起来的是形式参数;
{}中是函数体,我们解决问题主要代码。
3.2 函数举例
写一个函数,完成加法运算
分析:在创建函数时,我们首先要确定函数的返回类型,给函数命名,函数的参数类型,参数的个数。在创建函数体解决问题!
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int Add(int x, int y)//创建形式参数,接收a和b的值
{
int z = 0;
z = x + y;//进行加法运算
return z;//将计算好的结果返回
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);//输入变量,进行相加
int ret = Add(a, b);//将a和b的值传递给Add函数
printf("%d", ret);
return 0;
}
4.形参和实参
在函数使用时,我们将变量分为两种,一种是实参,一种时形参!
4.1 实参
实参就是真实传递给函数的变量,在上述代码中,Add(a,b)中a和b就是实参。
4.2 形参
形参就是形式参数,我们用来接收实参而创建的变量。上述代码中Add(int x,int y)x和y就是形参。
4.3 形参和实参的区别
1)形参接受的是实参传过来的内容,只有在函数调用的时,传递过程才会发生
2)只有调用函数,形参才会被创建,才会向内存申请空间;如果不调用函数,形参就不会被创建也不会向内存申请空间。
3)形参和实参都是独立的,拥有各自单独的内存空间
我们可以通过代码观察上述代码a,b,x,y的地址,证明他们的空间是独立的
(观察代码利用监视窗口-----快捷键Ctrl+F10,进入函数内部按F11,后面会讲解VS使用技巧)
4.4 形参和实参的关系
我们可以通俗的认为形参是实参的一份临时拷贝。
5.return语句
我们每一次写的代码中都出现了return,那么今天我们说说return使用的注意事项
1)return后面可以返回值,也可以返回表达式,如果是表达式,那么返回表达式计算后的值。
2)return后面可以什么都没有,但是这种只适用于返回类型是void的情况下。
3)return的返回值如果和返回类型不符,会被强制转化为返回类型的值,就比如return 3.14,但是返回类型是int,那么3.14就会被强制转换为3。
4)return语句执行后,后面的代码将彻底不会被执行。因此在编写代码时,要注意return的位置,一般都放最后,如果有特殊要求,就按照要求来。
6.数组做函数参数
在使用函数时,我们难免会遇到数组作为参数传递给函数,在函数内部进行修改。
例如:写⼀个函数将⼀个整型数组的内容,全部置为-1,再写⼀个函数打印数组的内容。
我们要重置数组的内容,就要将数组作为参数传递过去,将数组传递后,我要把每一个元素都重置,那我不知道你数组中到底有几个元素啊?那怎么办?--这是我们就需要将数组的元素传递过去。同时呢因为我只是重置,那就没有返回值,那么返回类型就是void。用代码敲一敲试试呢?
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//写⼀个函数将⼀个整型数组的内容,全部置为-1,再写⼀个函数打印数组的内容。
void set_arr(int arr[], int sz)//创建同类型的变量来接受数组和元素个数
{
int i = 0;
for (i = 0; i < sz; i++)
{
arr[i] = -1;//遍历数组,每一个都重置为-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] = { 0 };
set_arr(arr, 10);
print_arr(arr, 10);//传递数组和元素个数
return 0;
}
6.1 数组传参的重点
1)函数的形参个数要与实参个数相匹配;
2)一维数组传参时,用形参接收时,数组的大小可以忽略;
二维数组传参时,行可以省略但列不可以省略;
3)数组传参时不会创建一个新的数组;
4)形参和实参所操作的内容是同一个数组。
7.嵌套调用和链式访问
7.1 嵌套调用
在之前,我们讲过if语句的嵌套和循环语句的嵌套,那么函数也有是嵌套的。
例如:我想知道每一个月的天数,让我们用函数求解。
分析:1)我们知道只有2月份的天数是会发生变化的,所以我们得用一个函数去判断是否是闰年。
2)每个月的天数我都要进行统计,那么又可以创建一个函数统计天数。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int is_leap_year(int arr[], int year)
{
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0)//判断是否是闰年
return 1;//是闰年就返回1
else
return 0;
}
int is_day_month(int arr[], int month)
{
int day = arr[month];//判断每个月天数
if (is_leap_year && month == 2)//判断是否是闰年2月份
day++;
return day;
}
int main()
{
int arr[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int sz = sizeof(arr) / sizeof(arr[0]);
int year = 0;
int month = 0;
scanf("%d %d", &year, &month);
is_leap_year(arr,year);
int day=is_day_month(arr,month);
printf("%d", day);
return 0;
}
这样就通过两个函数解决的每月天数的问题。
7.2 链式访问
什么是链式访问?链式访问就是将一个函数的返回值作为另一个函数的参数,重复几次像链条串珠子一样串起来就是链式访问。
了解之后,我们来看一道有意思的题目
printf("%d",printf("%d",printf("%d",138)));
看到这道题目,我们自己先想想或者猜一猜这个结果是什么?在屏幕上会打印什么?
结合printf返回值我们再猜一猜?
好,现在我们来分析一下。
程序显示的结果也是一样的。这就是链式访问,你理解了吗?
8.函数的声明和定义
8.1 单文件
什么是函数声明?函数声明通俗来说就是我告诉编译系统有这么一个函数,你别给我报警告。
在上述代码的中,int Add(int x,int y)就是函数声明,有同学就问我能不能把它放在后面?当然是可以的。
有同学尝试了之后,就来问小编了?你说可以放后面,为什么要报错?----小编说的不对?
不不不。小编话还没有说完,我们在把函数放在后面时,因为编译器是一行一行的往下解读的,当我解读到Add函数,我往前面找,欸没找到,编译器就报警告了。
这是因为我们没有对函数进行声明。当我们在前面进行声明时,我们看看编译器会不会报错?
这个时候编译器就没有再报类似的警告了。
总结:函数一定要先声明后使用
那么函数如何正确声明呢?
1)要有函数名;
2)要告知函数的形参个数;
3)形参类型(名字可以忽略);
4)函数的返回类型。
是不是很简单呢?动动小手操作一下吧~
8.2 多文件
在复杂的代码中,大佬们通常会把代码分装在几个文件中。
⼀般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现是放在源文件(.c)文件中。
就拿加法运算函数来说,我们把它分装在不同的文件中。
9.static和extern
static和extern是C语言中的关键字。
static的意思是静态的,可以用来修饰:
1)局部变量
2)全局变量
3)函数
extern是用来声明外部符号的。
在讲解static和extern之前,我们先来了解两个知识:作用域和生命周期
什么是作用域?
对于局部变量来说:创建这个变量开始,这个变量作用的范围,就是局部变量。
对于全局变量:作用域则是整个工程。举个简单例子
在这里a是局部变量,它的作用范围就if语句的范围内;b是全局变量,作用范围则是整个工程。
那什么是生命周期?
生命周期就是一个变量从被创建,再到最后被销毁,作用的时间段就是生命周期。
对于局部变量:变量创建周期开始,出了作用域变量被销毁,生命周期结束。
对于全局变量:生命周期则是整个工程。
9.1 static修饰局部变量
对比这两张图发现,除了多了static之外,其余都是一样的。那为什么他们的结果却不相同呢?
我们可以通过监视窗口来看!
你会发现当我们第二次进入循环时,i=6;这是为什么呢?这是因为static修饰局部变量i时,改变了i的属性,也就是改变了i的生命周期。
结论:static修饰局部变量时,改变了其生命周期,生命周期改变的本质时变量的存储类型被改变。局部变量时存放在栈区的,但是由于static的修饰,局部变量存放在了静态区。而在静态区,它的生命周期适合全局变量一样的。
9.2 static修饰全局变量
extern是用来声明外部符号。比如add.c文件中的变量我想在test.c中使用,就需要用extern声明。
结论:static修饰全局变量时,extern再想使用时,编译器就会发出警告,这是因为static改变了全局变量的外部链接属性。全局变量是具有外部连接属性的,当我用static修饰时,外部链接属性被改为了内部链接属性,也就是说,x变量只能在本源文件(add.c)中使用,外部文件(test.c)无法使用。
9.3 static修饰函数
static修饰函数和static修饰全局变量他们的问题是一样的。
使用建议:一个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用修饰。
那么本期函数到这里就结束了,下一期我们来介绍一个热门的小游戏~敬请期待!