C语言(三)函数与文件
C语言(三)函数与文件
1. 函数定义与声明
作用:将一段经常使用的代码封装起来,减少重复代码
函数的定义一般主要有5个步骤:
- 返回值类型:一个函数可以返回一个值。在函数定义中;
- 函数名:给函数起个名称;
- 参数列表:使用该函数时,传入的数据;
- 函数体语句:花括号内的代码,函数内需要执行的语句;
- return 表达式:和返回值类型挂钩,函数执行完后,返回相应的数据;
语法
// 未伪代码
返回值类型 函数名 (参数列表)
{
函数体语句;
return 表达式;
}
示例:定义一个加法函数,实现两个数相加
//函数定义
int add(int num1, int num2)
{
int sum = num1 + num2;
return sum;
}
- C 程序的执行是从
mian
函数开始的,如果main
函数中调用其他函数,那么在调用后返回到main
函数中,在main
函数中结束整个程序运行。 - 所有的函数都是平行的,即在定义函数时是分别进行的,并且相互独立。在 C 语言中函数不能嵌套定义,函数之间可以相互调用,但不能调用
main
函数。 - 函数的定义指对函数功能的确立,包括指定函数名、函数值类型、形参及其类型、函数体等,是一个完整的独立的函数单位。
- 函数的声明的作用是把函数的名字、函数类型及形参的类型、个数和顺序通知编译系统、以便在调用该函数时编译系统能正确识别函数并检查调用是否合法.
- 隐式声明:C 语言中有几种声明的类型名可以省略.例如,函数如果不显式地声明返回值的类型,那么它默认返回整型;使用旧风格声明函数的形式参数时,如果省略参数的类型,那么编译器默认它们为整型.然而,依赖隐式声明并不是好的习惯,因为隐式声明容易让代码的读者产生疑问:编写者是否是有意遗漏了类型名?还是不小心忘记了?显式声明能够清楚地表达意图!
函数的声明:
作用: 告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
- 函数的声明可以多次,但是函数的定义只能有一次
示例:
//函数的声明可以有多次
//函数的定义只能有一次
int getMax(int num1, int num2);
int getMax(int num1, int num2);
void test01()
{
int a = 10;
int b = 20;
int ret = getMax(a, b);
printf("较大的值为:%d\n", ret);
}
int main() {
test01();
system("pause");
return EXIT_SUCCESS;
}
//创建一个函数,两个整型数据比较,返回较大的值
int getMax(int num1, int num2)
{
return num1 > num2 ? num1 : num2;
}
函数参数返回值的形式
- 无参无返
- 有参无返
- 无参有返
- 有参有返
2. 源文件与头文件
- 一个 C 程序由一个或多个程序模块组成,每个程序模块作为一个源程序文件.对于较大的程序,通常将程序内容分别放在若干源文件中,再由若干源程序文件组成一个 C 程序.这样处理便于分别编写、分别编译,进而提高调试效率(考研复试有用).一个源程序文件可以为多个C程序共用。
- 一个源程序文件由一个或多个函数及其他有关内容(如命令行、数据定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位而不是以函数为单位进行编译的。
源文件和头文件本质上没有任何区别。有些类似于 java 中的接口文件实现类的文件的关系;
后缀为 .h 的文件是头文件,内含函数声明、宏定义、结构体定义等内容。
后缀为 .h 的文件是头文件,内含函数声明、宏定义、结构体定义等内容。
是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。这样分开写成两个文件是一个良好的编程风格。
编译器的工作:
- 预处理阶段;
- 词法与语法分析阶段;
- 编译阶段,首先编译成纯汇编语句,再将之汇编成跟 CPU 相关的二进制码,生成各个目标文件 (.obj文件);
- 连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息。(生成.exe文件)
在 C 语言的项目工程中,通常会自己定义一个头文件my_define.h
完成所有的声明,在各个相关的源程序模块中,实现对函数的定义,在main
函数中导入自定义的头文件和源程序文件;
3. C 工程文件
本案例主要以CLion为例进行讲解;
// my_define.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 调用标准库时使用尖括号
// 定义常量
const double PI = 3.1415926;
// 定义全局变量
int length;
int column;
// 函数声明
int add(int a, int b);
void print_message();
void print_msg();
在自定义的头文件中通常都会进行变量和函数的声明;
// func.c
// Created by 86152 on 2024/8/8.
//
#include "my_define.h" // 调用自定义的头文件使用 双引号;
void print_message() {
printf("This file is func.c .");
}
void print_msg() {
printf("================\n");
print_message(); // 此处进行的是函数的嵌套调用;
printf("================\n");
}
函数不能够嵌套定义,但是在函数内部可以调用别的函数,称作函数调用;
// main.c
#include <stdio.h>
#include "my_define.h" // 自定义的头文件 .h 使用双引号进行引用 ""
int add(int a, int b) {
return a + b;
}
int main() {
print_msg();
int a = 10;
int b = 20;
int c = add(a, b);
printf("%d", c); // 使用 %d 打印整数;
return 0;
}
补充:
在大型的 C 工程项目中常见的结构如下;
my_project/
├── include/ # 存放头文件
│ └── example.h
├── src/ # 存放源文件
│ ├── main.c
│ └── example.c
└── CMakeLists.txt # 项目配置文件
补充:
VS 工程中编写 C 的该工程文件 ;
目录与 CLion 中的基本无差别;
4. 函数的分类与调用
从用户角度来看,函数分为两种
- 标准函数:即库函数,这是由系统提供的,用户不必自己定义的函数,可以直接使用它们,如 printf 函数、scanf 函数。不同的 C 系统提供的库函数的数量和功能会有一些不同,但许多基本的函数是相同的。
- 自定义函数:用户自己定义的函数,用以解决用户专门需要;
从函数的形式看,函数分为如下两类。
-
无参函数:一般用来执行指定的一组操作,在调用无参函数时,主调函数不向被调函数传递数据;
类型标识符 函数名(形式参数列表){ 声明部分; 语句部分; }
-
有参函数:主调函数在调用被调用函数时,通过参数被调用函数传递数据;
类型标识符 函数名(形式参数列表){ 声明部分; 语句部分; }
5. 函数的递归调用
我们把函数自身调用自身的操作,成为递归函数,递归函数一定要有结束条件,否则会产生死循环;4
假设现在要求读者写一个程序来求数字 n 的阶乘。读者可能会觉得这很简单,写个for
循环就可以实现,然而,使用递归来实现更好一些,因为使用递归在解决一些问题时,可以让问题变得简单,降低编程的难度。比如接下来的题目:假如有n个台阶,一次只能上1
个台阶或2
个台阶,请问走到第n
个台阶有几种走法?为便于读者理解题意,这里举例说明如下:假如有3个台阶,那么总计就有3种走法:第一种为每次上1个台阶,上3次;第二种为先上2个台阶,再上1个台阶;第三种为先上1个台阶,再上2个台阶。具体实现请看如下代码;
// 求阶乘
int get_factorial(int num) {
if (1 == num) {
return 1; // 递归结束条件
}
// 函数自身的调用
return num * get_factorial(num - 1);
}
int main(){
// 使用值传递,求解 3 的阶乘;
int result = get_factorial(3);
printf("当前的阶乘是%d\n", result);
}
使用 VS 进行函数编写的时候可以使用中文进行显示;
// 走台阶
int get_step(int num) {
// 求台阶问题, 每次上台阶只有 1个或者 2个台阶;
if (num == 1) {
// 结束条件 1
return 1;
} else if (num == 2) {
// 结束条件 2
return 2;
}
// 函数自调用, 每次的步数相加;
return get_step(num - 1) + get_step(num - 2);
}
int main() {
int result = get_step(3);
printf("There are ways to climb the stairs %d\n", result);
return 0;
}
6. 全局变量与局部变量
在不同的函数之间传递数据时,可以使用的方法如下:
- 参数:通过形式参数和实际参数,使用的时候实参和形参要个数相等;
- 返回值:用 return 语句返回计算结果;
- 全局变量:外部变量;
全局变量示例
int g = 10;
void print_a(int a) {
printf("a=%d\n", a);
}
int main() {
{
int j = 5;
} // 局部变量的有效范围是离自己最近的 花括号;
printf("g=%d\n", g);
g = 5;
print_a(g);
return 0;
}
全局变量存储在哪里?如下图所示,全局变量i存储在数据段,所以main 函数和 print_a
函数都是可见的。全局变量不会因为某个函数执行结束而消失,在整个进程的执行过程中始终有效,因此工作中应尽量避免使用全局变量!在前几章中,我们在函数内定义的变量都称为局部变量,局部变量存储在自己的函数对应的栈空间内,函数执行结束后,函数内的局部变量所分配的空间将会得到释放。如果局部变量与全局变量重名,那么将采取就近原则,即实际获取和修改的值是局部变量的值。
-
内部变量
在一个函数内部定义的变量称为内部变量。它只在本函数范围内有效,即只有在本函数内才能使用这些变量,故也称局部变量。
局部变量的注意事项:
- 主函数中定义的变量只在主函数中有效,而不因为在主函数中定义而在整个文件或程序中有效。主函数也不能使用其他函数中定义的变量。
- 不同函数中可以使用相同名字的变量,他们代表不同的对象,互不干扰;
- 形式参数也是局部变量;
- 在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也称分程序或程序块。上述代码中的
int j=5
就是如此,只在离自己最近的花括号内有效,若离开花括号,则在其下面使用该变量会造成编译不通。 - 注意一个细节,
for
循环的小括号内定义的int i
,在离开for
循环后,是不可以再次使用的。
-
外部变量
函数之外定义的变量称为外部变量。外部变量可以为本文件中的其他函数共用,它的有效范围是从定义变量的位置开始到本源文件结束,所以也称全程变量。
全局变量需注意如下几点:
- 全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。
- 使用全局变量过多会降低程序的清晰性。在各个函数执行时都可能改变外部变量的值,程序容易出错,因此要有限制地使用全局变量(初试时尽量不用)。
- 因为函数在执行时依赖于其所在的外部变量,如果将一个函数移到另一个文件中,那么还要将有关的外部变量及其值一起移过去,然而,如果该外部变量与其他文件的变量同名,那么就会出现问题,即会降低程序的可靠性和通用性。C语言一般要求把程序中的函数做成一个封闭体,除可以通过
实参→形参
的渠道与外界发生联系外,没有其他渠道。
度过大难,终有大成。
继续努力,终成大器!