目录标题

  • 一.前言
  • 二.函数是什么
  • 三.c语言中函数的分类
  • 四.为什么会有库函数
  • 五.如何使用库函数
  • 六.为什么会有自定义函数
  • 七.如何来创建自定义函数
  • 八.函数的参数
    • 1.实际参数(实参)
    • 2.形式参数(形参)
  • 九.函数的传值调用
  • 十.函数的传址调用
  • 十一.函数的声明和定义
    • 1.函数的声明
    • 2.函数定义
    • 3.为什么要将函数声明和函数定义分开
  • 十二.有关自定义函数的几个例子
  • 十三.函数的链式访问
  • 十四.函数的嵌套调用
  • 十五.函数的递归
    • 1.什么是递归
    • 2.递归的两个必要条件
    • 3.递归的例题
  • 十六.函数的一些注意事项
  • 十七.小结

一.前言

亲爱的小伙伴们大家好啊,我们又见面了,其实写这篇文章的时候我还是特别激动的哈,因为在写这篇文章的时候我的csdn上面的浏览量达到了5000非常的欣慰啊,这里谢谢大家的支持,谢谢大家,那么这里我们就不多废话开始今天的内容,那么安全带系好,我们发车了。

二.函数是什么

  数学中我们常见到函数的概念。比如说指数函数,对数函数,三角函数等等等,但是你了解c语言中的函数吗?维基百科中对函数的定义称为子程序。那子程序中的意思是什么呢?我们来看看百度百科怎么解释:在计算机科学中,子程序,是一个大型程序中的某部份代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。是不是感觉看了之后跟没有看一一样,这里不慌啊,不慌我们接着往下看。

三.c语言中函数的分类

  我们c语言中将函数大致分为两类,一个叫库函数,一个叫自定义函数。接下来我们就一个一个详细的介绍这两种函数的由来,和使用方法放心这篇文章保证能够看的明白。

四.为什么会有库函数

  c语言一开始其实是没有库函数的,在c语言早期设计出来的时候,其实只有语法规则的设立,并没库函数,但是后来啊程序员们就发现,我们在做某些项目的时候总是要用到功能,比如说将信息按照一定的格式打印到屏幕上面(printf),比如说在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy),比如说总是会计算n的k次方这样的计算(pow)等等,但是当时是没有这样的库函数的,所以为了实现这样的功能,我们的程序员就会写出对应的代码,但是我们程序员又不是都长着同样的脑袋,那么同一个功能就会写出不同的代码,有些程序员思路十分的清晰,所以他写出来的代码就非常的好懂,并且效率也很高。但是有程序员的思路却不是很好,他写出来的代码不仅不容易看懂,而且运行起来效率还很低,更有一些人压根就写不出来,那么我们一个团队在把项目的代码进行汇总的时候,是不是就会显得十分的臃肿,效率低下,而且错误率很高,并且在查询错误的时候,还不容易发现错误,那么为了解决这些问题,人们就把这些经常要使用的这些功能就写成函数,然后将这些函数进行分类再合成库,最后提供给每个程序员使用,这样的话我们就只用保证我们库里面的函数都是对的,那么程序员在使用的时候不仅可以极大的程度上减小错误的发生,而且还可以增加代码的可读性,并且这些库函数是由一些非常厉害的大佬甚至科学家写的,那么我们写代码使用这些库函数的时候是不是也可以提高我们代码运行起来的效率,而且我们写代码的时候要使用某些功能,也就可以直接使用库函数,而不用想着如何实现,这样我们办公的效率也就可以得到极大的提升,所以这就是我们库函数存在的意义,大家看完之后是不是能够感觉到我们的前辈真的是非常的厉害,可谓是用心良苦啊,我替大家向前辈们致敬。

五.如何使用库函数

  我们在买一些产品的时候经常可以从包装里面看到产品说明书,这些说明书就是为了让买家更好的了解这个东西,更好的使用这个东西比如说下面这个图片就是一个说明书:

他就可以教会我们如何与白痴进行沟通,我们再来看个图片:


我们看这个图片他这本书就是告诉我们如何成为一个吃货,那么我们的库函数也应该有个对应的说明书对吧,那么这里我就来告诉大家如何来看库函数的说明书,这里大家可以在uc浏览器打开这三个网站进行使用,当然其他的浏览器也可以的,只是有时候某些小伙伴用uc浏览器较多啊,哈哈哈哈。

三个网站如上,大家可以打开看一下哈!这里有个中文版的网站哈,但是我还是建议大家使用英文版的网站,因为我们后期的学习基本上都是英文版的我们早点接触英文版的学习对我们有好处哈,这里我们就用www.cplusplus.com这个网站来教大家如何使用。首先我们打开网站就长这样:
然后我们就点击左边篮筐里面的reference

然后我们就可以进入这个页面:

这个页面我们就可以看到这上面显示的都是一些各种各样的c语言库函数,我们仔细看一下这里有我们熟悉的库函数,比如说stdio.h这个我们经常可以见到每次写代码的时候都得用到他

那么这里我们随便点击一个库函数函数看看里面的内容

点击之后我们就可以看到的页面如下,里面的每个框框都是一个函数,我们点击进群就可以查看这个函数对应的具体的意思,这里我们点击一下strcpy这个函数看看他是什么意思?有啥用?

点进去之后我们看到的页面如下:我们来一块一块的带着大家进行分析首先看到第一个大块:

这里我们看到的是这个函数的基本形式

①对应的意思是该函数的名字
②对应的是该函数返回的类型是char*
③表示的意思是该函数需要一个char类型的指针,指针变量的名字叫destination。
④表示的意思是该函数还需要一个char
类型的指针,指针变量的名字叫source。
其实看到这里大家应该能够有点感觉,我们这里需要两个char类型的指针,然后通过这个函数之后还是会返回一个char类型的指针,然后我们看这两个指针变量的名字destination(目的地)和source(源头)也可以知道这里应该是将source指针所对应的内容,与destination指针所对应的内容进行一些操作使其怎么怎么样,那么我们这里再往下看

