C语言 12 函数

news2024/9/21 17:05:53

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2131939.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SMT | Kriging代理模型原理及应用

前言 代理模型工具箱 (surrogate modeling toolbox, SMT) 是一个基于Python开发的第三方包&#xff0c;其中包含代理模型方法、采样技术和基准测试函数。有关SMT的详细介绍参见&#xff1a; SMT | 代理模型Python工具包推荐 SMT可实现几个与高斯过程回归相关的代理模型&#x…

串口输出时:英文正常输出、中文乱码输出

一、问题&#xff1a;英文正常输出&#xff0c;英文乱码输出 二、解决方法 1、查看自己使用的串口助手的编码格式 2、查看自己使用输出的文件编码格式 以记事本的格式查看&#xff0c;原则上这两种应该保持相同&#xff0c;如果不相同&#xff0c;就需要把这个文件去另保存一…

图像与文本并存,多模态检索如何带来新的搜索革命

01 火热的多模态智能 回顾到2024的大型语言模型&#xff08;LLM&#xff09;的发展&#xff0c;让人欣喜的一点是scaling law依然奏效&#xff0c;智能随着资源的提高继续提高。但另一个让人担忧的点是高质量的文本语料似乎即将触及上限。为了加入更多的数据喂给模型&#xff0…

体育场座位【python实现】

题目来自此处 def main():seats list(map(int,input().split()))count 0for i in range(len(seats)):if seats[i] 0:if (i 0 or seats[i-1] 0) and(i len(seats)-1 or seats[i1] 0):count 1seats[i] 1print(seats)print(count) if __name__ "__main__":mai…

Win11 eNSP安装

前言 新买的电脑&#xff0c;安装eNSP总会遇到一些问题。如果你之前就是做网络安全而现在需要安装eNSP&#xff0c;你可能会因为安装过Wireshark导致一些问题。所以这里就为大家综合一篇文章&#xff0c;修复一些简单的问题。 下载地址&#xff1a;https://pan.baidu.com/s/17p…

Shell:初识sed、awk

Linux系统提供了两个常见的具备上述功能的工具。本节将会介绍Linux世界中最广泛使用的 两个命令行编辑器:sed和gawk。 1. sed编辑器 sed编辑器被称作流编辑器(stream editor)&#xff0c;流编辑器则会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。 sed编辑器可…

会做“减法”的项目经理,在工作中赢麻了!

都说我们在生活中要学会“断舍离”&#xff0c;其实工作中的一些事项、流程&#xff0c;也可以尽量精简&#xff01;对于项目经理来说也是如此&#xff0c;每天会议很多、需求不断&#xff0c;要适当做好“减法”&#xff0c;才能更好朝着目标方向前进&#xff01; 01、什么是做…

Linux 添加新用户之adduser 和 useradd 的区别 | 添加用户到 sudo 组【笔记型博文】

&#x1f947; 版权: 本文由【墨理学AI】原创首发、各位读者大大、敬请查阅、感谢三连 &#x1f389; 声明: 作为全网 AI 领域 干货最多的博主之一&#xff0c;❤️ 不负光阴不负卿 ❤️ 文章目录 ❤️ 创建新用户adduser 用法【推荐】useradd 用法 安装 sudo添加用户到 sudo 用…

Transformer学习(1):注意力机制

文章目录 什么是注意力如何实现注意注意力的计算过程总结 什么是注意力 在一张图像中&#xff0c;包含了各种信息&#xff0c;而我们会自动关注重要的信息。下图是注意力热力图&#xff0c;可以发现人们会注意兔子的脸这些重要信息。 而在深度学习中&#xff0c;输入数据包含…

Selenium自动化 Web 浏览器操作

文章目录 Selenium自动化 Web 浏览器操作Selenium简介安装Selenium安装WebDriver使用问题驱动加载报错版本不匹配 常用API创建实例定位元素鼠标事件表单相关多窗口切换等待显示等待隐式等待 文件上传下载经验总结 Selenium自动化 Web 浏览器操作 Selenium简介 Selenium可以模…

