🌟 前言
接着上次的数组,这次我们来简单的讲讲C语言里面的函数。
函数和指针这两大块,在C语言中占据着重要的位置,是C语言中的主体和核心,所以它们的重要性也就不言而喻了。
Let’s get it!
文章目录
1. 函数简介
函数可以把大的计算任务分解成若干较小的任务,然后通过调用的方式达到代码复用;一个逻辑不写多遍,减少代码维护成本。
调用函数的一方不需要了解函数的具体实现,对于它来说,这部分是一个“黑盒子”,从而使得程序结构更加清晰。
C语言在设计中考虑了函数的 高效性 和 易用性 两个原则。
函数的实现应该尽量简短,因为函数可以套函数,一个程序应该尽量由许多小的函数组成,而不是少量较大的函数组成。
🌳 函数的基本概念
在刷题的过程中,系统会给我们事先提供一个函数让我们来实现,而它则是调用函数的一方。
在C语言中,最常见的当属main
函数了。
int main() { printf("5201314\n"); return 0; }
以上就是一个函数,它被称为C语言的 入口函数,或者 主函数。
所有程序执行都是从这个函数开始的,以它为例,我们引出函数的一些基本概念。
🌳 函数的基本结构
如图:
通过这个图,我们类比main()
这个函数,它的:
返回类型 是int
32位整型,
函数名 为main
,
参数列表 为空
,
函数体 为 printf("5201314\n");
,
返回值 为0
。
🌳 返回类型
函数的返回类型可以是任意类型;
例如:整型int
,浮点型float
,字符型char
,自定义类型等待;
返回类型 和 返回值 是配套的,当返回类型为void
时,函数内部的返回值可以写return
;也可以省略不写。
🌳 函数名
函数名可以类比我们自己的名字。是给函数调用方用的。
例如:main
和printf
都是函数名。
🌳 参数列表
函数的参数列表必须用()
括起来,参数是函数需要处理的数据;
例如:
printf("hello\n");
这一段代码用来输出字符串,"hello\n"
就是一个参数,参数类型是字符串。
下面会详细解释:printf
;
🌳 函数体
函数内部就是你可以任意发挥的部分,也就是函数的核心逻辑部分,可以是各种语句的组合;当然也可以是另一个函数,也就是函数说,函数是支持嵌套的。
🌳 返回值
函数的返回值则表示:这个函数最后返回给调用方的数据;
如果返回值的类型和函数的返回类型不一致,则会进行强制类型转换;前提是能够强制转换的情况下。
2. C语言中函数的分类
- 库函数
- 自定义函数
🌳 库函数
为什么会有库函数?
- 我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(
printf
)。 - 在编程的过程中我们会频繁的做一些字符串的拷贝工作(
strcpy
)。 - 编程是我们也计算,总是会计算n的k次方这样的运算(
pow
)
像上面我们描述的基础功能,它们不是业务性的代码。
我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
这里推荐一个网站:cplusplus.com
简单的总结,C语言中,我们常用的库函数都有:
IO函数:输入和输出函数
字符串操作函数:比如求字符串长度的strlen函数
字符操作函数:比如判断字符的大小写,将小写字母转化为大写字母
内存操作函数:内存复制、查找等操作
时间/日期函数:获取时间等
数学函数:开平方sqrt等函数
等等…
❓ 如何使用库函数?
我们参照文档,学习几个库函数;
🐱🏍 strcpy
从上面的库函数网站可以找到strcpy的具体用法:
其实strcpy
就是字符串拷贝的意思
- 将源所指向的 C 字符串复制到目标所指向的数组中,包括终止空字符(并在该点停止)。
因此在使用这个函数的时候,我们需要向函数参数传入destination(目标数组)和source(源数组)两个字符串,同时该函数的返回值是char*
,它会将拷贝后的destination(目标数组)起始地址返回给我们。
- 注意使用
strcpy
时要引用头文件#include <string.h>
官方写法:
char* strcpy(char* strDestination, const char* strSource);
即第一个参数是目的地地址,第二个参数源地址;
代码示例:
#include <stdio.h> #include <string.h> int main() { char arr1[] = "Fighting Boy"; char arr2[] = "AAA"; strcpy(arr1, arr2);//将arr2数组的内容拷贝到arr1数组中 printf("arr1 = %s\n", arr1);//arr1 = "AAA" printf("arr2 = %s\n", arr2); return 0; }
运行结果:
注意:
这里的复制其实并不是真正的复制,准确的说是覆盖!
即把arr2的所有内容(包括\0
)都覆盖到arr1
对应位置;
也就是说,虽然打印出来arr1
是AAA
,但是实质上arr1
等于AAA\0ting Boy
;
我们可以运行调试一下:
补充:
- 源字符串必须以
\0
结束。如果源字符串没有\0
,就会一直拷贝源字符串地址后面的所有内容,直到找到值为0
。 - 也会将源字符串中的
\0
拷贝到目标数组中。 - 目标空间必须足够大,以确保能存放源字符串。如果目标空间不够大,则会导致源字符串拷贝不进去。
- 目标空间必须可变,即目标空间没有
const
修饰
(关于const
的用法可以看这篇文章:深入理解const的用法)
注意: 使用库函数,必须包含 #include
对应的头文件。
🌳 自定义函数
如果库函数能干所有的事情,那还要程序员干什么?
所有更加重要的是自定义函数。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。
但是不一样的是这些都是我们自己来设计。
自定义函数的定义语法为:
ret_type fun_name(para1, *) { statement;//语句项 return 返回值;//该返回值与返回类型必须相同,如果是void型函数,则不需要返回值,因此可以不写。 } ret_type 返回类型 fun_name 函数名 para1 函数参数
在使用函数的过程中,我们用函数名(函数参数)
的形式来调用自定义函数;
由于一般函数在调用完以后产生一个返回值(比如一个两个数相加的加法函数,实现两个数相加以后返回两个数的和),因此我们可以用一个变量来接收这个返回值。
📄例题:写一个函数可以找出两个整数中的最大值。
代码示例:
int get_max(int x, int y) { return (x > y) ? (x) : (y); //三目操作符,在操作符中讲过,如果x>y则返回x,反之则返回y; } int main() { int a = 20; int b = 10; int max = get_max(a, b);//定义一个max变量,用来接受最大值; printf("max=%d\n", max); }
运行结果:
在程序运行过程中,给get_max
这个函数传入a、b
这两个参数,函数调用完后会返回a和b中的最大值;
因此可以用max
来接收这个返回值。当然也可以不用接收,因为在函数运行完以后,get_max(a, b)
就相当于这个返回值,该返回值可以当做printf
的参数直接进行打印操作。
📄例题:写一个函数可以交换两个整形变量的内容。
void Swap(int* pa, int* pb) //我们只需要交换a和b当中的内容,不需要返回值,所以用void { int t = 0;//定义一个临时变量用于交换 t = *pa; *pa = *pb; *pb = t; } int main() { int a = 1; int b = 2; printf("交换前: a=%d b=%d\n", a, b); Swap(&a, &b); printf("交换前: a=%d b=%d\n", a, b); return 0; }
运行结果:
3. 函数的参数
分为:
- 实际参数(实参)
- 形式参数(形参)
🔴 实际参数
- 真实传给函数的参数,叫实参。
- 实参可以是:常量、变量、表达式、函数等。
- 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
🔴 形式参数
- 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
- 形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
代码示例:
void Swap1(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } void Swap2(int* px, int* py) //我们只需要交换a和b当中的内容,不需要返回值,所以用void { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 1; int b = 2; Swap1(a, b); printf("Swap1: a=%d b=%d\n", a, b); Swap2(&a, &b); printf("Swap2: a=%d b=%d\n", a, b); return 0; }
运行结果:
Swap1
和Swap2
函数中的参数 x,y,px,py
都是形式参数。
在main函数中传给Swap1
的a,b
和传给Swap2函数的&a,&b
是实际参数。
这里我们对函数的实参和形参进行分析:
可以看出:
实参a、b与形参x、y不是同一空间
&a:0x00d3fa28 &b:0x00d3fa1c &x:0x00def944 &y:0x00d3f948
这里可以看到Swap1
函数在调用的时候,x,y
拥有自己的空间,同时拥有了和实参一模一样的内容。
简单来说:形参实例化之后其实相当于实参的一份临时拷贝。
4. 函数的调用
分为:
- 传值调用
- 传址调用
🟠 传值调用
- 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
📄例题:写一个函数来实现对两个数的交换
void Swap1(int x, int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int a = 1; int b = 2; Swap1(a, b); printf("Swap1: a=%d b=%d\n", a, b); return 0; }
运行结果:
由于形参并不影响实参,函数在调用过程中只是对形参x,y进行了交换,并没有影响到a,b;
并且函数在调用结束以后,x,y就已经被销毁了。
调试看一下:
从监视中就可以看出,x,y
的地址和a,b
不同,他们相当于一块独立的空间,自身的改变并不会影响a,b
。
🟠 传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
还是看这个📄例题,只不过我们稍微做点改变
void Swap2(int* px, int* py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 1; int b = 2; Swap2(&a, &b); printf("Swap2: a=%d b=%d\n", a, b); return 0; }
运行结果:
通过这种方式可以使得变量进行真正的交换。
还是调试看一下:
通过监视可以看出,px、py
就是一个指针,其中放的就是a,b
的地址,*
是解引用操作符,它可以通过地址找到地址中存放的变量值;
比如px
就是a
的指针,它的值是a的地址,对px
解引用就可以找到a地址中存放的变量1,然后我们就可以对变量1进行操作了。
❓ 传值和传址的使用场景
函数内部的形参只需要借用函数外部实参的值的时候用传值调用,比如求两个数的较大值。
当函数内部需要对函数外部变量进行操作时用传址调用,比如交换两个数。
关于函数的形参、实参和传值、传址,可以看我之前写的这篇文章:重点详解函数的形参和实参、传值和传址
5. 函数的嵌套调用和链式访问
函数和函数之间是可以互相调用的。
🟡 嵌套调用
代码示例:
void fun2() { printf("hello\n"); } void fun1() { int i = 0; for (i = 0; i < 3; i++) { fun2(); } } int main() { fun1(); return 0; }
运行结果:
我们通过main
函数调用fun1
,通过fun1
调用三次fun2
,这就是函数的嵌套调用。
注意:函数可以嵌套调用,但是不能嵌套定义。
🟡 链式访问
把一个函数的返回值作为另外一个函数的参数。
int get_max(int x, int y) { return (x > y) ? x : y; } int main() { int num1 = 10; int num2 = 20; int max = get_max(num1, num2); printf("max=%d\n", get_max(num1, num2)); }
运行结果:
下面我们看一个有趣的代码:
int main() { printf("%d", printf("%d", printf("%d", 43))); //结果是啥? //注:printf函数的返回值是打印在屏幕上字符的个数 return 0; }
运行结果:
这个程序实际上就是用printf
的返回值作为printf
的参数;
因此想要弄明白这个程序,我们得先知道printf
的返回值;
通过查找printf的用法可以知道printf
返回值:
所以printf
返回值是写入的字符总数,也就是字符的个数。
- 最内层的
printf
打印43; - 第二层的
printf
打印的是最内层printf的返回值,也就是43
这个内容的元素个数2; - 最外层打印的是
printf("%d",2)
的返回值,返回值是其元素个数1; - 所以会打印出
4321
这四个数。
6. 函数的定义和声明
🌳 函数的声明
- 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
- 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
- 函数的声明一般要放在头文件中的。
🌳 函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
test.h
的内容
- 放置函数的声明
#ifndef __TEST_H__ #define __TEST_H__ //函数的声明 int Add(int x, int y);
test.c
的内容
- 放置函数的实现
#include "test.h" //函数Add的实现 int Add(int x, int y) { return x + y; }
这种分文件的书写形式,在后期写三字棋和扫雷的时候,就可以用分模块来写。
我之前写过一篇三子棋的小游戏,就是用这种分模块的方式来写的;
7. 函数递归
🌳 什么是递归?
- 递归就是程序自己调用自己的过程
- 它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解;
- 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
🌳 递归的条件
- 存在限制条件;当满足这个限制条件的时候,递归便不再继续,否则递归会不断进行,直到程序崩溃。
- 每次递归调用之后越来越接近这个限制条件。
🌳 函数的递归
- 函数的递归就是函数自己调用自己的过程。
这里我们通过代码来详解函数的递归
📄例题一
接受一个整型值(无符号),按照顺序打印它的每一位。
例如: 输入:1234,输出 1 2 3 4.
思考一下:
我们想要从高位向低位输出,就必须要依次获取:最高位到最低位的数字;
因此我们可以这1234
每次除以10,并把1234 / 10
的结果进行判断,看其是否小于10;
如果小于10则不再进行除以10的操作,这样我们就可以得到最高位1
了;
为了获取其他位的数字,我们可以在每次除以10之前进行取模10的操作,得到剩下的位。
代码示例:
void print(int n) { if (n > 9) { print(n / 10); } printf("%d ", n % 10); } int main() { int num = 1234; print(num); //这里的print是我们自己定义的函数,不是库函数printf return 0; }
运行结果:
关于这道题,我之前也做过详解,并且画了图,可以去看下:深入理解C语言中函数的递归算法
🌳递归与迭代
- 迭代就是重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。
🌳函数递归和迭代的区别与优势
有些问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销
8. 结语
其实后面关于函数的嵌套、递归、迭代都只是写了一些知识点,所以还是打算通过例题的方式来详解。
更多的一些重点函数(比如strlen
,strcmp
,strcat
等)的实现,我会在之后单独拿出来说明。
函数这一章的很多东西都需要练题+深刻理解,为后续的知识打下基础!
等例题剖析完了,会附上链接!
🌟你知道的越多,你不知道越多,我们下期见!