C语言 12 函数

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

c 复制代码
// 这就是定义函数
int main() {   
   ...
}

程序的入口点就是main函数,只需要将程序代码编写到主函数中就可以运行了,不过这个函数只是由我们来定义,而不是我们来调用。

当然,除了主函数之外,一直在使用的printf也是一个函数,不过这个函数是标准库中已经实现好了的,这样就是在调用这个函数:

c 复制代码
// 直接通过 函数名称(参数...) 的形式调用函数
printf("Hello World!");    

那么,函数的具体定义是什么呢?

函数是完成特定任务的独立程序代码单元。

简单来说,函数是为了完成某件任务而生的,可能要完成某个任务并不是一行代码就可以搞定的,但是现在可能会遇到这种情况:

c 复制代码
#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");
    }
}

每次要做这个任务时,都要完完整整地将任务的每一行代码都写下来,如果程序中多处都需要执行这个任务,每个地方都完整地写一遍,实在是太臃肿了,有没有一种更好的办法能优化代码呢?

这时就可以考虑使用函数了,可以将程序逻辑代码全部编写到函数中,当执行函数时,实际上执行的就是函数中的全部内容,也就是按照制定的规则执行对应的任务,每次需要做这个任务时,只需要调用函数即可。

创建和使用函数

首先来看看如何创建一个函数,其实创建一个函数是很简单的,格式如下:

c 复制代码
返回值类型 函数名称([函数参数...]);

其中函数名称也是有要求的,并不是所有的字符都可以用作函数名称,它的命名规则与变量的命名规则基本一致,这里就不一一列出了。

函数不仅仅需要完成任务,某些函数还需要返回结果,此时就需要定义返回值,并在函数中返回这一结果;当然如果函数只需要完成任务,不需要返回结果,返回值类型可以写成void表示空。

c 复制代码
#include <stdio.h>

// 定义函数原型,因为C语言是从上往下的,所以如果要在下面的主函数中使用这个函数,一定要定义到它的上面。
void test(void);

int main() {
    // 调用函数
    test();
}

// 函数具体定义,添加一个花括号并在其中编写程序代码,就和之前在main中编写一样
void test(void) {
    printf("我是测试函数");
}
c 复制代码
我是测试函数

这样,就可以很好解决代码复用性的问题。只需要将会重复使用的逻辑代码定义到函数中,当需要执行时,直接调用编写好的函数就可以了,这样就简单很多了。

c 复制代码
#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);
    }
}
c 复制代码
HelloWorld
HelloWorld
HelloWorld

当然函数除了可以实现代码的复用之外,也可以优化程序,让代码写得更有层次感,一个程序可能会有很多很多的功能,需要写很多的代码,但是谁愿意去看一个几百行上千行的main函数呢?可以将每个功能都写到一个对应的函数中,这样就可以大大减少main函数中的代码量了。

c 复制代码
int main() {
    func1();
    func2();
    func3();
}

而从一开始就在编写的 main 函数实际上是一种比较特殊的函数,C 语言规定程序一律从主函数开始执行,所以这也是为什么一定要写成int main()的形式。

全局变量和局部变量

现在已经了解了如何创建和调用函数,在继续学习后续内容之前,我们需要先认识一下全局变量和局部变量这两个概念。

首先来看看局部变量,实际上之前使用的都是局部变量,比如:

c 复制代码
int main() {
    // 这里定义的变量i实际上是main函数中的局部变量,它的作用域只能是main函数中,也就是说其他地方是无法使用的
    int i = 10;   
}

所以下面这种写法是完全没问题的:

c 复制代码
int main() {
    for (int i = 0; i < 10; ++i) {   

    }

    for (int i = 0; i < 20; ++i) {

    }
}

虽然这里写了两个 for 都使用了 i,但是由于处于两个不同的作用域,所以互不影响


那么如果现在想要在任何位置都能使用一个变量,该怎么办呢?这时就要用到全局变量了:

c 复制代码
#include <stdio.h>

void test();

// 可以直接将变量定义放在外面,这样所有的函数都可以访问了
int a = 10;

int main() {
    a += 10;
    test();
    printf("%d", a);
}

void test() {
    a += 10;
}
c 复制代码
30

因为现在所有函数都能使用全局变量,所以这个结果不难得到。

函数参数和返回

函数可以接受参数来完成任务,比如现在想要实现用一个函数计算两个数的和并输出到控制台。

这种情况就需要将进行加法计算的两个数,告诉函数,这样函数才能对这两个数求和,那么怎么才能告诉函数呢?可以通过设定参数:

c 复制代码
#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);
}
c 复制代码
30

实际上传入的实参在进入到函数时,会自动给函数中形参(局部变量)进行赋值,这样在函数中就可以得到外部传入的参数值了。

来看看printf函数是怎么写的:

c 复制代码
int  printf(const char * __restrict, ...) __printflike(1, 2);

这里主要关心它的两个参数:

  • 第一个参数是char *(由于还没有学习指针,这里就把它当做const char[]就行了),表示一个不可修改的字符串
  • 第二个参数是...,这三个点是个啥?

如果想要填写具体需要打印的值时,可以一直往后写:

c 复制代码
printf("%d, %d", 1, 2);

正常情况下函数的参数列表都是固定的,怎么才能像这样写很多个呢?

这就要用到可变长参数了,不过可变长参数的使用比较麻烦,这里就不做讲解了。


如果修改形式参数的值,外面的实参值会跟着发生修改吗?

