C语言初阶——3函数(保姆级胎教)


前言

作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言)只是对C语言的基础知识点过一遍,是为了能让读者和自己对C有一个初步了解。


日志

1.函数是什么?

  1. 数学中我们常见到函数的概念,C语言的函数形似数学上的函数
    在这里插入图片描述
  2. 函数的定义:子程序
    在计算机科学中,子程序是一个大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。(现在没有理解不要紧,了解完再回来看就是了。)
    一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
    在这里插入图片描述

2.函数的分类

2.1 库函数

在这里插入图片描述

2.1.1为什么要有库函数

在这里插入图片描述
在写一些C语言代码的时候,有一些小的功能点,可能会经常用到,比如说printf、scanf,你可能会频繁大量的使用。
而如果C语言本身没有提供这样的函数,输入输出等还需要你自己去实现这样的函数,来打印你的数据,来输入数据,不是会显得很麻烦吗?
这个时候,如果C语言没有提供这样的基础函数,用起来会觉得很不方便,什么东西都得自己去写,效率很低。
而且又不统一,你写的库函数和别写不一样,就产生了差异,效率也不够高,所以C语言就把这些频繁大量使用的这种函数,做了一个约定,把这些函数放到C语言的基础库里。
慢慢积累,C语言提供了一系列的库函数,这些库函数的出现使得程序员写代码的效率高了,同时提供了这种可移植性,因为约定都一样。
提升了整体的开发效率。但这并不意味着把世界上所有的函数都规定到库函数,因为有些函数,你需要,别人可能不需要。所以C语言把那些公共的大家都会用到的函数放到库函数里。
当然C语言并不直接去实现库函数,而是提出标准和规定。
在这里插入图片描述

那如何学习库函数?点击www.cplusplus.com

2.1.2手册的使用方法

  1. 点击链接后,来到这个网站(类似于手册)
    在这里插入图片描述

这是新版的网站,没有旧版的搜索功能,用起来不方便。

  1. 来到旧版
    在这里插入图片描述

  2. 来到旧版,并点击Reference
    在这里插入图片描述
    C Library就是C的库,下面就有一系列的头文件。点击头文件,进去能看到相应的库函数

  3. 点击我们熟悉的stdio.h,左边也会显示在这个头文件里了
    在这里插入图片描述

  4. 往下翻,你就能看到这个头文件相关的库函数
    在这里插入图片描述

  5. 点击一个函数进去你就能看到该函数的介绍
    在这里插入图片描述

  6. 也可以在左边,对头文件切换
    在这里插入图片描述

  7. 当然,在旧版的任何一个界面都可以直接搜索库函数
    在这里插入图片描述

2.1.3C语言常用的库函数

在这里插入图片描述
不用刻意去记,用多了就记住了。这些工具存在的意义也就是,你遇到不了解的东西,去查一下。查多了就记住了。

2.1.4用手册学习库函数

  1. 直接搜索函数,我搜索的是strcpy
    在这里插入图片描述
  2. function功能函数
    在这里插入图片描述
  3. 函数名字
    在这里插入图片描述
  4. 函数名、参数、返回类型的描述
    使用函数时,要关注它的名字、参数、有几个参数、返回类型分别是什么
    在这里插入图片描述
  5. 函数功能的介绍
    在这里插入图片描述
  6. Parameters参数
    介绍它的参数
    在这里插入图片描述
  7. Return Value返回值的描述
    在这里插入图片描述
  8. Example例子
    在这里插入图片描述
    在这里插入图片描述
  9. See also其他相关函数
    在这里插入图片描述
    这些就是函数的所有介绍了,还要深入就要解读这些英文了,不会英语。
  10. 自学能力很重要
    上述内容都是英文的,很多人可能觉得很难。包括我自己也看不懂,但学好编程,英文很重要。
    因此我们要在学编程的同时,加强自己的英文水平。起码能看懂文献。当然可能会有跟我一样的英语一点不会的,那就直接翻译。
    段落翻译:鼠标点中要翻译的段落,右击
    在这里插入图片描述

或者直接翻译整个页面
空白处右击鼠标
在这里插入图片描述
但要注意,这只是浏览器帮你解决的问题。真正的解决办法是把英文水平提高上来。

  1. 使用库函数一定要包含头文件
    有些人可能不使用头文件也能暂时编译过去,但迟早有一天你会吃亏的。

2.1.5其他工具

其实学习库函数不止这一个工具,但只要熟练了一个,其他的工具都是大同小异。这里列举几个学习的工具
MSDN(Microsoft Developer Network)
英文版
中文版

2.2自定义函数

如果库函数能干所有的事情,那还要程序员干什么?
所以更加重要的是自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数
在这里插入图片描述

但是不一样的是这些都是我们自己来设计,这给程序员一个很大的发挥空间
函数的组成:

ret_type fun_name(para1, *)
{
	statement;//语句项
}

ret_type  返回类型
fun_name  函数名
para1     函数参数

在这里插入图片描述
写上一个代码来体验一下
写一个函数求两个数之间的最大值
在没有学函数之前可以这么写

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	int m = a > b ? a : b;
	//输出
	printf("%d\n", m);
	return 0;
}

用函数的话,函数的作用就是用来求最大值,也就是计算的部分由函数来充当。

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	get_max();  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

而我们是希望在a和b之间求最大值,那就传参,把a和b的值传给get_max。此时的a和b叫做实际参数

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

求完之后,get_max得告诉较大值是谁,所以较大值得返回来。

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

但这个函数都还没有呢,要实现这个函数。
名字后面有括号

#include <stdio.h>

get_max()
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

括号里面是它的参数。传的是a和b,函数定义这边要接收。a传过去,a是整型,传过去就要有个整型来接收。

#include <stdio.h>

get_max(int x)
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

因为不止一个参数,用逗号隔开。b传过去,再用整型来接收。此时的x和y叫做形式参数

#include <stdio.h>
get_max(int x, int y)
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