Bio-Linux-shell详解-1-从0开始

21世纪是数据的世纪&#xff0c;蓬勃发展的生物学积累了大量的数据&#xff0c;急需计算生物学、生物信息学及系统生物学等交叉学科大放异彩&#xff0c;而windows作为我们最熟悉的操作平台&#xff0c;并不能承担如此巨大的工作量&#xff0c;课题组的服务器因此承担了这个责任…

玩崩坏星穹铁道手机配置低、卡顿发烫、内存不足 GameViewer远程助你手机畅玩星铁PC端

9月10日&#xff0c;《崩坏&#xff1a;星穹铁道》迎来2.5版本「碧羽飞黄射天狼」&#xff01;不知道大家有没有抽到 飞霄&#xff1f;这次崩铁还为我们送了10连和 1000星琼 &#xff0c;上半卡池还有五星角色飞霄、知更鸟、卡芙卡、黑天鹅这四位角色&#xff0c;深受大家喜爱。…

Redis常用操作及springboot整合redis

1. Redis和Mysql的区别 数据模型&#xff1a;二者都是数据库,但是不同的是mysql是进行存储到磁盘当中,而Redis是进行存储到内存中. 数据模型 : mysql的存储的形式是二维表而Redis是通过key-value键值对的形式进行存储数据. 实际的应用的场景: Redis适合于需要快速读写的场景&…

在Word中,用VBA比较两段文本的相似度

效果1: 去掉字符串中回车&#xff0c;进行改进后效果&#xff1a; 代码&#xff1a; Function LevenshteinDistance(s As String, t As String) As IntegerDim d() As IntegerDim i As IntegerDim j As IntegerDim cost As IntegerDim sLen As IntegerDim tLen As IntegersLen…

nginx实现https安全访问的详细配置过程

文章目录 前言什么是 HTTP&#xff1f;什么是 HTTPS&#xff1f;HTTP 和 HTTPS 的区别为什么 HTTPS 被称为安全的&#xff1f;配置过程配置自签名证书 前言 首先我们来简单了解一下什么是http和https以及他们的区别所在. 什么是 HTTP&#xff1f; HTTP&#xff0c;全称为“超…

IDEA 怎么编辑文件模板

1.打开设置2.打开编辑者&#xff0c;选择编辑文件模板 3.点击加号4.先随便编译一次5.之后编辑文件模板就有了

电脑浏览器访问华为路由器报错,无法访问路由器web界面:ERR_SSL_VERSION_OR_CIPHER_MISMATCH 最简单的解决办法!

This site can’t provide a secure connection192.168.2.1 uses an unsupported protocol. ERR_SSL_VERSION_OR_CIPHER_MISMATCH 以上是chrome浏览器访问报错 Secure Connection Failed An error occurred during a connection to 192.168.2.1. Peer using unsupported versio…

python 异步执行 apply_async 方法

1.项目需要读取hive表数据&#xff0c;并对返回的数据进行解析&#xff1b; select * from table ; 2.返回数据&#xff0c;根据库、表&#xff0c;对其进行 下一步执行&#xff1b; 中间遇到一个棘手的问题&#xff0c;在python线程池中&#xff0c;使用异步非阻塞 apply_a…

(k8s)kubernetes 挂载 minio csi 的方式(pod挂载pvc存在csi驱动问题,挂载不上)

一、安装Minio&#xff08;Minio分布式集群搭建部署_minio集群最少几台-CSDN博客&#xff09; 生成accessKeyID和secretAccessKey&#xff1a; 二、安装csi-s3插件(在k8s集群上) 首先我们把插件的yaml文件都下载下来&#xff0c;为了保证版本测试的一致性&#xff0c;我们下载…

如何在社交媒体上赚取(可观的)收入

有很多百万富翁是通过社交媒体粉丝建立起他们的财富的&#xff0c;而且其中大部分并不是Instagram网红或YouTube明星。 例如&#xff0c;Nick Huber通过社交媒体粉丝建立了几家企业&#xff0c;这些企业现在每月创造40万美元的收入。 而George Blackman在Twitter上仅有不到8,0…