C语言 12 函数

其实函数在一开始就在使用了:

// 这就是定义函数
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
posted @ 2024-09-13 17:11  天航星  阅读(6)  评论(0编辑  收藏  举报