当你把a和b传过去,交给了x和y,那求x和y的较大值和求a和b的较大值是一样的。所以在这个函数里,只要将x和y的较大值求出来。

#include <stdio.h>

get_max(int x, int y)
{
	int z = x > y ? x : y;
}
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

求出较大值把它放在z里,返回较大值就return z;

#include <stdio.h>

get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

而返回z,z的类型是int,所以函数的返回类型就是int

#include <stdio.h>

int get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	//int m = a > b ? a : b;
	//使用函数来计算
	int m = get_max(a, b);  //求最大值,那么就get获取最大值max
	//输出
	printf("%d\n", m);
	return 0;
}

这个时候我们就定义了一个函数get_max,它可以求出a和b较大值。
这个时候再来分析一下
在这里插入图片描述
注意,返回值z的类型就是函数的返回类型,要一致
可以这么去理解
在这里插入图片描述

2.3是否需要返回值

取决于你是否需要返回值,要则返回,不要则不返回
get_max找出两个最大值的时候,要把最大值带出来,也就是要返回一个值
下一个函数打印hehe

void test()
{
	printf("hehe\n");
}
int main()
{
	test();    //打印hehe
	return 0;
}

我只需要它打印hehe,任务已经完成。不需要带回任何值,所以不要返回值,而且不需要参数。

2.4传值调用和传址调用

写一个函数交换两个整型变量的内容
先有两个整型,并且能够自己输入

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	return 0;
}

当有了两个值后给上一个函数,来交换它。并且打印交换前和交换后的值。

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap();   //用来交换两个值内容的函数
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

交换a和b的值,那也就是说要把a和b给它。a和b传给Swap,Swap帮我们把a和b交换

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(a, b);   //用来交换两个值内容的函数
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

然后就差实现这个函数了
a和b传给Swap,a和b都是整型,Swap也就要用整型接收

#include <stdio.h>
Swap(int x, int y)
{
	
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(a, b);   //用来交换两个值内容的函数
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

Swap函数的功能是交换两个值的内容,不需要返回,也就是void

#include <stdio.h>

void Swap(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(a, b);   //用来交换两个值内容的函数
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

来一下效果
在这里插入图片描述
很明显没有达到想要的效果。就诚这种情况为代码有bug(缺陷)。而这种就是bug里面的运行时bug:代码没有问题,可以编译,执行后结果是错的。
用调试来查看问题
在没有输入之前
在这里插入图片描述
遇到scanf后等待输入
在这里插入图片描述
来到Swap,Swap是交换两值内容,但Swap没有交换a,b。Swap函数内部就出了问题,按f11进入函数。
在这里插入图片描述
进来一看,x的值是10,y的值是99。说明传参成功。但为什么没有达到效果,还需要&x,&y
在这里插入图片描述
再加上tmp,看看它们的值。
在这里插入图片描述
当程序走完y=tmp;的时候,x和y确实交换了。但ab和xy不是同一块空间,对x和y的交换并没有影响a和b。
没有交换的原因是:
刚刚传给Swap的a和b叫实参(实际参数),x和y叫形参(形式参数)。
当实参传递给形参的时候,形参是实参的临时拷贝。形参有自己独立的空间,只是把实参的值拷贝到一份放到形参里去了。所以对形参的修改,不会影响实参

#include <stdio.h>

void Swap(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	//实际参数-实参  形式参数-形参
	//当实际参数传递给形参的时候
	//形参是实参的一份临时拷贝
	//所以对形参的修改不会影响实参
	Swap(a, b);   //用来交换两个值内容的函数
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

在这里插入图片描述
xy和ab没有任何的关联,所以修改xy和不会影响a和b的。所以我们要想通过对形参的修改来影响实参,就必须要让他们之间建立联系
以前说过要找到一块内存块,可以通过地址来找
此时,我要找的时a和b的内存块,因此应该传它们的地址过去。

#include <stdio.h>
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(&a, &b);   //把地址交给Swap
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

传a和b的地址,那Swap应该拿地址来接收。所以Swap的类型也就是指针

#include <stdio.h>

Swap(int* pa, int* pb)
{
	
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(&a, &b);   //把地址交给Swap
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

这个时候形参和实参之间就建立了某种联系,pa能找到a,pb能找到b。这个时候再函数里写*pa就是外面的a,*pb就是外面的b。
这时再完善它的功能

#include <stdio.h>

Swap(int* pa, int* pb)
{
	int tmp = *pa;   //tmp = a;把外面的a放给tmp,a的值就空了,就可以存放外面的b了
	*pa = *pb;  //a = b;
	*pb = tmp;  //y = tmp;  把tmp的值放给外面的b
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(&a, &b);   //把地址交给Swap
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

最后,我只需要Swap来帮我交换两块空间的内容,不需要返回值。所以void

#include <stdio.h>

void Swap(int* pa, int* pb)
{
	int tmp = *pa;   //tmp = a;把外面的a放给tmp,a的值就空了,就可以存放外面的b了
	*pa = *pb;  //a = b;
	*pb = tmp;  //y = tmp;  把tmp的值放给外面的b
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(&a, &b);   //把地址交给Swap
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

此时就实现了交换的功能
在这里插入图片描述

而这是用指针来实现的。
在这里插入图片描述
仔细看当程序走完整个函数时,pa和&a对应,pb与&b对应,a,b的值已经发生了改变,并且跟*pa,*pb一致。
所以函数在设计的时候,函数名如何命名、函数的参数怎么设计、功能怎么设计、返回类型怎么设计,都很重要。

3.函数的参数

3.1实际参数(实参)

真实传给函数的参数,叫实参

#include <stdio.h>
          //形式参数-形参
void Swap(int* pa, int* pb)
{
	int tmp = *pa;   //tmp = a;把外面的a放给tmp,a的值就空了,就可以存放外面的b了
	*pa = *pb;  //a = b;
	*pb = tmp;  //y = tmp;  把tmp的值放给外面的b
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap(&a, &b);  //实际参数-实参
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

实参可以是:常量、变量、表达式、函数等

#include <stdio.h>
int get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int m = get_max(a, b);//变量,变量
	m = get_max(a, 7);//变量,常量
	m = get_max(a, 2+3);//变量,表达式
	m = get_max(a, get_max(4, 8));//变量,函数调用
	//get_max先求出4和8的较大值8,实际上相当于放的是8
	//然后再用get_max求a和8的较大值
	//所以函数的参数可以是函数
	printf("%d\n", m);
	return 0;
}

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传给形参

#include <stdio.h>
int get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);//输入后,a和b有明确的值
	int m = get_max(a, b);//变量,变量
	//a,b已经有了确定的10和99,调用get_max时实际上传的是10,99
	m = get_max(a, 7);//变量,常量
	//传参前先会计算出2+3=5,传参传的是a和5
	m = get_max(a, 2+3);//变量,表达式
	//先计算get_max(4, 8)的值8,实际上传参传的是a和8
	m = get_max(a, get_max(4, 8));//变量,函数调用
	printf("%d\n", m);
	return 0;
}

3.2形式参数(形参)

形式参数是指函数名后括号中的变量。

#include <stdio.h>
int get_max(int x, int y)//此处的x和y就是形参
{
	int z = x > y ? x : y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("%d\n", m);
	return 0;
}

当你没调用get_max函数的时候,x和y压根不会被分配空间。只有真正去调用这个函数的时候,形参的变量才回去创建,分配空间。所以绝大部分情况下,它只是形式上存在的,所以叫形式参数。形式参数调用完之后就自动销毁了。

#include <stdio.h>
int get_max(int x, int y)
{
	int z = x > y ? x : y;
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int m = get_max(a, b);//变量,变量
	//m = get_max(a, 7);//变量,常量
	//m = get_max(a, 2+3);//变量,表达式
	//m = get_max(a, get_max(4, 8));//变量,函数调用
	//get_max先求出4和8的较大值8,实际上相当于放的是8
	//然后再用get_max求a和8的较大值
	//所以函数的参数可以是函数
	printf("%d\n", m);
	return 0;
}

当我们调用的时候,x和y创建。return 0;之后x和y销毁。也是自动创建,自动销毁。因此在函数内部才可以使用x和y,出了函数就用不了了。因此形式参数只在函数中有效。

3.3传值还是传址?

什么时候传值?什么时候传址?
如果想在函数内部改变来自外部的值,就必须让函数形参和main函数实参之间建立联系,而指针就可以做到这一点,就需要把地址传过去。形参就可以通过指针的形式,远程的修改外部的值。如果不改变外部的值,传值和传址都可以

3.4栈区、堆区、静态区

在这里插入图片描述
在来做一点小小的补充,假设我申请了一个变量a。
在这里插入图片描述
变量名是给程序员看的,机器无所谓
在这里插入图片描述
所以变量名要起得有意义。

4.函数的调用

#include <stdio.h>

void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
void Swap2(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap1(a, b);//传的是a和b本身
	printf("交换后的值a = %d  b = %d\n\n", a, b);

	printf("交换前的值a = %d  b = %d\n", a, b);
	Swap2(&a, &b);//传的是a和b的地址
	printf("交换后的值a = %d  b = %d\n", a, b);
	return 0;
}

4.1传值调用

形参是实参的一份临时拷贝,函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参

4.2传址调用

传址调用是把函数外部创建的变量的内存地址,传递给了函数参数
让函数和函数外边的变量建立起真正的联系,函数内部可以直接操作函数外部的变量

4.3练习

4.3.1写一个函数可以判断一个数是不是素数(100-200)

  1. 要先产生100-200之间的数
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		
	}
	return 0;
}
  1. 产生这些数之后,要判断i是否为素数
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
	}
	return 0;
}
  1. 起一个有意义的函数名is_prime
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		is_prime();
	}
	return 0;
}
  1. 判断的时i,把i传过去,is_prime帮忙判断
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		is_prime(i);
	}
	return 0;
}
  1. 是素数返回1,不是素数返回0。那这个函数的返回值得利用起来
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 而传的是i,i是整型。所以is_prime的参数类型是int
#include <stdio.h>