这里为了大家更好的观看我这里就打到了world文档上面方便大家阅读,所以就会有些许不一样哈,首先我们可以看到头部有个copy string这个两个词的意思就是复制字符串的意思,那么看到这里我们再回想一下我们开头说的destination和source,我们这里是不是就会产生一个猜测,我们这个函数的作用是不是将source指针变量所对应的内容复制到destination指针变量所对应的空间里面去呢?那么这里我们就继续往看下一句话:啊这里我们发现这是一段很长的英语语句,那么这里大家不要怕啊!有我在这里就给大家展示一下我深厚的英语功底,
看到没功底很扎实吧,翻译的很不错吧,一点啊不对一下子就翻译好了,那么我们就可以看到这个函数的功能就是将source里面的内容复制到destination里面去,并且还包括字符串后面自动加的\0 。然后我们继续往下看,下面有一句话来提醒我们在使用该函数的一些点,这里的翻译如下:

就是我们这里是将source里面的内容复制到destination里面去的时候,我们得保证destination这个指针所指向的这个数组的空间要足够大,大到要能够存储下source里面的全部内容包括\0,不能说我source指向的字符串里面有8个字符,而你的destination里面却只能装5个字符这样是不行的哈,那我们这里就来举一个例子来看这段代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
int main()
{char arr[] = "abcdefg";char arr1[] = "hijklmn";strcpy(arr, arr1);printf("%s", arr1);return 0;
}

首先我们创建了一个数组arr里面的内容是abcdefg,然后又创建一个数组arr1里面的内容为hijklmn,这里我们想将arr1里面的内容复制到arr里面去,所以我们这里就用strcpy函数,在括号里面前面的destination放arr,后面的source放arr1,(这里的函数名就是首元素的地址我们后面会讲)然后我们再将其打印出来
我们发现这个运行结果确实跟我们所想的一模一样,然后我们这里为了验证之前说的那个注意的地方,我们将arr里面的字符串的元素去掉一个这样的我们的空间就会少1,再进行上述的操作

#include<stdio.h>
#include<string.h>
int main()
{char arr[] = "abcdef";char arr1[] = "hijklmn";strcpy(arr, arr1);printf("%s", arr1);return 0;
}

这时候我们就会发现编译器报错了,并且还显示溢出了,所以我们在使用这个函数操作之前,我们一定要保证放到destination处的指针所指向的数组的空间一定要大于等于source哈。

其实我们在翻译第一句话的时候我们就提过这么一件事情就是,我们在复制source里面的内容的时候也会顺便把\0复制进去,那我们这里如何来验证呢?这里我们以另一种形式创建一个数组arr,这样创建的数组里面的内容是没有\0代码如下:

#include<stdio.h>
#include<string.h>
int main()
{char arr[] = { 'a','b','c','d','e','f','g','A'};char arr1[] = "hijklmn";printf("%s", arr);return 0;
}

我们可以将其打印出来看看
我们发现这个数组arr很明显是没有\0的,该数组后面内存的内容也跟着打印了出来,但是我们这里的arr1里面的字符串后面是有\0的,所以我们这里使用strcpy函数进行复制,如果他打印的结果是hijklmn的话,那么我们就可以验证在复制的时候将\0也包括了进去,我们这里看一下运行的结果:

#include<stdio.h>
#include<string.h>
int main()
{char arr[] = { 'a','b','c','d','e','f','g','A'};char arr1[] = "hijklmn";strcpy(arr, arr1);printf("%s", arr);return 0;
}


这里的运行结果就跟我们想的一样确实会包括\0,我们可以在用监视的功能进一步的进行验证
在没有运行strcpy函数之前我们这里监视显示arr里面的内容是没有\0,在运行完strcpy之后
我们就发现有了一个\0这种方法也可以来验证。其实上面的这些过程是我带着大家学习这个函数是如何来使用的,我们这个网站上面也有具体的例子我们再来往下看:这一块就是向我们介绍括号里面两个参数的意思,这个我们之前看那个介绍的时候就已经知道,这里我们可以再进一步验证正确性哈

下面还有一个模块,这个模块告诉我们的是这个函数返回的是什么,这个就很明显返回的是destination这个char类型的指针。

最后这个网站还给我们一个非常好的例子来告诉我们这个函数的如何来使用,这里我们可以看到第13行这段代码,我们可以发现其实我们在用这个函数的时候,可以将想要复制的内容直接放道函数的括号里面,不用先创建数组,再将数组名放到里面,这样在某些方面就省事多了哈。

看到这里其实大家应该知道了如何使用库函数,那么接下来我们就来看看自定义函数又是怎么一回事。

六.为什么会有自定义函数

工厂大家应该都见过吧比如说这样:

还有这样的工厂

  我们c语言中的函数更像是一个工厂,某人提供了一些原材料,这个工厂就会对这些原材料进行加工,然后把新产品还给某人,当然不同的函数就相当于不同的工厂,比如有些工厂你把煤给他,他就能发电,有些工厂你把棉花等纺织物给他,他就能生产一些衣服出来,不同的工厂所需要的原材料不同,他们的机器也就长得不一样,他们所生产出来的东西也就不一样,你啥时候见过火电厂的装置跟服装厂的一样啦!但是这里又有一个情况就是这些厂还得分个类别,我们把一些厂分为别人的和自己的,这就跟我们之前说的一样别人的厂你要是想用的话,你是不是得跟别人打一下招呼啊!但是有时候啊你的商品十分的独特,你会发现你想要的商品这些工厂都加工不出来,这怎么办呢?那么这里我们就只能自己创建一个工厂来对原材料进行加工,我们就把这些自己创建出来的函数称为自定义函数。

七.如何来创建自定义函数

  我们就来举一个简单的例子,比如说我们的加法函数我们可以创建一个函数叫add,这个函数的功能就是给我两个整形的数字我能把这两个数字的和算出来,这就可以看成一个非常简单的加法函数,那我们加法函数是如何来实现的呢?
