其实函数在一开始就在使用了:
// 这就是定义函数
int main() {
...
}
程序的入口点就是main
函数,只需要将程序代码编写到主函数中就可以运行了,不过这个函数只是由我们来定义,而不是我们来调用。
当然,除了主函数之外,一直在使用的printf
也是一个函数,不过这个函数是标准库中已经实现好了的,这样就是在调用这个函数:
// 直接通过 函数名称(参数...) 的形式调用函数
printf("Hello World!");
那么,函数的具体定义是什么呢?
函数是完成特定任务的独立程序代码单元。
简单来说,函数是为了完成某件任务而生的,可能要完成某个任务并不是一行代码就可以搞定的,但是现在可能会遇到这种情况:
#include <stdio.h>
int main() {
int a = 10;
// 比如下面这三行代码就是要做的任务
printf("Hello");
printf("World");
printf("\n");
if(a > 5) {
// 这里还需要执行这个任务
printf("Hello");
printf("World");
printf("\n");
}
switch (a) {
case 10:
// 这里又要执行这个任务
printf("Hello");
printf("World");
printf("\n");
}
}
每次要做这个任务时,都要完完整整地将任务的每一行代码都写下来,如果程序中多处都需要执行这个任务,每个地方都完整地写一遍,实在是太臃肿了,有没有一种更好的办法能优化代码呢?
这时就可以考虑使用函数了,可以将程序逻辑代码全部编写到函数中,当执行函数时,实际上执行的就是函数中的全部内容,也就是按照制定的规则执行对应的任务,每次需要做这个任务时,只需要调用函数即可。
创建和使用函数
首先来看看如何创建一个函数,其实创建一个函数是很简单的,格式如下:
返回值类型 函数名称([函数参数...]);
其中函数名称也是有要求的,并不是所有的字符都可以用作函数名称,它的命名规则与变量的命名规则基本一致,这里就不一一列出了。
函数不仅仅需要完成任务,某些函数还需要返回结果,此时就需要定义返回值,并在函数中返回这一结果;当然如果函数只需要完成任务,不需要返回结果,返回值类型可以写成void
表示空。
#include <stdio.h>
// 定义函数原型,因为C语言是从上往下的,所以如果要在下面的主函数中使用这个函数,一定要定义到它的上面。
void test(void);
int main() {
// 调用函数
test();
}
// 函数具体定义,添加一个花括号并在其中编写程序代码,就和之前在main中编写一样
void test(void) {
printf("我是测试函数");
}
我是测试函数
这样,就可以很好解决代码复用性的问题。只需要将会重复使用的逻辑代码定义到函数中,当需要执行时,直接调用编写好的函数就可以了,这样就简单很多了。
#include <stdio.h>
void test(int a) {
printf("Hello");
printf("World");
printf("\n");
}
int main() {
int a = 10;
test(a);
if(a > 5) test(a);
switch (a) {
case 10:
test(a);
}
}
HelloWorld
HelloWorld
HelloWorld
当然函数除了可以实现代码的复用之外,也可以优化程序,让代码写得更有层次感,一个程序可能会有很多很多的功能,需要写很多的代码,但是谁愿意去看一个几百行上千行的main
函数呢?可以将每个功能都写到一个对应的函数中,这样就可以大大减少main
函数中的代码量了。
int main() {
func1();
func2();
func3();
}
而从一开始就在编写的 main
函数实际上是一种比较特殊的函数,C 语言规定程序一律从主函数开始执行,所以这也是为什么一定要写成int main()
的形式。
全局变量和局部变量
现在已经了解了如何创建和调用函数,在继续学习后续内容之前,我们需要先认识一下全局变量和局部变量这两个概念。
首先来看看局部变量,实际上之前使用的都是局部变量,比如:
int main() {
// 这里定义的变量i实际上是main函数中的局部变量,它的作用域只能是main函数中,也就是说其他地方是无法使用的
int i = 10;
}
所以下面这种写法是完全没问题的:
int main() {
for (int i = 0; i < 10; ++i) {
}
for (int i = 0; i < 20; ++i) {
}
}
虽然这里写了两个 for 都使用了 i,但是由于处于两个不同的作用域,所以互不影响
那么如果现在想要在任何位置都能使用一个变量,该怎么办呢?这时就要用到全局变量了:
#include <stdio.h>
void test();
// 可以直接将变量定义放在外面,这样所有的函数都可以访问了
int a = 10;
int main() {
a += 10;
test();
printf("%d", a);
}
void test() {
a += 10;
}
30
因为现在所有函数都能使用全局变量,所以这个结果不难得到。
函数参数和返回
函数可以接受参数来完成任务,比如现在想要实现用一个函数计算两个数的和并输出到控制台。
这种情况就需要将进行加法计算的两个数,告诉函数,这样函数才能对这两个数求和,那么怎么才能告诉函数呢?可以通过设定参数:
#include <stdio.h>
// 函数原型中需要写上需要的参数类型,多个参数用逗号隔开,比如这里需要的就是两个int类型的参数
void test(int, int);
int main() {
// 这里直接填写一个常量、变量或是运算表达式都是可以的,一般称实际传入的值为实际参数(实参)
test(10, 20);
}
// 函数具体定义中也要写上,这里的a和b称为形式参数(形参),等价于函数中的局部变量,作用域仅限此函数
void test(int a, int b) {
printf("%d", a + b);
}
30
实际上传入的实参在进入到函数时,会自动给函数中形参(局部变量)进行赋值,这样在函数中就可以得到外部传入的参数值了。
来看看printf
函数是怎么写的:
int printf(const char * __restrict, ...) __printflike(1, 2);
这里主要关心它的两个参数:
- 第一个参数是
char *
(由于还没有学习指针,这里就把它当做const char[]
就行了),表示一个不可修改的字符串 - 第二个参数是
...
,这三个点是个啥?
如果想要填写具体需要打印的值时,可以一直往后写:
printf("%d, %d", 1, 2);
正常情况下函数的参数列表都是固定的,怎么才能像这样写很多个呢?
这就要用到可变长参数了,不过可变长参数的使用比较麻烦,这里就不做讲解了。
如果修改形式参数的值,外面的实参值会跟着发生修改吗?
#include <stdio.h>
void swap(int, int);
int main() {
int a = 10, b = 20;
swap(a, b);
printf("a = %d, b = %d", a, b);
}
void swap(int a, int b) {
// 这里对a和b的值进行交换
int tmp = a;
a = b;
b = tmp;
}
a = 10, b = 20
通过结果发现,虽然调用了函数对 a 和 b 的值进行交换,但并没有什么影响。这是为什么呢?
还记得前面说的吗,函数的形参实际上就是函数内的局部变量,它的作用域仅仅是这个函数,而外面传入的实参,仅仅只是将值赋值给了函数内的形参而已,并且外部的变量跟函数内部的变量作用域都不同,这里交换的仅仅是函数内部的两个形参变量值,跟外部作实参的变量没有任何关系。
那么,怎么样才能实现通过函数交换两个变量的值呢?这个问题会在指针部分进行讨论。
不过数组却不受限制,我们在函数中修改数组的值,是直接可以生效的:
#include <stdio.h>
void test(int arr[]);
int main() {
int arr[] = {4, 3, 8, 2, 1, 7, 5, 6, 9, 0};
test(arr);
printf("%d", arr[0]);
}
void test(int arr[]) {
// 数组就可以做到里面修改,外面生效
arr[0] = 999;
}
999
如果就是希望每次调用函数时保留变量的值,可以使用静态变量:
#include <stdio.h>
void test();
int main() {
test();
test();
}
void test() {
// 静态变量会在函数创建时就定义,后续不会再定义,且不会在函数结束时销毁其值
static int a = 20;
a += 20;
printf("%d ", a);
}
40 60
接着来看函数的返回值,并不是所有的函数都是执行完毕就结束了的,可能某些时候需要函数告诉我们执行的结果如何,这时就需要用到返回值了,比如现在希望实现一个函数计算 a + b 的值:
#include <stdio.h>
// 现在要返回a和b的和,因为参数都是int,所以这里需要将返回值类型也设定为int
int sum(int ,int);
int main() {
// 计算a和b的和
int a = 10, b = 20;
// 函数执行后,会返回一个int类型的结果,可以接收它,也可以像下面一样直接打印,也可以参与运算
int result = sum(a, b);
printf("a+b=%d", sum(a, b));
}
int sum(int a, int b) {
// 通过return关键字来返回计算的结果
return a + b;
}
a+b=30
接着来看下一个例子,现在希望通过函数找到数组中第一个小于 0 的数字并将其返回,如果没有找到任何小于 0 的数,就返回 0:
#include <stdio.h>
// 需要两个参数,一个是数组本身,还有一个是数组的长度
int findMin(int arr[], int len);
int main() {
int arr[] = {1, 4, -9, 2, -4, 7};
int min = findMin(arr, 6);
printf("第一个小于0的数是:%d", min);
}
int findMin(int arr[], int len) {
for (int i = 0; i < len; ++i) {
// 当判断找到后,直接return返回即可,这样的话函数会直接返回结果,无论后面还有没有代码没有执行完,整个函数都会直接结束。
if (arr[i] < 0) {
return arr[i];
}
}
// 如果没有找到就返回0
return 0;
}
第一个小于0的数是:-9
这里使用了return
关键字来返回结果,注意当程序走到return
时,无论还有什么内容没执行完,整个函数都将结束,并返回结果。
带返回值(非void)的函数中都需要有一个对应的返回值:
int test(int a) {
if (a > 0) {
// 当a大于0时有返回语句
return 10;
} else{
// 但是当a不大于0时就没有返回值了,这样虽然可以编译通过,但是会有警告(黄标),运行后可能会出现一些无法预知的问题
}
}
如果是没有返回值的函数,也可以调用return
来返回,如果在函数结束之前返回,代表提前结束函数;如果在函数末尾返回,就代表函数正常结束(默认情况下是可以省略的)
void test(int a){
if(a == 10) return; //因为是void,所以什么都不需要加,直接return
printf("%d", a);
}
递归调用
函数除了在其他地方被调用之外,也可以自己调用自己,这种方式称为递归。
#include <stdio.h>
void test(){
printf("Hello World!\n");
// 函数自己在调用自己,这样的话下一轮又会进入到这个函数中
test();
}
int main() {
test();
}
如果运行上面的程序,会发现程序直接无限打印Hello World!
这个字符串,这是因为函数自己在调用自己,不断地重复进入到这个函数。理论情况下,它将永远都不会结束,而是无限地执行这个函数的内容。
但是到最后程序还是终止了,这是因为函数调用有最大的深度限制,因为计算机不可能放任函数无限地进行下去。
(选学)大致了解一下函数的调用过程,实际上在程序运行时会有一个叫做函数调用栈的东西,它用于控制函数的调用。
以下面的程序为例:
#include <stdio.h>
void test2(){
printf("调用test2");
}
void test(){
test2();
printf("调用test");
}
int main() {
test();
printf("调用main");
}
其实可以很轻易地看出整个调用关系,首先是从 main 函数进入,然后调用 test 函数,在test函数中又调用了 test2 函数,此时就需要等待 test2 函数执行完毕,test 才能继续,而 main 则需要等待 test 执行完毕才能继续。而实际上这个过程是由函数调用栈在控制的:而当 test2 函数执行完毕后,每个栈帧又依次从栈中出去:当所有的栈全部出去之后,程序结束。
所以这也就不难解释为什么无限递归会导致程序出现错误,因为栈的空间有限,而函数又一直在进行自我调用,所以会导致不断地有新的栈帧进入,最后塞满整个栈的空间,就爆炸了,这种问题称为栈溢出(Stack Overflow)
当然,如果按照规范使用递归操作,是非常方便的,比如现在需要求某个数的阶乘:
#include <stdio.h>
int test(int n);
int main() {
printf("%d", test(3));
}
int test(int n) {
// 因为不能无限制递归下去,所以我们这里添加一个结束条件,在n = 1时返回
if (n == 1) {
return 1;
}
// 每次都让n乘以其下一级的计算结果,下一级就是n-1了
return test(n - 1) * n;
}
6
通过给递归调用适当地添加结束条件,这样就不会无限循环了,并且程序看起来无比简洁,那么它是如何执行的呢:
它看起来就像是一个先走到底部,然后拿到问题的钥匙后逐步返回的一个过程,并在返回的途中不断进行计算最后得到结果。
所以,合理地使用递归反而是一件很有意思的事情。
实战:斐波那契数列解法其三
前面介绍了函数的递归调用,来看一个具体的实例吧,还是以解斐波那契数列为例。
既然每个数都是前两个数之和,那么是否也可以通过递归的形式不断划分进行计算呢?依然可以借鉴之前动态规划的思想,通过划分子问题,分而治之来完成计算。
#include <stdio.h>
int fib(int n) {
if (n == 1 || n == 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
int main() {
printf("%d", fib(7));
}
13