is_prime(int n)
{
	
}
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 是素数返回1,不是素数返回0。返回的都是整数,所以返回int就可以了
#include <stdio.h>

int is_prime(int n)
{
	
}
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 试除法。用2-sqrt(n)的数试除
#include <stdio.h>
#include <math.h>
int is_prime(int n)
{
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)
	{
		
	}
}
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 是素数就返回1,不是素数返回0
#include <stdio.h>
#include <math.h>
int is_prime(int n)
{
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)
	{
		if (n % j == 0)
		{
			return 0;
		}
	}
	return 1;
}
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 还可以进行计数
#include <stdio.h>
#include <math.h>
int is_prime(int n)
{
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)
	{
		if (n % j == 0)
		{
			return 0;
		}
	}
	return 1;
}
int main()
{
	int i = 0;
	int count = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
			count++;
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}
  1. 函数的功能要独立,单一
#include <stdio.h>
#include <math.h>
//只判断了素数
int is_prime(int n)
{
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)
	{
		if (n % j == 0)
		{
			return 0;
		}
	}
	return 1;
}
int main()
{
	int i = 0;
	int count = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
			count++;
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}

例如is_prime函数里面再加上打印

#include <stdio.h>
#include <math.h>
//只判断了素数
int is_prime(int n)
{
	int j = 0;
	for (j = 2; j <= sqrt(n); j++)
	{
		if (n % j == 0)
		{
			printf("不是素数\n");
			return 0;
		}
	}
	printf("是素数\n");
	return 1;
}
int main()
{
	int i = 0;
	int count = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,需要一个函数
		if (is_prime(i) == 1)//是1为素数打印,0不是素数不打印
		{
			printf("%d ", i);
			count++;
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}

在这里插入图片描述
就会显得很乱。判断函数就判断函数,不要加其他功能了。别人不想打印的时候,你的函数又有打印,就没法用你的函数。所以尽量在函数设计的时候,让它的功能独立

4.3.2写一个函数判断一年是不是闰年(1000-2000)

  1. 先产生1000-2000之间的数
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 1000; i <= 2000; i++)
	{
		//用函数来判断是否为闰年
	}
	return 0;
}
  1. 起一个有意义的函数名
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 1000; i <= 2000; i++)
	{
		//用函数来判断是否为闰年
		(is_leap_year(i);    //把i传进去判断
	}
	return 0;
}
  1. 是素数返回1打印,不是返回0
