欢迎来到白刘的领域 Miracle_86.-CSDN博客
系列专栏 C语言知识
先赞后看,已成习惯
创作不易,多多支持!
这里是白刘,函数想必大家都不陌生,在数学中,我们学到了很多函数,像y=kx+b这种一次函数,y=ax²+bx+c这种二次函数,对数函数,指数函数,三角函数......在C语言中同样也存在着函数,此函数非彼函数。
我们可以将C语言中的函数,看作程序员在编程时的工具箱,假如说这个地方我需要拧螺丝,那我就需要螺丝刀,这个地方需要拧螺母,我就需要扳手。而在C语言中,我们要完成一个项目,可以将其看作一个玩具或者机器,而每一处需求都是一个小零件,这个时候我们就需要工具箱来把这一处处给拼好。
目录
一、函数的概念
C语言中的函数,有个名字叫做:子程序,其实叫子程序比叫函数容易理解。因为C语言的函数就是为了完成某个特定的任务的一小段代码。而这段小代码,我们有特殊的写法和调用方法。
C语言的程序是由无数个小的函数组合而成的,就像玩具或机器由无数个零件组成的。也可以说是:一个大的问题由好几个小问题组成(类似于算法中的分治)。如果这个函数,不光能完成这个任务,还能复用,那么就大大提升了开发效率。
而在C语言中,我们见到的函数分为两种:库函数和自定义函数。
二、库函数
1.标准库和头文件
C语⾔标准中规定了C语⾔的各种语法规则,C语⾔并不提供库函数;C语⾔的国际标准ANSI C规定了⼀些常⽤的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。
很好理解,之前我们学过的printf以及scanf,这俩就属于<stdio.h>这个头文件里的一个库函数。库函数是不需要自己再加工的了,是之前的一些人已经写好的,我们直接用就好了。
关于如何学习库函数,这里给大家两个网址:C 标准库头文件 - cppreference.comhttps://zh.cppreference.com/w/c/headerC library - C++ Reference (cplusplus.com)
https://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.cint 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.cstatic 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修饰全局变量,那修饰函数和修饰全局变量简直是一模一样。
————关于函数就先说这么些吧,任重而道远啊各位朋友们。