对函数的认识

发布于:2022-12-15 ⋅ 阅读:(988) ⋅ 点赞:(0)

目录

1.函数是什么?

2.C语言中函数的分类

2.1库函数

2.2自定义函数

3.函数的参数

3.1实际参数(实参)

3.2形式参数(形参)

4.函数调用

4.1传值调用

4.2传址调用

4.3练习

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

5.1嵌套调用

5.2链式访问

6.函数的声明和定义

6.1函数声明

6.2函数定义

7.函数递归

7.1什么是递归

7.2递归的两个必要条件

7.3递归与迭代

7.3.1练习1:

7.3.2练习2:



1.函数是什么?

y=f(x)这是数学中的函数。在C语言中的函数是:子程序。

子程序是大型程序中的某部分代码,由一个或多个语句块组成,它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性一般有输入参数并有返回值可以没有),提供对过程的封装和细节的隐藏。这些代码通常被集成软件库。

2.C语言中函数的分类

  • 1.库函数
  • 2.自定义函数 

库函数是C语言本身提供给我们可以直接使用的函数,比如说printf()函数,strlen()函数,system()函数,strcmp()函数等都是库函数。

自定义函数是自己创造实现的一种函数。

2.1库函数

为什么会有库函数呢?

支持可移植性(跨平台性)和提高程序的效率,把常用的函数提供给我们使用。

怎么学习库函数呢?

学习网址:(电脑端)

​​​​​​Reference - C++ Reference (cplusplus.com)

cppreference.com

C语言常用的库函数有:

  • IO函数:输入输出函数(如scanf、printf、getchar、putchar)
  • 字符串操作函数:与字符串相关的(如strcmp)
  • 字符操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数

使用库函数,必须包含#include对应的头文件。

如一个库函数:1.strlen()(函数名)

size_t strlen ( const char * str );

Get string length

strlen()函数的参数是:const char * str;返回类型是:size_t

作用是:获取字符串长度,返回的是无符号整型(size_t)

即使用时是:传一个字符串给它(放在它的括号里),然后再把它的返回值((size_t)类型)接收一下。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abc";
	int len = strlen(arr);//返回值保存起来放到len里
	printf("%d", len);
	//printf("%s",len)是错误的。返回的值是打印的值,是整型。
	return 0;
}//3

更标椎的写法:

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abc";
	size_t len = strlen(arr);
	printf("%u", len);
	return 0;
}//3
//因为size_t—>unsigned int 
//打印无符号字符串时用%u

所以:

%d——打印有符号整数,能打印出正负;

%u——打印无符号整数,打印正数。

2.strcpy库函数——功能是:拷贝一个字符串

char *strcpy( char *strDestination, const char *strSource );

                                         目的地                                 源头

char *是一种指针,即strcpy的参数是两个指针:strDestination和strSource

返回类型也是char *也是一种指针。

指针存放的是地址。所以传的参数是地址。

即意思是:把源头拷贝到目的地中。

返回的是:目的地,即返回的是目标字符串

strDestination是目的地字符串,strSource 是源字符串

在文档中看到的Null表达的意思是\0

#include<stdio.h>
#include<string.h>
int main()
{
	//把arr2中内容拷贝到的arr1
	char arr1[20] = {0};//目的地
	char arr2[] = "HELLO";//源数据
	strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}//HELLO

3.memset库函数——功能为一个指针设置内存

void *memset( void *dest, int c, size_t count );

void *dest:dest是设置内存块的起始位置即目的地,指向目的地的指针dest。要设置哪个空间,初始化哪个空间dest就是谁。

int c:c是设置的字符

size_t count :count是设置字符的个数 

即函数的意思是:把从dest位置开始向后的count个字节的内容初始化我们想要的c里放的字符。

函数返回的是:dest

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "hello world";
	//把hello改成xxxxx,h开始,就是目的地
	memset(arr,'x',5);
	printf("%s", arr);
	//先把arr传过去,数组名代表的是数组首个元素(h)的地址,要改成x字符,把x传过去,要改5个字符。
	return 0;
}//xxxxx world

注意:int c是整型,这里传的是字符x,因为字符本身是ASCII值。

4、scanf()函数

scanf()函数在读取正常的时候会返回有效数字1(因为只读一个数字);在读取失败的时候会返回EOF,利用这个特点可以写多组输入。

(OJ在线判题中题型分为I/O型和接口型,这两种都又分为一组输入和多组输入,大多是多组输入。接口型只需完成规定的函数就可以了)。

如果scanf()返回的结果不等于EOF,说明读取正常。

//判断整数奇偶性——一组输入(只能输入一个判断一个)
#include <stdio.h>
int main()
{
	int m = 0;
	scanf("%d", &m);
	if (m % 2 == 1)
	{
		printf("Odd\n");
	}
	else
	{
		printf("Even\n");
	}
	return 0;
}
//判断整数奇偶性——多组输入(可以输入多个判断多个)
#include <stdio.h>
int main()
{
	int m = 0;
	while (scanf("%d", &m) != EOF)
	{
		if (m % 2 == 1)
		{
			printf("Odd\n");
		}
		else
		{
			printf("Even\n");
		}
	}
	return 0;
}

有些编译器下可以Ctrl+z结束循环。

EOF——end of file的缩写,文件的结束标志,一般放在文件末尾,作为文件的结束标志而存在的。scanf()函数本身读取失败的时候会返回EOF。

 多组输入还可以写成~的形式:

#include <stdio.h>
int main()
{
	int m = 0;
	while (~scanf("%d", &m))
	{
		if (m % 2 == 1)
		{
			printf("Odd\n");
		}
		else
		{
			printf("Even\n");
		}
	}
	return 0;
}

因为:

EOF的本质是-1
-1的在内存中存储的补码是:11111111111111111111111111111111
~EOF是:00000000000000000000000000000000(~是取反)
全0为假,即不会进入循环。
这种写法也对,说明当scanf()函数返回的结果是EOF时循环就停下。

借此题再看一道例题:判断是元音还是辅音

——输入描述:多组输入,每行输入一个字母。

——输出描述:针对每组输入,输出为一行,如果输入字母是元音(包括大小写),输出“Vowel”,如果输入字母是非元音,输出“Consonant”。

#include <stdio.h>
int main()
{
	char ch = 0;
	char v[] = { 'a','A','e','E','i','I','o','O','U','u' };
	while (~scanf("%c", &ch))
	{
		//遍历数组
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			if (ch == v[i])
			{
				printf("Vowel\n");
				break;
			}
		}
		if (i == 10)
		{
			printf("Consonant\n");
		}
		//清理缓冲区中的\n
		getchar();
	}
	return 0;
}

注意:无论是scanf()函数还是getchar()函数他们在读取数据的时候是从缓冲区读的,在键盘和函数之间还放了一个缓冲区。

