1、函数的概念
C语言中的函数(function),也叫子程序,就是一个完成某项特定任务的一小段代码。这段代码是有特殊的写法和调用方法的。C语言的程序其实是由无数个小的函数组合而成的,一个大的计算任务可以分解成若干个较小的函数来完成。一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率。
在C语言中我们一般会见到两类函数:库函数 自定义函数。
2、库函数
2.1 标准库和头文件
C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSI C规定了一些常用的函数的标准,被称为标准库,不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。这些函数就被称为库函数。
例如我们已经熟悉的printf、scanf都是库函数。库函数也是函数,不过这些函数是现成的,只要学会了就能直接使用。有了库函数,一些常见的功能就不需要程序员自己实现了,一定程度提升了效率,同时库函数的质量和执行效率上都更有保障。
各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。
库函数相关的头文件:https://zh.cppreference.com/w/c/header
3、自定义函数
除去提供便利的库函数外,自定义函数则是为程序员写代码提供了更多的创造性。
3.1 自定义函数的语法形式
ret_type fun_name(形式参数)
{
}
ret_type 是函数返回类型,有时候返回类型可以是void,表示什么都不返回。
fun_name 是函数名,可以自定义,方便调用。
() 中的是形式参数,可以是void,表示函数没有参数。如果有参数则要交代清楚参数的类型、名字以及参数个数。
{} 中的是函数体,函数体就是完成计算的过程。
3.2 自定义函数举例
自定义一个加法函数:
#include<stdio.h>
int Add(int x,int y)//加法函数
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b);//输入
int c = Add(a,b);//调用加法函数,将求和结果放入c中
printf("%d\n",c);
return 0;
}
4、形参和实参
在函数使用过程中,把函数的参数分为形参和实参。
用上述代码举例:
int c = Add(a,b);//这里a,b为实参
实际参数就是真实传递给函数的参数。
int Add(int x,int y)//这里的x,y为形参
{
return x + y;
}
只是定义Add函数,而不去调用的话,Add函数的参数x和y只是形式上存在的,不会向内存申请空间,叫形式参数。形参只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。
4.1 形参和实参的关系
虽然是实参传递给形参,但形参和实参各自都是独立的内存空间。
如上,在调试的时候可以观察到,x,y确实得到了a,b的值,但x,y的地址和a,b的地址是不一样的,所以可以理解为形参是实参的一份临时拷贝。
5、return语句
在函数的设计中,函数中经常会出现return语句,这里讲一下return语句使用的注意事项:
1.return后面可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
2.return后面也可以什么都没有,直接写return;这种写法适合void返回类型的函数。
3.return返回的值和函数返回类型不一样,系统会自动将返回的值隐式转换为函数的返回类型。
4.return语句执行后,函数就彻底返回,后面的代码不再执行。
5.如果函数中存在if等的分支语句,则要保证每种情况下都有return返回,否则会出现编译错误。
举例:判断一个整数是不是奇数。
#include<stdio.h>
int odd(int n)
{
if (n % 2 == 1)
return 1;
else
return 0;
}
int main()
{
int n = 0;
scanf("%d", &n);
if (odd(n)==1)
printf("是奇数\n");
else
printf("不是奇数\n");
return 0;
}
6、数组做函数参数
在使用函数解决问题时,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。
举例:写一个函数将一个整型数组的内容全部置为-1,再写一个函数打印数组的内容。
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);//重置函数
print_arr(arr,sz);//打印函数
return 0;
}
将数组以及元素个数作为参数传递给了set_arr和print_arr函数之后,先需要知道数组传参的几个重要知识点:
1.函数的形参个数要和函数的实参个数匹配。
2.函数的实参是数组,形参也可以写成数组形式。
3.形参如果是一维数组,数组大小可以省略不写。
4.形参如果是二维数组,行可以省略,列不能省略。
5.数组传参,形参是不会创建新的数组。
6.形参操作的数组和实参的数组是同一个数组。
接着就可以实现set_arr函数:
void set_arr(int arr[], int sz)
{
int i = 0;
for (i = 0;i < sz;i++)
{
arr[i] = -1;
}
}
print_arr函数:
void print_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
完整代码:
#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[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
set_arr(arr,sz);
print_arr(arr,sz);
return 0;
}
7、嵌套调用和链式访问
7.1 嵌套调用
嵌套调用就是函数之间的互相调用,通过函数互相之间的有效调用,最终“组装”出一个相对大型的程序。
举例:计算某年某月有多少天。
分析:可以拆分为两个函数来完成
1. is_leap_year():根据年份确定是否为闰年
2. year_month_day:调用 is_leap_year 函数确定是否为闰年后,再根据月计算这个月的天数。
代码实现如下:
#include<stdbool.h>
#include<stdio.h>
bool is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return true;
else
return false;
}
int year_month_day(int y,int m)
{
int days[] = {0,31,28,31,30,31,30,31,31,30,31,30,31};
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12
//判断闰年
int d = days[m];
if (is_leap_year(y) && m == 2)
{
d += 1;
}
return d;
}
int main()
{
int year = 0;
int month = 0;
scanf("%d %d",&year,&month);
int day = year_month_day(year,month);
printf("%d\n",day);
return 0;
}
7.2 链式访问
链式访问就是将一个函数的返回值作为另外一个函数的参数,像链条一样将函数串起来就是函数的链式访问。
#include<stdio.h>
#include<string.h>
int main()
{
//size_t len = strlen("abcdef");
printf("%zd\n", strlen("abcdef"));//不通过len,直接将strlen作为printf的参数
return 0;
}
8、函数的声明和定义
8.1 单个文件
一般在使用函数的时候,直接将函数写出来就可以使用了
举例:写一个函数判断一年是否为闰年。
#include<stdio.h>
#include<stdbool.h>
bool is_leap_year(int y)
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return true;
else
return false;
}
int main()
{
int y = 0;
scanf("%d",&y);
is_leap_year(y);
if (is_leap_year(y))
{
printf("是闰年\n");
}
else
printf("不是闰年\n");
return 0;
}
这种情况是函数的定义在函数调用之前。
但如果将函数的定义放在函数的调用之后呢?
#include<stdio.h>
#include<stdbool.h>
int main()
{
int y = 0;
scanf("%d", &y);
is_leap_year(y);
if (is_leap_year(y))
{
printf("是闰年\n");
}
else
printf("不是闰年\n");
return 0;
}
bool is_leap_year(int y)//函数的定义
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return true;
else
return false;
}
则会出现如下警告:
解决问题的办法就是函数调用之前先声明is_leap_year这个函数,声明函数只要交代清楚:函数名,函数的返回类型和函数的参数。
改动如下:
#include<stdio.h>
#include<stdbool.h>
//先声明后使用
bool is_leap_year(int);//函数的声明
int main()
{
int y = 0;
scanf("%d", &y);
is_leap_year(y);
if (is_leap_year(y))
{
printf("是闰年\n");
}
else
printf("不是闰年\n");
return 0;
}
bool is_leap_year(int y)//函数的定义
{
if (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0))
return true;
else
return false;
}
函数的调用一定要满足先声明后使用。
8.2 多个文件
代码较多时,我们往往会根据程序的功能,将代码拆分放在多个文件中。
一般情况下,函数的声明、类型的声明放在头文件 .h 中,函数的实现放在源文件 .c 文件中。
Add.h
//函数的声明
int Add(int, int);
Add.c
//函数的定义
int Add(int x,int y)
{
return x + y;
}
test.c
#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;
}
8.3 static 和 extern
static 和 extern 都是C语言中的关键字。
static 是静态的 意思,可以用来:
修饰局部变量;修饰全局变量;修饰函数。
extern 是用来声明外部符号的。
在继续探讨static和extern之前,先说明一下:作用域和生命周期。
作用域是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
1.局部变量的作用域是变量所在的局部范围。
2.全局变量的作用域是整个项目。
生命周期指的是变量的创建到变量的销毁之间的时间段。
1.局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域生命周期结束。
2.全局变量的生命周期是:整个程序的生命周期。
举例:static修饰局部变量
//代码1
#include<stdio.h>
void test()
{
int n = 1;
n++;
printf("%d ",n);//2 2 2 2 2
}
int main()
{
int i = 0;
for (i = 0;i < 5;i++)
{
test();
}
return 0;
}
//代码2
#include<stdio.h>
void test()
{
static int n = 1;
n++;
printf("%d ", n);//2 3 4 5 6
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
test();
}
return 0;
}
代码1:test函数中的局部变量i是每次进入test函数先创建变量(生命周期开始)并赋值为0,然后i++再打印,出函数的时候变量生命周期结束(释放内存)。
代码2:test函数中的i创建好后,出函数并不会被销毁,直接上次累积的数值继续计算。
结论:static修饰局部变量改变了变量的生命周期,,生命周期改变的本质是改变了变量的存储类型,本来一个局部变量是存储在内存的栈区的,但被static修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的,只有程序结束,变量才销毁,内存才回收。
static修饰全局变量:
一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。本质原因是全局变量默认是具有外部链接属性的,当外部文件想使用,只要适当声明就可以使用;但全局变量被static修饰后,外部链接属性就变成了内部链接属性,只能在本源文件内使用,即使其他源文件声明了也是无法正常使用的。
举例:extern 声明外部符号
test2.c
int n = 20;//全局变量 - 外部链接属性
test.c
#include<stdio.h>
extern int n;
int main()
{
printf("%d\n",n);
return 0;
}