Files
BlogPosts/Collection/YoudaoyunNotes/02C语言/06-函数基础.md

13 KiB
Raw Blame History

一、概述

在C语言中函数指的是功能模块。一个典型的C语言程序就是由一个个的功能模块拼接起来的整体因此C语言也称模块化语言。

对于函数的使用者,可以简单的将函数视为一个黑箱子,使用者只管按照规定给黑箱一些输入,就会得到一些输出,使用者不需要关注黑箱内部的结构细节。

函数分为两种形式

  • 系统自带函数(库函数):只需了解如何使用和结果是什么,如:购买了一台电视机,我们只需要了解电视机对外的接口和使用的方法及最后的结果,不需要知道电视机内部构造----怎么用。

函数的头文件 函数的功能 函数的参数 函数的返回值

  • 用户自定义函数:要明确最终实现的功能,如:自己设计电视机 ----怎么写。

函数的声明式 函数的实现 函数的调用

二、函数入门

  • 函数头:函数对外的接口及运行的结果都在这里体现

  • 组成

  • 函数的类型是函数的运行结果数据类型即黑箱的输出数据类型函数返回值类型不是必须的没有返回值是使用void。

  • 函数的名字:函数在内存中的地址,代表这个黑箱的名称(必须满足用户标识符定义规则),一个函数必须要有名字。

  • 函数的参数列表:函数的输入,即黑箱的输入数据列表,不是必须的没有参数输入也不能省略括号()应当在参数列表中使用void进行修饰

  • 函数体:函数的功能实现,即黑箱子的内部构造。

/* 给定两个数,得到最大值 */
int Maxfun(int x, int y)  // 这是一个函数的定义该函数接收两个int类型的参数返回一个int类型数据
{
    return x>y?x:y;
}

/* 交换两个浮点数 */
void swap(double *p1, double *p2) // 该函数接收两个double *类型的参数,无返回值
{
    if (p1 == NULL || p2 == NULL)
        return;  // 退出当前函数
    
    double tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

/* 液晶屏初始化函数 */
char *initLCD(void)  // 该函数不接收参数返一个char *类型的数据
{
    int lcd = open("/dev/fb0", O_RDWR);

    struct fb_var_screeninfo vinfo;
    ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo);

    int bpp = vinfo.bits_per_pixel;
    int size = vinfo.xers*vinfo.yers*bpp/8;

    char *fbmmem = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHSRED, lcd, 0);

    return fbmmem;
}

总结:

  • 当函数的类型为void时表示函数不返回任何数据。

  • 当函数的参数列表为void时表示函数不需要任何参数。

  • 关键字return表示退出函数。

①若函数头中规定有函数的类型则return需要携带一个类型与之匹配的数据

②若函数头中规定函数的类型为void则return不需要携带数据

三、自定义函数

  • 函数的定义

  • 表示函数的功能实现及函数的确立。

返回值类型  函数名字(参数1, 参数2, ...)  // 没有返回值则写void  没有参数也写void
{
    功能语句;
}
  • 函数的声明

  • 表示告诉编译器函数将会被使用,位于函数被调用前的函数外部。

函数的类型 函数名称(参数类型及个数); 

注意

  • 当函数的调用出现在函数的定义位置前,则需要在调用前进行声明。

  • 函数的声明一般放在头文件中,调用头文件则携带函数声明式。

  • 函数的调用

  • 表示使用函数实现对应的功能,位于一个函数的内部

函数的名字(参数所对应的数据);

注意

  • 当函数被main直接调用或间接调用时函数才会被执行
#include <stdio.h>

//int Maxfun(int x, int y); // 函数的声明:当函数的调用发生在函数的定义前时需要书写
int Maxfun(int , int ); // 函数的声明:当函数的调用发生在函数的定义前时需要书写


int main(int argc, char *argv[])
{
    
    printf("%d与%d的最大值是%d\n", 80, 800, Maxfun(80,800));  // 将Maxfun函数的执行结果作为printf函数的一个参数

    return 0;
}