scanf在读取字符的时候,为了让它读取在输入数字(例如输入了a)后面还敲了\n,而scanf()函数只读取%c一个字符,当敲了一个a和\n的时候,a拿走放进ch里处理会打印Vowel,当下一次sxanf()读取的时候,缓冲区还有\n,scanf就把\n拿走了,\n放到ch里,\n不是元音字母还会打印Consonant,即\n会干扰打印结果。所以需要清理掉\n,即用getchar()函数把刚刚留下的\n拿走,达到清理缓冲区的作用。

或者:

while (~scanf("%c", &ch))—变成—>while (~scanf("%c\n", &ch))

#include <stdio.h>
int main()
{
	char ch = 0;
	char v[] = { 'a','A','e','E','i','I','o','O','U','u' };
	while (~scanf("%c\n", &ch))
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			if (ch == v[i])
			{
				printf("Vowel\n");
				break;
			}
		}
		if (i == 10)
		{
			printf("Consonant\n");
		}
	}
	return 0;
}

%c是从缓冲区拿走一个字符,后面如果有\n,则也会把缓冲区的\n顺便拿走——这种方法仅限于拿字符的时候可以拿走\n,拿整形的时候是不起效果的。

或者:

while (~scanf("%c", &ch))—变成—>while (~scanf(" %c", &ch))

#include <stdio.h>
int main()
{
	char ch = 0;
	char v[] = { 'a','A','e','E','i','I','o','O','U','u' };
	while (~scanf(" %c", &ch))
	{
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			if (ch == v[i])
			{
				printf("Vowel\n");
				break;
			}
		}
		if (i == 10)
		{
			printf("Consonant\n");
		}
		
	}
	return 0;
}

在%c的前面加上空格,会跳过空白字符。因为\n是看不见的空白字符,跳过\n也就相当于清理掉缓冲区的\n了。第一次输入a时a前面没有空白字符,scanf()直接拿走a,\n还在缓冲区,当下一次输入字符如b时,本质是想读b,因为在b之前还留有刚刚的\n,则跳过\n直接拿b。

2.2自定义函数

自己创建的函数(程序员也是艺术家,创造函数)

自定义函数和库函数一样,有函数名,返回值类型和函数参数。但不一样的是这些都是我们自己来设计——这给程序员很大的发挥空间。

前面一个库函数的讲解:

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abc";
	size_t len = strlen(arr);
//strlen是函数名,传给strlen的是它的参数arr,返回的值放到Len里面了。
	printf("%u", len);
	return 0;
}

函数的基本组成:

ret_type fun_name(paral, *)
{
	statement;//语句项
}
ret_type  函数返回类型
fun_name  函数名
paral     函数参数
大括号括的是函数体,函数体里是语句项

举例:

写一个函数可以找出两个整数中的最大值

代码展示:(用的是自定义的函数)

#include<stdio.h>
int get_max(int x, int y)
{
	int z = 0;
	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);
	printf("%d\n", m);
	return 0;
}

代码讲解:

#include<stdio.h>
//a和b是两个整数,传给get_max()函数,则int x来接收,int y来接收
int get_max(int x,int y) 
{
	//当x和y的较大值找到后就需要返回——创建z变量,z求出x和y的较大值
	int z = 0;
	z = (x > y ? x : y);
	//x大于y吗?如果大于,那较大值就是x,否则就是y
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	//写一个函数:get_max——获取最大值
	//get_max();算出a和b的较大值,则传进去a和b
	//get_max(a, b);找到较大值,要告诉较大值是几,则返回较大值,则把较大值放到m里去
	int m = get_max(a, b);//即get_max就把a和b的较大值找出来返回放到m中
	printf("%d\n", m);
	return 0;
}//还没有这个函数get_max,则在主函数上面创造

这里实现get_max函数时,get_max就是函数名,int x和int y是两个参数,即a和b传的就是它——这是输入操作
int z = 0;
z = (x > y ? x : y);//这两行是加工制造,最后return z;把算出的z再返回来。因为z的类型是int,所以在int get_max(int x, int y)前面写了int
所以get_max函数返回的是整型值,所以接收时也用整型来接收,所以是int m
(所以函数返回类型,函数返回值,接收值这三者是相呼应的)

上述函数只有一个语句块({}),有返回值。

如没有返回值的函数:menu()函数,只需要它打印菜单。

上述函数若x=y则返回x或y都可以,若x<=y则返回y。

#include<stdio.h>
void menu()
{
	printf("*******    1.play    *******\n");
	printf("*******    0.exit    *******\n");
}
int main()
{
	menu();
	return 0;
}//*******    1.play    *******
//*******    0.exit    *******

调用一次,在屏幕上就打印一次;

调用两次,就在屏幕上打印两次。

#include<stdio.h>
void menu()
{
	printf("*******    1.play    *******\n");
	printf("*******    0.exit    *******\n");
}
int main()
{
	menu();
	return 0;
}//*******    1.play    *******
//*******    0.exit    *******
//*******    1.play    *******
//*******    0.exit    *******

此时menu()函数没有参数,没有返回类型(void的意思就是不需要返回),后面也没有写return。

所以,对于函数来说,如果只是默默无闻地干一件事,干的这件事不需要让别人知道,和别也没有交互,所以可以没有参数,也可以没有返回值。

//写一个函数可以交换两个整型变量的内容
#include<stdio.h>
void Swap(int x, int y)
{
	int z = 0;
	z = x; 
	x = y;
	y = z;
}
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;
}//10 20
//交换前:a = 10 b = 20
//交换后:a = 10 b = 20

交换x和y的值: 

第一步:把x放到z里

第二步:把y放到x里

第三步:把z放到y里

没有交换,代码有问题——函数没有实现想要的效果。

代码能跑起来,能编译通过,说明没有语法问题,但可能有逻辑问题

找原因——一步步调试用F10,进入函数内部用F11

清楚地描述问题:

在C语言中规定,传给Swap函数的a和b叫实参;即“Swap(a, b)”处的a和b是实参。

当进到Swap函数内部时,“void Swap(int x, int y)”处的x和y叫形参。

遇到的问题是:用Swap1时,a和b传给x和y后,x和y的空间与a和b的空间是独立的空间,对x和y的修改是不会影响到a和b的,它们之间没有建立链接的。

所以用Swap2时,就要把Swap2内部和a,b直接建立联系——用指针

通过a变量本身修改

#include<stdio.h>
int main()
{
	int a = 0;
	a = 20;
	printf("%d", a);
	return 0;
}//20

不直接通过a变量本身修改:&a

#include<stdio.h>
int main()
{
	int a = 0;
	//a = 20;
	int *pa = &a;//a的地址取出来放到pa中,现在pa中放的是a的地址,所以其类型是:int *
	//此时pa是整型指针,这个指针变量存的是a的地址,如果通过pa找到a,写* pa即可。(pa前加一个星号叫解引用操作)* pa就可以找到a
	*pa = 20;//此时就相当于把a的值改成了20
	printf("%d", a);
	return 0;
}//20

即通过指针变量pa来改变a的值。通过pa和a建立联系就能操作a。

