1、函数的定义

        说明:在C语言中,函数意味着功能模块,一个典型的C语言程序(项目),就是由一个个功能模块拼接起来的整体,也因为如此,C语言被称为模块化语言(面向过程的语言),对于函数的使用者,可以简单地理解为一个黑箱,使用者只管按照规定给黑箱输入一些输入,就会得到一些输出,而不必理会黑箱内部的运行情况

  • 黑箱的输入和输出:

  • 举个例子(电视机): 日常使用的电视机可以被理解为一个典型的黑箱,它有一些公开的接口提供给使用者操作,比如:音量、频道等。作为使用者不需要理会其内存电路,更不需要管电视机的工作原理,只需要规定接口,操作接口就可以得到结果

        对于函数的设计者,最重要的工作是封装,封装意味着对外提供服务并隐藏细节,对于 一个封装良好的函数而言,其对外提供服务的接口应当是简洁的,内部功能应当是明确的

2、函数的说明和使用

  • 函数头:函数对外的接口
    • 函数名称:命名规则与变量一致,一般取与函数实际功能相符合的、顾名思义的名称(在这里建议大家采用ST公司的命名规则)
    • 参数列表:即黑箱的输入数据列表,一个函数可有一个或多个参数,甚至可以不要参数
    • 返回类型:即黑箱的输出数据类型,一个函数可不返回数据,但最多只能返回一个数据
  • 函数体:函数功能的内部实现(即使用花括号括起来的内容)
  • 语法说明:
    关键字 返回值类型 函数名(参数列表)
    {
        代码块;
        return 返回值;
    
        /*
            说明:
                关键字: 用来修饰函数的(一般系统会存在默认关键字,非必要的), 比如: static inline
                        1、使用 static 修饰的函数称之为静态函数, 仅限于本文件中有效;
                        2、使用 inline 修饰的函数称之为内联函数,类似与宏替换;
                返回值类型: 用来确定返回值是什么类型的数据(return 0; -> int return 'a'; -> char ...)
                函数名: C语言标识符,也就是给一块{ }代码起一个名字。(如果要调用此函数,则通过函数名调用即可)
                参数列表: 调用函数的时候,想要给这个函数传递什么样的数据。(传参)
        */
    }
    • 代码示例:
      • 示例1:有返回值,有传参
      • 示例2:没有返回值,有传参
      • 示例3:有返回值,没有传参
      • 示例4:没有返回值,没有传参
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    // 函数示例1(有返回值,有传参):比较两个int型数据的大小
    int MAX_OfTwoNum(int num1, int num2)
    {
        int max = 0;
        max = num1>num2?num1:num2;
        return max;
    }
    
    // 函数示例2(没有返回值,有传参):交换两个数据
    void SWAP_OfTwoNum(double *p1, double *p2)
    {
        // 1、判断p1和p2是否为NULL,是的话直接退出即可
        if ( (p1 == NULL) || (p2 == NULL) )
            return;
    
        // 2、数据交换
        double tmp = 0;
        tmp = *p1;
        *p1 = *p2;
        *p2 = tmp;
    }
    
    // 函数示例3(有返回值,没有传参):申请一块堆空间(申请资源的时候)
    char* MEM_Init(void)
    {
        // 堆空间申请内存(推荐)
        char *p = malloc(100);  // 堆空间, 只能由程序员申请和释放(或者整个程序结束了)
        bzero(p, 100);          
    
        // 栈区申请内存
        // char buf[128] = {0};    // 不建议在函数里面这么写,因为函数结束后,其内存空间会被系统释放掉(所以其它工程里面的其它函数用不了这个内存空间)
        // char *p = buf;
        return p;
    }
    
    // 使用函数示例3申请的内存空间
    void func(char *p)
    {
        strcpy(p, "人生没有彩排,只有现场直播,所以每一件事都要努力做到最好\n");
        // 只有这个指针p指向的这个内存有合法空间并且可读可写,才可以使用这个strcpy的字符串函数进行赋值操作
    }
    
    // 函数示例4(没有返回值,没有传参):消息提示、初始化
    void MSG_Tip(void)
    {
        printf("今天天气真好,花儿都开了\n");
    }
    
    // 主函数
    int main(int argc, char const *argv[])
    {
        // - 示例1:有返回值,有传参
        int ret = 0;
        ret = MAX_OfTwoNum(100, 200);
        printf("ret == %d\n", ret);
    
        // - 示例2:没有返回值,有传参
        double f1 = 3.14;
        double f2 = 6.28;
        printf("交换前:f1 == %.1f, f2 == %.1f\n", f1, f2);
        SWAP_OfTwoNum(&f1, &f2);
        printf("交换后:f1 == %.1f, f2 == %.1f\n", f1, f2);
    
        // - 示例3:有返回值,没有传参
        char *p1 = MEM_Init();      // 让指针p1指向一个合法内存空间
        func(p1);                   // 通过指针p1将数据赋值到指针p1指向的内存空间里面
        printf("p1 == %s\n", p1);   // 通过指针p1打印它指向的那个内存空间的数据
    
        // - 示例4:没有返回值,没有传参
        MSG_Tip();
    
        return 0;
    }
    
    • 语法汇总:
      • 当函数的参数列表为void时,表示该函数不需要任何参数
      • 当函数的返回类型为void时,表示该函数不返回任何数据
      • 关键字return表示退出函数:若函数头中规定由返回数据类型,则return需要携带一个类型与之匹配的数据;若函数头中规定返回的类型为void,则return不需要携带参数(直接return即可)
    • 图解:

    3、函数的实参和形参

    • 概念:
      • 函数调用中的参数,被称为实参,即:argument("实际参数": 在函数调用过程中,主调函数传递给被调函数的输入参数值,我们称为实际参数。(具有实际数据))。
      • 函数定义中的参数,被称为形参,即:parameter("形式参数": 被调函数在定义时的参数。(只有一个形式/框架,不具有实际数据))。
    • 实参和形参的关系:
      • 实参和形参的类型和个数必须一一对应
      • 形参的值由实参初始化
      • 形参与实参位于不同的内存区域,彼此独立
    • 示例代码:
      #include <stdio.h>
      
      // 宏定义 -- 比较两个数据的大小
      #define MAX(A, B) (A)>(B)?(A):(B)
      
      // 普通函数
      int MAX_OfThreeNum(int num1, int num2 , int num3)   // 函数定义的地方:形参(形式参数),实参与形参的类型和个数必须一一对应
      {
          int max = MAX(num1, num2);
              max = MAX(max, num3);
          return max;
      }
      // 主函数
      int main(int argc, char const *argv[])
      {
          
          int num1 = 100;                                 // 实参和形参位于不同的内存区域,彼此独立(因为它们处于不同的花括号{}中)
          int num2 = 200;
          int num3 = 300;
      
          int ret = 0;
          ret = MAX_OfThreeNum(num1, num2, num3);         // 函数调用的地方:实参(实际参数)
          printf("ret == %d\n", ret);
          return 0;
      }

    4、函数的局部变量和栈内存

    • 局部变量的概念:凡是被一对{}花括号包含的变量,称为局部变量
    • 局部变量特点:
      • 某一函数内部的局部变量,存储在该函数的特定的栈内存中
      • 局部变量只能在该函数内存可见,在该函数外不可见
      • 当该函数退出后,局部变量所占的内存被系统回收,因此局部变量也称为临时变量
      • 函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量
    • 栈内存的特点:
      • 每当一个函数被调用的时候,系统会自动分配一段栈内存给该函数,用于存放其局部变量
      • 每当一个函数退出的时候,系统会自动回收其栈内存
      • 系统为函数分配栈内存时,遵循从上(高位地址)到下(低位地址)分配的原则
    • 图解:

    • 示例代码:
      #include <stdio.h>
      #include <stdlib.h>
      #include <strings.h>
      
      // 普通函数
      int func1(int a, int b)     // 局部变量(栈区)3:函数的形参(虽然不被花括号{}括住,但是依然属于函数体的范畴)
      {
          // int a;               // 上面的形参,相当于在函数体里面定义了这两个变量
          // int b;
          int c = a+b;            // 局部变量(栈区)4:     
          return c;               // 在函数执行完前,已经将这个c变量里面的值,传了出去
      }
      
      char *func2(void)
      {
          // 栈内存空间申请
          char p[] = "hello world!";    // 局部变量(栈区)5:
          return p;                                    
          /*
              解析:
                  warning: function returns address of local variable [-Wreturn-local-addr]
                  翻译:警告,返回的是一个函数的临时变量的地址
              说明:
                  所以这个地址可以获取,但是其指向的内存空间已经被系统释放掉了
                  其它函数调用这个局部变量的地址的函数后,会出现这个问题
          */
      
          // 推荐:堆空间的申请(但是要注意的事情是,需要及时释放申请的空间,否则会造成内存泄漏)
          // char *p = malloc(100);
          // bzero(p, 100);
          // return p;
      }
      
      // 主函数
      int main(int argc, char const *argv[])
      {
          // (1)、局部变量的说明(凡是被一对{}花括号包含的变量,称为局部变量)
          //      (只要被花括号括起来的就是另一个内存区域,即使和其它区域的名字相同,也是相互独立的)
          // 局部变量(栈区)1:
          int a = 100;
          int b = 200;
      
          {
              int a = 100;        // 局部变量(栈区)2:
              int b = 200; 
          }
      
          // 函数调用
          int ret = func1(100, 200);  
          /*
              返回的是值(储存它的值的内存空间在赋值前才被释放掉,这个值重新
              找了现在这个变量ret来存放其数据了(记住一件事情:你看看你现在操作的
              内存是不是被释放掉的内存(或者说合不合法))
      
          */
          printf("ret == %d\n", ret);
      
          // (2)、函数运行完后,系统将自动将其内存释放,最好不要再次调用
          char *p = func2();      // 因为指针p指向的还是那块内存(已经被释放掉的内存,所以后续指针对其操作是不可以的)
          
          return 0;
      }

    5、回调函数

            概念:回调函数就是一个被作为参数传递的函数。

    int func(int a)
    {
        printf("func a:%d\n", a);
    }
    int show(int (*pfunc)(int))
    {
        pfunc(10);
    }
    
    // 主函数
    int main(int argc, char **argv)
    {
        show(func);    // 此时的func函数就是一个回调函数
    
        /*
        头文件:    #include <stdlib.h>
        函数原型:  void qsort(void *base, size_t nmemb, size_t size,
                      int (*compar)(const void *, const void *));
        参数说明:
             base    指向你要排序的数组的开头地址(就是你要排序的数组)
             nmemb   代表数组中的元素数量(就是数组有多少个元素)
             size    表示每一个元素的大小(元素数据类型大小)
             compar  为一个函数指针,这个函数用来判断两个元素间的大小关系的,
        若传给compar的第一个参数所指的元素大于第二个参数所指的元素则返回大于0的值,若两值相等,则返回0;
        */
        return 0;
    }
    执行过程;
        1 main()识别到show(func),执行show()函数,并将func函数传递到show()中;
        2 在show()函数中,识别到pfunc()的形参,则识别形参*pfunc的地址,并将10传递到func函数中,再跳转到func中;
        3 在func()函数中,接受形参a的值,并执行其中的代码语句;
    */

    6、内联函数

    • 概念:类似于宏替换,是由inline修饰的函数。意指:当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码,整段插入到当前位置。这样做的好处是省去了调用的过程,加快程序运行速度。(将函数的东西插入到原代码中) ----- (这不是原生C语言的函数,是引用C++里面的)
    • 应用场景:代码工程量大时,需要简短代码;函数被频繁调用时;

    示例代码:

    #include <stdio.h>
    
    inline int add(int a, int b)
    {
        return (a + b);
    }
    
    int main(void)
    {
        int a;
     
        a = add(1, 2);
        printf("a+b=%d\n", a);
     
        return 0;
    }
    • 使用注意:
      • 内联函数实际上是用空间代价(程序尺寸增大)来换取时间效率(不再需要切换函数);
      • 内联函数定义一般放在头文件中;
      • 内联函数的代码块尽量不超过5行;
      • 内联函数体中,不能有循环语句、if语句或switch语句,否则,函数定义时即使有inline关键字,编译器也会把该函数作为非内联函数处理。内联函数要在函数被调用之前声明。
      • 关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。而且内联函数在声明阶段也要加上inline关键字。
    • 内联函数与函数封转区别:
      • 资源开销区别:内联函数是在编译阶段展开,函数封装在代码运行时调用展开的,宏定义是在预处理阶段展开的;
      • 内联函数在编译阶段展开,所以可能会增加生成的可执行文件大小; (编译过程产生可执行文件)
      • 函数封装在代码运行时调用展开的,所以可执行文件相对较小,但运行效率相对低一点;(运行过程会影响CPU效率)
      • 宏定义在预处理阶段展开,且只做宏替换,所以代码编译过程中产生的文件较大。

    7、静态函数

    • 概念:由static修饰的函数,称之为静态函数
    • 作用:防止一个工程中不同文件因函数名冲突,静态函数仅限于本文件有效。(总结:限制函数作用域,就是防止函数名相同导致函数名冲突)
    • 示例代码
    #include <stdio.h>
    
    // 以下函数通过关键字static修饰,只能在本文件中使用
    // 通过static修饰的函数,就叫静态函数
    static int static_func()
    {
        printf("这是%s的static_func函数\n", __FILE__);
    }
    
    int func()
    {
        printf("这是%s的func函数\n", __FILE__);
        static_func();    // 这是test.c的static_func函数
    }
    
    static int static_func()
    {
        printf("这是%s的static_func函数\n", __FILE__);
    }
    
    int main() {
        func();        // 注释了main.c的func函数后,调用时的是test.c中的func函数
        static_func(); // 这是main.c的static_func函数
    
        /*
         vvc@DESKTOP-CFCSUDI:/mnt/e/share$ gcc main.c test.c -o main && ./main
         这是test.c的func函数
         这是test.c的static_func函数
         这是main.c的static_func函数
        */
    }
    

    8、递归函数

    • 概念:函数自己调用自己,称之为递归函数
    • 递归函数设计:

      • 问题模型本身要符合递归模型(递推模型);

      • 问题的解,当递归到一定层次时,答案是显而易见的,且能结束函数。

      •  先明确函数要实现的功能与参数的关系,暂不管功能具体的实现。

      • 呈现第n层与第n-1层的递推关系。

    • 示例代码:
    #include <stdio.h>
    
    // 幂运算函数
    int func(int base, int mi)
    {
        if (mi == 0)        // 1、可退出条件(任何数的0次方,都是1)
        {
            return 1;
        }
    
        if (mi > 1)         // 1、可退出条件
        {
            base = base*func(base, mi-1);   // 2、逐渐递进(条件越来越接近)的过程
        }
        return base;                        // 3、逐渐回归(答案越来越接近)的过程
    }
    
    // 斐波那契数列fibonacci sequence
    int f_s(int N)  // 6
    {
        if ( (N == 1) || (N == 2))  // 1、可退出条件
        {
            return 1;
        }
        return f_s(N-1)+f_s(N-2);   // 2、答案和条件越来越近的过程
    }
    /*
        解析
        f_s(6-1)+f_s(6-2)+ 
        f_s(5-1)+f_s(5-2)  f_s(4-1)+1
        f_s(4-1)+1 1+1 1+1+1
        1+1+1 1+1 1+1+1
    */
    
    // 例子1:输出自然数(大于等于0的正整数:0 1 2 3 4 5 6 7 8...)
    void func1(int num)  // 5 4 3 2 1 0 -1
    {
        if ( num < 0)               // 1、可退出条件
        {
            return;
        }
    
        func1(num-1);               // 2、逐渐递进的过程(退出条件越来越接近)
        printf("%d ",num);          // 3、逐渐回归的过程(答案越来越接近)
    }
    
    
    // 例子2:阶乘(不能乘0,也没有必要乘1:0*1*2*3*4*5)
    int func2(int num)  // 5*func2(5-1)、5*4*func2(4-1)、5*4*3*func2(3-1)、5*4*3*2func2(2-1)、1
    {
        if (num>1)                  // 1、设置可退出条件
        {
           num = num*func2(num-1);  // 2、逐渐递进的过程(退出条件越来越接近)
        }
        return num;                 // 3、逐渐回归的过程(答案越来越接近) 
    }
    
    // 主函数
    int main(void)
    {
        // main();  这么写,而且没有退出条件,直接撑爆了栈内存(报错误:Segmentation fault (core dumped))
        // 例子1:输出自然数
        func1(5);
        printf("\n");
        
    
        //练习1: 幂运算。写一个函数,输入参数数据(底数和幂数), 比如,底数为5,幂数为5,返回值(相当于 ==  5*5*5*5*5 )
        printf("数据 == %d\n", func(5, 5));
    
    
        //练习2:编写一个程序,用户输入整数 N,程序输出第 N 项斐波那契数。
        int ret = f_s(7);
        printf("ret == %d\n", ret);
    
        return 0;
    }
    
    • 程序图解:

    • 递归函数注意事项:
      • 自己调用自己的才叫递归函数,自己调用别的函数叫函数嵌套;
      • 递归函数一定要有一个终止条件,否则容易出现栈溢出(段错误);
      • 递归函数最好不要递归太深(太多次),否则容易出现栈空间奔溃;
    • 递归函数和循环的区别:
      • 递归函数是一种调用自身的方法,通常用于解决可以分解成相似子问题的任务,但注意栈溢出,不能递归深度过大;
      • 循环是一种重复执行一段代码的机制,通常用于处理重复性高的任务,但执行代码没递归直观;
      • 递归函数每调用函数时,都会在栈上创建一个新的帧,循环只使用一个帧;
      • 递归适用于自然分解子问题的任务,如树的遍历、阶乘计算等;循环一般用在数组便利、计数等比较简单的任务;

    Logo

    AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

    更多推荐