一、函数概述
函数是C语言的核心之一(因为还有一个核心是指针),了解函数有助于了解“面向过程”,未来学习“面向对象”的编程语言时,能够理解二者的差别。
二、函数思想
在初中数据就接触过函数,给定一个输入x,经过函数后会得到一个输出y,如下:
也就是说输入x就会得到所需要的y,而中间的函数就是设计出来完成某项目的的模块,当为了完成某个目标时,就会去使用这个函数。
同样,程序设计中也有这种思想,将完成某个功能的代码单独独立成模块,就是函数。而为了完成某个更大的目标,就会去调用这个函数。比如某个函数的功能是判断某个数是不是奇数,当需要判断时,调用这个函数即可。
所以面向过程的语言又叫结构化语言,而函数是结构化程序设计的核心。
在C语言中有且仅有一个主函数,也就是main函数,程序的执行永远都是从main函数开始,然后由main函数结束。允许主函数调用其他函数,但是不允许其他函数调用主函数。调用者被称为主调函数,被调用者则称为被调函数。允许函数自己调用自己,那就是函数的递归。
在实际项目中,每个人负责的模块有限,因此需要通过函数完成一部分功能,从而搭建起整个项目。函数在其中起到了模块化的作用。比如我们需要将用户的输入转化为小写字母,但随着项目的进行需要将输入转化为大写字母,如果没有函数,那么需要修改所有代码,但是有了函数,我们只需要修改完成这部分功能的函数即可,大大简化了开发时间。
面向过程语言最基本的单位不是语句,而是函数。
三、函数
函数分为有参函数和无参函数。
- 无参函数:主调函数在调用被调函数时,主调函数不向被调函数传递数据,简单来讲就是没有输入参数的函数,这种函数一般是用来执行某种特定的功能。比如延时函数,调用一次就延迟5s,不需要传入参数。无参函数可以有返回值,也可以没有返回值,一般以没有返回值居多。
- 有参函数:那就是只在调用过程中会有参数传递。一般情况下,有参函数会有返回值。
有参函数的基本格式如下:
函数类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, …, 参数类型n 参数名n) {
函数体;
}
3.1 形参和实参
看下面的例子
#include <stdio.h>
void swap(int x, int y){
int temp;
temp =y;
y = x;
x = temp;
printf("x = %d, y = %d", x, y);
return;
}
int main(){
int a = 3, b = 4;
swap(a, b);
return 0;
}
形参:上述在定义swap()函数时,括号内的x和y就是形式参数,简称形参。
实参:上述主函数中的a和b为实际参数,也就是是实际传给函数的参数,简称实参。实参可以是常量、变量或表达式,但是必须有确定的数值,在调用时将实参传递给形参,也叫赋值给形参。
注意:
- 定义函数时,必须指定形参的类型
- 实参与形参的个数必须相等
- 实参与形参的类型要相同或兼容,最好是相同。如果不相同则实参按照形参的类型转化,因为实参传递给形参,本质上是实参赋值给形参。
其实实参和形参是总结出来的,弄明白实参和形参实际上是值传递即可。C语言中,只能由实参传给形参,不能由形参传回给实参。
因此执行被调函数时,形参的值发生改变,并不会影响主调函数中实参的值。而在计算机中,没有出现函数调用时,形参并不占用内存中的储存单元。只有调用时,形参才会分配储存单元,调用结束后,形参所占用的储存空间会被释放。
3.2 函数的返回值
函数可以有返回值,也可以没有返回值,上述交换两个数的例子就是没有返回值。
函数的返回值类型在定义函数时就要确定下来,return语句中表达式的类型应该要与定义函数时制定的返回值类型一致。如果不一致,则以定义函数时的返回值类型为准。
函数的返回值是怎么传递给主调函数的呢?
被调函数运行结束后才会返回主调函数,但是被调函数运行结束后系统给被调函数分配的内存空间就会释放。也就是说,return返回的值在被调函数运行结束后就被释放了。
这个时候就需要临时变量,实际上计算机在执行return语句时,系统会在内部自动创建一个临时变量,然后将return返回的值赋值给这个临时变量。所以虽然被调函数运行结束后,return的返回值被释放掉了,但是有这个临时变量可以将值传递给主调函数。而定义函数时**指定的函数返回值类型实际上就是指定这个临时变量的类型**。这也是为什么当return语句中的表达式类型与函数返回值类型不同时,是将return类型转换成函数返回值类型的原因。因为在赋值运算中,当赋值运算符两边类型不相等时,是右边的类型会转换成左边的类型,然后在赋值给左边。
create temp value; //被调函数return结束后,创建临时变量
temp value = return expression value; //return的返回值赋值给临时变量
f(x) type = temp value; //临时变量赋值给主调函数指定的类型
四、变量的作用域
变量按作用域可以分为局部变量和全局变量。
按照存储方式可以分为自动变量(auto),静态变量(static),寄存器变量(register),外部变量(extern)。
注:寄存器是CPU内部用来存储数据的区域,是一块小型存储区域,暂时存放参与运算的数据和结果,与内存一样,但是比内存要小得多。
4.1 局部变量
局部变量是指定义在“函数”内部的变量,只在本“函数”内有效。但实际上局部变量的作用范围准确说不是以函数来限定的,而是以大括号{}来限定的。只不过函数都是用大括号括起来了,所以才会这么说。比如下面的例子:
#include <stdio.h>
int main(void) {
int a = 1, b = 2;
{
int c;
c = a + b;
}
printf("c = %d\n", c);
return 0;
}
以上程序会报错,如下:
这是因为c是局部变量,出了大括号后,程序就不认识了,所以才会报错。
4.2 全局变量
局部变量定义在函数内部,那全局变量就是定义在函数外部。全局变量可以被整个C程序中所有函数共用。
当全局变量与局部变量名称相同时,全局变量补齐作用。如下:
#include <stdio.h>
int a = 10;
int main(void) {
int a = 5;
printf("a = %d\n", a);
return 0;
}
//此时a的值为5
所以全局变量和局部变量最好不要重名。
注:
- 局部变量是在栈中分配的,而全局变量是在静态存储区中分配的
- 如果在静态存储区的变量没有初始化,系统会将它初始化为0
一般不建议使用全局变量,理由如下:
- 全局变量在程序整个过程中都占用存储单元,而局部变量仅在需要时才开辟存储单元
- 全局变量降低的程序的可移植性,因为全局变量跟程序中的所有函数都可能存在联系
4.3 自动变量
没有标明static的变量都是auto变量。存储在栈区。
4.4 静态变量
用static修饰过的局部变量就是静态局部变量。存储在静态存储区。
静态存储区主要存放静态数据和全局数据,静态存储区的数据会作用于整个程序。所以静态局部变量与普通局部变量不同。
静态局部变量仍然是局部变量,不是全局变量,仍然不能在它的作用范围之外使用。
静态全局变量指的是用static修饰的全局变量,修饰全局变量后,会限定全局变量的作用范围,使得该全局变量的作用域仅限于本文件中。也就是说其他文件不论通过什么方式都不能访问该全局变量。但是如果没有用static修饰,其他文件需要访问该全局变量时,只需要使用extern对该全局变量进行声明即可使用。
4.5 寄存器变量
无论是存储在静态存储区或栈区的变量,都是存储在内存中的。这部分的变量当程序需要使用某个变量时,计算机的CPU就会发出指令将该变量的值从内存中读取到CPU中,然后经过CPU处理后再讲结果数据写回内存。
但是CPU也有一部分地方可以用来存储数据,这个地方就是寄存器。虽然存储数据的量比内存小得多,但是读取数据的速度要快很多。计算机中硬件运行速度由快到慢如下:
寄存器>>>>缓存>>>>内存>>>>固态硬盘>>>>机械硬盘
基本格式如下:
register int a;
注:只有局部变量才能定义成寄存器变量
随着科技的发展,现在都有计算机自动分析分配了,简单说如果发现变量经常使用,计算会自动将该变量放到寄存器中。或者说即使你使用register声明了某个变量,但是寄存器满了,数据仍然会被放到内存中。
4.6 外部变量
外部变量是针对全局变量而言的,主要作用就是扩展全局变量的作用域,因为在实际编程中,程序是有多个c文件组成的。
假如在1.c中定义了全局变量a,如果2.c和3.c也想使用变量a,但是我们并不能在2.c和3.c中重新定义a,这样编译时会报“重复定义”的错误。所以我们会在2.c和3.c中分别声明外部变量,即在2.c和3.c中使用a之前做如下声明:
extern int a;