C语言基础3:函数(分类、库函数、自定义函数、参数、传值传址调用、链式访问和嵌套、声明和定义、递归)

文章目录

C语言基础3:函数(分类、库函数、自定义函数、参数、传值传址调用、链式访问和嵌套、声明和定义、递归)

1. 函数是什么?

  数学中我们常见到函数的概念。
  C语言中对于函数的概念维基百科是这样定义的:在计算机科学中,子程序,是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
  一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

2. C语言中函数的分类:

2.1 库函数

2.1.1 库函数的使用

(1)什么情况下会用到库函数
  C语言本身提供给我们的函数是库函数,为什么会有库函数?

①在编程的时候,当我们想把程序输出的结果打印在屏幕上,会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
②在编程的过程中会频繁的做一些字符串的拷贝工作(strcpy)。
③编程中总是会计算n的k次方这样的运算(pow),

  对于上述基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

学习库函数的网站:
https://www.cplusplus.com
https://zh.cppreference.com/w
打开①链接,点击左上角C++栏下的Refence,之后在下方栏就有库函数的摘要,点击后右侧即有详细解释。
在这里插入图片描述

(2)C语言中常用的库函数都有:
①IO函数
②字符串操作函数
③字符操作函数
④内存操作函数
⑤时间/日期函数
⑥数学函数
⑦其他库函数

2.1.2 库函数的举例
2.1.2.1 strcpy 函数

  我们点开一个常用的库函数 String.h ,之后点入其中的 strcpy字符串拷贝函数。然后我们可以看到他的使用语法、简介、示例等:

//语法
strcpy:char * strcpy ( char * destination, const char * source );

在这里插入图片描述
(2)这里用 strcpy 函数举例:
  首先用定义两个字符串 arr1[]=“Hello!”、arr2[20]=“############”,然后把 arr1 拷贝到 arr2 上,输出 arr2。输出的结果是“Hello”,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
\0 作为字符串的结束标志,当拷贝后,就不再拷贝后续的内容了。

2.1.2.1 memset 函数

  memset:memory set 即内存设置,这里还是点击库函数中的memset 函数,我们可以看到它的信息如下:
在这里插入图片描述
(1)关于阐述:
Fill block of memory
Sets the first num bytes of the block of memory pointed by ptr to the specified value (interpreted as an unsigned char).

可以翻译为:
  填充内存块,将Prt 所指向的第一个 字符 字节设置为指定的值(解释为一个无符号字符)

语法

void * memset ( void * ptr, int value, size_t num );

接着是函数的举例:

#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] = "almost every programmer should know memset!";
  memset (str,'-',6);
  puts (str);
  return 0;
}

最后是输出结果

------ every programmer should know memset!

从示例看就发现字符前6个单元被 “-” 给填充了。

2. 2自定义函数

  库函数不能完成所有的工作,而更加重要的是自定义函数。自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。
函数由: 返回类型、函数名、函数参数 组成:

ret_type fun_name(para1, * )
{
statement;//语句项
}
其中:
ret_type 返回类型
fun_name 函数名
para1 函数参数
2.2.1 自定义交换函数举例

  写一个函数可以交换两个整形变量的内容:
在这里插入图片描述
  我们按F10+(Fn)打开调试窗口,之后按F11+(Fn)进入函数体内,这里可以看到 swap 函数在调用的时候,x 和 y 拥有了自己的空间,同时拥有了和实参一模一样的内容,所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝
在这里插入图片描述
  也可以通过更改地址的方式进行数据交换。
在这里插入图片描述

3. 函数的参数

3.1 实际参数(实参):

  真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

3.2 形式参数(形参):

  形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有
效。

3.3 举例:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
void swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}
void swap2(int* pa, int* pb)
{
	int tmp = 0;
	tmp = *pa;
	*pa = *pb;
	*pb = tmp;

}
int main()
{
	int a = 10;
	int b = 20;
	//调用swap1函数,传值调用
	swap1(a, b);
	printf("a = %d, b = %d\n", a, b);
	//调用swap2函数,传址调用
	swap2(&a, &b);
	printf("a = %d, b = %d\n", a, b);
	return 0;
}

为什么第一次:当函数调用的时候,把a和b传给形参 x、y,
当实参传给形参的时候,形参其实是实参的临时拷贝,对形参的修改是不会改变实参的

4. 函数的调用

4.1 传值调用

  函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。相当于 3.3 节中的 swap1 函数。

4.2 传址调用

  传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
  这种传址调用的传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。相当于 3.3 节中的 swap2 函数。

  而3.3.节中的找最大值函数,swap1 函数不会改变变量的值

4.3 练习

4.3.1 写一个函数可以判断一个数是不是素数
#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#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++)
	{
		if (is_prime(i) == 1)
			printf("%d\n", i);
	}
			
	return 0;
}

在这里插入图片描述

4.3.2 写一个函数判断一年是不是闰年

  首先需要分析什么是润年:能被4整除的年份且要求该年份不能被100整除,或者能够被400整除的年份。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <math.h>
