1、函数的概念
其实我们再数学就见过函数了;比如一次函数y=kx+b;给一个任意的x,返回一个y;
在C语言我们也有函数,也叫子程序;函数就是完成一部分功能的代码;比如一个大任务,分成几个小任务;由多个函数实现出来;C语言程序其实是由多个函数组合而成的;
C语言中主要分为两类函数:库函数和自定义函数;
2、库函数
2.1标准库和头文件
C语言并不提供库函数,它只是提供某些函数的规定要实现什么功能;什么参数,什么返回类型。
然后编译器厂商就根据C语言提供的规定去实现一些库函数;那么这些规定就是标准;并把这些称为标准库;
那么要运用这些库函数,就要运用到头文件,库函数根据功能的划分,在不同的头文件进行定义,使用库函数,要使用对应的头文件;
那我们要查看头文件和库函数可以到
C/C++官⽅的链接:https://zh.cppreference.com/w/c/header
cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/
查看
double sqrt (double x)
//sqrt 是函数名
//double x 是函数的参数是double类型的
//double sqrt函数的返回值是double类型的
2.1.1功能
Compute square root 计算平方根
Returns the square root of x. 返回平方根
2.1.2头文件
sqrt函数需要包含头文件;#include<math.c>
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<math.h>
int main()
{
double x = 25.0;
double r = sqrt(x);
printf("%.1lf\n",r);
return 0;
}
3、自定义函数
自定义函数是最重要的;可以让我们根据不同的情况,不同的要求去实现不同功能的函数;
3.1函数的语法
ret_type fun_name (形式参数)
{
}
ret_type;函数的返回值类型
fun_name;函数名
();括号里面是形式参数
{ };里面是函数体
函数就相当于一个工厂,你给它原材料(形式参数);工厂就会给你返回一个零件(计算结果);
那么ret_type就是计算结果的返回类型;函数也可以什么都不返回,那这个函数的返回类型就void 空;
fun_name函数名起的时候;要尽量由意义,不容易搞混;
如果函数由参数,那我们就要确定参数的类型,个数,名字;
比如我们定义一个加法函数;
输入add函数a,b的值;然后a,b的值赋给形参x ,y
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int add(int x, int y) //函数返回值类型int
{ //函数名add
int z = x + y; //参数类型int
return z; //参数名x,y
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = add(a, b); //加法函数
printf("%d", r);
return 0;
}
4、形参和实参
使用函数过程中,我们把函数的调用里面的参数就是实参;函数的定义里面的参数就是形参
int add(int x, int y) 函数定义 形参
{
int z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = add(a, b); 函数调用 实参
printf("%d", r);
return 0;
}
4.1实参
实参就是调用函数时传给它的实际参数就叫实参;
4.2形参
就是函数定义时,用来接收的参数就叫形式参数,形参;
4.3实参和形参的关系
如果没有调用add函数,那么形参x,y会向内存申请空间来存放吗?
是不会的,你没有调用,形参不会申请内存空间,你调用了,形参才会向内存申请空间;
所以形参就是形式上的参数;
int add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
int r = add(a, b);
printf("%d", r);
return 0;
}
我们看一下a和b的地址,x和y的地址,它们的地址是不也一样,但它们的值是一样的;
形参就是实参一次拷贝;把实参的值拷贝过来;
注意,我们形参的个数可以有多个,但是要和实参的个数要保持一致;
形参的数据类型和实参的数据类型要一样;
形参的名字可以和实参的名字一样;
5、return语句
return语句就是返回的意思;
return后面可以跟一个变量或者是表达式;表达式会先执行,计算返回结果;
int add(int x, int y)
{
return x + y;
}
比如打印函数;你不用return返回值;没什么可以返回的,函数的数据类型就是void
void menu()
{
printf("hehehe\n");
}
假设返回的是一个浮点型小数,但是函数的返回值类型是int;那么它会隐形类型转换,返回3;
int add(n)
{
return 3.4;
}
如果没有指定函数的返回类型,默认是int类型
add(n)
{
return 3.4;
}
如果x>5就执行return语句;返回,后面的语句就不再执行;打印bababa;
void text(int x)
{
if (x > 5)
return;
printf("hahaha");
}
int main()
{
int n = 10;
text(10);
printf("bababa");
return 0;
}
这个函数会发一个警告;意思就是如果是x<5,这种情况呢?
我们还需要考虑到这种情况;
int text(int x)
{
if (x > 5)
return 1;
}
int main()
{
int k = 10;
printf("%d",text(10));
return 0;
int text(int x)
{
if (x > 5)
return 1;
else
return 2;
}
int main()
{
int k = 10;
printf("%d",text(10));
return 0;
}
6、数组做函数的参数
比如我们把一个函数的内容都为-1;然后函数打印出来;
void set_arr(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
arr[i] = -1;
}
}
void printf_arr(int arr[],int sz)
{
int j = 0;
for (j = 0; j < sz; j++)
{
printf("%d ",arr[j]);
}
printf("\n");
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
printf_arr(arr, sz); //打印数组
set_arr(arr,sz); //赋值数组
printf_arr(arr,sz);
return 0;
我们写一个set_arr函数把数组的每个元素赋值为-1;我们给个arr数组名做参数,既然每个元素都要赋值,那就需要遍历每个元素;需要元素的个数sz做参数;
到形参部分;你给一个数组,那形参也要有个数组,数组的大小可以不写,
打印部分就用printf_arr函数,跟set_arr函数基本差不多;
数组做参数,实际上给的是数组的地址给形式参数;所以形式参数(被调函数)可以改变main(主调函数)里的数组元素;并且数组做参数,形参不会给数组申请内存空间创建新的数组;所以不用在形式参数上写上数组的大小;
注意数组函数;
1.形式参数的个数要个实参的参数个数要相同;
2.实参是数组,那形参也要写成数组的形式;
3.数组是一维数组,大小可以省略
4.数组是二维数组,列不可以省略
5.数组做形参,不会创建新的数组,不用写数组的大小
6.形参数组跟实参数组其实是同一个数组;
7、嵌套使用和链式访问
7.1嵌套使用
函数嵌套是函数之间互相调用,一个函数中调用另一个函数;
写一个某年某月有几天的函数;输入年份,月份,判断有几天;我们知道每个月份都是固定的,出来二月,如果是闰年二月就有29天;那么我们就在判断有几天的函数里,再调用一个判断闰年的函数,这叫叫做函数的嵌套;mian函数调用scanf函数和printf函数;函数定义之间是不能嵌套的,你不能把函数的定义放在另一个函数里面;
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int year(int y) //判断是否是闰年,是返回1;否返回0
{
return ((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0);
}
int year_month(int y, int m)
{
//31,28,31,30,31,30,31,31,30,31,30,31
// 29
int day[] = { 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 r = day[m];
if (year(y) == 1 && m == 2)
r++;
return r;
}
int main()
{
int year = 0;
int month = 0;
scanf("%d %d",&year,&month);
int day = year_month(year,month);
printf("%d\n",day);
return 0;
}
7.2链式访问
链式访问就是把一个函数的返回值当成另一个函数的参数;
比如,我们把printf函数的返回值当成另一个printf函数的参数;
printf函数的返回值,printf打印几个数,就返回几个数;
第四个函数,打印43,返回2;第三个函数打印2;返回1;第二个函数打印1;返回1;第一个函数打印1,返回1;结果就是43211;
int main()
{
printf("%d",printf("%d",printf("%d",printf("%d",43))));
//结果:43211
return 0;
}
8、函数的声明和定义
8.1单个文件
假设我们再写一个函数的时候,把函数放在了后面,编译器会给一个警告;因为程序在执行的时候,是从上面往下面开始编译,当你用这个函数的时候,你把函数放在后面,编辑器调用函数之前没有见过这个函数,所以会发生一个警告;
注意我们你在使用函数之前要
先声明后使用
把声明发在函数的前面就可以;你可以放在main函数前面后者就放在函数上面都行;
int add(int x, int y);//函数的声明
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b); //函数调用
int r = add(a, b);
printf("%d\n",r);
}
int add(int x, int y) //函数定义
{
return x + y;
}
那之前我们把函数的定义直接放在前面为什么也可以?
你定义都给它看了;函数体都给它看了,那肯定是可以的;其实函数的定义也是一种特殊的声明;
8.2多个文件
在一些正儿八经的工程中一定多文件去写这个工程的;
我们把函数的定义放在.c文件,函数的声明放在.h文件,当你想调用这个函数的时候,只需要包含这个函数的头文件就可以调用;
注意库函数的头文件是< >这样写的
自定义函数是” “双引号写的;
add.c
int add(int x, int y) //函数定义
{
return x + y;
}
add.h
int add(int x, int y);//函数的声明
main.c
#include<stdio.h>
#include "add.h"
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b); //函数调用
int r = add(a, b);
printf("%d\n",r);
}
一个大的工程,会分为几个模块;比如一个计算器;分成几个模块;A,做乘法;B,做除法;
C,做加法;D,做减法;它们分别把每个功能实现是出来,写成.c\.h文件;再由E去把它们整合起来,只要写程序的时候包含它们写的.h文件,头文件;就能使用这个函数;实现这个工程;
那么这样做的话,效率高,模块化;还能对重要的代码进行隐藏;
9、static和extern
static和extern都是C语言里面的关键字;
static是静态的意思;它们修斯局部变量;全局变量;函数;
exterm是声明外部符号;
首先我们讲一下作用域和生命周期
作用域就是一个变量能够被使用的范围;
局部变量:它的作用域就是变量所在的局部范围;
比如变量a只能在括号里面使用,不能再括号外面使用;所以外面的printf不会打印,还会报错;
int main()
{
{
int a = 6;
printf("%d\n",a);
}
printf("%d\n",a);
return 0;
}
全局变量;它的作用就是整个工程;
都能打印b;
int b = 1; //全局变量
int main()
{
{
int a = 6;
printf("%d\n",a);
printf("%d\n",b);
}
printf("%d\n",b);
return 0;
}
生命周期指的是,这个变量的创建(申请内存)到变量销毁(内存回收)的时间段;
局部变量的生命周期:进入作用域创建变量,生命开始,出作用域变量销毁,生命结束;
全局变量的生命周期:程序从mian函数开始执行,main可以使用全局变量,程序结束,全局变量销毁,也就是说全局变量的生命周期就是整个程序的生命周期;
9.1static修饰局部变量
我们分析下面代码,循环5次,打印5次text函数,本以为是6 7 8 9 10;结果是6 6 6 6 6;
为什么呢?因为变量a是一个局部变量。它的作用域只是在text函数里面;第一次循环进来,变量a创建,a=5;a++;a=6;打印6;然后变量销毁,退出text函数;变量没有保存数值;第二循环进来也是一样的,出了text函数生命周期就结束了。
text()
{
int a = 5;
a++;
printf("%d ",a);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
text();
}
return 0;
}
但是当static修饰局部变量的时候,结果为6 7 8 9 10;为什么呢?
我们倒退一下,打印了6,能理解,但是打印7;那么变量就是6啊,a++;打印7啊;下次a=7;a++;才打印8啊;那就说明static修饰后的局部变量是保存上次的数值的;这么看,局部变量的生命周期延长了;
text()
{
static int a = 5; //static修斯局部变量
a++;
printf("%d ",a);
}
int main()
{
int i = 0;
for (i = 0; i < 5; i++)
{
text();
}
return 0;
}
本质上;static修饰的局部变量改变它的存储类型,本来是放在栈区,static修饰后就放在了静态区;这样使局部变量的生命周期和全局变量的生命周期一样长了,但是它的作用域使不变的;
9.2static修饰全局变量
假设我们在别个文件有一个全局变量,想调用,我们可以用extern声明外部符号,对它进行一个声明,就能使用了。
main.c
int a = 8;
text.c
//extern--声明外部符号
extern int a;
int main()
{
printf("%d",a);
return 0;
}
假设static修饰全局变量;那当调用的时候就会报错;
//static修饰全局变量
static int a = 8;
因为全局变量是有外部链接属性,能被其他的函数调用,但是当static修饰全局变量的时候,就把它的外部链接属性,变为内部链接属性,就只有在本源文件内使用,外部文件不能使用,只有自己能用,别人不能用;即使你已经extern声明了,也不能使用;
使用这个的话,就想这个全局变量只有在这个自己的文件内使用,不想给别的文件使用,就可以用static修饰全局变量;
9.3static修饰函数
其实static修饰全局变量和static修饰函数是一样的
假设在别的文件定义了一个函数,想调用的时候,你可以写上一个.h头文件,把函数的声明写在里面,然后调用头文件,这是一种方法,或者是extern声明外部符号,声明函数,跟头文件一样也是对函数进行一种声明;
add.c
int add(int x, int y) //函数定义
{
return x + y;
}
text.c
extern int add(int x, int y);//函数的声明
int main()
{
int a = 0;
int b = 0;
scanf("%d %d",&a,&b); //函数调用
int r = add(a, b);
printf("%d\n",r);
}
当static修饰函数时,调用函数会报错;
static int add(int x, int y) //函数定义
{
return x + y;
}
本质上跟static修饰全局变量是一样的;函数是有外部链接属性的,能够被别的文件所调用,当static修饰函数是,就把函数的外部链接属性变为内部链接属性,只能在自己的文件内使用,别的文件不能调用;
如果想就在自己的文件内使用函数,别的文件不能使用,那就使用static修饰全局变量;
感谢观看!感谢指正!