则Swap2内部和a,b建立连接,就把a和b的地址交给Swap2,Swap2就能通过a的地址找到a,通过b的地址找到b。

#include<stdio.h>
void Swap1(int x, int y)
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}
//把a的地址放到pa中,把b的地址放到pb中。所以pa能找到a,pb能找到b,即建立了联系(借助pa和pb来操作a和b)
void Swap2(int* pa, int* pb)
{//交换a和b
	int z = 0;
	z = *pa;
	*pa = *pb;
	*pb = z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	//交换两个变量
	printf("交换前:a=%d b=%d\n", a, b);
	//Swap1(a, b);
	Swap2(&a, &b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}//10 20
//交换前:a = 10 b = 20
//交换后:a = 20 b = 10

即成功。

则发现:

 

 所以写一个函数可以交换两个整型变量的内容的代码是:

#include<stdio.h>
void swap(int* pa, int* pb)
{
	int z = 0;
	z = *pa;
	*pa = *pb;
	*pb = z;
}
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);
}

想一下:为什么写加法和求两个数的较大值不用指针?

因为a和b加的值与x和y加的值是一样的,加的结果放到z中了,z返回一个和,根本不需要改变a和b,不传地址也可以。

不用return,swap函数仅仅交换两个变量的内容,不需要返回,所以也写了void,只要交换了就可以了。

求两个数的较大值则需要返回较大值。

3.函数的参数

函数的参数有两类:

  • 实际参数(实参)
  • 形式参数(形参)

3.1实际参数(实参)

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

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

无论实参是何种类型的量,在进行函数调用时,它们都必须是确定的值,以便把这些值传给形参。比如上面的a、b、&a、&b都叫实参。

到代码中看:

#include<stdio.h>
int get_max(int x, int y)
{
	int z = 0;
	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);//实参是变量
	int m = get_max(10, 20);//实参是常量
	int m = get_max(10 + 22, 20);//实参是表达式
	int m = get_max(10 + 22, get_max(5, 40));//实参是函数get_max,计算的是5和40的较大值。即把一个函数的返回值作为一个参数,这儿不是回调函数,也不是嵌套。
	printf("%d\n", m);
	return 0;
}

3.2形式参数(形参)

调用的函数在定义时,函数名后面括号中的变量,如上述的x、y、pa、pb都叫形参。

因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。

形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

#include<stdio.h>
int get_max(int x, int y)
{
	int z = 0;
	z = (x > y ? x : y);
	return z;
}
int main()
{
	int a = 0;
	int b = 0;
	
	printf("%d\n", m);
	return 0;
}
//此时没有调用get_max函数,就不会给x和y开辟空间的,即不给形式参数分配空间。
//当调用函数的那一刻,就要给x和y分配空间,因为要接收传过来的实

所以形式参数在没有调用时,是形式上的存在,没有分配内存空间。

即函数没有调用时,形参没有空间。

如果调用两次,第一次调用后也会自动销毁。下次调用时再分配空间。

形式参数是在栈区保存的,和局部变量一样都在栈区。

注意:

形参的内存不是动态分配的。

形参只能是变量。

形参和实参在不同的函数中,即不同的作用域,因此形参和实参可以同名。

4.函数调用

  • 传值调用
  • 传址调用

4.1传值调用

函数的形参和实参分别占有不同内存块(有各自的内存空间),对形参的修改不会影响实参。

Swap1(a, b);//去调用Swap1函数时,把变量a和b本身直接传给Swap1,这种调用函数时就叫传值调用。

#include<stdio.h>
int get_max(int x, int y)
{
	int z = 0;
	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);//当调用get_max函数时,a和b直接传给了x和y,把变量本身传过去了,叫传值调用。
	//用a和b本身传给x和y只是为了求出a和b的较大值,用x和y求出较大值与拿a和b求出较大值效果是一样的,然后再返回这个较大值,返回值放到m中,这是传值调用。
	return 0;
}

“形参的修改不会影响实参”,x和y的修改不会影响到a和b。

4.2传址调用

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

Swap2(&a, &b);//把a和b的地址传过去,叫传址调用

void swap(int *pa, int *pb)
{
	int z = 0;
	z = *pa;
	*pa = *pb;
	*pb= z;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d%d", &a, &b);
	int m = get_max(a, b);
	printf("改变前:a=%d b=%d\n", a, b);
	swap(&a, &b);//外部创建的a和b的地址,传上去有两个值pa和pb来接收叫传址调用。
	printf("改变后:a=%d b=%d\n", a, b);
	return 0;
}

注意:

传值调用:

形参按照的方式传递,形参就是实参的一份临时拷贝,修改形参不会影响外部的实参

传址调用:

形参按照指针方式传递,形参就是实参地址的一份拷贝,形参指向的是实参,修改形参指针指向的内容,  就是在操作实参。

即是:

传参时不论是按照值还是指针方式(地址)传递,形参拿到的都是实参的一份拷贝。

如果是按照的方式传递,形参和实参各自有各自的空间,改变形参不能改变外部的实参

,因为形参和实参是两个不同的变量。

传值和传指针(传地址)的区别
传值

形参是实参的一份拷贝,函数运行起来后,形参是形参,实参是实参,形参和实参没有任何关联性,

改变形参时,不会对实参造成任何影响。

传地址

形参是实参地址的一份拷贝,形参指向的实体是实参,对形参解引用后,拿到的内容就是实参,

因此对形参解引用之后的内容进行修改,改变的就是实参

4.3练习

1、写一个函数可以判断一个数是不是素数。

代码实现:

#include<stdio.h>
int is_prim(int n)
{
	int j = 0;
	for (j = 2; j < n; j++)
	{
		if (n % j == 0)
		{
			return 0;
		}
	}
	return 1;
}
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		if (is_prim(i) == 1)
		{
			printf("%d ", i);
		}
	}
}

结果展示:

代码讲解:

#include<stdio.h>
//(接收i,判断i是否为素数),因为它返回的要么是1要么是0都是整数,所以函数类型是int
int is_prim(int n)//用参数n来接收i
{
	//判断n是不是素数—拿2到n-1的数字试除n
	//那要产生2到n-1的数字,用for循环
	int j = 0;
	for (j = 2; j <n; j++)
	{
		//试除
		if (n % j == 0)
		{
			return 0;//返回0
		}
	}
	//当所有的j都不能整除n时,n就是素数
	return 1;//是素数
}//返回0后直接跳到这,后面跳到if语句,经判断返回的0不等于1,所以进入不到if语句中即不会打印。
//return只要执行,这个函数就此返回,以后不管有多少次循环它都不循环了,return比break的功能更加强大,break只能跳到循环外面
int main()
{
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断i是否为素数,如果是素数就打印。
		if (is_prim(i)==1)//不写等于1判断的效果也一样,因为返回0为假也不会进入
			//is_prim是函数,把i传过去,只是用来判断i是否为素数,只想知道是,不是
			//是素数返回1,不是素数返回0,0为假就不打印了
		{
			printf("%d ", i);//实现函数
		}
	}
}