c 复制代码
#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;
}
c 复制代码
a = 10, b = 20

通过结果发现,虽然调用了函数对 a 和 b 的值进行交换,但并没有什么影响。这是为什么呢?

还记得前面说的吗,函数的形参实际上就是函数内的局部变量,它的作用域仅仅是这个函数,而外面传入的实参,仅仅只是将值赋值给了函数内的形参而已,并且外部的变量跟函数内部的变量作用域都不同,这里交换的仅仅是函数内部的两个形参变量值,跟外部作实参的变量没有任何关系。

那么,怎么样才能实现通过函数交换两个变量的值呢?这个问题会在指针部分进行讨论。

不过数组却不受限制,我们在函数中修改数组的值,是直接可以生效的:

c 复制代码
#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;   
}
c 复制代码
999

如果就是希望每次调用函数时保留变量的值,可以使用静态变量:

c 复制代码
#include <stdio.h>

void test();

int main() {
    test();
    test();
}

void test() {
    // 静态变量会在函数创建时就定义,后续不会再定义,且不会在函数结束时销毁其值
    static int a = 20;   
    a += 20;
    printf("%d ", a);
}
c 复制代码
40 60

接着来看函数的返回值,并不是所有的函数都是执行完毕就结束了的,可能某些时候需要函数告诉我们执行的结果如何,这时就需要用到返回值了,比如现在希望实现一个函数计算 a + b 的值:

c 复制代码
#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;   
}
c 复制代码
a+b=30

接着来看下一个例子,现在希望通过函数找到数组中第一个小于 0 的数字并将其返回,如果没有找到任何小于 0 的数,就返回 0:

c 复制代码
#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;
}
c 复制代码
第一个小于0的数是:-9

这里使用了return关键字来返回结果,注意当程序走到return时,无论还有什么内容没执行完,整个函数都将结束,并返回结果。

带返回值(非void)的函数中都需要有一个对应的返回值:

c 复制代码
int test(int a) {
    if (a > 0) {
        // 当a大于0时有返回语句
        return 10;   
    } else{
          // 但是当a不大于0时就没有返回值了,这样虽然可以编译通过,但是会有警告(黄标),运行后可能会出现一些无法预知的问题
    }
}

如果是没有返回值的函数,也可以调用return来返回,如果在函数结束之前返回,代表提前结束函数;如果在函数末尾返回,就代表函数正常结束(默认情况下是可以省略的)

c 复制代码
void test(int a){
    if(a == 10) return;   //因为是void,所以什么都不需要加,直接return
    printf("%d", a);
}

递归调用

函数除了在其他地方被调用之外,也可以自己调用自己,这种方式称为递归

c 复制代码
#include <stdio.h>

void test(){
    printf("Hello World!\n");
    // 函数自己在调用自己,这样的话下一轮又会进入到这个函数中
    test();   
}

int main() {
    test();
}

如果运行上面的程序,会发现程序直接无限打印Hello World!这个字符串,这是因为函数自己在调用自己,不断地重复进入到这个函数。理论情况下,它将永远都不会结束,而是无限地执行这个函数的内容。

但是到最后程序还是终止了,这是因为函数调用有最大的深度限制,因为计算机不可能放任函数无限地进行下去。


(选学)大致了解一下函数的调用过程,实际上在程序运行时会有一个叫做函数调用栈的东西,它用于控制函数的调用。

以下面的程序为例:

c 复制代码
#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)


当然,如果按照规范使用递归操作,是非常方便的,比如现在需要求某个数的阶乘:

c 复制代码
#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;
}
c 复制代码
6

通过给递归调用适当地添加结束条件,这样就不会无限循环了,并且程序看起来无比简洁,那么它是如何执行的呢:

它看起来就像是一个先走到底部,然后拿到问题的钥匙后逐步返回的一个过程,并在返回的途中不断进行计算最后得到结果。

所以,合理地使用递归反而是一件很有意思的事情。

实战:斐波那契数列解法其三

前面介绍了函数的递归调用,来看一个具体的实例吧,还是以解斐波那契数列为例。

既然每个数都是前两个数之和,那么是否也可以通过递归的形式不断划分进行计算呢?依然可以借鉴之前动态规划的思想,通过划分子问题,分而治之来完成计算。

c 复制代码
#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));
}
c 复制代码
13
相关推荐
dragon090727 分钟前
Python打卡day49!!!
开发语言·python
摩天崖FuJunWANG28 分钟前
c语言中的hashmap
java·c语言·哈希算法
LUCIAZZZ32 分钟前
Java设计模式基础问答
java·开发语言·jvm·spring boot·spring·设计模式
IsPrisoner35 分钟前
Go 语言实现高性能 EventBus 事件总线系统(含网络通信、微服务、并发异步实战)
开发语言·微服务·golang
秋水丶秋水1 小时前
电脑桌面太单调,用Python写一个桌面小宠物应用。
开发语言·python·宠物
大得3691 小时前
go全局配置redis,全局只需要连接一次,然后全局可以引用使用
开发语言·redis·golang
字节高级特工1 小时前
【Linux篇】细品环境变量与地址空间
linux·运维·服务器·c语言·c++·ubuntu·centos
虾球xz1 小时前
CppCon 2015 学习:Give me fifteen minutes and I’ll change your view of GDB
开发语言·c++·学习
Cyrus_柯1 小时前
网络编程(Modbus进阶)
linux·c语言·网络·tcp/ip