#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 1000; i <= 2000; i++)
	{
		//用函数来判断是否为闰年
		if ((is_leap_year(i) == 1))
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 把i传给is_leap_year,那参数类型就是int。要么返回1,要么返回0。所以函数返回类型是0。
#include <stdio.h>

int is_leap_year(int y)
{
	
}
int main()
{
	int i = 0;
	for (i = 1000; i <= 2000; i++)
	{
		//用函数来判断是否为闰年
		if ((is_leap_year(i) == 1))
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 实现判断闰年的功能
#include <stdio.h>

int is_leap_year(int y)
{
	if ((y % 4 == 0 && y % 100 != 0) || (y % 400) == 0)
	{
		return 1;
	}
	return 0;
}
int main()
{
	int i = 0;
	for (i = 1000; i <= 2000; i++))
	{
		//用函数来判断是否为闰年
		if ((is_leap_year(i) == 1))
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 而这种写法还可以再简化一点
#include <stdio.h>

int is_leap_year(int y)
{
	return ((y % 4 == 0 && y % 100 != 0) || (y % 400) == 0);
}
int main()
{
	int i = 0;
	for (i = 1000; i <= 2000; i++)
	{
		//用函数来判断是否为闰年
		if ((is_leap_year(i) == 1))
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 在计算一下个数
#include <stdio.h>

int is_leap_year(int y)
{
	return ((y % 4 == 0 && y % 100 != 0) || (y % 400) == 0);
}
int main()
{
	int i = 0;
	int count = 0;
	for (i = 1000; i <= 2000; i++)
	{
		//用函数来判断是否为闰年
		if ((is_leap_year(i) == 1))
		{
			printf("%d ", i);
			count++;
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}

4.5.3TDD测试驱动开发

先写主函数,想明白函数怎么用,先写函数怎么用、怎么传参、怎么用它的返回值的时候,再去实现这个函数。这种思路更容易把函数写出来,叫做TDD。

4.3.4写一个函数,实现一个整型有序数组的二分查找

  1. 给上一个数组和要找的数
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	//二分查找
	return 0;
}
  1. 起一个有意义的名字
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	binary_search();
	return 0;
}
  1. 去arr里找,找k,arr里有sz个元素
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	binary_search(arr, k, sz);
	return 0;
}
  1. sz元素个数求一下
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	binary_search(arr, k, sz);
	return 0;
  1. 找到了就返回下下标,找不到不能返回0,会和下标0冲突。找不到就返回-1
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k, sz);
	if (ret == -1)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}
  1. 一一对应上函数的参数,实参名可以和形参名相同。只要不搞混。函数的返回类型int
#include <stdio.h>
//注意int arr代表都是整型arr,不死数组
int  binary_search(int arr[], int k, int sz)
{
	
}
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k, sz);
	if (ret == -1)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}
  1. 通过左右下标锁定中间元素下标
#include <stdio.h>
//注意int arr代表都是整型arr,不死数组
int  binary_search(int arr[], int k, int sz)
{
	int left = 0;
	int right = sz - 1;
	
	int mid = (left + right) / 2;
}
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k, sz);
	if (ret == -1)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}
  1. mid只是个下标,要找的是元素。拿arr[mid]和要找的k比较。找到返回下标mid,找不到返回-1。
#include <stdio.h>
//注意int arr代表都是整型arr,不死数组
int  binary_search(int arr[], int k, int sz)
{
	int left = 0;
	int right = sz - 1;
	
	int mid = (left + right) / 2;
	if (arr[mid] < k)
	{
		left = mid + 1;
	}
	else if (arr[mid] > k)
	{
		right = mid -1;
	}
	else
	{
		return mid;
	}
	return -1;
}
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k, sz);
	if (ret == -1)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}
  1. 通过左右下标锁定中间元素下标,再用中间元素跟k比较。找不到,产生新左右下标,和新中间元素下标。所以这是个循环,判断部分是左右下标为交叉。
#include <stdio.h>
//注意int arr代表都是整型arr,不死数组
int  binary_search(int arr[], int k, int sz)
{
	int left = 0;
	int right = sz - 1;
	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (arr[mid] < k)
		{
			left = mid + 1;
		}
		else if (arr[mid] > k)
		{
			right = mid - 1;
		}
		else
		{
			return mid;
		}
	}
	return -1;
}
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int k = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int ret = binary_search(arr, k, sz);
	if (ret == -1)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}
  1. 求两个数平均值溢出问题
    在这里插入图片描述

4.3.5写一个函数,每调用一次这个函数,就会将num的值加1

  1. 定义num变量。经过计算加1后,打印测试结果
#include <stdio.h>
int main()
{
	int num = 0;
	//计算,让num+1
	test();
	printf("%d\n", num);
	return 0;
}
  1. num对于test函数来说,属于外部的值,要改变它,就必须让test和num建立联系。因此要传num的地址
#include <stdio.h>
int main()
{
	int num = 0;
	//计算,让num+1
	test(&num);
	printf("%d\n", num);
	return 0;
}
  1. 传的是整型num的地址,参数为指针
#include <stdio.h>
test(int* p)
{
	
}
int main()
{
	int num = 0;
	//计算,让num+1
	test(&num);
	printf("%d\n", num);
	return 0;
}
  1. 函数的功能是让num加1,*p找到num
#include <stdio.h>
test(int* p)
{
	(*p)++;
}
int main()
{
	int num = 0;
	//计算,让num+1
	test(&num);
	printf("%d\n", num);
	return 0;
}
  1. 而函数只需要让num加1就好了,不需要返回值
