5 min to read
椋鸟C语言笔记#12
函数、参数、多文件协作、static与extern
萌新的学习笔记,写错了恳请斧正。
函数的概念
完成特定功能的小段代码(子程序)。将一个大的程序拆解为实现不同功能的函数,有利于提高开发效率,也方便部分功能的反复使用。
一般函数分两类:
- 库函数
- 自定义函数
库函数
标准库
C 语言规定了一些常用的函数(比如 printf、scanf)的标准,即标准库。然后各个编译器厂商按照标准给出这些函数的实现,即库函数。这些函数不需要程序员自己实现,只要会用即可。这大大提高了开发效率。
这些库函数根据功能的划分,在不同的头文件中进行了声明。如果想要使用,只要包含头文件即可。
C 语言自带头文件:头文件
自定义函数
自定义函数很重要,它赋予了代码更多的可能性
自定义函数的语法格式
returntype func_name(形参)
{
...
}
- returntype 是函数返回值的类型,可以是任何数据类型,如果写 void 则不返回值
- func_name 是函数名,自定义
- 括号里放形式参数,是函数的 “原材料”,可以是各种类型的数据,如果写 void 则函数没有参数
- 花括号里放函数体,是具体的 “加工”、计算的过程
比如说,我们可以写一个加法函数并在主函数中使用:
#include <stdio.h>
int Add(int x, int y)
{
int sum = x + y;
return sum;
}
int main()
{
int a = 0, b = 0;
scanf("%d%d", &a, &b);
printf("%d", Add(a, b));
return 0;
}
其中,return 是确定函数返回值的关键字,下面会讲
上方加法函数代码也可简化为:
int Add(int x, int y)
{
return x + y;
}
形参与实参
实参
在函数使用时我们把函数的参数分为形参与实参
上方示例中主函数中向 Add 函数传递的 a、b 即为实参,是真实传递给函数的参数
形参
上方代码中定义函数时,函数名后面括号里的 x 与 y 即为形式参数
形式参数只在形式上存在,不会向内存申请空间
只有在运行时函数被调用时,为了存放实参传递过来的值,才会向内存申请一块临时的空间——这个过程被称为形参的实例化
形参与实参的关系
在程序运行到调用函数时,就会在内存开辟一块空间,并复制一份实参的值在函数中使用
所以说形参是实参的一份临时拷贝
return 语句
- return 后面可以是一个数值,也可以是一个表达式(如果是表达式就先执行后返回)
- return 后面可以什么都没有,那么直接结束函数并不返回值
- 如果 return 后面的值的类型与函数的返回值类型不一致,就会自动进行强制类型转换
- return 语句将直接结束函数,后面的代码不会执行(可以理解为用于函数的 break)
- 如果函数中存在分支语句,应使每一条分支后都有 return 返回,否则编译不通过
数组作为函数的参数
如果将数组作为参数传递给函数,那么情况会有所不同:
- 函数的形式参数与传递过来的实参个数一致
- 实参是数组时,形参可以写成数组形式
- 形参如果是一维数组,数组大小不用写,写了也会被忽略
- 形参如果是二维数组,行可以省略,但是列不可以
- 数组传参时,形参不会创建新的拷贝,所以函数中对数组的操作会被保留
嵌套调用与链式访问
嵌套调用
每一个函数组件施行某个特定功能,可以看做一个零件
而函数中也可以调用函数,甚至调用自身,以达成复杂目的
但是函数可以嵌套调用但不能嵌套定义
而且注意不要互相调用造成死循环
链式访问
链式访问就是将一个函数的返回值作为另一个函数的参数
比如说上方举例的 Add 函数的返回值就作为了 printf 函数的参数
这里就有一道有趣的题:
printf("%d", printf("%d", printf("%d", 123)));
上方代码的输出结果是什么?
是:12331
为什么?
先执行了最里层的 printf 打印 123,而 printf 的返回值是打印字符的长度,则中间的 printf 接收到 123 的长度 3 并打印出来,最终外层的 printf 接受 3 的长度 1 并打印出来
类似的,下方代码将输出 123 4 2 (3、4、2 后有空格)
printf("%d \n", printf("%d ", printf("%d ", 123)));
函数的声明与定义
单文件
一般我们直接就把函数在调用函数的函数前面写出来用了
如上方举例的 Add 函数的定义直接写在调用其的主函数(main)前
如果将函数的定义放在调用它的函数的后面,就会报警告了,如下方示例:
#include <stdio.h>
int main()
{
int a = 0, b = 0;
scanf("%d%d", &a, &b);
printf("%d", Add(a, b));
return 0;
}
int Add(int x, int y)
{
int sum = x + y;
return sum;
}
编译器在编译时从上往下扫描结果突然遇到没见过的函数,不知道这个函数的返回类型,就会警告
像解决这个问题,除了把函数挪到前面,也可以直接在前面对函数进行声明
函数的声明需要包括函数名、返回类型、和参数
比如说,把上面的代码改成这样就能正常运行了:
#include <stdio.h>
int Add(int x, int y);
int main()
{
int a = 0, b = 0;
scanf("%d%d", &a, &b);
printf("%d", Add(a, b));
return 0;
}
int Add(int x, int y)
{
int sum = x + y;
return sum;
}
由于函数声明只需要告诉编译器参数的类型,不需要定义一个形参,所以也可以直接写成这样:
int Add(int, int);
综上,函数必须要先声明再使用(函数的定义本身也是一种声明,不能重定义但是可以重声明)
多文件
在企业中,程序员实际编写代码时,不会把所有代码放在一个文件中,而是将代码按照功能拆分在多个文件中。这即方便了程序员之间的协作、增强了代码的泛用性,也能起到保密的作用(需要结合其他内容实现)
一般来说函数的声明与类型的声明会放在头文件(.h)中,而函数的实现放在源文件(.c)中
比如我们把上方 Add 的实例代码拆分:
add.h
//加法声明
int Add(int, int);
add.c
//加法定义
int Add(int x, int y)
{
return x + y;
}
test.h
#include <stdio.h>
#include "add.h"
int main()
{
int a = 0, b = 0;
scanf("%d%d", &a, &b);
printf("%d", Add(a, b));
return 0;
}
这样我们依旧能正常运行编译出来的可执行程序
注意这样多文件协作需要在 test 这里包含我们自己写的头文件 add.h
包含自己的头文件应该用双引号而不是尖括号!!!
注意,头文件可以包含头文件,如果我们在 add.h 前面加上包含 stdio.h
那我们就不要在 test.c 中包含 stdio.h 了,直接包含 add.h 就同时包含了 stdio.h
static 与 extern
static 与 extern 都是 C 语言中的关键字
static
static 是静态的意思,可以用来修饰局部变量、全局变量、函数
static 修饰局部变量
static 修饰局部变量改变了变量的生命周期,本质上改变了变量的存储类型。将本身存储在栈区的局部变量改为存储在静态区。这样,被修饰的变量就与全局变量的生命周期一样了,直到程序结束才会被销毁内存回收。但是 static 不会改变变量的作用域,这点与全局变量不同。
如下方代码的运行结果为 1 2 3 4 5 ,而不是 1 1 1 1 1
#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;
}
如果一个变量出了函数我们还想保留其值等下一次进入函数使用,就可以用 static 修饰
static 修饰全局变量
全局变量本身可以在整个工程中被调用(需要 extern 声明),但是被 static 修饰后就只能在本文件中使用了,如果被其他文件声明只会报链接错误
static 修饰函数
与 static 修饰全局变量相同,被 static 修饰的函数将不能在其他文件中调用。
extern
extern 用来声明外部符号,如果在 a 文件定义了一个全局的符号,想在 b 文件使用,就要用 extern 声明,然后再使用,如:
a.c
int val = 114514;
test.c
#include <stdio.h>
extern int val;
int main()
{
printf("%d\n", val);
return 0;
}
Comments