函数计划分成3篇来讲: 上篇是函数的基础篇,中篇是函数的进阶篇,下篇是函数的高级篇。
在上篇文章说道,为了符合函数使用的标准,通常我们会按照行先声明、后使用的原则操作函数。不过同时把太多的内容写在一个文件中,后期管理起来就变得非常困难。应该采用模块化的思维,把描述一类功能的函数放在一起管理。
一、多文件编程
在编写函数的时候,我们知道了有函数声明,和函数定义(实现),为了使得管理起来更加方便,通常我们会把函数的声明、宏定义、结构体和枚举 等放在头文件(以.h后缀名结尾)中,而把函数的定义(具体实现)放在源文件(以.c 后缀名结尾)中,并且为了使源文件能够访问到头文件中的声明,在源文文件中还会使用 #include 指令来包含头文件。
1.1 多文件编程示例
下面是一段计算器包含加减乘除的示例代码,把4个功能函数声明到 calc.h 头文件中,而4个功能函数的具体实现放在calc.c 源文件中。未来如果还有其他的计算功能呢,则只需要在头文件中添加声明,然后再源文件中添加具体实现即可。
- 头文件: : calc.h
#ifndef CALC_H
#define CALC_H
// 加法函数声明
int add(int a, int b);
// 加法函数声明
int subtract(int a, int b);
// 乘法函数声明
int multiply(int a, int b);
// 除法函数声明
float divide(int a, int b);
#endif // CALC_H
- 源文件:: calc.c
#include "calc.h"
// 加法函数实现
int add(int a, int b) {
return a + b;
}
// 减法函数实现
int subtract(int a, int b) {
return a - b;
}
// 乘法函数实现
int multiply(int a, int b) {
return a * b;
}
// 除法函数实现
float divide(int a, int b) {
return b ? (float)a / b :0
}
- 入口调用文件:: main.c
#include <stdio.h>
#include "calc.h"
int main() {
// 使用 calc.h 中的函数
int result_add = add(5, 3);
int result_subtract = subtract(5, 3);
int result_multiply = multiply(5, 3);
float result_divide = divide(5, 3);
// 输出计算结果
printf("加法运算结果: %d\n", result_add);
printf("减法运算结果: %d\n", result_subtract);
printf("乘法运算结果: %d\n", result_multiply);
printf("除法运算结果: %.2f\n", result_divide);
return 0;
}
1.2 头文件细节解释
1.2.1 防止头文件重复包含
几乎在每一个头文件中,都会在开始和末尾有这几句代码,目的是为了防止头文件重复包含。
#ifndef :
全称是 if not define 用来判定是否有定义过宏 CALC_H , 宏可以理解为一种标记,代号。如果是首次包含这个头文件,那么则表示没有定义过。若头文件已经被包含过,则 if no define 将不会成立,也就不再把头文件中的内容包含过去。
#define:
熟悉C语言的小伙伴都知道这个指令是用来定义宏的,正好联系上面的指令#ifndef 。 如果以前没有定义过宏 CALC_H , 那么就定义这个宏,接着把下面的代码包含过去。若这个宏在当前这个源文件中已经存在过,则不会再包含该头文件的内容
#endif:
作为判断结束的边界,一般可以把它看成是头文件的结尾。
1.2.2 导入头文件写法
注意观察main.c文件,会发现在导入头文件的时候,有两种写法,分别使用 <> 和 "" 来引入头文件,它们两者有什么区别呢?
<> :
适合用来导入编译器系统提供头文件,这种文件一般位于编译器文件夹中,比如:比如: <stdio.h> 、<stdlib.h>
"" :
适合用来导入我们自己写的头文件,这些头文件一般就在当前工程的目录中。
二、函数传参
函数在声明的时候可以要求调用者传递进来参数,但是在C语言和C++的世界中,传递进来的普通参数其实是原来数据的一份备份而已。这种方式的出发点是函数内部无论如何操作这个参数都不会导致外部的原始数据发生变化,很好的起到了数据隔离的作用。但是假设我们真心希望在函数内部操作,从参数传递进来的这份参数的数据呢,该怎么做?
2.1 函数传参(值传递)
下面我们通过一段示例,定义一个函数用来修改传递进来的参数的值,然后在函数外部和函数外部都各自打印该数据
#include <stdio.h>
// 函数声明
void modifyValue(int x);
int main() {
int number = 5;
// 调用函数
modifyValue(number);
// 打印原始值
printf("函数外部打印: %d\n", number); // 5
return 0;
}
// 函数定义
void modifyValue(int x) {
// 在函数内部修改参数的值
x = 10;
printf("函数内部打印: %d\n", x); // 10
}
柯南有话说:
- 运行程序后会发现根本改变不了!!函数内部打印的是10 ,函数外部打印的还是原来的数值:5.
- 这原因就是C语言默认采用的是值拷贝的方式传递数据,在函数内部拿到的仅仅是一个数据副本而已,就算在里面怎么折腾,也不影响外部的数据
- 我们试图拆解函数的参数,让这个事情变得更加容易理解: 当我们把number=5赋值给修改函数的时候, 其实就等价于存在这样的代码: int x= number。 看到了吧,函数内部使用一个局部变量 x 承接住了 5这个数字,修改的只是这个x 的数据而已。
2.2 函数传参(指针)
如果我们需要在函数内部修改外部数据的话,指针就不得不摆上台面了,它真的太重要了。未来参数不传递普通的变量类型了,而是改为传递指针类型,这就要求调用不得不把地址传递进来,在函数内部有了地址,想改成什么数据就改成什么数据。
#include <stdio.h>
// 函数声明
void modifyValue(int * x);
int main() {
int number = 5;
// 调用函数
modifyValue(&number);
// 打印原始值
printf("函数外部打印: %d\n", number); // 10
return 0;
}
// 函数定义
void modifyValue(int * x) {
// 在函数内部修改参数的值
*x = 10;
printf("函数内部打印: %d\n", *x); // 10
}
柯南有话说:
1. 当把函数的参数修改为传递指针之后,运行程序会发现函数内部和函数外部打印的都是10,这是函数内部使用了一个 int * x 接住了从外部传递进来的变量number的地址,因此在函数内部解引用修改值,其实修改的外部的数据。
2. 或许有的小伙伴会有一个疑问: 为什么需要在函数内部修改函数外部的值呢?其实换个角度想一下就可以了,我们一直想着是在函数内部修改函数外部的值,如果把它看成是在函数内部给函数外部返回数据呢?当然我猜你会跟我说,函数的返回值不是可以做这个事情么?是的,函数的返回值确实是用来返回的数据的。但是假设我们希望这个函数在运行结束之后,需要给我们返回运行的各种分支代号(比如返回0表示正常执行成功,返回-1表示条件不允许),同时还使用指针来设置数据。则调用者在处理数据的时候,就可以先根据返回值判断是否正确执行成功,再去操作指针数据。说起来有绕~~需要小伙伴们多想一想~~~把这个想通,编程思维将更上一个台阶。【这段话太重要啦~~~】。
2.3 函数传参(数组)
在函数可传递的参数里面,除了普通数据类型、指针之外,其实有时候我们也会传递数据,但是传递数据时需要提防一些特殊情况,否则有可能因为使用不当而出现问题。
#include <stdio.h>
// 函数声明,接收整数数组的指针和数组长度作为参数
void printArray(int *arr, int length);
int main() {
int numbers[] = {1, 2, 3, 4, 5};
// 调用函数,传递数组的地址和数组长度
printArray(numbers, 5);
return 0;
}
// 函数定义,接收整数数组的指针和数组长度作为参数
void printArray(int *arr, int length) {
// 遍历数组并打印每一个元素
for (int i = 0; i < length; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
柯南有话说:
上述代码设计的是在printArray函数中打印一个由外部传递进来的数组的每一个元素。在声明数组的时候,其实可以写 int arr[5] 或者 int arr[] ,但是考虑到当调用函数的时候传递的只是数组的名字而已。而且我们知道数组名字永远首元素的地址,所以大多数程序员喜欢吧参数声明为指针类型,以便接收首元素地址。
也正因为数组名字单独使用的时候,表示的是首元素的地址,所以将无法在函数内部计算出数组的长度,因为传递进来的只是一个指针而已。这通常需要协同第二个参数用来传递数组的长度,这样才能在函数内部遍历数组。
虽然参数传递过来的是数组的名字(首元素的地址),但是在函数内部还是依然可以使用 【下标】的方式来操作数组中的元素,其实这是因为它的背后使用指针偏移的方式取找到每一个元素。
三、总结
好啦~快乐的时光总是如此短暂,又到时间说拜拜啦~~~~
进阶篇虽然只有多文件编程和函数传参两节内容,多文件编程首度给大家明确了未来写代码的方向。如果是功能模块有很多,则需要去拆解它们,以模块化的思维去搭建整个程序。函数传参部分让我认识到C语言传递参数默认是采用值传递方式,如果需要修改或者获取函数内部的数据,则需要传递指针。数组在函数参数种出现的比例也不容小觑,它虽然给的是首元素的地址进来,但是数组的特性(一串连续的地址存放数据),使得在函数内部还是很容易操作数组的。
此篇在内容在精而不在多。在这两块内容里面有许多需要大家开动脑筋才能有顿悟的感觉,我并不希望大家为了完成任务式的学习,许多时候多想一想,若能想通一个问题,那么你会喜欢上这种感觉。那么这种茅塞顿开的喜悦之感给你带来的正反馈,又将激励着你去完成下一个 “顿悟”。久而久之,编程思想就上来了。
四、思考
先来回答上一篇布置的思考题吧。
1. 函数为什么要先声明,后使用呢?
函数之所以要先声明后使用,主要是为了配合编译器自上而下编译的规则,如果没有在前面先告诉编译器函数的基本信息,那么在遇见函数的代码,将有可能报错。再者把函数的声明和定义拆分开,有助于我们未来分门别类的管理代码。(声明放在头文件、实现放在源文件)
2. 能否出现同名的函数呢?
在C语言里面不允许出现同名函数,虽然在其他的语言有函数重载的概念(函数名相同,参数列表不同),但是C语言里面默认是采用函数名称作为函数的唯一标识,不使用参数信息加以辅助,所以只要是同名的函数,都会出现提示: redefinition 的错误。
按照惯例这里也放几道思考题吧。
- 多文件编程好处是什么?
- 函数参数传递指针有哪些应用场景?
- 为什么在函数内部无法计算数组的长度?