#include <stdio.h>
void test(int* p)
{
	(*p)++;
}
int main()
{
	int num = 0;
	//计算,让num+1
	test(&num);
	printf("%d\n", num);
	return 0;
}

5.函数的嵌套调用和链式访问

5.1嵌套调用

  1. 简单来说就是函数a里边调用了函数b
#include <stdio.h>
void new_line()
{
	printf("hehe\n");
}
void three_line()
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		new_line();    //three_line函数调用了new_line
	}
}
int main()
{
	three_line();//main函数调用了three_line
	return 0;
}

函数之间都是互相调用的,你调用我完成某个任务,别人又调用我来完成某个任务,进行这样的有机组合,最终完成了大的任务。

  1. 可以嵌套调用,不存在嵌套定义
#include <stdio.h>
int test()
{
	int a = 0;
	int b = 0;
	return 0;

	void fun()        //在test函数内又定义了fun函数,这是绝对错误的
	{
		printf("hehe\n");
	}
}
int main()
{
	return 0;
}

C语言不存在嵌套定义。正确的写法是这样

#include <stdio.h>
int test()
{
	int a = 0;
	int b = 0;
	return 0;
}

void fun()
{
	printf("hehe\n");
}
int main()
{
	return 0;
}

但是可以嵌套调用

#include <stdio.h>
int test(int x, int y)
{
	int a = 0;
	int b = 0;
	return 0;
}

void fun()
	{
		test(1, 2);      //这是嵌套调用
		printf("hehe\n");
	}
int main()
{
	return 0;
}

未来,你会发现,所有功能的实现,基本上都是使用函数,把这些小的功能有机的拼装起来实现大的功能。

5.2链式访问

把一个函数的返回值作为另外一个函数的参数
正常来说求一个字符串长度是这样的

#include <stdio.h>
#include <string.h>
int main()
{
	int len = strlen("abc"); //1.求出长度
	printf("%d\n", len);     //2.打印长度
	return 0;
}

链式访问则把这两步合在了一起。把strlen函数的返回值3作为printf函数的参数。

#include <stdio.h>
#include <string.h>
int main()
{
	printf("%d\n", strlen("abc"));
	return 0;
}

5.2.1链式访问4321

printf函数的返回值是本次打印在屏幕上字符的个数

#include <stdio.h>

int main()
{
	printf("%d", printf("%d", printf("%d", 43)));
	//printf("%d", printf("%d", 2));//打印43   两个字符,printf返回2
	//printf("%d", 1);              //打印432        一个字符,printf返回1
	//最终打印4321
	return 0;
}

如果在每一个printf后都加上空格

#include <stdio.h>
int main()
{
	printf("%d ", printf("%d ", printf("%d ", 43)));
	//printf("%d ", printf("%d ", 3));//打印43空格     三个字符,printf返回3
	//printf("%d ", 2);              //打印43 3空格   两个个字符,printf返回2
	//最终打印43 3 2空格
	return 0;
}

6.函数的声明和定义

  1. 先来写一个两个数的相加add
#include <stdio.h>
//函数的定义 创造出来一个函数,这个函数有函数名,参数,返回类型,函数体
//从头到尾都写了出来叫函数定义
int add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	int c = add(a, b);//函数调用 使用这个函数
	//打印
	printf("%d\n", c);
	return 0;
}
  1. 大部分教科书是这么写的
#include <stdio.h>
//函数的声明
int add(int x, int y);
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	int c = add(a, b);//函数调用 使用这个函数
	//打印
	printf("%d\n", c);
	return 0;
}

//函数的定义
int add(int x, int y)
{
	return x + y;
}

这样也可以跑起来,但为什么非要放一个声明在前面呢?假设没有声明,代码实际上是这样走的

  1. 编译器在编译代码时,要从第一行进行扫描
    在这里插入图片描述
  2. 函数要保证先声明后使用
    在这里插入图片描述
  3. 声明不需要写形参的名字
#include <stdio.h>
//函数声明
int add(int, int);
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	int c = add(a, b);//函数调用 使用这个函数
	//打印
	printf("%d\n", c);
	return 0;
}

//函数的定义
int add(int x, int y)
{
	return x + y;
}

只不过说,在函数定义那里直接拷贝,带有形参名,是不用特意去删掉的。

  1. 函数定义写在前面不需要声明
#include <stdio.h>
//函数的定义
int add(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 0;
	int b = 0;
	//输入
	scanf("%d %d", &a, &b);
	//计算
	int c = add(a, b);//函数调用 使用这个函数
	//打印
	printf("%d\n", c);
	return 0;
}

函数定义就是一种特殊的声明。函数的定义从头到尾就在说明这是个函数,是一种更为彻底的声明。所以函数定义写在前面是非常合理方便的。教科书写在后面,主要是讲清楚什么是声明。

6.1函数声明

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,声明决定不了。
  2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
  3. 函数的声明一般要放在头文件中的。

6.2函数定义

函数的定义是函数的具体实现,交代函数的功能实现。

6.3函数声明和定义通常不是写在一起用的

前面写的其实通常不是函数声明和定义的用法。它们通常是分离使用的。
函数的声明是放在头文件中的。

  1. 定义一个add.h头文件和add.c源文件
    在这里插入图片描述
  2. 函数的声明放在add.h里面,函数的定义放在add.c里。
    在这里插入图片描述
    这个时候,如果你想让它跑起来,还需要在test.c里声明。因为要使用的add函数是在add.h里声明的。
    在这里插入图片描述
    #include "add.h"作用相当于把add.h里面的信息拷贝一份到test.c。这就起到声明的作用。当声明后,编译器也能正常找到add.c里的函数。这里没有写extern,也可以正常编译。因为这是跨文件写代码的一种方式,是可以不用加extern的。
    正常用extern来声明外部函数
    在这里插入图片描述
  3. add.c是否包含头文件取决于你的需求
    如果说add.h里有add.c需要的东西,add.c就需要包含头文件
    在这里插入图片描述