return只要执行,这个函数就此返回,以后不管有多少次循环它都不循环了,return比break的功能更加强大,break只能跳到循环外面

代码可以优化:

//打印100-200之间的素数,用函数
#include<stdio.h>
#include<math.h>
is_prim(int n)//用n来接收i,i是整型
{
	//判断n是否为素数
	//试除
	//用n除以2到n-1的数字
	//产生2到n-1的数字——用for循环
	int j = 0;
	for (j = 2; j<=sqrt(n); j++)//如果在小于等于开平方n时能找到一个因子能整除n那n就不是素数;如果找不到,那另外一端大于开平方n也不可能找到一个因子能整除它。需要引用头文件
	{
		//试除判断——用if
		if (n % j == 0)
		{
			return 0;
		}
	}
	return 1;
}
int main()
{
	//出现100-200之间的数字——用for循环
	int i = 0;
	for (i = 100; i <= 200; i++)
	{
		//判断——用if,函数实现,传i,得到的结果返回是0说明不是素数,返回1说明是
		if (is_prim(i) == 1)
		{
			printf("%d ", i);
		}
	}
	return 0;
}

也可以优化为“j<=n/2”,但“n<=sqrt(2)”更好。若是100,一个是50,一个是10。

2、写一个函数判断一年是不是闰年

判断规则:年份能被4整除并且不能被100整除是闰年,能被400整除是闰年两者之一满足一个就是闰年了。

#include<stdio.h>
int is_leap_year(int n)//返回1或0都是整数,前面写类型int
{
	if (((n % 4 == 0) && (n % 100 != 0)) ||( n % 400 == 0))
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
int main()
{
	//用函数打印1000-2000年之间的闰年
	//出现1000-2000年的年份
	int y = 0;
	for (y = 1000; y <= 2000; y++)
	{
		//判断-用函数
		if (is_leap_year(y) == 1)
		{
			printf("%d ", y);
		}
	}
	return 0;
}

若统计一下闰年年份的个数:

#include<stdio.h>
int is_leap_year(int n)
{
	if (((n % 4 == 0) && (n % 100 != 0)) ||( n % 400 == 0))
	{
		/*count++;*///放错位置
		return 1;
	}
	else
	{
		return 0;
	}
}
int main()
{
	//用函数打印1000-2000年之间的闰年
	//出现1000-2000年的年份
	int y = 0;
	int count = 0;
	for (y = 1000; y <= 2000; y++)
	{
		//判断-用函数
		if (is_leap_year(y) == 1)
		{
			count++;
			printf("%d ", y);
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}

运行结果:

注:要保持函数功能够独立,功能相对简洁单一,输入,打印,判断这三者之一就可。

int is_leap_year(int n)
{
	if (((n % 4 == 0) && (n % 100 != 0)) || (n % 400 == 0))
		return 1;
	return 0;
}

问题:对但函数可读性不高,风格不够好,而且会报警告:不是所有的控件路径都有返回值,有些情况下是没有返回值。编译器没有报错并不能说明代码是对的。而有些警告是可以忽略的。

注:一个项目(工程)可以有多个.c文件,但多个.c文件中只能有一个同名函数,因为一个文件中定义的函数在另一个文件中是可以使用的,否则多重定义符号,函数名重复。

形参和实参能用相同的字母:

#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 y = 0;
	int count = 0;
	for (y = 1000; y <= 2000; y++)
	{
		if (is_leap_year(y) == 1)
		{
			count++;
			printf("%d ", y);
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}

因为上述逻辑操作符判断条件为真就返回1为假就返回0,所以可以优化:

int is_leap_year(int y)
{
	return (((y % 4 == 0) && (y % 100 != 0)) || (y % 400 == 0));
}

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

代码实现:

#include<stdio.h>
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 (-1 == ret)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n",ret);
	}
	return 0;
}//找到了,下标是6

代码讲解:

#define _CRT_SECURE_NO_WARNINGS 1
//写一个函数,实现一个整型有序数组的二分查找
#include<stdio.h>
//然后上来实现binary_search()这个函数
//因为它返回的是下标或-1都是整型,所以函数类型是int,参数是?下面传过来的是(arr,k),传过来的第一个参数是数组,所以接收数组的参数就写:int arr[];下面传过来的第二个数组是k,所以就用参数:int k接收;形参和实参的名字是可以一样的
int binary_search(int arr[],int k)
{
	int sz = sizeof(arr) / sizeof(arr[0]);
	//在有序数组中查k,怎么查?用二分查找或说是折半查找法来解决
	int left = 0;//初始数组的第一个元素下标是0
	int right = sz - 1;//元素个数-1,这样的思路是错误的
	while (left<=right)//圆括号里是判断条件,因为在查找过程中left和right越来越近,当left<=right时,则至少有一个中间元素让我们查找,当left>right时,就没有元素被查找了。
	{//注:此时不能直接while(1),如果是这样,while就陷入死循环了,就不能返回下面的-1了,查找不到元素也就不能告诉我们了)
        int mid = (left + right) / 2;//mid是中间元素下标
		if (arr[mid] < k)
		{
			left = mid + 1;
		}
		else if (arr[mid] > k)
		{
			right = mid - 1;
		}
		else
		{
			return mid;//当找到时要返回,因为下面的调用需要带回一个值。
		}//从if前一行到这里是一个循环,并不是一下就能找到,每次产生新的范围都要通过调整新的left和right去找新的中间元素的下标。
	}
	return -1;//left>right了,找不到所要查找的元素了。
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7;//查找
	//用函数查找,不是在主函数中查找,而是在主函数中调用
	//binary_search();//函数名:二分查找
	//去哪里查?查谁呢?怎么查?——去arr中查,查k,即binary_search()帮我们在arr中查找k,所以要传参,传的参数是(arr,k),意思是在arr中找一个值k,找到或没找到需要告诉我们一下
	//所以还需要加上返回类型和谁接收
	//找到了就返回下标,找不到就返回-1:(注意不能返回0,0可以作为下标)(想返回什么就返回什么,是自己约定的)
	int ret = binary_search(arr, k);
	//判断
	if (-1 == ret)
	{
		//说明没有找到
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n",ret);
	}
	return 0;
}//运行结果是找不到,则代码出错

 

 所以在主函数里还没有传参时算好这个sz再在上面传。

//写一个函数,实现一个整型有序数组的二分查找
#include<stdio.h>
//改动三:下面又传进来了个sz所以也要接收一下:
int binary_search(int arr[], int k, int sz)//这时用这个sz就没有问题了。
{
//改进一:把这行放下面//int sz = sizeof(arr) / sizeof(arr[0]);
	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]);
//算好之后则需要接收一下,所以在下面传参数时也要传sz
//改动二:
	int ret = binary_search(arr, k, sz);
	if (-1 == ret)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n",ret);
	}
	return 0;
}//找到了,下标是6

4.写一个函数,每调用一次这个函数,就会将num的值增加1。如代码:

