个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)

前言个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

C语言编程又称模块化设计,讲的就是对于一个程序而言,每一个功能都要类似于独立的实现,就像一个个板块,需要的时候拿出来即可。模块化的设计思想是一个程序员必不可缺少的思想。
如果有不明白的地方,或者想讨论的题目,欢迎大家找我QQ:482999194个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

这是我的gitee地址,每日更新一些有趣的题目等:濡白记录一些有趣题目的小仓库
目录
函数类型?
【个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)】main主函数
库函数?
自创函数?
函数使用?
函数类型与函数的返回值?
形参与实参?
传址调用与传值调用?
函数设计要求?
高内聚低耦合?
好的函数名?

函数类型个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片
说到模块化设计,就不得不提到函数。什么叫函数,就像高中数学中提到的,函数的定义就是映射,对于C语言而言,函数的意思就是,一系列固定的操作,或者说过程。
main主函数个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

main主函数是一个程序的入口,也就是说,所有的程序将从主函数进入,然后顺序结构执行。对于任何一个项目,只能有一个主函数,这也就是很多同学初学C语言的时候遇到的一个bug,往往写完一个程序之后接着打开了另一个源文件创建了一个新的main主函数,又不把原先的主函数删除或者注释掉,所以也就运行不了啦!
库函数个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

顾名思义,库函数就是库里面的函数。什么叫库呢?就是已经存在的一个仓库一样的东西。像printf这类函数,很多人都会用到,那么为了防止每个人的printf函数不一样,导致你的程序没法在我的电脑上运行,C语言本身附带了一些仓库,这些仓库就储存了一些平时经常遇到的函数,这些就是库函数。
而想要从库中用函数,首先我们就需要让我们的程序包含这些库,这也就是我们平时写程序基本上都要带上这样一句话:
#include

这个语句的意思就是告诉系统,我的这个程序包含了stdio.h这个仓库,由此我们才能用这个仓库里的库函数。同样的,对于一些我们不需要用到的库,我们自然也就不需要引用了,就像你去上数学课,背上一本近代史课本当时是不必要的,不仅不用,而且背在身上也是一种负担。
自创函数个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

但是光有库函数是不够的,比如存在一个库函数是将以一个数字×2,那么如果你是想要×3+2呢?又或者是×65+52呢?又或是......所以说,库函数往往不能满足我们写程序时的一些个性化需求,这个时候就需要我们自己创建我们自己需要的函数了。而这类函数就被我们称之为自创函数。
函数使用个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片
函数类型与函数的返回值个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

说到函数,大家最先可能产生疑惑的就是为什么是int main(),而不是其他的float main之类的,还有为什么所有的main函数最后都需要加上一句return 0,为什么不是return 1 呢?这就要说到函数类型与函数的返回值了。
就像之前说的,函数就是一个映射,通过一系列操作得到某个值,例如给定一个字母,得到下一个字母,那么结果就是字母类型,如果给定一个数得到他的两倍,结果就是数字类型。同样的C语言中的函数也是一样的,函数类型就是告诉计算机最后得到的会是什么类型的结果,而函数的返回值也是一样的道理,已经知道了最后的结果是字母类型,就肯定不能得到一个数字类型。也就是说 函数的返回值是在声明函数的时候就已经确定好的。
函数的声明与定义 个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

我们已经知道函数的返回值是在函数声明的时候确定好的,那么也许同学们经常会听到两个不同的词——函数的声明与函数的定义。
这两个乍一听非常的接近,但是实际上还是有些许的不同。
首先对于函数的声明,就是告诉计算机,存在这样一个函数,这样计算机才愿意继续运行下去。这就像数学老师让你做一道很难的题,如果超出你的知识水平你当然不愿意尝试,但是如果老师提前告诉你只需要你把一个数带入她告诉你的一个公式计算就好了,这个时候你当然愿意去尝试一下。那么函数的声明就是这样的一个过程。
void function(int num);

这就是函数的声明,只需要短短的一句话就可以了,需要注意的是这句话一定要放在主函数的前面哦,就像老师一定要在你去参加比赛之前告诉你一样。还有一点,就是函数的声明最后一定要跟上一个分号 “;”,因为声明就是一句话的事情,那么C语言中一句话,就肯定对应着一个分号。还有非常非常重要的一点就是,在函数的定义之中是不能进行函数的声明的。
接下来让我们看一下函数的定义,类比刚才提到的例子,老师已经告诉你只需要带入一个式子计算机就可以了。那么函数的定义,就相当于老师告诉你那个式子是什么,只有知道那个式子才能解决那道问题不是吗?
不过值得区别的是,函数的定义可以放在源项目的任何一个地方除了函数的声明之前。但是在此说一个特殊的情况,就是项目没有函数的声明,直接就是函数的定义,这个时候来说,函数的定义同时也充当了函数的声明的作用,不过此时函数的定义也需要放在主函数之前。但是并不推荐这种写法,原因很简单,就是当一个项目中的函数多了起来之后,如果都采用这种方法,主函数就势必跑到了最后,这样的写法emmm......可以说是非常的丑,使得你自己代码的可读性非常的低。而如果采用增加函数声明的办法,那么阅读者可以非常直接的看到你采用了哪些函数,同时也可以非常快速的浏览到你的主函数。
形参与实参个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

让我们看一下这个代码:
#includeint func(int num); int main() { int a = 0; a = func(a); printf("%d", a); return 0; }int func(int num) { return num + 1; }