6.4函数声明和定义分离使用的意义

  1. 分离模块,方便协作
    在公司写代码,是需要合作的。如果所有人写代码都在test.c里写,那还在怎么协作。你写一会,我写;我写一会,他写。合适吗?
    因此这个时候都往一个文件里写,就不合适了。这个时候就分模块去写
    在这里插入图片描述
    程序员ABCD就各自在自己文件里写代码就可以了。写完后,程序员Q需要把ABCD写的代码进行整合。Q写了一个test.c文件,而Q需要把加减乘除这些函数的功能整合起来,成为一个计算器程序。要去调用这些函数就得包含其他模块的头文件。
    在这里插入图片描述
  2. 函数实现和声明分离
    比如说遇到这样的场景
    在这里插入图片描述

新建工程
在这里插入图片描述
创建一个add工程,写了一个add.c和add.h
在这里插入图片描述
M不想卖add.c,但卖给别人,别人只需要有add.h,加上里面介绍函数的参数,返回类型等大概就知道怎么用了。
M不想让别人知道这个函数怎么实现,就可以这么做
项目名称点中右击鼠标
在这里插入图片描述
注意是项目add,不是源文件add.c。
属性-常规-配置类型-.lib静态库
在这里插入图片描述

改完之后,Ctrl+f5,点击确定。
在这里插入图片描述
此时此刻在这个路径下,就已经生成了一个.lib文件
在这里插入图片描述
找到它,并用记事本打开
在这里插入图片描述
在这里插入图片描述
add.c和add.h1经过编译所生成的.lib文件。.lib文件叫静态库。打开之后是一堆乱码,因为这个.lib文件已经是二进制文件了。
这时,M就可以把这个.lib文件卖出去给别人。别人就说你这里到底有什么啊?我都没法用啊,我怎么知道怎么用。这个时候你就可以顺带卖了add.h。因为这里只是函数的描述,描述函数的参数、返回类型以及使用方法等。即卖方M卖了.h文件和.lib文件
假设真的有个人N买走了这两个文件,又该怎么去使用?
N打开需要用到这个函数的工程
在这里插入图片描述

打开test.c所在文件夹
在这里插入图片描述
将刚买的add.lib和add.h文件拷贝一份到该文件夹下。
在这里插入图片描述

这里的test.c是想要调用add函数的源文件,我希望我买了add.lib和add.h文件能够被test.c使用。
所以我们要在test.c所在的项目里添加add.h和add.lib文件
在这里插入图片描述

然后包含add.h,但是如果这个时候,直接编译代码会报错。
在这里插入图片描述
实际上你只是对add函数声明了一下,但是具体函数的定义在哪里?还在add.lib里面吧?还缺少一步。
刚刚买的add.lib文件叫做静态库,要导入静态库才能正常使用。
在这里插入图片描述
导入之后,程序没有任何问题,正常运行。
在这里插入图片描述
如果最开始,把函数的声明和定义放在一起,就做不了分离。你只是想卖.h文件而已,不想卖.c源文件,不想让他看到函数的定义。不分离是做不到这种情况的。
所以在做工程时,把一个模块提出出来的时候,对应都有一个.c文件和.h文件。这是非常方便的操作。

7.函数递归

7.1什么是递归?

  1. 函数自己调用自己
//史上最简单的递归
#include <stdio.h>
int main()
{
	printf("haha\n");
	main();
	return 0;
}

程序一直在打印haha,直到最后程序挂了,程序停止了,卡死了。这是因为死递归导致了栈溢出。
进入调试,会报一个错误
在这里插入图片描述
这先不用管,留个印象就好。
这就已经介绍了什么事递归,只不过是相对错误的,死递归了。

  1. 递归作为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。
    递归主要的思考方式在于:大事化小

7.2递归的两个必要条件

  1. 存在限制条件,当满足这个限制条件的时候,递归便不再继续
  2. 每次递归调用之后越来越接近这个限制条件

7.2.1练习

接收一个整型值(无符号),按顺序打印它的每一位
例如:
输入:1234 输出:1 2 3 4
先把基本的框架写出来

#include <stdio.h>
int main()
{
	unsigned int num = 0;
	//输入
	scanf("%d", &num);
	return 0;
}

在这里插入图片描述
函数递归,要有函数。

#include <stdio.h>
int main()
{
	unsigned int num = 0;
	//输入
	scanf("%d", &num);
	//print函数负责打印1234的每一位
	print(1234);//注意不是printf
	return 0;
}

把printf(1234)拆分print(123),最后打印4。而4很容易就打印出来了。拆分成了两个小的问题。下来也如此
在这里插入图片描述

最后打印1的每一位,printf就搞定了,就不用拆了。只有它是两位数以上的时候才要拆,只剩下一位的时候就不用拆了,直接打印就好了。就这样每一次剥下来一位,大的问题简化成小的问题。接下来实现这个print
传的是1234,所以函数参数的类型是unsigned int。这个函数只需要把每一位打印在屏幕上,不需要返回。

#include <stdio.h>
void print(unsigned int n)
{
	
}
int main()
{
	unsigned int num = 0;
	//输入
	scanf("%d", &num);
	//print函数负责打印1234的每一位
	print(1234);
	return 0;
}

首先去判断n是几位数。n如果是1位数就不用拆了,n如果是两位数就要拆,也就是说大于9的都要拆。两种情况,分支语句。