int main()
{
    int num = 0;
//调用函数,使得num每次增加1
    return 0;
}

解题:

#include<stdio.h>
void add(int n)//int n整数n来接收传过来的num,因为不需要返回,所以是void而不是int
{
	n++;
}
int main()
{
	int num = 0;
	add(num);//add是给num增加1的效果,所以需要把num传过去
	printf("%d\n", num);
	add(num);
	printf("%d\n", num);//调用一次,打印一次
	add(num);
	printf("%d\n", num);//调用一次,打印一次
	return 0;
}
//0
//0
//0

没有达到效果——没有让num增加1
原因:num是一个变量,把这个变脸以传值的形式传给n,n和num是两块独立的空间,n有自己独立的空间,只是拿到了num里的值。所以对n的自加是不会影响num的,所以num并不会发生变化。
怎么让num随着n的自加而跟着自加呢?——让函数外部(定义add函数的那部分,相对于主函数是在函数外部)和函数内部(主函数main内)建立联系。
建立联系——用指针的方式,把num的地址:&num传过去,然后在上面用指针:*p来接收num的地址,所以p间接的指向了num。*p就是外面的num,所以*p=*p+1就是num=num+1

#include<stdio.h>
void add(int *p)
{
	*p = *p + 1;
}
int main()
{
	int num = 0;
	add(&num);
	printf("%d\n", num);
	add(&num);
	printf("%d\n", num);
	add(&num);
	printf("%d\n", num);
	return 0;
}
//1
//2
//3

还有其他的设计方法:

#include<stdio.h>
int add(int n)
{
	n++;
	return n;
}
int main()
{
	int num = 0;
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	return 0;
}
//1
//2
//3

√,但是这种设计方法即既要传参又要返回,相对来说有些啰嗦。

#include<stdio.h>
int add(int n)
{
	return n++;
}
int main()
{
	int num = 0;
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	return 0;
}
//0
//0
//0

return n++处是后置加加,先使用再加加,当num是0的时候,n先返回0然后自增加1,+1的效果没有产生意义。与此相反的是前置加加可以产生想要的效果。

#include<stdio.h>
int add(int n)
{
	return ++n;
}
int main()
{
	int num = 0;
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	return 0;
}
//1
//2
//3

代码写的简洁易懂是好的代码

#include<stdio.h>
int add(int n)
{
	return n+1;
}
int main()
{
	int num = 0;
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	num = add(num);
	printf("%d\n", num);
	return 0;
}
//1
//2
//3

注意:

函数返回类型一般写成两种:

  • void——无返回,不需要返回;
  • 像char  int  float等具体的某种类型就需要返回;
void test1()
{
	//此时这个函数不需要返回,只需要默默地干,这里某种情况系下也可以写return
	return 1;//这样写会出错,这样是带回了值,是错误的写法。
}
void test2()
{
	int n = 5;
	printf("hehe\n");//在某种情况下:需要走到这就行了,不要打印haha了
	if (n == 5)
		return;//return和分号的意思是:返回了但没有带回任何值。即让函数从这结束了。这种写法是正确的。
	printf("haha\n");

}
//所以写void时写return和分号就可以了,不能带回任何值。
int test3()
{
	//此时这个test2函数最终需要返回一个值,一个整型值。
	return 1;//这样就没有语法问题了。
}
//写的是具体类型时,return后面有值和分号,一定要带回值的。
#include<stdio.h>
int main()
{
	//如调用一下test2:
	test2();
	return 0;
}
//调试时可以看到把调用test2的函数提前结束了:只打印hehe,不会打印haha。
//即可以用来提前结束调用(跳出函数)的。

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

嵌套调用:函数和函数之间可以根据实际的需求进行组合,也就是互相调用。

5.1嵌套调用

#include<stdio.h>
void new_line()//写了一个函数,函数名是new_line,没有参数
{
	printf("hehe\n");
}//这个函数只能打印一个hehe。若要打印3个hehe:则再定义一个函数:three_line:
void three_line()
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		new_line();//即调用3次new_line。three_line函数里调用new_line函数就是嵌套调用
	}
}//而three_line函数自己不会执行,则需要在main函数再去调用three_line函数
int main()
{
	three_line();
	return 0;
}
//所以以上形成了关系是:main函数里调用three_line函数,three_line函数里调用new_line函数。即是函数的互相调用——嵌套调用

函数可以嵌套调用,但不能嵌套定义。

意思是:

#include<stdio.h>
void test1()
{

}
void test2()
{

}
//test1和test2函数两者是并列的。以上的写法是正确的。而:
void test1()
{
	void test2()
	{
		//把test2函数写到test1中去定义,在定义test1函数的时候也把test2函数定义了。
	}   //这样写是错误的。
}
int main()
{

	return 0;
}

所以是:可以在three_line函数里调用new_line函数但不能在three_line函数里定义new_line函数

函数不能嵌套定义,但是可以嵌套调用。(函数都是公平的)

5.2链式访问

意思是:把一个函数的返回值作为另外一个函数的参数

举例:

#include<stdio.h>
int main()
{
	int len = strlen("abc");//strlen用来求abc这个字符串的长度,是整型,用len来接收
	printf("%d\n", len);
	return 0;
}//3
//这样写的代码可能有些啰嗦了——strlen函数的返回值先放到len里边去,然后再打印len,可以直接打印它的返回值,两句话变成一句话。
#include<stdio.h>
int main()
{
	printf("%d\n", strlen("abc"));
	return 0;
}//3
//即此时就把strlen函数的返回值作为了printf函数的第二个参数,作为要打印的那个值("%d\n"是printf函数的第一个参数)
//即是链式访问。

函数在调用时,它的参数是另一个函数的返回值

#include<stdio.h>
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));//从左到右依次是第一个printf、第二个、第三个print函数
	//当打印第一个printf函数时,打印的这个整型值是来自第二个printf函数的返回值,第二个printf函数又是打印一个整型值,这个整型值又是第三个printf函数的返回值,第三个printf函数调用打印43,那么这个代码打印的结果是?
	return 0;
}//打印的结果是:4321
//这里用的是函数的链式访问
//第一个printf函数的第一个参数是:"%d",第二个参数是:printf("%d", printf("%d", 43))这一堆都是,即这一堆的返回值就被打印出来了
//第二个printf函数的调用:里面又打印了一个整数,打印的整数是:printf("%d", 43),即是第三个printf函数调用的返回值。所以肯定先在屏幕上以%d的形式打印43
//printf函数返回的是:它在屏幕上打印字符的个数或者如果错误发生则返回的是一个负数。(不是参数个数也不是整数个数)
//第三个printf函数结果产生第二个printf函数才能打印,第二个printf函数结果产生第一个printf函数才能打印。
//第三个printf函数在屏幕上打印的结果是:43,这两个字符,所以第三个printf函数返回的就是2,打印在屏幕上就是2;第二个printf函数在打印时只打印了一个字符2,所以第二个printf函数返回的值是1,所以第一个printf函数打印的值是1,所以就是:4321