第一步:
  之前我们说过我们用别人函数的时候得跟别人打招呼,那这个人是不是可能不一样啊?有可能是叶超凡,也有可能是叶凡超,那我们在创建自己的函数的时候是不是也得取个名字那我们第一步就是取名字:这里我们取个名字就叫get_add吧。

#include<stdio.h>
int main()
{int a = 1;int b = 2;int c=get_add();printf("%d", c);return 0;
}

第二步:
  我们说过函数是一个工厂,工厂在加工时是离不开原材料的,那么我们这里的原材料
是怎么送到工厂的呢?这里我们就可以举个非常形象的例子比如get_add(a,b)我们可以把这个大括号和里面的实参看成一个火车,一个火车是有很多节车厢的,那我们这里的逗号就相当于不同车厢之间连接的东西,一个车厢里面事装着同样的东西,那我们这里第一个车厢装的就是a,第二个车厢装的装的就是b了,ab两个车厢使用逗号相连接,并且分隔开以免混淆了。

#include<stdio.h>
int main()
{int a = 1;int b = 2;int c=get_add(a,b);printf("%d", c);return 0;
}

第三步:
  火车到了工厂是不是得有工人将其卸下火车,那么这里就有个问题就是这个工厂的地点在哪呢?如果是同一个文件的话,这个工厂得在int main()的前面,在#include的后面,那么火车到了工厂是不是得有工人卸货吧,不同的车厢就得有不一样的工人,并且该工人只能负责这一个车厢不能说有些车厢的工人搬的快,他搬完了还能帮别人搬。那我们来get_add(int x,int y),这里的工人就是x y (当然这里的x和y只是我们随便取的名字大家自己在写函数的时候,可以根据情况表取不一样的名字)并且不同的工人会搬的东西也不一样,但是这些工人的记忆力不好,他们总是忘记自己会搬什么,所以你得在工人的前面写上他会搬的类型,这里就是x 和 y前面的int。这些工人他们在搬完物品之后就会把自己的名字贴在物品上面,最后好算工钱。

#include<stdio.h>
get_add(int x, int y)
int main()
{int a = 1;int b = 2;int c=get_add(a,b);printf("%d", c);return 0;
}

第四步:
  东西搬下来了是不是就得有加工的机器啊,并且这些机器得有个围墙将其围住,以免误加工时误伤工人,那么这里的围墙就是大括号{},工具就是int z= x+y;因为我们前面说了这些工人会把物品写上自己的名字,所以这里的x就是名字叫x的物品a实际上还是a,所以x和a的内容其实时一样的(本质不一样内容一样),而且我们加工之后得出来的成品就是这里的z。

#include<stdio.h>
get_add(int x, int y)
{int z=x+y;
}
int main()
{int a = 1;int b = 2;int c=get_add(a,b);printf("%d", c);return 0;
}

第五步:
  我们的物品做出来了,是不是得寄快递给买主,那么我们这里就得有个return z;这个return 就相当于是一个快递,这个快递里面装了一个物品z。

#include<stdio.h>get_add(int x, int y)
{int z = x + y;return z;
}
int main()
{int a = 1;int b = 2;int c=get_add(a, b);printf("%d", c);return 0;
}

第六步:
  快递寄出去了是不是就得有一个人来接收这个快递,什么样的人就接受什么样的快递,那么我们就得在这个函数的前面加上这个返回物品的类型,这里的get_add的类型就是int。那么我们这里的函数就说完了具体的实现就是一下的代码

#include<stdio.h>
int get_add(int x, int y)
{int z = x + y;return z;
}
int main()
{int a = 1;int b = 2;int c=get_add(a, b);printf("%d", c);return 0;
}

看到这里想必大家应该能够明白自定义函数实现的过程和实现方式,但是上述的例子其实是不全面的哈,就是我们有时候是不用进行参数的传递的,比如说我们一个函数的功能就是打印一句hello world,你觉得他需要函数传参和接收参数吗?不需要,他需要一个返回类型(就是我们前面说收快递的那个人)也不需要,那么我们这里就用void表示这个函数没有返回类型,在函数调用的括号里面什么都不写表示这个函数不需要传参。代码表示如下:

#include<stdio.h>
void print()
{printf("hello world");
}
int main()
{print();return 0;
}

看到这里想必大家能够很好的理解如何实现自定义函数的实现,那么这里我们就来看函数的声明和定义。

八.函数的参数

1.实际参数(实参)

  真实传给函数的参数,叫实参。
  实参可以是:常数,变量,表达式,函数等,但是如果是函数的话这个函数的返回值得是我们需要的类型。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参 。

2.形式参数(形参)

  形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了,因此形式参数只在函数中有效。来举个例子吧比如说我们上面的代码:

#include<stdio.h>
int get_add(int x, int y)//这里的x y就是形参
{int z = x + y;return z;
}
int main()
{int a = 1;int b = 2;int c=get_add(a, b);//这里的a,b就是实参printf("%d", c);return 0;
}

九.函数的传值调用

  我们函数在调用的时候,会把括号里面的参数传上去,比如说我们上面的例题get_add函数他就会把a和b的值传上去,但是函数在接收值的时候,他并不会拿传过来的a和b本身进行操作,而是另外开辟两个内存空间,并且把a和b的值复制到这两个空间当中,所以我们上面的那道题对x和y进行操作的时候,压根就影响不到我们下面的a和b,我们函数调用完之后,我们的编译器就会把这新开辟出来的x和y的空间全部销毁,而a和b还是原封不动的在那里,我们可以用监视这个功能来验证:

我们发现这个a b x y里面对应的内存的地址是完全不一样的,所以这里大家要注意以下,我们的传值调用中对形参的改变,是不会影响到实参的,但是我们要想改变实参呢?怎么办呢?接下来我们就来了解了解传址调用。

十.函数的传址调用

  传址调用就是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式,这种方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部能够操作函数外部的变量。这里我们如何来理解呢?我们之前说过我们创建一个变量,该变一般都有对应的地址,而且我们还可以通过这个取地址操作符(&)得到该地址的编号,得到该地址之后我们还可以使用解引用操作符(*)来对该地址所对应的内容进行修改,那么我们是不是可以将这个运用到函数调用上面,因为我们知道函数在接收参数的时候,他是会创建出两个新的空间来复制你传过来的值,那么我们这里把地址传过去,那么这个空间里面装的不就是我函数的地址了吗?那么我们再对这个变量进行解引用操作的话,不可以对函数外部的那个变量进行操作了吗?大家能否理解,这里我给大家画个图来理解理解:

这样我们对x和y进行解引用的时候就可以对函数外部的a和b的值进行改变,这里希望大家能够明白传址调用存在的意义。

十一.函数的声明和定义

  在看这部分的内容之前我们先看一个代码,其实这段代码应该很常见,在我们的一些书中经常将函数的实现放到一段函数的最后面,比如我们这里写了一个加法函数:

#include<stdio.h>
int main()
{int a = 10;int b = 20;int c = 0;c = add(a, b);printf("c的值为:%d", c);return 0;
}
int add(int x,int  y)
{int z = 0;z = x + y;return z;
}

我们将这个函数的实现放到了后面,这样就会导致一个什么样的结果呢?我们来vs2022编译器运行的结果

我们发现这个运行的结果是正常的,但是我们看编译器的下面就会发现这个还是不是很愉快,如果是vs2013的话压根编译不过去
他说我们这个add函数未定义,但是我们运行的结果却是正常的,这是又是为什么呢?因为我们编译器在运行的时候,他执行的顺序是从上往下的执行,这样就会出现一个问题就是我们的add函数的具体实现的过程是在最下面的,当编译器扫到add的时候他发现前面并没有定义这个函数啊,所以他这里就会报错说add未定义,但是当编译器扫到下面的时候他发现add这个函数又定义了,所以我们这里就代码依然可以正常的运行,看到这里大家应该能够梳理清楚这个逻辑,按这个逻辑来看的话,我们把这个函数的实现放到前面去的话就应该不会报错我们来看

#include<stdio.h>
int add(int x, int  y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 10;int b = 20;int c = 0;c = add(a, b);printf("c的值为:%d", c);return 0;
}


我们发现这样的话就不会报出未定义的情况,但是这时候有小伙伴就要说了,啊叶超凡我就是要放到后面,怎么样你有没有办法让他不报错啊,有!当然有,我们这里报错的原因是编译器在扫描到add的时候,发现这个东西前面是没有的,所以会报出未定义的情况,那么这么来看的话,我们在前面声明一下,告诉编译器这个东西他是存在的不就ok了吗!那么这里的告诉编译器存在的这个操作就是我们所谓的函数的声明,我们来看如何声明?

1.函数的声明

其实函数的声明跟我们写的函数中间的一个部分是十分的相似的,我们声明的作用就是告诉编译器有xxx这个东西,他的返回类型,他的名字,他的参数类型是什么,那这么看的话不就是我们之前写的函数的头吗?我们把上面的代码进行改一下加个声明大家看看:

#include<stdio.h>
int add(int, int);//函数的声明
int main()
{int a = 10;int b = 20;int c = 0;c = add(a, b);printf("c的值为:%d", c);return 0;
}
int add(int x,int  y)//函数定义
{int z = 0;z = x + y;return z;
}

我们在前面加了一个函数的声明,大家可以看到他的样子和下面函数的头部(就是大括号的上面那个)非常相似,就是这里只用写参数的类型不用把参数的名字写上去,但是我们这里为了统一我们还是建议大家在写函数的声明的将接收参数的名字也写上去保持一致哈。那么看到这里想必大家应该了解了函数的声明,我们可以归纳一下函数声明的一些重要点:
第一点:
高数编译器有一个函数叫什么,参数是什么,返回的类型是什么。但是具体存不存在,函数的声明是决定不了的。
第二点:
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
第三点:
函数的声明一般要放到头文件里面。这点大家可能不理解,大家不要慌啊等我们说完函数定义就来给大家解答

2.函数定义

既然我们函数的声明是告诉编译器有一个名字叫xxx的函数,那么我们函数的定义就是告诉编译器这个函数的具体是如何实现的,那不就是我们平时写的那个函数嘛,只不过我们之前是将函数放到前面,这就导致了函数定义之后顺便声明了,比如说我们写的add函数:

int add(int x ,int y)
{return x+y;
}

这就是我们函数定义。其实我们往后在写代码的时候都是将函数的定义和声明放到另外的两个文件里,并不是放到同一个文件里面,这是为什么呢?可能我们小伙伴们都习惯将函数的声明定义放到同一个文件里面,甚至压根没有声明,编译器能按照我想要的结果输出就行,那么这里我就结合上面说到的第三点来给大家加讲解讲解。

3.为什么要将函数声明和函数定义分开

我们先来聊聊为什么要另外创建两个文件,而不是全部写道一个.c文件里面?
因为我们现在还是初学者我们写的代码都是放到一个文件,但是这样的情况是因为我们是初学者啊,我们以后步入到公司都是很多人写一个项目啊,比如说100个人写一个项目,如果我们把全部的代码都放到同一个.c文件的话,这不就很尴尬嘛,难道今天你写一下这个.c文件,明天我写后天他写,你这开火车呢,所以我们经常是分多个文件实现这个项目,比如说我们团队想搞一个计算器出来,我们团队就开始分工,a实现加法,b实现减法,c实现乘法,d实现除法,那么这样的话a就写出来了add.h 和add.c文件,b就写出来了sub.h和sub.c文件,c就写出来了mul.h和mul.c文件,d就写出来了div.h和div.c文件。然后再写一个总的文件将这些不同的功能所对应的文件进行汇总,这样的话是不是就比把所有的代码写到同一个文件里要好的很多啊对吧,以后我们修改bug的时候也更好的查找。那么这里我们就来举一个例子,这里我们就将加法进行改进,我们再创建两个文件一个叫add.h用来存放函数的声明,一个叫add.c这个来存放函数的具体的实现:



大家这里要注意一点就是我们的在使用自己的函数的时候我们也要用#include来引用文件,但是后面不是尖括号而是双引号,在双引号里面填入函数定义的文件的名称,如上图所示,而我们这里#include“add.h”的作用就是将add.h文件里面的所有代码全部拷贝到当前这个文件,也就相当于在使用函数之前先声明了一下,这样我们的程序就可以正常的跑起来了。那这时有小伙伴说啊:哟西哟西原来如此,但是为什么要写两个文件呢?我写成一个文件不行吗?而且你之前说过函数也是具有外部链接属性啊,那么这里我又要跟同学们讲讲故事了。
为什么要写成两个文件?
这里我们先假设一下,假设自己是个非常厉害的程序员,在每天下班休息的时候写了一个非常牛逼的游戏引擎出来,然后你就拿这个游戏引擎卖给了腾旭,假设你当时是把这个游戏引擎写成了一个文件,卖给了某讯,某旭一看哎呦有这好东西,赶快出了大价钱买了下来说这样好吧小叶,你把这个游戏引擎卖给我我一年给你100万好吧,你当时一心动立马二话不说一手交钱一手交货,第一年你拿着这一百万过的风生水起,各种探店各种网红地点打卡,然后一年过去了,你跑到某讯去找老马要第二年的钱结果发现老马脸色一变说,这个游戏引擎你拿回去吧,我们不用了我们有了其他的,后来你一想咋他们的程序员这么厉害一下子又写出来了新的,后来你看了自己的代码发现完了不妙之前写这个游戏引擎的时候将函数的声明和定义放到一起去了,某讯集团的程序员又复刻了一份,完了这回他们吃饱了打厨子了。大家看到这个故事之后是不是有点感觉,如果小叶当时把函数的声明和函数的定义分开,把函数的定义进行加密的话,那他是不是可以继续拿钱潇洒,那我们如何来对这个函数进行加密,我们这里所说的加密就是将这个add.h文件变成静态库,大家继续往下看
首先我们再创建一个新的项目我,里面写上新的项目头文件为add.h源文件为add.c这时候我们再鼠标右击这里的add

这时候我们就可以看到这里弹出来了一个菜单,在这个菜单的最下面有属性这一栏,再点击属性

这时我们就会弹出这样的一个窗口,然后点击常规就可以看到配置类型这一栏旁边是有个选择点一下就又出现了菜单,这里我们就选着静态库,然后点击应用再确定。这样我们就配置完了,最后关闭这个属性的页面,再来到编程的页面按ctrl fn f5就可以了
看到这个出现就说明静态库编好了,那么我们就可以在这个文件里面找打当时储存的文件夹,里面有个x64,再点击debug,里面就有个add.lib这个就是我们要的静态库
我们可以将他放到编译器里面看一下他的内容
我们发现全部都是二进制啊,几乎看不懂,不对自信点完全看不懂,那么这样就达到了我们要的效果。那我们如何使用这个静态库呢?这里我们就要继续想象一下我们之前那个场景,我们之前说到我们把游戏引擎给了某讯公司,但是我们不想让里面的程序员复刻一份出来,那么我们就将我们这个进行加密,但是我们的头文件是不能进行加密的,因为这个头文件我们得给他们的程序员来告诉他们如何使用这些函数,他们的程序员看到这个返回类型啊,函数的名字所需的参数啊就知道了这个的大致的使用方法,那么我们这时候卖给这个公司的就是头文件加上这个静态库。这里我们将角色互换一下我们就变成了买家继续回到我们之前的函数文件里面。我们买回来了一个add.h的头文件和一个add.lib的静态库

那么这里我们如何来使用这个静态库呢?这时候我们就可以在头文件的下面加上这么一段代码

#pragma comment(lib,"add.lib")

这句话的功能就是导入静态库,那么我们全部的代码如下:

#include<stdio.h>
#include"add.h"
//导入静态库
#pragma comment(lib,"add.lib")
int main()
{int a = 10;int b = 20;int c = 0;c = add(a, b);printf("c的值为:%d", c);return 0;
}

这样我们的程序就可以成功的运行下去,并且买家还不知道这个函数是如何实现的,看到这里想必大家应该能够明白将一个函数的声明和定义分开的好处,那么接下来我们就来看几个自定义函数的例子。

十二.有关自定义函数的几个例子

第一个例子:
我们创建一个函数,该函数的功能就是返回两个数中的最大值,假设有两个变量a和b,那么这个函数的功能就是求出a和b中的最大的值。
第一步:
首先我们要创建两个变量a和b,并且我们还要想好一个函数的名字,这里我们就叫这个函数为get_max吧我们的代码实现如下:

int main()
{int a = 10;int b = 20;get_max();return 0;
}

第二步:
这里我们要求的是a和b中的最大值,那么我们这里是不是就要将a和b传上去那我们就得在add后面的括号里面填入a,b代码如下:

int main()
{int a = 10;int b = 20;get_max(a, b);return 0;
}

第三步:
函数的传参实现了,那么我们就要来接收这个这些参数,根据上面的步骤我们先在#include下面,int main()上面先把函数名写上去,因为这里有参数传过来,那么我们这里就要有形参进行接收,那么我们这里就用x和y来进行接收,因为参数的类型为整型,所以我们在x,y前面都加上int,代码实现如下:

#include<stdio.h>get_max(int x,int y)
{}
int main()
{int a = 10;int b = 20;get_max(a, b);return 0;
}

第四步:
形参的接收实现了,那么接下来就是函数体的实现,这里我们要求得两个数中的最大值,那么我们这里就可以先创建一个变量z,再使用条件表达式得到两个数中的最大值,再将该表达式的结果赋值给z,最好再返回z,因为z的类型为整型,所以我们就可以在函数函数的前面加上int来进行接收最后在下面打印出这个函数的返回值就可以。代码形式如下:

#include<stdio.h>int get_max(int x,int y)
{int z = x > y ? x : y;return z;
}
int main()
{int a = 10;int b = 20;get_max(a, b);printf("%d", get_max(a, b));return 0;
}

