首页 > 其他分享 >C语言 12 函数

C语言 12 函数

时间:2024-09-13 17:15:53浏览次数:12  
标签:12 函数 int void C语言 printf test main

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

// 这就是定义函数
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

标签:12,函数,int,void,C语言,printf,test,main
From: https://www.cnblogs.com/skysailstar/p/18412548

相关文章

  • YOLOv9改进策略【损失函数篇】| 引入Soft-NMS,提升密集遮挡场景检测精度,包括GIoU-NMS、
    一、背景:传统的非极大值抑制(NMS)算法在目标检测中存在一个问题,即当一个物体的检测框与具有最高得分的检测框M有重叠(在预定义的重叠阈值内)时,会将该检测框的得分设置为零,从而导致该物体可能被遗漏,降低了平均精度。为了解决这个问题,作者提出了Soft-NMS算法。本文将YOLOv9默认......
  • 随机生成加减法题目函数
    importrandomfromdocximportDocument#创建一个新文档doc=Document()doc.add_heading('100以内连续加减法训练题',level=1)#随机生成加减法题目函数defgenerate_question():num1=random.randint(1,100)num2=random.randint(1,100)num3=r......
  • 金典120GB固态硬盘SM2258XT量产修复成功记录,附SM2258XT B16A开卡软件,VM29F01TEME1(2CA
    偶得一块二手的120G金典SSD,闲来无事搞一下量产,先上外观图片给大家看看:玩量产的一般都知道,找量产工具,肯定是要根据主控型号和闪存颗粒制程,来找相匹配的软件才行。因此我们拆开外壳,下图看到里面主控SM2258XT,颗粒丝印VM29F01TEME1-B16A,这块固态比较方便的地方是,单从丝印上就能看出是B1......
  • java父类、子类构造函数调用过程
    java父类、子类构造函数调用过程由此看出java类初始化时构造函数调用顺序:初始化对象的存储空间为零或null值;按顺序分别调用父类成员变量和实例成员变量的初始化表达式;调用父类构造函数;(如果实用super()方法指定具体的某个父类构造函数则使用指定的那个父类构造函数)按顺序分别......
  • 前端中的new函数:深入解析与实战应用
    前端中的new函数:深入解析与实战应用在JavaScript(以及许多其他面向对象编程语言中),new关键字扮演着创建对象实例的重要角色。它不仅用于调用构造函数来初始化新对象,还涉及一系列复杂的内部步骤来确保新创建的对象能够正确地与构造函数相关联。本文将深入探讨new函数的工作原......
  • springboot JZ车行系统-计算机毕业设计源码93812
    目 录摘 要1绪论1.1研究背景与意义1.2开发现状1.3论文结构与章节安排2 系统分析2.1可行性分析2.1.1技术可行性分析2.1.2 经济可行性分析2.1.3操作可行性分析2.2系统功能分析2.2.1功能性分析2.2.2非功能性分析2.3 系统用例分析2.4......
  • 【C总集篇】第七章 函数
    第七章函数函数复习首先,什么是函数?​函数(unction)是完成特定任务的独立程序代码单元。语法规则定义了函数的结构和使用方式。虽然C中的函数和其他语言中的函数、子程序、过程作用相同,但是细节上略有不同。一些函数执行某些动作,如printf()把数据打印到屏幕上;一些函数......
  • matlab基础之脚本与函数
    脚本与函数MATLAB命令的两种执行方式:交互式命令执行方式程序执行要充分利用MATLAB数据结构的特点(矩阵运算、矢量化编程),充分利用MATLAB自带的函数M文件:是一个文本文件,拓展名为.m可以用任何编辑程序来建立和编辑,默认matlabM文件就是将处理问题的各种命令融合到一个文件中......
  • SAP_ABAP_BAPI函数清单案例教程
    SAPABAP顾问能力模型(同心圆方法论)_sapabap顾问能力模型(同心圆方法论)-CSDN博客文章浏览阅读1.8k次,点赞5次,收藏35次。目标:基于对SAPabap顾问能力模型的梳理,给一年左右经验的abaper快速成长为三年经验提供超级燃料!_sapabap顾问能力模型(同心圆方法论)https://blog.csdn......
  • 南沙C++信奥老师解一本通题: 1212:LETTERS
    ​ 题目描述】给出一个row×col的大写字母矩阵,一开始的位置为左上角,你可以向上下左右四个方向移动,并且不能移向曾经经过的字母。问最多可以经过几个字母。【输入】第一行,输入字母矩阵行数R和列数S,1≤R,S≤20。接着输出R行S列字母矩阵。【输出】最多能走过的不同字母......