//函数的返回值作为另一个函数的参数——链式访问

6.函数的声明和定义

6.1函数声明

函数声明:

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在,声明决定不了。函数定义才决定了函数是否真实存在。如果下面定义了函数真实存在,若下面没有定义即使前面声明了函数也不存在。函数声明只是告诉:有。
  2. 函数的声明一般出现在函数的使用之前,一定要满足先声明后使用
  3. 函数的声明一般要放在头文件中。
#include<stdio.h>
//函数的定义
int Add(int x, int y)
{
	int z = x + y;
	return z;
}
int main()
{
	int a = 10;
	int b = 20;
	int ret = Add(a, b);//先写的是函数是怎么调用的,然后再去写那个函数,这样的思路不容易出错——即先想好它怎么用,然后再去想它怎么实现
	printf("%d\n", ret);
	return 0;
}//30

当Add函数放在main主函数的下面时,代码正常运行但会报警告或错误:Add未定义。因为编译器在扫描代码时是从前到后的,遇到Add的使用时因为之前还没扫描到Add的定义,错误就出来了,后面再遇到而警告已经报出来了。
不让它报警告则想办法让它遇到Add,所以在使用时有这样的习惯:如果函数的定义放在后面去了,那么在前面使用它的时候必须要保证它被编译器知道一下,即做函数声明。
函数声明不是定义,在前面先声明一下是告诉编译器一下有这样的函数Add,它的参数是int类型和int类型,返回类型是int类型,即把这一行代码写在上面就可以了。关于这个函数如何定义的下面说了算:

#include<stdio.h>
//函数声明
int Add(int x, int y);
int main()
{
	int a = 10;
	int b = 20;
	int ret = Add(a, b);//先写的是函数是怎么调用的,然后再去写那个函数,这样的思路不容易出错——即先想好它怎么用,然后再去想它怎么实现
	printf("%d\n", ret);
	return 0;
}
//函数的定义
int Add(int x, int y)
{
	int z = x + y;
	return z;
}

6.2函数定义

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

即函数定义决定函数是怎么实现的。函数的定义可以放在任意位置,函数的声明必须放在函数的使用之前。

注意:函数的的定义也是一种特殊的声明,所以其实函数的定义放在前面也是一种特殊的声明,但是必须保证先声明后使用,先声明后定义。

在工程中写代码的时候很少像上面这样写代码的,一般都会把它剥离出来:在头文件中新建add.h文件,在源文件中新建add.c文件,add.c和add.h加起来是加法模块。会把Add函数的声明放在头文件add.h中,把函数的定义放到源文件add.c中。

所以此时创建的有三个文件:一个add.c放了Add函数的定义;一个add.h放了Add函数的声明;一个test.c实现Add函数的调用,所以这个调用是来自其他地方的,是来自add.c和add.h文件中定义和声明的的函数,就像使用库函数时要用头文件,所以这里也要包含头文件:#include"add.h",使用的是自己的头文件,用的是双引号。

 test.c可以去调用这个加法模块,加法模块的代码是完全独立的,不仅仅test.c可以调用,其他文件也可以调用。

引自己的用双引号“”(自己创建的头文件);引C语言提供给自己的用<>

为什么一个代码要分成两半分别放在两个文件中?(add.c和add.h)

因为正常在工程中(公司)写代码的时候,肯定不会把所有代码都放在test.c中,而是分模块进行。如写一个计算器:写一个加法交给A程序员,写一个减法交给B程序员,写一个乘法交给C程序员,写一个除法交给D程序员,分别各自独立进行,不会互相干扰,最后组后起来形成一个计算器,统一调用他们创建的文件就形成了。

 所以把代码分模块很有必要。各自拆分成一个头文件一个源文件是因为:假如我一个很厉害的程序员自己业余写了一个软件库拿来卖给A公司和B公司和C公司……让这些公司去用,而又不希望买方看到自己的源代码,所以这个程序员就把他的代码拆成两部分,像写了一个加法的功能,把加法的实现写在add.c源文件中去定义,把函数的声明放在add.h头文件中,要用Add函数的时候看到头文件里的这些代码就基本上知道了该如何使用——有函数名,函数参数和函数的返回类型;但是这个函数是如何实现的仅仅看到头文件是不够的。程序员就把add.c和add.h文件编出来一个静态库,是二进制的,没有人能看懂,然后把头文件卖掉,买方买的是一个二进制文件和一个.h文件(头文件)即可以正常使用,此时多个买方就仅仅可以使用而不知道这个代码是如何实现的。

所以需要把函数进行分离,函数的声明(相当于说明书)是必须暴露给别人的,否则别人使用不了。

若要实现一个加法的功能又不会写:则找了一个其他厉害的程序员,这个程序员就创建了一个add的项目,把add.c和add.h添加到该项目中,把add.c和add.h编译产生了一个静态库。

编译静态库:

点中项目名称:add右击选属性,常规属性栏中点击配置类型,将应用程序(.exe)改为静态库(.lib)。此时按Ctrl+F5编译代码时程序不能启动,因为还没写主函数,但是在后台:在add项目中有add.lib文件,这个文件就是刚刚编译产生的静态库,用记事本打开后发现是乱码,是二进制的文件,看不懂,不知道里面是什么东西,所以程序员就把这个add.lib文件和add.h文件卖掉了,不会卖add.c因为add.c文件中有这个代码的实现。

 所以将一整个代码拆成一个个.c和.h,一个个的.c和.h……文件才能做到上图的实现。

.h文件中放的是函数的声明,.c文件中放的是函数的定义。

其中add.h是对函数进行声明了,但对于test.c是:#include "add.h"这句话相当于声明。因为#include相当于包含,包含add.h里的东西。所以“#include "add.h"”这句话就=int Add(int x, int y);所以就是先声明后使用。

当函数的声明放到头文件中,而调用函数的文件又包含头文件,就是把函数进行声明。

头文件里放的一般是函数的声明,类型的声明。

注意:

模块和文件的拆分是按自己的需求的,不一定要拆分也可以合在一起。

函数的声明可以放在main函数内部,只要在调用之前声明即可。

add.h里的#pragma once是自动加上去的,是编译器自带的。

静态库一般不会破解,但可以破解,因为它也是技术。

7.函数递归

7.1什么是递归

程序调用自身的编程技巧称为递归。

递归又叫函数递归。递是给,归是回。

把一段程序封装成函数,函数自己调用自己就是递归

史上最简单的递归:

#include<stdio.h>
int main()
{
	printf("hehe\n");
	main();//调用main函数,main函数也是函数。即函数自己调用自己语法上就是递归。
	return 0;
}//运行结果是:死循环地打印hehe,最后停下来结束,因为程序崩溃,函数的递归导致程序崩溃结束的,是不正常的结束
//这种递归是错误的,这种写法会报错误。

经调试:

递归作为一种算法,在程序设计语言中广泛应用,一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。

递归策略:只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的主要思考方式:把大事化小