那么这里我们这道题就完成了,我们再来做一道题。
第二题:
用函数实现交换两个变量的内容。这里我们假设两个变量分别为a和b,a的值为10,b的值20,那么我们这里就要用函数实现将a的值变为20,将b的值变为10.
看到这个题有些小伙伴们就笑了,哎呀这题还不简单嘛,跟着上面的步骤走不就够了嘛。第一步:
先创建两个变量a和b,将a的初始化为10,将b初始化为20,然后我们创建一个函数名叫swap吧,再把a和b放进去用,(逗号)进行隔开,代码如下:

#include<stdio.h>
int main()
{int a = 10;int b = 20;swap(a, b);return 0;
}

第二步:
函数名想好了,传参也写好了,那么接下来就是参数的接收,这里的a和b都是整型,那么我们就用 x和y进行接收,并且在x和y的前面都加上int。代码如下:

#include<stdio.h>
swap(int x ,int y)
{}
int main()
{int a = 10;int b = 20;swap(a, b);return 0;
}

第三步:
参数的接收写完了,我们接下来就来写函数体,这里我们是要将a和b的变量的内容进行交换,那么按照我们上面的说法就是将x和y的内容进行交换,那么这就很简单了,首先创建一个变量z,先把x的值赋值给z,再让x的值等于y,最后再让y的值等于z这样我们就可以实现将变量的内容进行交换的功能,因为我们这里是没有返回值的,所以我们就不用写return,并且在函数名前面写上void表示没有返回值,那么我们的代码实现如下:

#include<stdio.h>
void swap(int x, int y)//函数的声明
{//函数体的实现int z = 0;z= x;x = y;y = z;
}
int main()
{int a = 10;int b = 20;swap(a, b);//函数的调用return 0;
}

第四步:
函数写完了,接下来我们就要看实现的结果了,这里我们就可在函数的调用下面用printf语句来打印交换后的值,为了做对比我们在调用之前也用printf语句来看看打印之前的值,代码如下:

#include<stdio.h>
void swap(int x, int y)
{int z = 0;z= x;x = y;y = z;
}
int main()
{int a = 10;int b = 20;printf("交换之前a和b的值分别为:%d %d\n", a, b);swap(a, b);printf("交换之后a和b的值分别为:%d %d\n", a, b);return 0;
}

我们运行一下发现不妙有内鬼交易结束
哎这里就奇怪了,为什么我们函数调用前后a和b的内容没有发生改变呢?我们再在函数里面看看x和y的值是否发生交换了没?我们代码如下:

#include<stdio.h>
void swap(int x, int y)
{int z = 0;z= x;x = y;y = z;printf("函数中x和y的值分别为:%d %d\n", x, y);
}
int main()
{int a = 10;int b = 20;printf("交换之前a和b的值分别为:%d %d\n", a, b);swap(a, b);printf("交换之后a和b的值分别为:%d %d\n", a, b);return 0;
}

我们的运行结果看看:

我们发现在函数的内部确实将数据的内容进行了交换,但是出了函数之后我们的a和b却没有进行交换,这是为什么呢?我们将这件事情分为两段:第一段是数据的传参,第二段是函数的实现,既然第二段是没有问题的,那么问题就一定出现在第一段也就是数据的传参上面,我们上面讲过函数的传值调用,我们说函数在接收你传过来的参数的时候,他会再创建几个变量,这几个变量里面的内容跟你传过来的值是一模一样的,我们把这个新创建出来的变量叫做形参,但是函数内部的操作执行的对象是这个形参,我们函数把形参进行交换啊,啥加减啊,啥乘除啊但是这些都跟我们实参没关系啊,你函数执行完之后形参都会销毁的,而我的实参还是在这原封不动,这也就是为什么我们上面的例子能够看到的在函数内部x和y的数据进行了交换,但是函数执行完之后我们的a和b的数据内容却没有发生变化原因,那么为了解决这个问题我们上面还讲了传址调用,既然你会再创建新的变量,变量里面的内容和我们传过去的一模一样,那么我们直接把地址传过去不就够了,再在函数里面进行解引用操作不就可以对函数外部的变量进行操作了嘛,那么我们这里就可以用到取地址操作符和接应用操作符其代码如下:

#include<stdio.h>
void swap(int* x, int* y)
{int z = 0;z = *x;*x = *y;*y = z;
}
int main()
{int a = 10;int b = 20;printf("交换之前a和b的值分别为:%d %d\n", a, b);swap(&a, &b);printf("交换之后a和b的值分别为:%d %d\n", a, b);return 0;
}

我们再来看运行结果

跟我们想的一样哈。那么这时候可能有小伙伴就有点迷糊了,说啊叶超凡我们咋知道啥时候得有传值调用,啥时候得用传址调用啊,这里大家就记住如果你想改变函数外面的值的化就需要用传址调用,如果不需则用传值调用,比如说我们上面的第一个例子,我们只需要得到两个数中的最大的值,不需要改变啥的那么我们就用传值调用,但是我们的第二个例子他是要将a和b这两个函数外部的变量的内容进行交换,那么这个是要改变这两个变量的值的,所以我们要使用传址调用,大家可以暂停理解一下。

十三.函数的链式访问

  这个链式访问的意思就是一个函数的返回值作为另一个函数的参数,前提是这个函数得用返回值啊。那么我们用一个例子来带大家理解一下:

#include<stdio.h>
int main()
{printf("%d", printf("%d", printf("%d", 43)));return 0;
}

来来来大家看看这个运行的结果是多少?大家根据我们上面所说的,一个函数的返回值作为另一个函数的参数,那么我们这里最先执行的函数就最里面的printf(“%d”,43),那么这个函数执行完之后我们的屏幕上面就会打印43出来,但是这时候就有小伙伴们纳闷了,啊!printf函数还有返回值吗?这里我们用再来看看说明书
在这个网站的头部有个search我们在这个里面输入printf再回车就直接进入printf函数的介绍页面
同个这个一开始的函数的大致介绍页面我们发现这个printf函数他确实是有返回值的,而且返回值的类型还是整型,那么我们再往下看看对返回值的介绍
我们通过这个便可以知道,如果这个printf函数成功的化,那么他的返回值就是他打印的字符的总数,比如说printf("hello world")这个函数的返回值就是11因为这里打印出了11个字符,那我们再看这例题他一开始是打印出了43,打印完之后就会返回一个2 ,那么再进行一次printf函数就会打印出2,因为这里就只有一个字符了所以就返回一个1,最后一个printf再打印出1 ,那么我们这里显示的结果就是4321。看到这里大家应该对链式访问应该有一些了解了,那我们接下来继续看函数的嵌套调用