int is_leap_year(int i)
{
   int y = 0;
   if ((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0))
   	return 1;
   else
   	return 0;
}

int main()
{
   int year = 0;
   for (year = 1000;year <= 2000; year = year + 4)
   {
   	if (is_leap_year(year) == 1)
   		printf("%d ", year);
   }
   return 0;
}

在这里插入图片描述

4.3.3 通过函数实现一个整形有序数组的二分查找

  首先将之前写的二分查找函数粘贴过来,查找 arr[ ] ={1,2,3,4,5,6,7,8,9,10} 数组中 7 的下标,我们执行程序后,发现提示找不到指定的数字。
在这里插入图片描述
  接着我们进行调试,看程序出错再哪里了:
在这里插入图片描述
  调试发现,这条语句执行后,sz并没有得到我们想要的数组长度,而是得到了1

  原因:在主函数中 binary_search 函数在传数组参数的时候,不会向普通的变量(例如整形变量)直接复制过去,数组的存放是一段连续的空间(假设数组中有1w个变量,那1w个变量在传的时候也会拷贝1w个变量对内存空间占用非常巨大),而数组在传参数的时候,并不会传整个数组,仅仅传过去的是数组第一个元素的地址,所以函数中其实是个指针。所以需要先将数组的长度算出来,然后再将参数传过去。
在这里插入图片描述
  之前说的,数组在传参数的时候,仅仅传过去的是数组的第一个元素的地址,那么要想计算数组的长度,我们可以在传参前计算完成。我们修改代码如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <math.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 (ret == -1)
	{
		printf("找不到指定的数字");
	}
	else
	{
		printf("找到数字的下标是%d\n", ret);
	}
	return 0;
}

  最终程序的输出结果,如下:
在这里插入图片描述

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

  当一个函数内部想改变函数外部变量的时候,需要进行传址,通过改变传地址后,改变地址中存储的数据,来改变函数外部的变量。
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
void Add(int* p)
{
	//++的优先级比较高一定要加(),不然会作用于地址,而不是地址中存储的数据
	(*p)++;
}
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;
}

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

5.1 函数的链式访问

  函数的链式访问是把一个函数的返回值作为另外一个函数的参数

5.1.1 printf 链式访问 strlen 函数

  函数和函数之间可以根据实际的需求进行组合,也就是互相调用的。例如 printf 函数中, strlen 函数将计算出的结果返回给 printf 函数
在这里插入图片描述

5.1.2 printf 链式访问 printf 函数

  printf 函数也是可以嵌套 printf 函数,下面的嵌套的结果是 4321,为什么呢?
在这里插入图片描述
  此时,我们需要打开 MSDN 查一下 printf 函数的定义了。
  我们根据查询结果发现:每一个printf函数都返回的是一个打印的字符的个数,当printf("%d", 43)打印了43之后,这个printf的返回值应该是2,当printf("%d", 2)运行完后,函数的返回值是1,所以打印的是4321。

在这里插入图片描述

5.2 函数的嵌套调用

  函数可以嵌套调用,但是不能嵌套定义。
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
void hehe()
{
    printf("hehe\n");
}
void three_hehe()
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        hehe();
    }
}
int main()
{
    three_hehe();
    return 0;
}

6. 函数的声明和定义

6.1 函数的声明:

  函数的声明我们可以从以下几个维度去理解:
(1)告诉编译器有一个函数,这个函数叫什么、参数是什么、返回类型是什么,但是具体是不是存在,无关紧要
(2)函数的声明一般出现在函数的使用之前,要满足 先声明后使用
(3)函数的声明一般要放在头文件中

6.2 函数的定义:

  函数的定义是指函数的具体实现,交待函数的功能实现。
在这里插入图片描述

6.3 函数的真正用法

  函数在使用的过程中,一般会单独创建函数的头文件、函数体,方便管理

6.3.1 创建函数头文件

在这里插入图片描述
  但是这个头文件被引用了多次,相当于头文件每被引用一次,代码就会被复制一次,为了防止这种情况出现,可以采用下面的方法,用 define 定义一个函数:
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
//如果没有定义过 __ADD_H__
#ifndef __ADD_H__
//就把下面的代码定义为 __ADD_H__
#define __ADD_H__

int Add(int, int);

#endif
6.3.2 创建函数体的源文件

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
int Add(int x, int y)
{
	int z = x + y;
	return z;
}
6.3.3 主函数中调用该函数

  首先声明 “add.h” 的头文件,然后直接调用 “ Add ” 函数即可。
在这里插入图片描述
  最终计算出结果为30
在这里插入图片描述


#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
#include "add.h"
int main()
{
	int a = 10;
	int b = 20;
	int sum = 0;
	sum = Add(a, b);
	printf("%d\n", sum);
	return 0;
}

7. 函数的递归

7.1 什么是递归

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

7.2 递归的两个必要条件

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

7.3 递归函数举例

7.3.1 main()函数循环

  这里通过主函数来循环打印 “ hello world !!”,可以发现函数在不停的循环打印
在这里插入图片描述
  最后,可以看到程序运行后报错了
