函数的概念
C 语言中的函数就是一个完成某项特定的任务的一小段代码。这段代码有特殊的写法和调用方法。
在C语言中常见的两类函数:
- 库函数:已存在,可以直接使用
- 自定义函数:自己创建的函数
库函数
标准库和头文件
C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSI C规定了一些常用的函数的标准,被称为标准库。也就是说,C语言不去实现,只是规定标准。
那么,编译器会根据标准库来实现功能,这就是库函数。常见编译器如Linux中有gcc。
库函数的使用和功能是一样的,但是函数的实现可能有所差异。
各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。
库函数相关的头文件:zh.cppreference.com/w/c/header
cplusplus.com:cplusplus.com
举例:sqrt 开平方函数
使用方法:legacy.cplusplus.com/reference/c…
double sqrt (double x);
float sqrtf (float x);
long double sqrtl (long double x);
代码
#include <math.h>
int main()
{
double r = sqrt(16.0);
printf("%lf", r);
return 0;
}
执行结果
库函数文档的一般格式
- 函数原型
- 函数功能介绍
- 参数和返回类型说明
- 代码举例
- 代码输出
- 相关知识链接
自定义函数
库函数功能是固定的,自定义函数可以根据需求发挥程序员的想象力编写,并实现其功能。
ret_type fun_name(形式参数)
{
}
函数名 返回类型(形式参数)//函数头
{
//函数体
}
函数返回类型只有两种:void 和 其他
void menu(void)
{
printf("**************************\n");
printf("**************************\n");
printf("**************************\n");
}
- void:什么都不返回
- 其他:int char short ...
代入参数的函数
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);//真实传递给函数Add的参数,是实际参数,简称实参
printf("%d\n",d);
return 0;
}
- 形参和实参最大区别在于;对于没有调用函数时,形参不会创建内存空间。
- 形参和实参有自己不同的地址,传参时,实参会把值传给形参。形参是实参的一份临时拷贝。
return 语句
在函数的设计中,函数中经常会出现return语句。
return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达,再返回表达式的结果。return后边也可以什么都没有,直接写return;这种写法适合函数返回类型是void的情况。return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。return语句执行后,函数就彻底返回,后边的代码不再执行。- 如果函数中存在
if等分支语句,则要保证每种情况下都有return返回,否则会出现编译错误。
看下一段代码
void test()
{
int n = 0;
scanf("%d", &n);
printf("Point A\n");
if (n == 5)
return;
printf("Point B\n");
}
int main()
{
test();
return 0;
}
执行结果
并没有打印Point B,而是直接返回。
换句话说,return可以提前结束掉函数。 强调一下,break只能跳出循环,而return是跳出整个函数。
继续看代码
int test()
{
return 3.14;
}
int main()
{
int n = test();
printf("%d\n",n);
return 0;
}
执行结果
继续看代码
int test()
{
int n = 0;
if (n == 5)
return 1;
}
int main()
{
int m = test();
printf("%d\n",m);
return 0;
}
执行结果
当 n不等于 5 时,是没有返回值的,所以会出现如上编译问题。
数组做函数参数
比如:写一个函数对将一个整型数组的内容,全部置为-1,再写一个函数打印数组的内容。
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]);
//写一个函数,将arr中的内容全部置为 -1
set_arr(arr,sz);
//在写一个函数,将arr中的内容打印
print_arr(arr,sz);
return 0;
}
执行结果
- 函数的形参和实参个数要匹配
- 函数的实参是数组,形参也可以写成数组
- 形参如果是一维数组,数组大小可以省略
- 形参如果是二维数组,行可以省略,但是列不能省略(与二维数组初始化一样)
- 数组传参,形参是不会创建新的数组
- 形参和实参是同一个数组
二维数组传参
print_arr(int arr[3][5], int r ,int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { 1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7 };
print_arr(arr, 3, 5);
return 0;
}
执行结果
- 行可以省略,列是不能省略的;
arr[3][5]可以写成arr[][5]
函数嵌套调用和链式访问
嵌套调用
函数嵌套调用就是函数之间的互相调用,俄罗斯套娃一样。
用过Oracle的老DBA估计都经历过Oracle存储过程的各种嵌套调用。
判断每个月中有多少天
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[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
int d = days[m];
if (is_leap_year(y) && m == 2)
{
d += 1;
}
return d;
}
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;
}
执行结果
链式访问
看如下代码
#include <string.h>
int main()
{
size_t len = strlen("abc");
printf("%zd\n", len);
//也可以直接写成
printf("%zd\n",strlen("abc"));//链式调用
return 0;
}
执行结果
在看一段代码
int main()
{
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
执行结果
- 最里层:
printf打印在屏幕上的数字为43,printf本身返回值其实是打印字符的个数,也就是2 - 第二层:
printf("%d",printf("%d",2)),同理屏幕上打印2,printf函数本身返回1 - 最外层:
printf("%d",1),打印 1 - 所以最终结果为4321
其实这里要注意的点是,printf打印到屏幕上,这是函数本身的功能,链式访问考虑的是函数本身的返回值。
函数的声明和定义
单个文件
函数的声明
伪代码:
//函数声明
int is_leap_year(int);
int main()
{
is_leap_year(y);
...
return 0;
}
//函数的定义
int is_leap_year(int)
{
xxxxx
}
函数要先声明,后使用。
定义放在前面,编译器不会报错,那是因为定义也是一种特殊的声明
多个文件
一般情况下,函数的声明、类型的声明放在头文件中,函数的实现是放在源文件中。
main.c
#include <stdio.h>
#include "add.h"
int main()
{
int a = 10;
int b = 20;
scanf("%d %d",&a,&b);
int c = Add(a,b);
printf("%d\n",c);
return 0;
}
add.h
//存放函数的声明
int Add(int x,int y);
add.c
//存放函数的实现
int Add(int x,int y)
{
return x+y;
}
导入静态库
#pragma comment(lib,"add.lib")
static和extern
static修饰变量
学习static和extern之前,先铺垫几个概念:
作用域:是程序设计的概念,通常来说,一段程序代码中所用到的名字并不总是有效的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
- 局部变量的作用域是变量所在的局部范围。
- 全局变量的作用局是整个工程。
大括号内部定义的是局部变量,大括号外部定义的就是全局变量
生命周期:指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的时间段。
- 局部变量的生命周期是,进入作用域生命周期开始,出作用域生命周期结束
- 全局变量的生命周期是,整个程序的生命周期。
static
- 修饰局部变量
- 修饰全局变量
- 修饰函数
extern 用来声明外部符号
伪代码:
add.c
int g_val = 2023;
main.c
extern int g_val;
此时,main.c中就可以使用g_val这个全局变量了。
看如下代码:
void test()
{
int i = 0;
i++;
printf("%d ",i);
}
int main()
{
int i = 0;
for(i = 0;i<5;i++)
{
test();
}
return 0;
}
执行结果
在看一段代码
void test()
{
static int a = 0;
a++;
printf("%d ",a);
}
int main()
{
int i = 0;
for(i = 0;i<5;i++)
{
test();
}
return 0;
}
执行结果
int i进入函数创建,出函数即销毁,所以打印结果是5个1static修饰之后,虽然作用域没有变,但是生命周期发生了变化,即出函数时并没有对其进行销毁
生命周期边长的本质是什么?本来一个局部变量是存储在内存的栈区,但是被static修饰后存储到了静态区。全局变量也是存储在静态区。也就是说,static修饰后,生命周期与全局变量一样了。
使用建议:一个局部变量出了函数,还想保留其值时,就可以使用static来修饰。
再看一段代码:
Add.c 文件
int g_val = 2024; //定义了全局变量
main.c 文件
//声明外部符号
extern int g_val;
int main()
{
printf("%d\n",g_val);
return 0;
}
- 全局变量默认具有外部连接属性;也就是说在其他文件只要声明,就可以使用。
注意:static 修饰全局变量后,外部链接属性变成了内部链接属性,也就是作用域变了,只能在本源文件内使用,其他源文件无法使用了。
static修饰函数
add.c
int Add(int x,int y)
{
return (x + y);
}
main.c 文件
extern int Add(int x,int y);
int main()
{
int a = 10;
int b = 20;
int ret = Add(a,b);
printf("%d\n",ret);
return 0;
}
- 函数默认也具有外部连接属性
同理,加上static修饰,外部连接属性变成了内部连接属性。
static int Add(int x,int y)
{
return (x + y);
}
使用建议:一个函数指向在所在的源文件内部使用,不想背其他源文件是用,就可以使用static修饰。