十四.函数的嵌套调用

  其实嵌套调用也很简单,就是我们可以在一个函数的里面使用其他的函数。我们来看个例子:我们首先创建一个函数,该函数的功能就是打印出hello word,那么这时我们再创建一个函数该函数的作用就是打印三遍hello world,那么我们这里就可以使用嵌套调用:

#include<stdio.h>
void one_line()
{printf("hello world\n");
}
int main()
{one_line();three_line();return 0;
}

我们先将打印一遍的函数进行实现,然后我们再实现打印三遍函数的时候,我们就可以使用for循环,然后在里面的循环体就不用printf函数,而是使用我们自己创建的函数one_line,那么这就是我们的函数嵌套调用全部代码如下:

#include<stdio.h>
void one_line()
{printf("hello world\n");
}
void three_line()
{int i = 0;for (i = 1; i <= 3; i++){one_line();}
}
int main()
{one_line();three_line();return 0;
}

但是这里大家要注意一点的就是我们函数可以嵌套调用但是不能嵌套定义。比如说:

#include<stdio.h>
void one_line()
{printf("hello world\n");void three_line()//嵌套定义函数three_line{printf("hello world\n"); printf("hello world\n"); printf("hello world\n");}three_line();//函数嵌套调用
}
int main()
{one_line();return 0
}

大家看如果这么写的话,我们编译器就会报出种种错误所以大家在写函数的时候千万不要嵌套定义。

十五.函数的递归

  这个内容应该是大家最难理解的内容,那我们来详细的讲解哈。首先在了解函数的递归之前,我们先像个问题,我们上面讲函数嵌套调用的时候,我们是调用其他的函数,那么我们这里能调用自己本身吗?答案是可以的,这里我们就引入递归这个概念。

1.什么是递归

  程序调用自身的编程技巧成为递归。递归做为一种算法在程序设计语言中广泛的应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略,只需少量的程序就可描述出解题过程所需的多次重复计算,从而大大地减少了程序的代码量。

2.递归的两个必要条件

第一个:
存在限制条件,当满足这个限制条件的时候,递归便不再继续。
第二个:
每次递归调用之后越来越接近这个限制条件。

3.递归的例题

首先我们来看一道题,就是将1234的每一位依次输出,用递归的方法实现。
在做这道题之前我们先想一个问题就是我们的递归的意义是什么?我们递归的作用是将一个复杂的问题一步一步的简单化,就像我们平时拨竹笋一样一层一层的拨开。那我们来看这个题如何来一步一步的简化,我们要打印1234中的每一位的数字,那你们觉得最容易得到哪一位的数据,我想大家应该都觉得是个位吧,我们将1234%10就可以得到个位上的数据,那我们第一次简化就可以将这个问题变成:先打印123的每一位再打印4,那么我们这里我们接下来的问题就是打印123中的每一位,那么这里的3又是我们最容易得到的,我们又可以将这个问题(打印123中每一位)变成打印12中的每一位和3 ,那么这样的话我们接下来的问题就变成打印12中的每一位,那么同上这个问题(打印12中的每一位)是不是又可以将其简化,变成打印1中的每一位和2,但是这里的1就是一位所以我们最后就只用打印1 ,大家这里可能有点蒙,其实我们上面的讲了这么多其实就一个问题不好解决,就是如何把打印1234中的每一位变成打印123中的每一位加上打印4 ,对吧!大家再想一下我们的递归是什么,是自己调用自己,而我们上面其实是有个规律的,就是每次打印一个多位数加上打印最后一位,那么我们这里再想一个问题我们的1234打印出来的结果应该是1 2 3 4这样的,那么最先打印的是谁?应该是1 对吧,最后打印的是谁应该是4 对吧,知道了这些我们就可以去尝试的去操作一下我们先创建一个函数名字叫print

#include<stdio.h>
void print(int x)
{}
int main()
{int a = 1234;print(a);return 0;
}

我们是把打印1234中的每一位简化为打印123中的每一位加4 ,那么我们这个函数的功能就是打印一个数中的每一位,因为4 是在最后面的,那么我们就应该先把123中的每一位打印出来,再来打印4这一位,那么我们是不是就应该这么写:

#include<stdio.h>
void print(int x)
{print(x / 10);//打印123中的每一位printf(x % 10);//打印4
}
int main()
{int a = 1234;print(a);return 0;
}

好这时候我们就在这个print函数的内部调用了print函数而且传过去的数据是123,这个时候我们这个函数就又从头开始执行了,并且一开始的形参x变成了123,那么我们往下执行的时候就又要调用print函数,所以就又要传参这时的参数就变成了12,并且我们这里又要从函数头部开始执行,往下执行的时候又要调用print函数所以又要传参,这时传的就是1了,而且又得从头开始,那么这时大家觉得还能调用这个print函数了吗?不能了吧,因为这样调用下去的话,它传的值还是1 ,到时候还是继续调用这样的话不就造成了死循环了吗?我们之前说过函数的递归得存在限制条件,当满足这个条件的时候递归就不再继续,而且每次递归之后都要接近这个条件,那么我们这里就需要这么一个条件,大家可以往前看一下,我们说过当值为1的时候,打印的就是自己,而且我们这个print下面还有个printf函数来执行打印最后一位的作用,那么我们这里就一位,所以我们下面的printf函数就可以完成这个任务,那当参数是1 的时候就可以再调用print函数,所以我们这里可以有个if语句来创造这个结束的条件,我们if后面的括号就可以写上x>9也就是x是两位数的时候才能调用函数,我们加个if的话这样子就不会出现死循环的现象了,那么我们的代码如下:

#include<stdio.h>
void print(int x)
{if (x > 9){print(x / 10);}printf("%d ",x % 10);
}
int main()
{int a = 1234;print(a);return 0;
}

大家可以结合这个图来理解这个递归的顺序就是按照我们上面的123456的顺序执行。那么这道题就结束了,当然做一道题怎么可能能够理解递归呢?我们递归其实是非常容易导致溢出的,那么这一部分内容我们就在这一章的习题里面去更加详细讲解,我们这章的习题就全部讲递归,带着大家全面的理解好函数的递归的真谛。

十六.函数的一些注意事项

第一点:
当函数没有放回值时,你还要用变量进行接收的话就会报错。我们来看个例子:

#include<stdio.h>
void print()
{printf("hello world");
}
int main()
{int a = print();return 0;
}


第二点:
当我们不写返回类型时,这个函数默认返回的是int类型。

#include<stdio.h>
add(int x, int y)
{return x + y;
}
int main()
{int a = 10;int b = 20;int c = add(a, b);printf("%d", c);return 0;
}

第三点:
当函数没有返回值的时候默认返回的时最后一段代码的返回结果
我们来看一段代码:

#include<stdio.h>
int add(int x, int y)
{printf("hehe\n");
}
int main()
{int a = 10;int b = 20;int c = add(a, b);printf("%d", c);return 0;
}

第四点:
当一个函数不需要参数的时候你还要传参的话,那么函数还是不会接收这个参数的。就是传了跟没传一样。

十七.小结

很开心有写完了一篇文章,感谢大家看到了这里,因为最近要期末考试了所以更新文章有点慢啊,我又不大喜欢水文章所以更新会有点慢但是依然会持续更新,谢谢大家支持谢谢大家!

为了大家方便学习,这章的所代码点击此链接获取
点击此处获取

c语言入门---函数相关推荐

  1. C语言入门-函数专辑(一)

    函数的概念 函数是一段可以重复使用的代码,用来独立地完成某个功能,它可以接收用户传递的数据,也可以不接收. 函数的类型 C语言中函数一般分为两类. 一类是库函数,即系统自带函数,如printf.sca ...

  2. 超详细讲解C语言入门函数(一)

    解析已经很详细了,可以说相当入门级别了,如果喜欢的话那就请支持一下,后续会继续更新~ 代码网上搜索,并加以更改,侵权请联系删除,谢谢~ 部分例子没有详细解释是因为前面的例子已经说过了 3×4矩阵求最大 ...

  3. c语言入门函数大全,C语言函数大全(适合初学者).doc

    C语言函数大全(适合初学者)C语言函数大全(适合初学者) A 函数名: abort 功 能: 异常终止一个进程 用 法: void abort(void); 程序例: #include #includ ...

  4. C语言入门---函数类型与返回值(int和void)

    int和void的区别? 哈喽,各位小伙伴们,在我们学习C语言的过程中经常会看到书中的main函数前带有int和void,就像下面这样: 上面这两种有什么区别呢?在C语言中,可以把函数分为有返回值函数 ...

  5. c语言入门——函数的递归

    一.什么是递归? 程序调用自身的编程技巧称为递归( recursion). 递归做为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接 调用自身的一种方法,它通常把一个大型 ...

  6. else if函数 c语言,C语言入门 — if else

    C语言入门简单条件判断语句,if else, 本文章会使用到< 1.if else 可以简单的理解为"如果 就 否则"的语句,下面以举例子来进行解释,使用if else 判断 ...

  7. c语言printf清屏,C语言入门 — printf 使用方法

    本章节讲如何使用printf打印不同形式的内容,printf是c语言里常用的打印接口,也是c标准函数库,使用时需要#include ,下面讲讲如何使用printf: 一.printf 的基础知识: 1 ...

  8. c语言短整型变量字符,C语言入门 — 整型 char,short,int,long

    一.c 语言变量,有整型变量,浮点型变量,字符变量,布尔型变量. 1.整型变量位数,以下基于32位的操作系统: (1个字节等于8bits)点击查看二进制 长整型(long),短整形(short),整型 ...

  9. c语言char字符判断条件,C语言入门 — 字符型char

    本篇文章将会学习字符类型,并会结合 Ascii码对照表进行学习, 首先我们先来看下 #include // 标准输入输出头文件 stdio.h int main(void) // main 函数,vo ...

最新文章

  1. 第二十二课.XGBoost
  2. OAF TABLE中添加序号列
  3. 关于雅可比迭代的Python实现
  4. JZOJ 100035. 【NOIP2017提高A组模拟7.10】区间
  5. Windows系统顽固型文件清除方法
  6. 只在当前页面生效的css样式,修改页面中的一个样式 仅在当前页面生效
  7. RadHat 6 系列心路历程、新功能及变化
  8. Missing artifact com.oracle:ojdbc6:jar:11.2.0.3 Maven中不能引入ojdbc解决方法,错误
  9. shell脚本之安装ansible(centos7环境)
  10. 文本文件与二进制的区别
  11. UE4/UE5 虚幻引擎,Light光照系列(一)
  12. setup factory 会话变量
  13. 041创建MDI程序
  14. 华为云notebook在线解压压缩包问题
  15. #C语言学习笔记#猴子偷桃问题
  16. Oracle官网登录用户名密码
  17. Glide加载相同URL时由于缓存无法更新图片的问题
  18. SpringBoot实现分布式session
  19. 【web】React-hooks
  20. 解决SVN不显示绿色小对勾

热门文章

  1. 服务器七雄争霸官方网站,腾讯七雄争霸微端登录器
  2. 注册ArcGIS Online账号||免费使用21天(保姆级)
  3. 实体店运营:能提高顾客留存率的店铺陈列方式
  4. 如何清除(登录)缓存
  5. 我的世界服务器服主无限圈地,我的世界服务器怎么圈地 圈地命令使用方法
  6. 【Unity脚本】鼠标常用点击事件
  7. pythontext函数用法汇总_Python - Text Summarization
  8. 转载:Python 的关键字 yield 有哪些用法和用途?
  9. 获取当天或某天的开盘价
  10. 第一次独立使用无人船记录日志—第1天