目录
1. 函数的概念
2. 库函数
2.1 标准库和头文件
2.2 库函数的使用方法
2.2.1 功能
2.2.2 头文件包含
2.2.3 库函数文档的一般格式
3. 自定义函数
3.1 函数的语法形式
4. 形参和实参
4.1 实参
4.2 形参
4.3 实参和形参的关系
5. return 语句
6. 数组做函数参数
7. 嵌套调用和链式访问
7.1 嵌套调用
7.2 链式访问
8. 函数的声明和定义
8.1 单个文件
8.2 多个文件
8.3 多文件书写的好处
8.3.1 如何创建静态库分享代码
编辑
8.4 static 和 extern
8.4.1 static 修饰局部变量
8.4.2 static 修饰全局变量
8.4.3 static 和 extern 修饰函数
1. 函数的概念
数学中我们其实就⻅过函数的概念,⽐如:⼀次函数 y=kx+b ,k和b都是常数,给⼀个任意的x,就得到⼀个y值。
其实在C语⾔也引⼊函数(function)的概念,有些翻译为:⼦程序,⼦程序这种翻译更加准确⼀些。C语⾔中的函数就是⼀个完成某项特定的任务的⼀⼩段代码。这段代码是有特殊的写法和调⽤⽅法的。C语⾔的程序其实是由⽆数个⼩的函数组合⽽成的,也可以说:⼀个⼤的计算任务可以分解成若⼲个较⼩的函数(对应较⼩的任务)完成。同时⼀个函数如果能完成某项特定任务的话,这个函数也是可以复⽤的,提升了开发软件的效率。
在C语⾔中我们⼀般会⻅到两类函数:
- 库函数
- 自定义函数
2. 库函数
2.1 标准库和头文件
C语⾔标准中规定了C语⾔的各种语法规则,C语⾔并不提供库函数;C语⾔的国际标准ANSI C规定了⼀些常⽤的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。
各种编译器的标准库中提供了⼀系列的库函数,这些库函数根据功能的划分,都在不同的头⽂件中进⾏了声明。比如微软的msvc、苹果的clang、开源组织gcc的编译器,都有他们的开发人员根据 C 语言标准创建自己的 标准库函数。
库函数相关头文件链接:https://zh.cppreference.com/w/c/header
2.2 库函数的使用方法
库函数的学习和查看工具很多,比如:
C/C++ 官方的链接:https://zh.cppreference.com/w/c/header
常用这个 cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/
举例:sqrt
double sqrt (double x);
//sqrt 是函数名
//x 是函数的参数,表⽰调⽤sqrt函数需要传递⼀个double类型的值
//double 是返回值类型 - 表⽰函数计算的结果是double类型的值
2.2.1 功能
Compute square root 计算平⽅根
Returns the square root of x.(返回平⽅根)
2.2.2 头文件包含
库函数是在标准库中对应的头⽂件中声明的,所以库函数的使⽤,务必包含对应的头⽂件,不包含是可能会出现⼀些问题的。就是说,使用某个库函数,必须对应的在前边包含它所对应的头文件。
例如:
#include <stdio.h>
#include <math.h>
int main()
{
double d = 16.0;
double r = sqrt(d);
printf("%lf\n", r);
return 0;
}
2.2.3 库函数文档的一般格式
1. 函数原型
2. 函数功能介绍
3. 参数和返回类型说明
4. 代码举例
5. 代码输出
6. 相关知识链接
3. 自定义函数
3.1 函数的语法形式
其实⾃定义函数和库函数是⼀样的,形式如下:
ret_type fun_name(形式参数)
{
}
- ret_type 是函数返回类型
- fun_name 是函数名
- 括号中放的是形式参数
- {}括起来的是函数体
我们可以把函数想象成⼩型的⼀个加⼯⼚,⼯⼚得输⼊原材料,经过⼯⼚加⼯才能⽣产出产品,那函数也是⼀样的,函数⼀般会输⼊⼀些值(可以是0个,也可以是多个),经过函数内的计算,得出结果。也可以把函数看作是一个黑箱子,有输入、输出,当你输入一个东西,就会输出一个。
- ret_type 是⽤来表⽰函数计算结果的类型,有时候返回类型可以是 void ,表⽰什么都不返回
- fun_name 是为了⽅便使⽤函数;就像⼈的名字⼀样,有了名字⽅便称呼,函数有了名字⽅便调⽤,所以函数名尽量要根据函数的功能起的有意义。
- 函数的参数就相当于,⼯⼚中送进去的原材料,函数的参数也可以是 void ,明确表⽰函数没有参数。如果有参数,要交代清楚参数的类型和名字,以及参数个数。
- {}括起来的部分被称为函数体,函数体就是完成计算的过程。
举例:
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x+y;
return z;
}
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;
}
函数的参数部分需要交代清楚:参数个数,每个参数的类型是啥,形参的名字叫啥。
4. 形参和实参
4.1 实参
以上述的代码为例:
调⽤ Add 函数时,传递给函数的参数 a 和 b,称为实际参数,简称 实参。实际参数就是真实传递给函数的参数。
4.2 形参
在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称 形参。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,⽽不去调⽤的话, Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调⽤的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。
4.3 实参和形参的关系
实参是传递给形参的,他们之间是有联系的:
① 形参和实参各自是独立的内存空间
② 形参是实参的一份临时拷贝
③ 形参的修改不会影响实参
我们在调试的可以观察到,x 和 y 确实得到了 a 和 b 的值,但是 x 和 y 的地址和 a和b 的地址是不⼀样的,所以我们可以理解为形参是实参的⼀份临时拷⻉。
5. return 语句
在函数的设计中,函数中经常会出现return语句,这⾥讲⼀下return语句使⽤的注意事项。
- return 后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
- return 后边也可以什么都没有,直接写 return;这种写法适合函数返回类型是 void 的情况。
- return 返回的值和函数返回类型不⼀致,系统会自动将返回的值隐式转换为函数的返回类型。
- return 语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
- 如果函数中存在 if 等分支的语句,则要保证每种情况下都有 return 返回,否则会出现编译错误。
最有一项说明:
int test(int n)
{
if(n % 2 == 1)
return 1;
else
return 0;
}
也就是说,保证传入的参数每种情况都有对应的返回值,否则会报错;比如没有else,传入2,岂不是没有返回值,系统会认为出错了。
6. 数组做函数参数
在使⽤函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进⾏操作。⽐如:写⼀个函数对将⼀个整型数组的内容,全部置为-1,再写⼀个函数打印数组的内容。
#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;
}
数组作为参数传递给了set_arr 和 print_arr 函数了,那这两个函数应该如何设计呢?
这⾥我们需要知道数组传参的⼏个重点知识:
- 函数的形式参数要和函数的实参个数匹配
- 函数的实参是数组,形参也是可以写成数组形式的
- 形参如果是⼀维数组,数组大小可以省略不写
- 形参如果是⼆维数组,行以省略,但是列不能省略
- 数组传参,形参是不会创建新的数组的
- 形参操作的数组和实参的数组是同⼀个数组
根据上述的信息,我们就可以实现这两个函数:
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");
}
子程序中的数组参数可以不写数组大小;这里的 sz 和上边 main 中的 sz 是不同的存储空间;但数组传参,即使形参名字不同,但形参、实参是同一个数组。
7. 嵌套调用和链式访问
7.1 嵌套调用
嵌套调⽤就是函数之间的互相调⽤,也正是因为函数之间有效的互相调⽤,最后写出来了相对⼤型的程序。
假设我们计算某年某⽉有多少天?如果要函数实现,可以设计2个函数:
• is_leap_year():根据年份确定是否是闰年
• get_days_of_month():调⽤is_leap_year确定是否是闰年后,再根据⽉计算这个⽉的天数
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
未来的稍微⼤⼀些代码都是函数之间的嵌套调⽤,但是函数是不能嵌套定义的。
7.2 链式访问
所谓链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
printf函数返回的是打印在屏幕上的字符的个数。
上⾯的例⼦中,我们就第⼀个printf打印的是第⼆个printf的返回值,第⼆个printf打印的是第三个
printf的返回值。
第三个printf打印43,在屏幕上打印2个字符,再返回2
第⼆个printf打印2,在屏幕上打印1个字符,再放回1
第⼀个printf打印1
所以屏幕上最终打印:4321
高内聚低耦合:尽量让模块与模块之间的功能独立,但并不是说不能调用函数。
8. 函数的声明和定义
8.1 单个文件
#include <stido.h>
//函数的声明:
int is_leap_year(int y);
//函数的定义:
//判断⼀年是不是闰年
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;
}
在单个文件中,函数的调用之前,一定要在调用之前进行函数的声明,函数的定义也是一种特殊的声明,如果函数定义放在调用之前也是可以的。 但函数定义放在了调用之后,必须在调用之前进行函数声明。声明函数只要交代清楚:函数名,函数的返回类型,函数参数。函数的调用一定要满足,先声明后使用。
8.2 多个文件
⼀般情况下,函数的声明、类型的声明放在头⽂件(.h)中,函数的实现是放在源⽂件(.c)⽂件中。如下:
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;
}
运行结果:
add.c 文件中其实也可以不用写 #include "add.h",但是也要分情况,如果add.h中有一些宏定义,add.c 中也要用到,就要写 #include "add.h"
8.3 多文件书写的好处
1、逻辑清晰
2、方便多人协同
3、适当隐藏代码(静态库)比如,别人需要你的代码功能,可以调用,但你又不想暴露你的源代码,所以可以用以下方法创建静态库,分享代码:
举例:比如你写了一个很牛逼的加法代码,你想分享给别人,只用其功能,而不能查看其源代码,怎么办?这个问题,面试时可能会被问到
程序员可以选择,只把文件的头文件和源文件编译后产生的文件给到你,这样就不会暴露你的源代码。
8.3.1 如何创建静态库分享代码
① 找到你要分享的代码文件,add.c 和 add.h 文件,拷贝一份
② 创建一个空的工程项目命名为 add
③ 再把 add.c 和 add.h 文件拷贝到该空工程项目下
④ 添加到工程项目中,注意这个工程是无法运行的,因为没有main函数,无法编译成可执行程序,但可以编译成静态库
⑤ 但是此时可以 右键工程名字,找到属性,常规中,配置类型点击静态库
⑥选择静态库
⑦ 然后,ctrl+f5,编译加运行,无法生成可执行程序,但可以生成add.lib,静态库
在 add文件夹下的 Debug文件下可以找到该文件 add.lib文件
然后可以把该 add.lib文件和 add.h文件卖给别人;
⑧ 拿到 add.lib文件和 add.h文件 该如何用呢,放到自己工程目录下
打开工程,把买回来的头文件添加进去
此时还是无法运行的,需要导入静态库,这时候就可以编译运行,但要注意上边的Debug是 32位还是64位的,如果你的静态库生成时是64位,此时添加静态库后,运行环境需要改为 64位的。x86是 32位的。
8.4 static 和 extern
static 和 extern 都是C语⾔中的关键字。
static 是 静态的 的意思,可以⽤来:
• 修饰局部变量
• 修饰全局变量
• 修饰函数
extern 是⽤来声明外部符号的。
作⽤域(scope)是程序设计概念,通常来说,⼀段程序代码中所⽤到的名字并不总是有效(可⽤)的,⽽限定这个名字的可⽤性的代码范围就是这个名字的作⽤域。
1. 局部变量的作⽤域是变量所在的局部范围。
2. 全局变量的作⽤域是整个⼯程(项⽬)。
⽣命周期指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的⼀个时间段。
1. 局部变量的⽣命周期是:进⼊作⽤域变量创建,⽣命周期开始,出作⽤域⽣命周期结束。
2. 全局变量的⽣命周期是:整个程序的⽣命周期。
8.4.1 static 修饰局部变量
{
代码块,代码块里创建的变量只能在代码块中使用
}
对⽐代码1和代码2的效果,理解 static 修饰局部变量的意义。
代码1的 test 函数中的局部变量 i 是每次进⼊ test 函数先创建变量(⽣命周期开始)并赋值为0,然后++,再打印,出函数的时候变量⽣命周期将要结束(释放内存)。
代码2中,我们从输出结果来看,i 的值有累加的效果,其实 test 函数中的 i 创建好后,出函数的时候是不会销毁的,重新进⼊函数也就不会重新创建变量,直接上次累积的数值继续计算。i的作用域没变,只能在该函数中使用。
结论:static 修饰局部变量改变了变量的⽣命周期,⽣命周期改变的本质是改变了变量的存储类型,本来⼀个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是⼀样的,⽣命周期就和程序的⽣命周期⼀样了,只有程序结束,变量才销毁,内存才回收。但是作⽤域不变的。
使⽤建议:未来⼀个变量出了函数后,我们还想保留值,等下次进⼊函数继续使⽤,就可以使⽤static修饰。
8.4.2 static 修饰全局变量
extern 是⽤来声明外部符号的,如果⼀个全局的符号在A⽂件中定义的,在B⽂件中想使⽤,就可以使⽤ extern 进⾏声明,然后使⽤。
代码1正常,代码2在编译的时候会出现链接性错误。
结论:⼀个全局变量被static修饰,使得这个全局变量只能在本源⽂件内使⽤,不能在其他源⽂件内使⽤。本质原因是全局变量默认是具有外部链接属性的,在外部的⽂件中想使⽤,只要适当的声明就可以使⽤;但是全局变量被 static 修饰之后,外部链接属性就变成了内部链接属性,只能在⾃⼰所在的源件内部使⽤了,其他源⽂件,即使声明了,也是⽆法正常使⽤的。
使⽤建议:如果⼀个全局变量,只想在所在的源⽂件内部使⽤,不想被其他⽂件发现,就可以使⽤static修饰。
8.4.3 static 和 extern 修饰函数
代码1是能够正常运⾏的,但是代码2就出现了链接错误。
其实 static 和 extern 修饰函数和 static 和 extern 修饰全局变量是⼀模⼀样的,⼀个函数在整个⼯程都可以使⽤,被static修饰后,只能在本⽂件内部使⽤,其他⽂件⽆法正常的链接使⽤了。
本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个⼯程中只要适当的声明就可以被使⽤。但是被 static 修饰后变成了内部链接属性,使得函数只能在⾃⼰所在源⽂件内部使⽤。
使⽤建议:⼀个函数只想在所在的源⽂件内部使⽤,不想被其他源⽂件使⽤,就可以使⽤ static 修饰。