函数func后面的括号,就是函数的参数,表示这个函数需要用到哪些变量来进行映射。
那么看一下main函数中func被调用的时候传入的就是int 类型的a,在被调用的函数之中,类似于a这样的就被称作实参,表示这个是实际意义上的参数。而进入到函数之中就会产生一个和a几乎一样的变量,也就是函数定义中的int num,这个num就是所说的形参,这个变量是进入函数的时候创建,一旦出函数就会被销毁,也就是说main函数之中无法调用num,因为func函数一出来就不存在num这样的一个变量了。而更多的细节,接下来让我们以函数的传值与传址来讲解。
传址调用与传值调用个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

首先在此阐述一下我个人的观点,所谓传值和传址,本质上都是传值,真正做到改变实参值的操作是”解应用“。刚才已经说到,函数的形参就是进入函数的时候创建,出函数的时候销毁,那么如果我们在函数中对变量进行修改自然而然也就无法影响到实参,如下:
#includevoid func(int num); int main() { int a = 0; func(a); printf("%d", a); return 0; }void func(int num) { num = 1; }

打印的结果是0,正好验证了我们的思路,而这种操作就是我们通常意义上的”传值调用“
而传址调用,意思就是传地址调用。代码如下:
#includevoid func(int* num); int main() { int a = 0; func(&a); printf("%d", a); return 0; }void func(int* num) { *num = 1; }

这里我们注意到此时的形参变成了int* num,这里的变量是一个int类型的指针,而在此大家理解指针就是地址即可,主函数中的传参自然也就需要变成一个地址,&就是一个取地址符号。&a就是表示a在内存中存放的地址。
再回到func函数中,我们可以看到num前面加上了一个*,这个*的意思就是解应用,也就是通过地址找到main函数中的a,这个时候对其进行修改,那么main函数中的a的值自然而然也就会发生更改。这种也就是我们通常意义上的”传址调用“。
但是为什么作者会认为传址本质上还是一种传值呢?让我们先来解决一道简单的交换变量的题目:
给出两个数字,调用函数对其的值进行交换。
#includevoid exchange1(int*, int*); void exchange2(int*, int*); void exchange3(int*, int*); int main() { int a = 1, b = 2; exchange1(&a, &b); printf("%d %d", a, b); exchange2(&a, &b); printf("%d %d", a, b); exchange3(&a, &b); printf("%d %d", a, b); return 0; }void exchange1(int* a, int* b) { int* t; *t = *a; *a = *b; *b = *t; }void exchange2(int* a, int* b) { int t = *a; *a = *b; *b = t; }void exchange3(int* a, int* b) { int *t = a; *a = *b; *b = *t; }

上面给出了三种非常经典的写法,其中只有exchange2是正确的,下面让作者逐一进行分析。
exchange1
首先,这个函数可能在部分编译器上是无法运行的,比如我正在使用的vs2022,系统会给出一个报错叫int* t 未初始化。而在有些编译器上也许是可以运行的,而且有的同学会惊奇地发现,好像运行出来的结果还是正确的,那为什么我坚持说这是一种错误的写法呢?答案就是系统给出的这个报错,也许有的同学会知道这个就叫做野指针,野指针所指向的地址是不确定的,也就是说他可能恰好指向一块不可以被修改的空间,例如在之前的bolg中说到的常量,常量是不可以被更改的,那么系统此时就会出现运行错误了,故这种方法是非常不可取的。
exchange2
对于第二个正确的函数简单分析,就是我们平时用的三变量交换法,唯一的区别就在于,此处对于a,b需要我们采用解应用的方式找到main函数中对应的实际上的a,b才能实现变量的交换。故在此不做过多解释,如果不理解的欢迎大家私信我。
exchange3
其实最多人会犯的错误就是这一种,乍一看好像是没有什么错误的,但是实际上的运行结果确实没有实现交换,为什么呢?其实让我们再次理解一下函数传参的过程,函数传入一个参数,然后函数会创建一个几乎一模一样的参数,然后对于这个新创建的函数进行操作。到此我们可以这样理解,就是指针num是一个变量,这个变量存放的是a的地址,当我们对这个地址进行操作的时候,我们实际上改变的是num存放的地址,但是实质上我们并没有改变a的地址,而如果想要改变a的地址,那么我们就需要用到二级指针进行操作,再次不做过多阐述。
既然这样,那么我们就可以推出,传址调用实质上也是传值,只不过传的值存放了一个地址而已,如果有会调试技巧的同学也可以采用单步调试的方式,观察一下num的地址,相信这样能更深入地理解我的意思。
函数设计要求个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片
高内聚低耦合个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

高内聚低耦合的意思就是,函数之间的关联要尽可能的小,也就是一个函数往往只完成特定的某一个步骤,这样的函数才是一个好的函数。比如一个打印1000-2000之间所有的闰年的函数,那么对于这个函数,它所需要的应该就只有判断是不是闰年,而函数内则不需要打印,判断完之后则有主函数决定是否打印。
那么这样的好处是什么呢?举个例子就是,假如我们还需要计算某一年有多少天,那么我们此时也需要对这一个年进行一个判断,判断他是不是闰年,但是此时我们仅仅需要判断是不是闰年即可,并不需要对其进行打印,那如果之前那个函数增加了打印,对于这个问题就无法解决,也就意味着我们还需要另一个函数来实现我们的功能,无形之中也就增加了无效的代码。所以函数设计时,做到高内聚低耦合是非常有必要的一个好习惯。
好的函数名个人的小白成长经历|【濡白的C语言】初学者-从零开始-5(模块化设计——函数,传值和传址)
文章图片

一个好的函数名可以增加代码的可读性,也就是当别人看到你的函数声明的时候就可以很快理解你这个函数大概是要做一件怎么样的事情。一个非常有名的笑话就是”冒泡排序“,很多同学的命名就是void maopao,更有甚者void gulugulu表示冒泡的声音。

    推荐阅读