如:接收一个整型值(无符号),按照顺序打印它的每一位。(如输入:1234;输出:1 2 3 4)

(原题目:递归方式实现打印一个整数的每一位)

代码实现:

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

代码讲解:

#include<stdio.h>
void Printf(unsigned int n)
{
	//怎么实现Printf函数的呢?(借助Printf函数把1234打印在屏幕上)
	//Printf(1234)
	//最容易剥离的是4,把大事化小——Printf(123)和4=Printf(1234/10)和1234%10,把123的每一位打印出来再打印4;123的每一位打印又可以拆成12的每一位打印再打印3,再加上打印4;12的每一位又可以拆成1的每一位打印,再打印2,再加上打印已经拆下来的3和4,;1的每一位打印是直接打印这个数字1。
//所以是:
	//Printf(1234)
	//Printf(123) 4
	//Printf(12) 3 4
	//Printf(1) 2 3 4
	//即逐层剥离下来一位一位,直到不能剥离。
	//当数字>=10时表示的是两位数,还需要拆开,即判断条件是数字<10
	//if (n < 10)//表示只有一位了
		//printf("%d ", n);
	if (n > 9)//n=1234
	{
		//把n拆下来一位
		Printf(n / 10);//打印的是123:printf(123)此时要把123打印:1 2 3
//这一行Printf函数自己调用自己,是递归。
	}
	//那这个Printf是怎么打印123的每一位的呢?此时又是一次函数调用,
	printf("%d ", n % 10);//打印的是4
}
int main()
{
	//创建一个整型值(无符号)
	unsigned int num = 0;
	//输入(1234)
	scanf("%u",&num );//%u是无符号,%d包括正数和负数,写%u说明输入的都认为是正数
	//(模几除几这种顺序不符合),怎么按照顺序打印呢?——递归
	Printf(num);
	
	return 0;
}
//1234
//1 2 3 4

理解:

假设主函数输入的是123,即num是123,走到“Printf(num);”时向上去调用Printf函数,递(给),此时n是123,123>9,进到 if 里,又要去调用Printf函数,所以又从void Printf(unsigned int n)进入,这一次的调用,传进去的是n是12,此时还没有打印上次一的123模10的结果,因为这一次调用还没有结束,当n=12>9进入 if ,又一次调用Printf函数,又从void Printf(unsigned int n)进入,此时n是1,上一次的printf也没有执行,即也没有打印12模10的结果,没有执行。n=1<9,没有进入 if 语句走到printf,1模10的结果是1(商0余1),所以在屏幕上打印的是1和空格,

上面一次次的调用Printf函数,是递出去的,接下来是回归。

在屏幕上打印出来1和一个空格后从printf这一行回去,到n是12的printf那一行,12模10的结果是2,即打印了2和一个空格,这一次后又要回归上一次n是123的printf那一行,123模10的结果是3,即在屏幕上打印的是3和一个空格,然后再回归到主函数中的Printf函数所在的那一行,即此函数执行彻底结束了。

图像实现:(代码是如何实现的)

即过程复杂,代码简洁。(以上可以一步步调试更详细的观察)

注意:从哪开始递的就从哪开始回,是要回到递的出发点的——其实就是函数的调用过程。

递、归的具体位置可以举例调试观察:如下面代码调试的过程中看调用后具体回到主函数的哪:

//看一个代码的执行流程:
#include<stdio.h>
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("heihei\n");
	test();
	printf("haha\n");
	return 0;
}

若递归代码没有限制条件(判断条件)

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

把 if 语句去掉,没有了,函数一进来就自己调用自己,函数一进来就自己调用自己……会无限的递归下去,原来是n>9的时候才调用自己,如果不大于9就不调用自己了,就去执行下面的打印了,而如果没有这个if 语句了,进来后就调用自己,无限的递归下去,所以判断条件非常重要,像刚开始main函数自己调用自己的现象,都会造成栈溢出的结果。

为什么会造成栈溢出呢?

每一次函数调用都会在内存的栈区申请一块空间(区域)。

7.2递归的两个必要条件

  • 存在限制条件,当满足这个限制条件的时候,递归便不再继续,就不用自己调用自己了。
  • 每次递归调用之后越来越接近这个限制条件

这两个条件是必要条件,不是充分条件,即使写上可能也是错误的,不写肯定错误。

#include<stdio.h>
void Printf(unsigned int n)
{
	if (n > 9)
	{
		Printf(n);//若是变成这样
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	scanf("%u", &num);
	Printf(num);
	return 0;
}

写了限制条件但是传参的时候写了n,也不行。若输入的是123,num为123,上去调用Printf函数时,n=123,123>9,进入 if 语句中,123又传给了n,即每一次调用Printf函数n都是123,这个条件恒成立,也是死递归。

所以之前写的“Printf ( n / 10 ) ”是有必要的,因为这一句话使n不断的变小,直到不满足条件递归停下来

练习1、:

练习2、:(关于递归很好的一个例子)

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

代码实现:

#include<stdio.h>
int my_strlen(char* s)
{
    while(* s != '\0')
    {
    count++;
    s++;
    }
    return count;
}
int main()
{
	char arr[] = "abc";
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

代码讲解:

#include<stdio.h>
int main()
{
	char arr[] = "abc";
	int len = strlen(arr);//求arr数组放的字符串的长度
	printf("%d\n", len);
	return 0;
}//3
//这里用的是库函数的strlen求的。
//而题目是自己编写一个函数,不允许创建临时变量去求字符串的长度。
//实际考察的是模拟实现strlen。

所以用自己模拟的函数实现是:(这里my_strlen是自己写的,不是库函数)

#include<stdio.h>
int my_strlen(char *s)//因为返回的是长度,是整型,所以写int类型,参数用char*指针来接收,用s,即s是a的地址
{
	int count = 0;
	//printf("%c\n", *s);//因为s里面放的是a的地址,*s就是a,s指向的是a,这里打印的结果就是a。
	while (*s != '\0')//不等于\0说明还没有结束,后面随着s的加加,*s是b,后面*s是c,当*s是\0的时候,\0!=\0为假就跳出循环。这里\0不要忘记用单引号引起来。
	{
		count++;//统计字符个数
		s++;//把a就跳过去了,指向了b,此时s里面放的是b的地址
	}
	return count;//count就是个数
}
int main()
{
	char arr[] = "abc";
	int len = my_strlen(arr);//这里传的参数是arr,arr是数组名,而数组名是数组首元素的地址。因为数组里放的是:a b c \0这四个字符,a是第一个数组元素,所以数组名就是a的地址,因为a是char类型变量,所以首元素的地址的类型是char*。
	//所以这里传过去arr的时候,相当于传的是char*这样的地址(字符的地址用char*指针来接收)
	printf("%d\n", len);
	return 0;
}//3

这里用count去记录字符个数,而count是临时变量,局部变量就是临时变量。这样就不满足题目要求了,导致这个方法不能用。

则用递归的方法:

用my_strlen去求“abc”

 这里是+1,不是+4,因为是char*指针,char*指针+1就会跳一个字符,(一个字符char是一个字节)而此题刚好需要跳一个字符,从a跳到b,跳一个字符,所以+1。

如果是Int*指针,应该+4,要跳过一个整型所以+4

如果是double*指针,要跳过一个应该+8

若不创建临时变量count:

my_strlen("abc");

1+my_strlen("bc");

1+1+my_strlen("c");

1+1+1+my_strlen("");

即:1+1+1+0=3

代码实现:

#include <stdio.h>
int my_strlen(char* s)
{
	if (*s == '\0')
	{
		return 0;//如果第一个字符就是'\0',则该字符数组的长度是0;
	}
	else//如果第一个字符不是'\0'
	{
		return 1 + my_strlen(s + 1);
	}
}
int main()
{
	char arr[] = "abc";
	int len = my_strlen(arr);
	printf("%d\n", len);
	return 0;
}

总结:关于strlen函数求字符串长度的模拟实现的两种方法:

  • 1、计数方法
  • 2、递归方法

若是整型数组,则:

#include <stdio.h>
int my_strlen(int* s)
{
	if (*s == '\0')
	{
		return 0;
	}
	else
	{
		return (4 + my_strlen(s + 4));
	}
}
int main()
{
	int arr[] = {1,2,3,4,'\0'};
	int ret = my_strlen(arr);
	printf("%d\n", ret);
	return 0;
}

7.3递归与迭代

迭代就是非递归,不断地重复重复,迭代里包括一种形式就是循环,循环其实就是迭代。

7.3.1练习1:

求n的阶乘

用两种方法:

1、循环形式:

#include <stdio.h>
int main()
{
	int n = 0;
	int ret = 1;
	scanf("%d", &n);
	//循环产生1~n的数字:
	int i = 0;
	for (i = 1; i <= n; i++)//这里产生的i就是1~n的数字
	{
		ret = ret * i;
	}
	printf("ret=%d\n", ret);
	return 0;
}

其实:

f(n)=1(n<=1)或n*f(n-1)(n>1)

2、写成函数的形式:递归形式

#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);
	printf("%d\n", ret);
	return 0;
}

递归做的也是循环。

7.3.2练习2:

求第n个斐波那契数

斐波那契数规律:

1 1 2 3 5 8 13 21 34 55……

规律是:前两个数字之和等于第三个数字……

fib(n)=1(n<=2)或fib(n-1)+fib(n-2)(n>2)

当n是两个数字的时候,都是1,当给定后面的数字时,才可以算。

1、递归方法:

#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;
}

