㊙️小明博客主页:➡️ 敲键盘的小明 ㊙️
✅关注小明了解更多知识☝️
前言
提示:本篇文章为C语言函数的个人总结,内容如若有误,请及时联系我更正。
- 转载请注明原创,谢谢。
提示:以下是本篇文章正文内容:
一、什么是函数?
在现实生活中,厨师会根据 菜谱(类似于 C 语言中的函数声明)来制作不同的菜肴。
例如
:
:
:
厨师可以调用 " 煮饭 " 函数(在 C 语言中对应于
s
c
a
n
f
scanf
scanf 函数)来煮米饭,调用 " 炒菜 " 函数(对应于
p
r
i
n
t
f
printf
printf 函数)来炒菜。这样,厨师可以通过调用不同的函数(在 C 语言中称为函数调用)来完成一道菜的制作。
总之,C 语言中的函数就像是现实生活中的一个个工具或方法,可以帮助我们完成各种各样的任务。通过调用这些函数,我们可以简化代码结构,提高代码复用性,使程序更加易于理解和维护。
函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数main( ),所有简单的程序都可以定义其他额外的函数。
我们可以把代码划分到不同的函数中。 如何划分代码到不同的函数中是由我们来决定的。
二、函数的分类
2.1 函数分类
2.1.1 库函数
库函数是C语言发布时就已经封装好的函数,也就是C语言为我们提供的函数,我们直接可以调用来使用,不需要自己写函数里面的内容,他们由不同的头文件管理。
使用库函数需要调用相应的头文件。
下面我们就来看几个库函数例子。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h> //所包含的头文件为:stdio.h
int main()
{
int a = 0;
int b = 0;
scanf("%d",&a);
b = a + 1;
printf("%d\n",b);
return 0;
}
运行结果:
本段代码的作用是给输入的数 +1,在我们输入时就会调用 s c a n f scanf scanf 函数,而输出 +1 后的数字时,我们又用到了 p r i n t f printf printf 函数,这两个函数均是C语言中自带的函数,但是不能被直接使用,所以我们在使用之前需要提前包含头文件:
#include <stdio.h>
库函数的学习网站
那么,库函数到底有哪些呢?我们又该如何查看嘞?小明给大家将网址放在在下面:
c
p
l
u
s
p
l
u
s
cplusplus
cplusplus:http://www.cplusplus.com/reference/
C
/
C
+
+
C/C++
C/C++官网:http://zh.cppreference.com
或者大家可以直接点击下方链接访问:
➡️【库函数的查找链接网站】
➡️【C / C++官网(中文版)】
✅点击学习库函数☝️
cplusplus:
C / C++ 官网【中文版】:
常用的库函数:
-
IO函数:
<stdio.h>
作用:标准输入输出头文件,一般在使用 s c a n f scanf scanf 和 p r i n t f printf printf 的时候都需要用到,否则就会报错未找到相应的库函数。 -
数学函数:
<math.h>
作用:该头文件下方有许多分支,不同的函数可以起到不同的作用,比如说开平方,求绝对值,以及求次方等多个函数。 -
字符串操作函数:
<string.h>
作用:一般用于引用对字符串进行操作的函数,如: s t r l e n strlen strlen、 s t r c p y strcpy strcpy、 s t r c m p strcmp strcmp 等函数。 -
实用函数:
<stdlib.h>>
作用: r a n d rand rand 函数用于生成随机数,经常和 s r a n d srand srand 函数一起使用。srand函数用于生成一个随机初始化数值。 -
时间/日期函数:
<time.h>
作用:在time函数括号里需要传于一个指针,在这里我们用空指针我们不需要向其中传入具体的指针内容,所以我们传入一个空指针NULL进行替代。 -
系统函数:
<Windows.h>
作用:该函数为系统函数,常用的有 s y s t e m system system 清屏函数和 S l e e p Sleep Sleep 休眠函数等。 -
其他库函数
下面小明给大家分别使用一个字符串操作函数,让我们来看一下如何使用头文件和函数:
✅练习: strcpy函数(头文件为:#incude <string.h>
)
网站内显示的使用方法:
strcpy函数:
#include <stdio.h>
#include <string.h> //需包含头文件 <string.h>
int main()
{
char arr1[20] = "xxxxxxxxxxxxxxxxxxxx";
char arr2[] = "Hello Ming!";
strcpy(arr1, arr2);
printf("%s\n", arr1); //输出的是 arr1
return 0;
}
运行结果:
我们可以看到
s
t
r
c
p
y
strcpy
strcpy 函数作用是将一个一段字符串内容复制给另一段字符串。但是,我们需要思考一个问题,那就是字符串的结束标志 \0 会被一起复制过去嘛?
要探究这个问题,我们就需要调用监视来查看一下:
当我们再继续执行一步后,
s
t
r
c
p
y
strcpy
strcpy 函数将一个arr1字符串内容复制给arr2字符串:
由此可见, s t r c p y strcpy strcpy 函数作用在将一个一段字符串内容复制给另一段字符串时,\0 会被一同复制过去,但是输出时也同样在 \0 后停止,不会输出 \0 之后的原有字符。
2.1.2 自定义函数
自定义函数是由用户按需求自行编写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用。
自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计,这就给了我们很大的发挥空间。
下面小明来写一个自定义函数例子。
#include<stdio.h>
int sum(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int c = sum(a, b);
printf("%d\n", c);
return 0;
}
运行结果:
本段代码的作用是计算 a a a 和 b b b 相加的值,在 a a a 和 b b b 被定义后,定义 c c c 时,调用到了我们的 s u m sum sum 函数,这就是我图中的第一步;
第二步就是通过 s u m sum sum 函数,将 a a a 和 b b b 的值传给 x x x 和 y y y 来帮我们进行计算;
第三步则是 r e t u r n return return 返回 x x x 和 y y y 相加的值;
最后 p r i n t f printf printf 输出相加的值后的值。
但是为什么要使用函数?
根据我们之前的内容来说,计算两数之和的代码明明几行就可以完成,又何必像上面这样自己定义一个函数,这么麻烦的去写一个函数,大费周章的完成呢?
这就不得不讲一下自定义函数的好处了:
代码复用: 自定义函数可以让我们在不同地方,重复使用相同的代码,避免重复编写相同的代码,从而提高代码的复用性和减少代码量。
模块化: 自定义函数可以将程序划分为不同的模块,每个模块负责完成特定的任务。这样可以使程序的结构更加清晰,便于我们后期维护。
提高执行效率: 自定义函数可以减少程序的执行次数,从而提高程序的执行效率。例如,将重复计算的代码封装成一个函数,只需要计算一次,其他地方可以直接调用这个函数。
使程序更易于测试和调试: 通过将程序划分为不同的函数模块,可以更容易地对每个模块进行测试和调试,找出程序中的问题。
提高代码的可读性: 通过自定义函数,可以将冗余复杂的逻辑划分为简单的函数,使代码更加简洁明了,提高代码的可读性。
2.2 有无返回值的函数
2.2.1 有返回值的函数
例:
在函数调用之前,我们需要事先想好接收类型,两者需一致,否则将会影响函数返回值!!!
2.2.2 无返回值的函数
当我们调用函数后,如果不需要用到返回值时,可以定义为空类型: v o i d void void ,是否需要返回值就需要根据实际情况来定义了。
2.3 有无参数的函数
2.3.1 含参
int sum(int x,int y)
{
return x + y;
}
此处的 x x x 和 y y y 就是被传递过来的参数。
注:return 是有限制的,一次只可返回一个值。
2.3.2 无参
void ming()
{
printf("敲键盘的小明\n");
}
自定义函数 ming() 后面的括号里面为空,则表示无参数。
三、函数的参数
就像学校,但是我们身份证没带在身边,家距离又有点远,暂时回不去,情况紧急,我只能弄一个复印件交给学校。
3.1 实际参数
实参:
- 实际参数是出现在函数调用中的表达式,是真实的传给函数的参数。
- 实参可以是:常量、变量、表达式、以及函数。
- 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
3.2 形式参数
形参:
- 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
- 形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
- 形式参数出现在函数定义中,它们以假名字来表示函数调用时需要提供的值。
3.3 例子
#include <stdio.h>
void swap1(int x, int y)
{
int tep = 0;
tep = x;
x = y;
y = tep;
}
void swap2(int* p1, int* p2)
{
int tep = 0;
tep = *p1;
*p1 = *p2;
*p2 = tep;
}
int main()
{
int num1 = 1;
int num2 = 2;
swap1(num1, num2);
printf("swap1:%d %d\n", num1, num2);
swap2(&num1, &num2);
printf("swap2:%d %d\n", num1, num2);
return 0;
}
运行结果:
这是互换数值的函数,让我们打开监视瞅一瞅他们的区别:
这里我们可以看到 s w a p 1 swap1 swap1 函数在调用的时候, x x x 、 y y y 拥有自己的空间,并且同时拥有了和实参一模一样的内容。
所以,我们可以简单认为:形参实例化之后相当于实参的一份临时拷贝。
四、函数的调用
4.1 传值调用
传值调用是一种最基本的函数调用方式。当使用传值调用时,函数的形参是实参的副本。函数的形参和实参分别占有不同内存块,也就是说,函数内部对形参所做的修改并不会影响到实参。形参只是实参的一份临时拷贝。
4.2 传址调用
与传值调用相反,传址调用是指函数的形参是实参的地址。
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
这意味着,当函数内部修改形参时,实际上是在修改实参的值。
4.3 练习
✅练习一:
写一个函数可以判一个数是不是素数
#include <stdio.h>
int num(int y)
{
int i = 0;
for (i = 2; i < y; i++)
{
if (y % i == 0)
{
break;
}
}
if (i == y)
{
return 1;
}
return 0;
}
int main()
{
int x = 0;
scanf("%d", &x);
int ret = num(x);
if (ret == 1)
{
printf("%d是素数\n", x);
}
else
{
printf("%d不是素数\n", x);
}
return 0;
}
运行结果:
✅练习二:
写一个函数判断是不是闰年
#include <stdio.h>
int is_leap_year(int y)
{
if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0))
{
return 1;
}
else {
return 0;
}
}
int main()
{
int year;
scanf("%d", &year);
if (is_leap_year(year))
{
printf("%d是闰年\n", year);
}
else
{
printf("%d不是闰年\n", year);
}
return 0;
}
运行结果:
✅练习三:
写一个函数,实现一个整型有序数组的二分查找
#include<stdio.h>
int BinarySearch(int arr[], int sz, int k)
{
int left = 0;
int right = sz - 1;
int mid = 0;
while (left <= right)
{
mid = (left + right) / 2;
if (k > arr[mid])
{
left = mid + 1;
}
else if (k < arr[mid])
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 数组下标: 0 1 2 3 4 5 6 7 8 9
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int k = 0;
printf("请输入要查找的数字:\n");
scanf("%d", &k);
int n = BinarySearch(arr, sz, k);
if (n != -1)
{
printf("找到了下标是:%d\n", n);
}
else
{
printf("没找到!\n");
}
return 0;
}
运行结果:
✅练习四:
写一个函数,每调用一次这个函数,就会将num的值增加1
#include <stdio.h>
void add(int* a)
{
*a = *a + 1;
}
int main()
{
int num = 0;
add(&num);
printf("%d\n", num);
add(&num);
printf("%d\n", num);
add(&num);
printf("%d\n", num);
add(&num);
printf("%d\n", num);
return 0;
}
运行结果:
五、函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
5.1 函数的嵌套调用
函数的嵌套调用是指在一个函数内部调用另一个函数。
C 语言中,函数的嵌套调用可以实现复杂的逻辑,使代码更加模块化和清晰。
下面让我们通过一段代码来感受一下函数嵌套调用的奇妙:
#include<stdio.h>
void test3()
{
printf("哈哈\n");
}
void test2()
{
test3();
printf("呵呵\n");
}
void test1()
{
test2();
printf("哼哼\n");
}
int main()
{
test1();
printf("嘿嘿\n");
return 0;
}
小明已经将代码的执行过程全部标注出来啦,我们来看一下流程:
(1)首先程序从 main函数 开始执行,然后调用了 test1() 。
(2)进入到 test1() 后,程序又调用了 test2() 。
(3)进入到 test2() 后,程序又调用了 test3() 。
(4)然后在调用 test3() 后在屏幕上打印了 哈哈 。
(5)当 test3() 执行完后,程序回到了 test2() 中嵌套调用语句的下一条语句,在屏幕上打印了 呵呵 。
(6)当 test2() 执行完后,程序回到了 test1() 的调用语句的下一条语句,在屏幕上打印了 哼哼 。
(7)最后,当 test1() 执行完后,程序回到了 主函数main()中,在屏幕上打印了 嘿嘿 ,至此,程序的嵌套调用全部结束。
注意:
函数可以嵌套调用,但绝不可以嵌套定义!
5.2 函数的链式访问
函数的链式访问:就是把一个函数的返回值作为另一个的参数。
那么同样的,让我们再来用一个例子感受一下函数的链式访问:
【我们先看一下原代码】:
#include<stdio.h>
#include<string.h>
int main()
{
int len = 0;
len = strlen("xiaoming");
printf("%d\n", len);
return 0;
}
运行结果:
下面我们用链式访问来简化代码
#include<stdio.h>
#include<string.h>
int main()
{
printf("%d\n", strlen("xiaoming"));
return 0;
}
运行结果:
函数的链式访问:就是把一个函数的返回值作为另一个的参数。
我们知道,库函数 s t r l e n strlen strlen函数 的作用就是求字符串的长度,在计算出长度后会返回一个数值,此时我们就可以利用这一点,直接将返回的数值进行打印,从而达到简化代码的目的。
既然提到函数的链式访问,那就不得不说一下这一段代码了:
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
大家可以在这里思考一下,想一想这段代码的运行结果是什么?
先让我们看一下官网对于
p
r
i
n
t
f
printf
printf函数的返回值是如何描述的:
翻译:
通过查阅资料,我们可以看到:如果成功,则返回写入的字符总数 。
那我们就知道了第三层 p r i n t f printf printf函数的返回值应该是 2,因为 43 是两个字符,以此类推我们这个代码将打印: 4321 出来.
运行结果:
六、函数的声明和定义
6.1 函数声明
1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体不是存在,函数声明决定不了。
2.函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3.函数的声明一般要放在头文件中的。
如果自定义函数放在main函数后面,一定要对其进行声明,否则main函数将无法识别该自定义函数。
我们可以举个例子来说明:
我们知道,main函数可以放在我们程序的任一位置,假如我们现在这样写代码来实现两个数相加:
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
printf("%d\n", sum);
return 0;
}
int Add(int x, int y)//函数的定义
{
return x + y;
}
如果自定义函数放在main函数后面,一定要对其进行声明,否则main函数将无法识别该自定义函数:
情况1:
【定义在后,声明在前】自定义在后面,但是在前面已经声明:
情况2:
【定义在后,没有声明时】因为代码在扫描的时候是从前往后扫描,当我们代码扫描到调用函数这一行时,发现我们前面从来没有遇见过Add函数,在程序编译时就会报错!但有的编译器会忽略这个错误,进行所谓的优化,不过我们还是需要注意自己的代码,尽量避免不必要的错误。
情况3:【定义在前】如果函数的定义放在main函数前面,这样定义与声明就融为一体了,程序可以正常运行。
6.2 函数的定义
函数的定义是指函数的具体实现,交代函数的功能实现。
七、函数递归
7.1 什么是递归
函数自己调用自己的过程叫做函数递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小。
在 C 语言中,递归函数通常使用递归调用和递归终止条件来工作。递归调用是指函数在其内部调用自身,而递归终止条件是指当问题规模足够小或满足某种特定条件时,函数不再调用自身。
下面是一个简单的 C 语言递归函数示例,用于计算阶乘:
#include <stdio.h>
int factorial(int n)
{
if (n == 0)
{
return 1; // 递归终止条件:当 n 为 0 时,返回 1
}
else {
return n * factorial(n - 1); // 递归调用:计算 n * (n - 1) 的阶乘
}
}
int main()
{
int n;
printf("请输入一个正整数:");
scanf("%d", &n);
printf("阶乘是:%d\n", factorial(n));
return 0;
}
运行结果:
在这个例子中,factorial 函数用于计算给定整数 n 的阶乘。函数通过递归调用自身来计算阶乘。
当 n 等于 0 时,递归终止,函数返回 1。
否则,函数返回 n 乘以 factorial(n - 1) 的结果。通过递归调用和递归终止条件,factorial 函数可以计算任意正整数的阶乘。
7.2 使用递归的两个必要条件
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
7.3 用实例学习递归
7.3.1 练习一:
✅接受一个整型值(无符号),按顺序打印它的每一位。
例如: 输入:1234 输出:1 2 3 4
#include <stdio.h>
void Print(int n)
{
if (n > 9)
{
Print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num = 0;
scanf("%d", &num);
Print(num);
return 0;
}
运行结果:
7.3.1 练习二:
✅写一个函数要求不使用临时变量求字符串长度
#include <stdio.h>
int MyStrlen(const char* str)
{
if (*str == '\0')//递归结束条件
{
return 0;
}
else
{
return 1 + MyStrlen(str + 1);
//str+1,不断往后读取字符串,不断靠近递归结束条件
}
}
int main()
{
char* p = "xiaoming";
int len = MyStrlen(p);
printf("%d\n", len);
return 0;
}
运行结果:
7.3.1 练习三:
✅获取一个整数的每一位之和
#include<stdio.h>
int GetBitSum(unsigned int n)
{
if (n > 9)
{
return n % 10 + GetBitSum(n / 10);
}
else
{
return n;
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int sum = GetBitSum(n);
printf("%d\n", sum);
return 0;
}
运行结果:
7.3.1 练习四:
✅求n个斐波那契数
#include<stdio.h>
int Fib(int n)
{
if (n == 1 || n == 2)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int num = Fib(n);
printf("%d\n", num);
return 0;
}
运行结果:
八、调试代码
8.1 进入调试
先按 F10 进入调试页面,如果笔记本电脑按下没反应的话,可以按 fn+F10。
8.2 打开监视
然后我们需要依次点击【调试】---->【窗口】---->【监视】,然后监视中的1234都可以使用,我们随便选一个即可。
8.3 观察值
打开监视以后,我们可以输入我们想观察的值,例如我们可以输入 n 和 num 两个值,观察其在运行过程中的变化。
8.4 逐过程 / 逐语句
注:我们在进入调试后,可以在上方找到各种执行命令,我们可以根据自己的需求进行调试。
小明用一张图告诉指令的意思及快捷键:
第一次调试,我们可以使用【逐语句】观察程序在执行每一步时的变化。
8.5 调试演示视频
下面这段视频是小明为大家录制的调试【7.3.1 练习一】的方法。
这道题是让我们编写一个程序:接受一个整型值(无符号),按顺序打印它的每一位。
例如: 输入:1234 输出:1 2 3 4
演示视频:
Visual Studio 调试
完结
好啦,阅读到这里就已经看完了本期博客的全部内容了