/* 交换两个浮点数 */
void swap(double *p1, double *p2) // 函数的定义 该函数接收两个double *类型的参数,无返回值
{
    printf("%s is running\n", __FUNCTION__);
    if (p1 == NULL || p2 == NULL)
        return;
    
    double tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

/* 给定两个数,得到最大值 */
int Maxfun(int x, int y)  // 这是一个函数的定义该函数接收两个int类型的参数返回一个int类型数据
{
    printf("%s is running\n", __FUNCTION__);
    double a = 3.14, b=5.26;
    printf("交换前a = %f  b = %f\n", a,b);
    swap( &a, &b);  // 调用一个函数:定义在调用前可,免去声明
    printf("交换后a = %f  b = %f\n", a,b);
    return x>y?x:y;
}

练习:编写一个函数实现从键盘获取三个数,从小到大输出

/* 输入三个数从小到大输出 */
#include <stdio.h>

void outputNum(void);

int main(int argc, char *argv[])
{
    outputNum();
    return 0;
}


void swap(int *p1, int *p2) // 函数的定义 
{
    
    if (p1 == NULL || p2 == NULL)
        return;
    
    *p1 ^= *p2;
    *p2 ^= *p1;
    *p1 ^= *p2;

}

void outputNum(void)
{
    int a,b,c;
    printf("请输入三个数:");
    scanf("%d%d%d", &a,&b,&c);

    if(a>b)
    {
        swap(&a,&b);
    }
    if(a>c)
    {
        swap(&a,&c);
    }
    if(b>c)
    {
        swap(&b, &c);
    }
    printf("%d < %d < %d\n",a,b,c);
}

函数参数的传递

  • 形参与实参

  • 形参:函数声明式定义中参数列表中的参数,属于函数的局部变量,在函数定义中只是一个形式参数没有实际的值。

  • 实参:函数调用中参数列表中的参数,具有实际的数值或表示对象,在函数执行期间这个数值或对象将作用与函数内部,实参会在函数执行时初始化函数的形参。

  • 值传递:表示实参的类型时一个数值,形参的类型是基本数据类型;

  • 址传递:表示实参是一个地址,形参的类型是一个指针;

  • 当需要将函数内部的形参的改变作用与实参时则需要使用址传递(传递地址),否则使用值传递(传递数值)。

总结

  • 函数的优点

  • 提高代码的重用性。 试想一下, 一个函数也许会在 N 多地方被使用, 如果没有将该功能封装起来, 而在每一个用到这个功能的地方都写一遍代码, 将会浪费很多资源。就像一个企业给每一个员工都配备一台打印机, 虽然每个人独占资源用起来很便捷, 但却浪费了大量的成本。

  • 方便维护和升级源代码。 假设需要对一个的算法修正或者修改, 那只要不改变函数接口和功能的情况下, 可以方便地进行, 不需要知道该函数在何处被调用。 调用者也感觉不到代码的改变, 因为函数的封装性使得他们让调用者感觉起来是透明的。

  • 有利于结构化代码。 将一个个功能封装在相对独立的函数里, 再将函数组装成程序,那么整个逻辑就很清晰, 出错了也较容易排查。 否则, 在一个没有结构化的代码中, 所有的功能杂乱地挤作一团, 逻辑复杂, 也极易出错。

  • 函数封装的要求

  • 高内聚:一个功能集中在一个函数的内部。

  • 低耦合:功能函数与功能函数间的影响要低。

四、主函数传参

  • 函数参数列表不为void则表示函数可以接收数据主函数"int main(int argc, char *argv[])"意味着主函数也支持接收数据。

注意:

  • 每个命令行参数间使用空格(一个或多个皆可),若参数本身就带有空格则需要使用单引号或双引号将整个内容包含

  • 一般情况下若需要接收命令行参数,则需要在主函数中判定参数是否符号要求,以保障程序的正确执行

练习

接收命令行参数三个,输出这个三个数的和

五、变参函数

  • 概念:调用函数时可根据实际需求来决定函数参数的个数
int printf(const char *restrict format, ...);
int scanf(const char *restrict format, ...);

printf("%d", a);
printf("%d, %d", a, b);
printf("%d, %d, %d", a, b, c);
  • 定义变参函数

  • 添加头文件"#inclded stdarg.h"

  • 定义函数时末尾参数使用省略号(...)表示可以更具需求来确认,省略号前可以自由设置参数类型(至少一个,强制参数)

  • 在函数定义中创建va_list类型变量用于存放可变参数

  • 使用va_start()用于初始化va_list类型变量初始化内存。

  • 使用va_arg() ,来访问可变参数列表中的每个项。

  • 使用va_end()来清理va_list变量的内存释放内存。

#include <stdio.h>
#include <stdarg.h>

int fun(int n,...);

int main(int argc, char *argv[])
{
    fun(2, 1, 2);
    fun(5, 1, 2);

    return 0;
}

int fun(int n,...)
{
    // 在函数定义中创建一个va_list变量list将来调用时传递的参数存储在list中
    va_list list;

    // 使用强制参数初始化变量list
    va_start(list, n);

    // 使用va_arg()访问list中的每个项
    for (int i = 0; i < n; i++)
    {
        printf("%d\n", va_arg(list, int));
    }
    
    // 清除list中的缓存
    va_end(list);
}

练习

编写一个变参函数用于计算多个数的平均值double保留2位精度。

六、递归函数

  • 概念:如果一个函数在内部调用自身,那么这个函数就是递归函数

  • 组成:必须要有一个结束条件(简单条件),否则会导致程序栈溢出。

void fun(void)
{
    fun();
}

#include <stdio.h>
/* 计算阶乘 */
int func(int n)
{
    if(n==1)
        return n;
    return n*func(n-1);
}

int main(int argc, char *argv[])
{
    int n = 0;
    scanf("%d", &n);
    printf("%d", func(n));
    return 0;
}

注意:递归虽好但不是所有情况都适合,比如数据规模太大会导致"栈溢出"

递归函数的特点:代码精简但效率低。

练习

  • 使用递归打印N的斐波拉契数列

1 1 2 3 5 8 13 ......

  • 有5个人坐在一起问第五个人多少岁?他说比第四个人大两岁。问第四个人岁数他说比第三个人大两岁。问第三个人又说比第二个大两岁。问第二个人说比第一个人大两岁。最后问第一个他说是10岁。编写程序当输入第几个人时求出其对应年龄。

七、回调函数

  • 回调callback 是一种非常重要的机制, 主要可以用来实现软件的分层设计, 使得不同软件模块的开发者的工作进度可以独立出来, 不受时空的限制, 需要的时候通过约定好的接口(或者标准) 相互契合在一起, 也就是 C++或者 JAVA 等现代编程语言声称的所谓面向接口编程。 同时回调也是定制化软件的基石, 通过回调机制将软件的前端和后端分离, 前端提供逻辑策略, 后端提供逻辑实现。

  • 作用:统一操作接口,开放功能定制。

八、内联函数

  • 使用关键字inline关键字修饰的函数称为内联函数
inline  void func(void);
  • 节省函数间切换所需的时间,提高函数的运行效率

  • 原理:一个普通函数在调用过程中,会在调用这个函数的函数中形成保护现场和恢复现场的过程,这个需要花费时间,降低程序的运行效率,这时可以将这个函数设计内联函数,在编译器编译过程中,编译器会将符合标准的内联函数直接展开(使用函数的功能代码替换函数的调用),这样就会节省保护现场和恢复现场的时间,同时又做到了模块化编程。

  • 内联函数的使用场景

  • 代码精简功能语句简短不具有循环、switch等语句。

  • 调用频繁

注意内联函数不是写了inline声明的函数就是内联函数能否构成内联函数是由编译器决定。若添加inline关键字但不符合编译器的标准则编译器会将其视为普通函数。

函数练习题.docx

作业

函数作业题.docx