#include <stdio.h>
void print(unsigned int n)//1234
{
	if (n > 9)
	{
		print(n / 10);//当这条语句执行完,打印123的每一位
		//最后打印4
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	//输入
	scanf("%d", &num);
	//print函数负责打印1234的每一位
	print(1234);
	return 0;
}

函数就完成了,但print(123)怎么打印123的每一位?这时就要靠画图来理解了
在这里插入图片描述
再新调用一次函数之后,上一级函数还在等待
在这里插入图片描述
n值的保存
在这里插入图片描述
这里要普及一个知识
函数的调用堆栈或者叫函数栈帧
当我真正去调用main函数,就会在栈区里面为main函数开辟一块空间
在这里插入图片描述
main函数里这种num变量就在这块空间里
在这里插入图片描述
main函数会去调用print(1234)时,当调用这个print(1234)的时候,这一次函数调用又要分配一块函数空间。n参数的开辟是一块单独的空间。
在这里插入图片描述
print(1234)去使用n,其实是print向下找到这块空间去使用。
在这里插入图片描述
在这里插入图片描述
当n = 123进行递归时,进入到print(123)。但是print(1234)这个函数栈帧还是存在的,并没有被销毁,因为它还没有运行完,还在等待print(123)返回,所以这个n = 1234还是在的,这块空间并没有销毁。往后调用函数,一直开辟空间,且不销毁。
在这里插入图片描述

当开辟到print(1)时,if条件不符合,就走了printf("%d ", n % 10),开始打印4。
在这里插入图片描述
一旦print(1),走到printf打印完1后,这个函数调用完成。函数结束,这块空间就瞬间不存在了,还给操作系统了。因为n是这个函数里参数,这个函数都结束了,n就没有存在的必要了。也还给操作系统了。
在这里插入图片描述
当print(1)调用完成,回到print(12)的时候。使用访问到的是绿色空间数据。绿色空间里的数据,之前保护好了。所以这时n是12
在这里插入图片描述
当print(12)调用完后,空间也还回去了
在这里插入图片描述
来到print(123)的时候,n就是123。因为上次保存的n就是123。当print(123)结束后,空间又还给操作系统了。
在这里插入图片描述
最后当print(1234)调用结束,所以的print空间都还给了操作系统。
在这里插入图片描述
最后main函数结束,main函数的空间也会还回去。这些所生成的空间,全部还给了操作系统。
在这里插入图片描述
刚刚一个一个申请的空间,现在又一个一个还回去了。在这个过程中,这种保护现场的的功能,每一次函数调用,都会用栈帧空间把这些信息保存起来,因为回头还要回来。
如果这个时候没有if语句呢?

#include <stdio.h>
void print(unsigned int n)//1234
{
	print(n / 10);
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	//输入
	scanf("%d", &num);
	//print函数负责打印1234的每一位
	print(1234);
	return 0;
}

在这里插入图片描述
所以递归不应该无限下去,要满足一定的条件。满足才递归,不满足递归停下来。这是递归的第一个条件。
如果再把n/10改成n会发生什么效果?

#include <stdio.h>
void print(unsigned int n)
{
	if (n > 9)
	{
		print(n);
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	scanf("%d", &num);
	print(1234);
	return 0;
}

在这里插入图片描述
n没有被改变,一直是1234。永远大于9,不可能打印,一直递归。n/10,能让1234最后变成1,不在递归,打印4,递归有机会停下来。
所以每次递归之后越来越接近这个限制条件(n<=9)。这是递归的另一个条件。
所以递归的两个条件必须有,但也不一定对。
递归每一次使用数据都要开辟空间,而循环不一定,循坏每次使用的数据可能是固定的。因此递归会导致程序崩溃,循环不一定会。
所以当函数死递归的时候,就会导致栈区一直被开辟,栈区是有限的,当栈区被开辟的函数栈帧耗干时,就会发生栈溢出。这就是前面报的栈溢出的原因。

7.2.2练习2

编写函数,不允许创建临时变量,求字符串长度。

  1. 先不要管不允许创建临时变量这个条件,只求字符串长度。
    用scanf函数可以求字符串长度
#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "abc";
	int len = strlen(arr);
	printf("%d\n", len);
	return 0;
}

但这是strlen的功能,题目要求的是写一个函数。实际上也就是模拟strlen函数的功能

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "abc";
	//[a b c \0]
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

传的是字符数组arr,数组名是数组首元素地址,所以函数参数应为char*。要返回长度,返回类型为int。

#include <stdio.h>
#include <string.h>
int my_strlen(char* s)
{
	
}
int main()
{
	char arr[] = "abc";
	//[a b c \0]
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

怎么实现这个函数?
在这里插入图片描述
字符串求长度要找到\0,\0之前出现多少个字符,长度就是多少。\0前面有三个字符,所以求得长度应该是3。
看s指向的是谁,如果不是\0就往后走一步。
在这里插入图片描述
s指向的字符’\0’,计数器就++。s再向前一步。而这个过程是计数器++,往后走,计数器++往后走,显然是循环。最后返回count。

#include <stdio.h>
#include <string.h>
int my_strlen(char* s)
{
	int count = 0;
	while (*s != '\0')
	{
		count++;
		s++;//找下一个字符
	}
	return count;
}
int main()
{
	char arr[] = "abc";
	//[a b c \0]
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

这里用s去遍历这个字符串,因为这个字符串是放在数组里面的,它是连续的。找完一个就找下一个,每找一个判断是不是\0。不是\0,count++,s向后挪找下一个。直到找到\0,返回count。
现在回过头,虽然它能求长度,但它创建了一个临时变量count用来计数的。不满足要求。真正的做法应该用递归来求解。

  1. 递归
    在这里插入图片描述
    就这样每次剥下来一个字符,把这个问题化简完
#include <stdio.h>
#include <string.h>
int my_strlen(char* s)
{
	if (*s == '\0')
		return 0;//第一个字符就是\0,长度为0
	else
		return 1 + my_strlen(s+1);//不进入if,说明至少有一个字符,s++再看看后面还有没有字符
}
int main()
{
	char arr[] = "abc";
	//[a b c \0]
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

在这里插入图片描述

7.3递归与迭代

有些递归能做的事迭代也可以,循环就是一种迭代。

7.3.1练习3

求n的阶乘(不考虑溢出)

#include <stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	//函数求阶乘
	int ret = Fac(n);//ret接收n的阶乘
	printf("%d\n", ret);
	return 0;
}

传过去的是n,参数类型是int。要返回阶乘,返回类型为int。

#include <stdio.h>
int Fac(int n)
{
	
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	//函数求阶乘
	int ret = Fac(n);//ret接收n的阶乘
	printf("%d\n", ret);
	return 0;
}

用循环产生1-n的并把它们相乘,返回相乘的结果。

#include <stdio.h>
int Fac(int n)
{
	int r = 1;//初始不能为0
	int i = 0;
	for (i = 1; i <= n; i++)
	{
		r = r * i;
	}
	return r;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	//函数求阶乘
	int ret = Fac(n);//ret接收n的阶乘
	printf("%d\n", ret);
	return 0;
}

这种就是用循环,或者说迭代的方式实现n的阶乘。其实n的阶乘是有一个公式的
在这里插入图片描述
直接按照这个公式写

#include <stdio.h>
int Fac(int n)
{
	if (n == 1)
		return 1;
	else
		return n * Fac(n - 1);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	//函数求阶乘
	int ret = Fac(n);//ret接收n的阶乘
	printf("%d\n", ret);
	return 0;
}

达到的效果和循环是一样的。所以很多的代码既可以又迭代来解决,又可以用递归(关键是找公式)。

7.3.2练习4

求第n个斐波那契数(不考虑溢出)
在这里插入图片描述
根据公式就能写出递归了
先写出它的框架

#include <stdio.h>
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}

然后传的是n,形参类型即为int。功能是计算出第n个斐波那契数,要返回这个数,返回类型int

#include <stdio.h>
int Fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	return 0;
}

检测一下第6个和第50个
在这里插入图片描述
第6个确实是8
在这里插入图片描述
光标一直在等待,还是没有算出来。换成40看看。
在这里插入图片描述
在等待1,2s后算出来了。当然这个结果不一定对,因为数字太大可能产生溢出。
不考虑溢出,考虑效率。算40等了1,2s,算50一直算。这是因为代码进行来大量重复的工作。
当你求第50个数的时候,要求第49个和48个。
在这里插入图片描述
而你要求49的时候要求48和47。求48又要求47和46
在这里插入图片描述
往下,你会发现每一个数字都会分为两个
在这里插入图片描述
每次都分成两个,指数级的增加计算量。
在这里插入图片描述

而且里面有很多的重复计算
在这里插入图片描述
而要算到第3个斐波那契数才能停止,因为第1和2个数有具体的值,第3个数就能直接得出来了。
在这里插入图片描述

算到还要分开的3,就要进行2的48次方,前面的计算又差不多相当于2的48次方,也怪不得会卡住。可以统计一下最后算的第3个斐波那契数一共算了多少次

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	if (n == 3)  //Fib(3)就加加
		count++;
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	printf("%d\n", count);
	return 0;
}