在这里插入图片描述
  提示有以下报错:
在这里插入图片描述

在这里插入图片描述
  内存可以分为3个区域:栈区、堆区、静态区
(1)栈区会存放局部变量函数形参
(2)堆区用于存放动态开辟的内存,例如malloc、calloc 等函数
(3)静态区用于存放全局变量,例如static 修饰的变量
在这里插入图片描述
  栈溢出原理:例如当 调用main() 函数,内存单元中就会为栈区开辟一块儿内存空间,当如下函数不停调用自身时,栈区不断开辟新的内存空间,直到最后一块儿内存空间耗干了,就提示报错:stack overflow(栈溢出)

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
int main()
{
	printf("hello world !!\n");
	main();
	return 0;
}

在这里插入图片描述

7.3.2 依次打印一个整数型(无符号)的最高位到最低位,并用空格隔开

  依次打印一个整数型(无符号)的最高位到最低位,并用空格隔开,例如输入123,之后打印 1 2 3。
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}

int main()
{
	unsigned int num = 0;
	scanf("%d", &num);
	print(num);
}

7.3.3 计算字符串的长度
7.3.3.1 自定义计算字符串长度的函数

  arr是数组,数组传参,传过去的不是整个数组,而是第一个元素的地址
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
int my_strlen(char* n)
{
	int i = 0;
	while (*n != '\0')
	{
		i++;
		n++;
	}
	return i;
}

int main()
{
	char arr[] = "bit";
	int len = my_strlen(arr);
	printf("len = %d", len);
	return 0;
}
7.3.3.2 使用递归函数,计算字符串长度

  我们可以通过判断字符串第一位是不是“\0”来判断字符串是否结束,如果不是“\0”说明第1位是有字符的,那么继续判断第二位,直到最后一位是“\0”为止。
my_strlen(“bit”);
1+my_strlen(“it”);
1+1+my_strlen(“t”);
1+1+1+my_strlen(“”);
1+1+1+0
3
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
int my_strlen(char* str)
{
	if (*str != '\0')
		return 1 + my_strlen(str + 1);
	else
		return 0;
}

int main()
{
	char arr[] = "bit";
	int len = my_strlen(arr);
	printf("len = %d", len);
	return 0;
}

8. 函数的迭代

  迭代和循环特别相似,迭代展现出的方式不一定就是循环,

8.1 常规方式求n的阶乘

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
int Fac1(int n)
{
	int i = 0;
	int ret = 1;
	for (i = 1;i <= n;i++)
	{
		ret *= i;
	}
	return ret;
}

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

8.2 递归方式求n的阶乘

  用数学公式表达求n的阶乘
Fac(n)
①如果 n <= 1,结果为1
②如果 n > 1,结果为 n*Fac(n-1)
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>

int Fac2(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * Fac2(n - 1);
}

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

8.3 求第n个斐波那契数

  先实现如何用这个函数,之后再看这个函数如何去写,这种方式叫做TDD测试驱动开发。
  方法同上面一样,先写出函数的表达式,然后再套用程序调用。
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
int Fib(int n)
{
	if (n < 2)
		return 2;
	else
		return Fib(n - 1) + Fib(n - 2);
}

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

  但是当要计算第50个斐波那契数的时候,需要算很长时间,大概10分钟,这是因为当我们要求第50个斐波那契数的时候,我们需要计算第49、48的斐波那契数,计算49的斐波那契数需要计算48、47的斐波那契数,计算48需要47、46的斐波那契数:

50
49-48
48-4747-46
47-4646-4546-4545-44

  修改代码如下,添加统计当n=3时,程序计算的次数。计算10的斐波那契数的时候,我们可以发现计算 n = 3的斐波那契数被计算了21次,我们可以发现效率低,速度慢,大量的重复工作
在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
int count = 0;
int Fib(int n)
{
	if (n == 3)
	{
		count++;
	}
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
	
}

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

  为了解决大量的重复工作,提高运行效率,我们可以把递归改成循环的方式来优化:
1,1,2,3,5,8,13,21,34,55,我们可以将第1个数看作a,第2个数看作b,第3个数看作c,然后循环往后进行计算。
  当1+1=2计算完成后,下一次计算为1+2=3,那么计算前把c(2)赋给b(1),b(1)赋给a(1)
  当1+2=3计算完成后,下一次计算为2+3=5,那么计算前把c(3)赋给b(2),b(2)赋给a(1)
  通过这种方式进行循环计算,但这种循环方式也只能计算第3个斐波那契数,第1、2个斐波那契数计算的时候不适用。
在这里插入图片描述

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

	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	};
	return c;
}

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

通过计算,我们发现通过循环的这种计算方式,计算的速度是非常块的。

8.4 递归注意事项

  在递归和循环在使用的时候,应该根据实际情况,在保证程序运行正确的前提下,循环和递归哪种更方便而使用哪个。但是需要注意在使用递归的时候容易出现栈溢出。在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <string.h>
#include <stdio.h>
void test(int n)
{
	if (n < 10000)
	{
		test(n + 1);
	}
}

int main()
{
	test(1);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值