但是:

#include <stdio.h>
int count = 0;//全局变量
int fib(int n)
{
	if (3 == n)
	{
		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("\ncount=%d\n", count);//产生的是累加的效果
	return 0;
}//40
//102334155

//count = 39088169
//即在求第40个斐波那契数时里面把第3个斐波那契数重复计算了,浪费了大量时间,用递归求效率低下

用递归的方式求第n个斐波那契数求解的方法效率低。

则用:

2、迭代(循环)方法:

#include <stdio.h>
int fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1;//保证n是1或2时返回的值是1
	while (n > 2)//圆括号里放的是循环的次数,当n是3时,c=a+b该动作执行1次,当n是4时,执行2次,n是5时执行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);
	return 0;
}

计算结果瞬间出来,因为它没有太多重复计算。

因为整型里放的数字是有上限的,太大的不能算出来。是用代码实现这些超大数字(几百位数字)的计算,内置数据类型不考虑这样的大数运算。

提示:

1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

递归思路:

一个问题直接求解时不好求解,如果可以将其划分成其子问题,并且子问题和原问题有相同的解法时,

就可以使用递归的方式解决。

递归的两个条件:

 1. 将问题划分成其子问题,要求:子问题要与原问题具有相同的解法

 2. 递归的出口

函数递归的几个经典题目(自主研究):
1. 汉诺塔问题
2. 青蛙跳台阶问题

汉诺塔问题:

有3个柱子:A、B、C,有64个大小不一的盘子在A柱子上,让这些盘子都一一移到C柱子上,在每次移动的过程中一定要保证大盘子在小盘子的下面。

解题:

如果在A柱子上有1个盘子:

A—>C

移动1次;(2^1-1)

如果在A柱子上有2个盘子:

A—>B、A—>C、B—>C

移动3次;(2^2-1)

如果在A柱子上有3个盘子:

A—>C、A—>B、C—>B、A—>C、B—>A、B—>C、A—>C

移动7次;(2^3-1)

……

如果在A柱子上有64个盘子:

则需要移动(2^64-1)次

插图理解:

 A柱子上有2个盘子

 A柱子上有3个盘子

 移动过程

代码实现:

#include <stdio.h>
void Move(char pos1, char pos2)
{
	printf("%c->%c ", pos1, pos2);
}

void Hanoi(int n, char pos1, char pos2, char pos3)
{
	if (1 == n)
	{
		Move(pos1, pos3);
	}
	else
	{
		Hanoi(n - 1, pos1, pos3, pos2);
		Move(pos1, pos3);
		Hanoi(n - 1, pos2, pos1, pos3);
	}
}
int main()
{
	Hanoi(1, 'A', 'B', 'C');
	printf("\n");
	Hanoi(2, 'A', 'B', 'C');
	printf("\n");
	Hanoi(3, 'A', 'B', 'C');
	printf("\n");
	return 0;
}

运行结果:

代码讲解:

#include <stdio.h>
void Move(char pos1, char pos2)//模拟鼠标实现例如A到C的过程
{
	printf("%c->%c ", pos1, pos2);
}
//写汉诺塔:
//第一个参数pos1:起始位置
//第二个参数pos2:中转位置
//第三个参数pos3:目的位置
void Hanoi(int n, char pos1, char pos2, char pos3)//n个盘子,其余是3个参数
{
	if (1 == n)//递归的终止条件,这里如果有1个盘子
	{
		Move(pos1, pos3);//从A柱子移到C柱子上,即从起始位置移到目的位置
	}
	//如果有n个盘子
	//则需要将n-1个盘子通过C柱子移到B柱子上,即递归汉诺塔
	else
	{
		Hanoi(n - 1, pos1, pos3, pos2);//将n-1个盘子将pos1通过中转位置pos3移动到pos2
		//把n-1已经移开后,起始位置只剩1个盘子,则需要把pos1上的移动到pos3上:
		Move(pos1, pos3);
		//再递归汉诺塔:还有n-1个盘子,即再将n-1个盘子将pos2通过中转位置pos1移动到pos3
		Hanoi(n - 1, pos2, pos1, pos3);
	}
}
int main()
{
	Hanoi(1, 'A', 'B', 'C');//首先写的是多少个盘子,第一个位置是A位置,第二个位置是B位置,第三个位置是C位置
	printf("\n");
	Hanoi(2, 'A', 'B', 'C');
	printf("\n");
	Hanoi(3, 'A', 'B', 'C');
	printf("\n");
	return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到