在这里插入图片描述
在计算第40个斐波那契数的时候,第3个斐波那契数要进行三千多万次计算。还别说其他斐波那契数重复计算的次数。效率非常低,里面进行了大量的重复计算。
所以在解决一个问题,可以用递归,也可以迭代的时候,要考虑递归会不会导致性能下降,会不会导致问题出现。当出现这种情况的时候,那就要想办法把递归转换成迭代的方式。
刚刚递归是倒着往前算的,那现在试试用循环正着往后算
在这里插入图片描述
当n为3时候就是这么算。那n为4的时候,1和2才是它的a和b,岂不是说把b和c的值赋值过去不就行了。
在这里插入图片描述
但在这个过程中,前两个数不要算。第3个数往后才要算
第3个数,1和1加赋给它
在这里插入图片描述

第4个数,1和1得到2,2在和1加赋给它
在这里插入图片描述
往后一直是这样。
在这里插入图片描述
你会发现算第n个数的时候,只需要计算n-2次就可以了
在这里插入图片描述
所以这样去写代码,定义abc,ab都是1,c暂时不知道放什么,给个0

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 0;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	printf("%d\n", count);
	return 0;
}

再写个while循环。刚刚画图说算第3个才要计算,第1和第2直接得结果。所以判读部分为n>=3

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 0;
	while (n >= 3)
	{

	}
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	printf("%d\n", count);
	return 0;
}

然后每次计算完,都要赋值。

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 0;
	while (n >= 3)
	{
		c = a + b;
		a = b;
		b = c;
	}
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	printf("%d\n", count);
	return 0;
}

每计算完一次之后,要计算的次数就少1。所以调整为n–

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 0;
	while (n >= 3)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	printf("%d\n", count);
	return 0;
}

比如说n是5,while进入了3次,n变成2。刚好是3次计算。
而当循环结束时,c就是我们要的结果。返回c就可以了。但当n是1或2的时候,while不进去,所以c是0不合适了。所以初识化为1。

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;
	while (n >= 3)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d\n", ret);
	printf("%d\n", count);
	return 0;
}

再来测试下50
在这里插入图片描述
一下子就出来了,不受限于时间了,结果虽然是错的。是因为一个整型放的值是有上限的,有最大值,超过最大值会溢出。虽然算错了,但效率高。

7.3.3递归和迭代用哪个?

在解决问题时候,到底用哪个?其实不在于使用递归和非递归,只要都能解决问题,都可以用。但如果使用递归的时候可能造成栈溢出和性能下降等一系列问题,就得改成非递归。
为了什么方式简单,你就用啥。在代码正确的前提下,想用什么就用什么。但如果有明显的缺陷,就要考虑清楚。
在这里还要注意栈溢出的问题。每一次函数调用都要开辟空间。假如你的代码很容易出现栈溢出的问题,尽量避免你的函数开辟太多的空间。因为你的函数开辟空间太多,在函数调用的时候,开辟的栈空间更多,更容易出现栈溢出。
解决这个问题可以尽量用static对象替代非static对象。因为static对象出了函数,它还在,下次进入还是那个对象,所以一个函数使用了静态变量的话,不管函数调用多少次,它只占一份空间。当如果你用非static对象的话,每一次函数调用都有自己独立的空间,很浪费空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值