C语言第五章:C语言函数
/* 十进制转换成二进制 */ void ttob(int n) { int i = n%2; if(n>0) { ttob(n/2); printf("%d",i); } }
一,为什么需要函数
在程序中,我们经常要写一些相同功能的代码,这时候C语言提供了函数这一概念,帮助我们把重复的代码进行抽取归类(可以想象成一个黑盒,我们在使用的时候,只注重其函数的功能),进而形成函数。
函数的如能如下:
- 避免了一些重复性的代码的编写
- 有利于代码的模块化
二,什么是函数
逻辑上:函数是能够完成特定功能的代码块。
物理上:
- 函数能够接收数据
- 函数能够对接收的数据进行加工处理
- 函数能够对处理结果进行返回
总结:函数就是一个工具,它是为了解决大量类似问题而设计的。函数可以把它当做一个黑匣子
三,函数的定义
函数的定义格式如下:
返回值类型 函数名称(函数接收的参数) { 函数的执行体; return 返回值类型; }
1.函数定义的本质是详细描述函数之所以能够完成某个特定功能的具体方法。
2.return表达式的含义:
- 终止被调函数,向主调函数返回被调函数的执行结果。
- 如果return后面为空,则表示终止函数。
- break和return的区别就是break是用来终止循环或者switch的,而return是终止函数的。
3.函数返回值的类型也叫做函数的类型
- 如果函数的类型和实际return返回的值不一样的,则以函数的类型为准。
- C语言中,如果函数没有返回值类型,则默认返回值类型为int类型。
四,函数的分类
函数的分类主要如下:
- 有参函数和无参函数
- 有返回值函数和无返回值函数
- 主函数和普通函数
- 库函数和用户自定义函数
- 值传递函数和地址传递函数
值传递和地址传递问题:
- 在C语言中,实参与形参的数据传递是“值传递”,即单向传递,只由实参传递给形参,而不能由形参传递给实参(即改变形参的值不会影响实参的值)。
- 在有参数函数接收参数的时候,实际上接收的都是值传递(这里理解为其实接收的参数都是一个值,而有的是具体的数值,而有的是数据值的地址),但是为了区分而做出的值传递和地址传递。
- 值传递在函数体执行的过程中不会改变实参的值。
- 地址传递在函数执行过程中会改变实参的值,因为地址传递是把具体数据的内存地址传递过来,函数执行过程中实际操作的就是地址所指向的数据,所以地址传递在函数执行完成后会改变实参的值(因为形参和实参是指向的同一个地址,所以修改的数据是同一个数据)。
五,函数的声明
C语言中的函数规定:定义函数的语句要放在调用函数的语句前面,因为C语言代码是一行一行的执行的。如果被调用函数的语句在定义函数的语句前面,那么编译器编译到这一行的时候是不认识这个函数的。因此C 语言又一个规定就是可以使用函数的声明来解决这个问题。
函数的声明:
- 格式:在要调用函数语句的前面将函数表达式写在前面。
- 对库函数的声明是包含在# include<stdio.h>里面的,所以我们才可以正常使用系统的函数。stdio这个头文件的意思是(标准的输入输出流)
- 示例:
# include<stdio.> // 函数的声明 void fun(void); int main(void) { fun(); // 调用函数的语句 return 0; } // 定义函数 void fun(void) { printf("Hello,World"); }
六,函数的实际执行方式和形参/实参
函数的执行方式:
- 函数是在栈结构方式中执行的,是不断的通过压栈和弹栈来执行不同的函数的。
- 根据上图,可以看出,当我们在函数内定义的变量或者一些操作,当函数执行完毕之后,即函数出栈之后,该函数在栈中所占的内存释放,所以函数内部的变量或者操作所占的内存就会释放掉。
实参和形参的区别:
- 函数的形参就是定义函数的时候所定义的函数接收参数,也就是我们在函数体内部执行的时候的函数参数。
- 函数的实参就是我们调用函数的时候,往函数里赋的值,这个值或者表达式就是函数的实参。
- 函数的形参和实参是要相互对应的。
七,变量的作用域和存储方式
变量按照作用域可以分为:
- 全局变量:就是定义在函数外面的变量就是全局变量,在每个函数中都可以引用(但是要放在调用语句的前面,和函数的声明一样必须放在前面)
- 局部变量:就是定义在函数里面的变量(形参也是局部变量)
- 全局变量和局部变量要注意的问题:
- 调用全局变量的函数要在全局变量定义的后面。因为C语言是一行一行执行的,如果放在前面,C语言会不清楚该变量到底是什么。
- 当全局变量的定义和局部变量的定义冲突的时候,C语言采用的是就近原则,即此时的值是局部变量的值。
变量按照储存方式可以分为:
- 静态变量
- 自动变量
- 寄存器变量
- 这个自己去看
寄存器是什么东西?
- 说的通俗一点就是CPU里面的存储数据指令的地方
- ROM可以理解成就是硬盘优盘
- RAM就可以理解成内存条
- 因此数据的处理如果从数据的存储地方的转变来看呢应该是:ROM->RAM->CPU的寄存器
八,C语言多文件编译
C语言中的函数和函数的声明以及在主函数中的调用通常是以如下方式来编写的:
- 函数单独写在一个.c文件中
- 函数的声明写在一个.h文件中
- 主函数导入头文件
通过上述的方式来构建一个合适的程序,具体的演示代码如下:
- 这里我们定义有两个函数的function.c文件
// 求最大值函数 int max(int a,int b) { return a>b?a:b; } // 求和函数 int sum(int a,int b) { return a+b; }
- 将函数的声明写在function.h文件中
#ifndef _MF_ // 如果宏_MF_没有定义,那么就编译到#endif之间的代码。定义了则不编译代码 #define _MF_ int max(int,int); int sum(int,int); #endif
- 编写主函数,并调用function.c的函数
# include <stdio.h> # include "function.h" int main(void) { printf("%d\n",max(10,20)); printf("%d\n",sum(10,20)); return 0; }
上述程序知识点解析:
- # include和# define的区别:
- 我们通过gcc -E预编译之后查看文件发现# include的本质只是简单的文件内容的替换。将包含的头文件找到后,将头文件内容原封不动的写入到预编译好的文件中。
- 我们此处再编写一个demo,查看# define的区别,这里我们宏定义一个常量MAX,看预编译后文件的内容:
# include <stdio.h> # define size int // 宏定义size是int # define MAX 300 // 宏定义MAX是300 int main(void) { size max = MAX; printf("%d",max); // 输出结果为:300 return 0; }
预编译后的文件内容如下,我们发现宏定义本质就是简单的文本内容的替换。
- #ifndef与#endif的意义:当我们在主函数文件引入多个自定义头文件的时候,发现主函数文件预编译之后出现了重复的头文件内容,这是不合理的。因此出现了这两个表达式。
- #ifndef _MF_ 如果没有宏定义_MF_,则编译#endif之间的代码,如果宏定义了则不编译。这样就保证了头文件内容不会被重复的编译。
- #endif结束判断。
九,函数的递归
函数的递归就是函数在执行过程中调用函数本身。要理解函数的递归就要理解函数的内存结构,函数的执行内存模型是栈内存,通过不断的压栈和弹栈的方式来执行函数的。
函数递归的例子:
- 将十进制转换成二进制代码
/* 十进制转换成二进制 */ void ttob(int n) { int i = n%2; if(n>0) { ttob(n/2); printf("%d",i); } }
- 斐波那契数列:0 1 1 2 3 5 8,第0项是0,第1项是1,这个数列从第二项开始,每一项等于前两项的和。
/* 斐波序列:0 1 1 2 3 5 8 13 */ int fib(int n) { if(n==0){ return 0; }else if(n==1){ return 1; }else{ return fib(n-1)+fib(n-2); } }
-
求n的阶乘
/* 求n的阶乘 5!=5*4*3*2*1 */ int sum(int n) { if(n==1){ return 1; }else{ return sum(n-1)*n; } }
- 利用递归实现求字符串的长度
#include <stdio.h> /* 函数的递归 */ /* 求字符串的长度 */ int sum(char str[],int n) { if(str[n]) { return sum(str,n+1); }else{ return n; } } int main(void) { printf("%d",sum("HelloWorld11",0)); return 0; }