第3章 函数


1. 函数是什么?

维基百科中对函数的定义:子程序

  • 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。

  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。

2. C语言中函数的分类

  • 库函数
  • 自定义函数

C语言标准规定了一些库函数:

// 函数名,参数类型,返回值类型,函数功能规定好。

编译器的厂商提供库函数。

2.1 库函数

  • C语言本身提供的函数 – 称为 库函数
  • C语言常见的库函数有:
    • IO函数(input ,output,输入输出函数)
    • 字符串操作函数
    • 字符操作函数
    • 内存操作函数
    • 时间/日期函数
    • 数学函数
    • 其他库函数

参考文档,学习几个库函数。

例一:strcpy – string copy – 字符串复制有关。

char * strcpy ( char * destination, const char * source );
#include <stdio.h>
#include <string.h>
int main()
{
	char arr1[] = "bit";
	char arr2[] = "#######";
	strcpy(arr2, arr1);
	printf("%s\n", arr2);

	// strlen  -- string length  -- 跟字符串长度有关的
	// strcpy  -- string copy    -- 字符串复制有关。// /0也会拷贝

	return 0;
}

// 运行结果
// bit

image-20210913111608995

例二:memset – memory – 内存 set – 设置

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!


#include <stdio.h>
#include <string.h>
int main()
{
	char arr[] = "hello bit";
	char* ret = (char*)memset(arr, 'x', 5);
	printf("%s\n", ret);
	return 0;
}

需要学会查询工具的使用:

MSDN(Microsoft Developer Network)

www.cplusplus.com

http://en.cppreference.com (英文版)

http://zh.cppreference.com (中文版)

2.2 自定义函数

函数的设计应追求高内聚低耦合

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

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

举例:

求两个整数中的最大值。

#include <stdio.h>

// 定义宏
#define MAX(X, Y) (X>Y?X:Y)

// 定义函数
int get_max(int a, int b) 
{
	return a > b ? a : b;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("get_max(a, b) = %d\n", get_max(a, b));

	printf("MAX = %d", MAX(a, b));
	return 0;
}

写一个函数,可以交换两个整型变量的值。

#include <stdio.h>

//void Swap1(int a, int b)
//{
//	int tmp = 0;
//	tmp = a;
//	a = b;
//	b = tmp;
//}

// 不能完成任务
// 当实参传给形参的时候,
// 形参是实参的一份临时拷贝
// 对形参的修改是不会改变实参的

void Swap2(int* pa, int* pb) {
	int tmp = 0;
	tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
int main()
{
	int num1 = 10;
	int num2 = 20;

    // Swap1(num1, num2);
	// printf("num1 = %d, num2 = %d\n", num1, num2);
	
	Swap2(&num1, &num2);
	printf("num1 = %d, num2 = %d\n", num1, num2);
	return 0;
}
// 运行结果
// num1 = 10, num2 = 20
// num1 = 20, num2 = 10

总结:

能将函数处理结果的两个数据返回给主调函数,的方法有:

  1. 形参用数组;
  2. 形参用两个指针;
  3. 用两个全局变量。

3. 函数的参数

实际参数(实参)

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

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

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

形式参数(形参)

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

上面例子中Swap1Swap2函数中的参数abpapb都是 形式参数。在main函数中传给Swap1函数的num1num2和传给Swap2函数的&num1&num2实际参数

这里我们对函数的实参和形参进行分析:

image-20210913115509648

可以看到Swap1函数在调用的时候,ab拥有自己的空间,同时拥有了和实参一模一样的内容。所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝

4. 函数的调用

传值调用:

函数的形参和实参分别有不同的内存块,对形参的修改不会影响实参。

形参是实参的一份 临时拷贝

传址调用:

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

练习:

写一个函数,判断一个数是否是素数。

#include <stdio.h>
#include <math.h>

int is_prime(int x)
{
	int i = 0;
	for (i = 2; i <= sqrt(x); i++)
		if (x % i == 0)
			return 0;
	return 1;
}

int main()
{
	int a = 18;
	if (1 == is_prime(a))
		printf("%d是素数\n", a);
	else
		printf("%d不是素数\n", a);

	return 0;
}

写一个函数,判断一年是否是闰年。

#include <stdio.h>
int is_leap_year(int x)
{
	return ((x % 4 == 0 && x % 100 != 0) || (x % 400 == 0));
}
int main()
{
	int year = 2008;
	if(1 == is_leap_year(year))
		printf("%d是闰年\n", year);
	else
		printf("%d不是闰年\n", year);
	return 0;
}

写一个函数,实现一个整形数组的二分查找。

#include <stdio.h>
				// 本质上这里的 a 是一个指针
int binary_search(int a[], int b,int sz)
{
	int left = 0;
	// 调用函数的时候不能使用这种方式,求数组长度。
	// 	   sizeof() 计算指针的大小就是 4 或者 8
	// int sz = sizeof(a) / sizeof(a[0]);
	 int right = sz - 1;

	while (left <= right)
	{
		int mid = (left + right) / 2;
		if (a[mid] < b)
			left = mid + 1;
		else if (a[mid] > b)
			right = mid - 1;
		else
			return mid;
	}
	return -1;
}

int main()
{
	// 二分查找
	// 在一个有序数组中查找具体的某个数
	// 如果找到了,返回这个数的下标,找不到返回-1
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 1;
	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;
}

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

#include <stdio.h>
void Add(int* p)
{
	(*p)++;
    // 由于++的优先级比*高,如果不给*p加括号,那么将得不出正确的结果。
}
int main()
{
	int num = 0;
	Add(&num);
	printf("num = %d\n", num);
	Add(&num);
	printf("num = %d\n", num);
	Add(&num);
	printf("num = %d\n", num);

	return 0;
}
// num = 1
// num = 2
// num = 3

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

5.1 嵌套调用

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

#include <stdio.h>

void new_line()
{
	printf("hello world\n");
}

void three_line()
{
	int i = 0;
	for(i=0; i<3; i++)
	{
		new_line();
	}
}
 
int main()
{
 	three_line();
 	return 0;
}
// 运行结果
// hello world
// hello world
// hello world

5.2 链式访问

把一个函数的返回值作为另外一个函数的参数。

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

int main()
{
    printf("%d",printf("%d",printf("%d",43)));
    return 0;
}


// printf的返回值是打印在屏幕上的字符的个数。
// 如果发生错误,将返回负数。
// 运行结果
// 4321

6. 函数的声明和调用

函数声明:

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

函数的定义:

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

  • 将函数声明放到Add.h文件中。

    // #pragma one  -- 使头文件只会被包含一次。 -- 等价于 ifndef
    
    #ifndef __ADD_H__    // 如果没有定义过__ADD_H__ 
    #define __ADD_H__    // 定义__ADD_H__ 
    
    // 函数声明
    int Add(int x, int y);
    
    #endif // !__ADD_H__
    
  • 将函数定义放到Add.c 文件中。

    // 函数的定义
    int Add(int x, int y)
    {
        int z = x + y;
        return z;
    }
    
  • test.c文件中进行使用。

    #include <stdio.h>
    #include "add.h"  // 自己写的头文件用 "" 双引号引 
    
    // 导入静态库
    // #pragma comment(lib,"add.lib")
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int sum = 0;
    	sum = Add(a, b);
    	printf("%d\n", sum);
    	return 0;
    }
    

分块去写的好处

  1. 多人协同;
  2. 封装和隐藏;

7. 递归

7.1 什么是递归?

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

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

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

  • 最简单的递归调用

    #include <stdio.h>
    int main()
    {
    	printf("haha\n");
    	main();
    	return 0;
    }
    // 该程序会进入死循环打印 haha ,栈内存耗光后会报如下图所示的错误。
    // 函数调用向栈区申请空间
    

    5

7.2 递归的两个必要条件

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

《函数栈帧的创建和销毁》

练习

接收一个整型值(无符号),按照顺序打印它的每一位。

例如:

输入: 1234,输出: 1 2 3 4.

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

	// 递归
	// print(1234)
	// print(123)   4
	// print(12)  3 4
	// print(1) 2 3 4   
	//
	print(num);
	return 0;
}

7

78

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

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

// 计数器的方式
//int my_strlen(char* str)
//{
//	int count = 0;
//	while(*str != '\0')
//	{
//		count++;
//		str++;
//	}
//	return count;
//}


// 递归的方法
// 把大事化小
// my_strlen("bit");
// 1+my_strlen("it");
// 1+1+my_strlen("t");
// 1+1+1+my_strlen("");
// 1+1+1+0
int my_strlen(char* str)
{
	if (*str != '\0')
		return 1 + my_strlen(str + 1);
	else
		return 0;
}

int main()
{
	char arr[] = "bit";
    // char* p = "bit"; // 也可以
	int count = 0;
	// int len = strlen(arr);
	// printf("%d \n", len);

	int len = my_strlen(arr);   // arr是数组,数组传参,传过去的不是整个数组,而是第一个元素的地址
	printf("len = %d \n", len);
	return 0;
}

#include <stdio.h>
#include <assert.h>
unsigned int my_strlen(const char* str)
{
	assert(str != NULL);
	int count = 0;
	while (*str++)
		count++;
	return count;
}
int main()
{
	char arr[] = "abcdef";
	printf("字符个数为:%u\n", my_strlen(arr));
	return 0;
}

7.3 递归与迭代

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

#include <stdio.h>
// 递归的方式
int Fac(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * Fac(n - 1);
}
int main()
{
	int n = 0;
	int ret = 0;
	scanf("%d", &n);
	ret = Fac(n);  // 递归的方式
	printf("%d\n", ret);
	return 0;
}

求第 n 个斐波那契数 (不考虑溢出)。

#include <stdio.h>
// 递归方法
int Fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return  Fib(n - 1) + Fib(n - 2);
}
int main()
{
	// TDD -- 测试驱动开发
	int ret = Fib(30);
	printf("ret = %d\n", ret);
	return 0;
}

上述代码存在问题:

  • 在使用Fac() 函数求10000的阶乘(不考虑结构的正确性),程序会崩溃。
  • 使用Fib()函数的时候,如果要计算第 50 个斐波那契数字的时候特别消耗时间。

为什么呢?

  • 我们发现 Fib() 函数在调用的过程中很多计算其实在一直重复。

    如果我们把代码修改一下:

    #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 ret = Fib(30);
    	printf("ret = %d\n", ret);
    	printf("3 == %d", count);
    	return 0;
    }
    
    // 运行结果
    // ret = 832040
    // 3 == 317811
    
    // 可以看到 在求第30个斐波那契数的时候3就被运算了 317811次。
    

那我们如何改进呢?

  • 在调试 Fac() 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

解决上述问题的方法:

  1. 将递归改成非递归。
  2. 使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对 象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销, 而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

采用非递归的方式实现上述案例:

// 循环的方式实现 求n的阶乘 
int Fac(int n)
{
	int i = 0; 
	int ret = 1;
	for (i = 1; i <= n; i++)
		ret *= i;
	return ret;
}


// 循环方式求斐波那契数
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1; // 若将 c 赋值 0,则 n =1 和 n = 2的情况无法计算,将 c 赋值为1,则可以解决这个问题。
	while (n > 2 )
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

什么时候用递归:

  1. 当解决一个问题递归和非递归都可以使用,且没有明显问题,那就可以使用递归。
  2. 当解决一个问题递归写起来很简单,非递归比较复杂,且递归没用明显问题,那就用递归。
  3. 如果说,用递归解决问题,写起来简单,但是有明显问题,那就不能使用递归。

函数递归的几个经典题目:

  1. 汉诺塔问题
  2. 青蛙跳台阶问题

. 将递归改成非递归。
2. 使用static对象替代nonstatic局部对象。在递归函数设计中,可以使用static对象替代nonstatic局部对象(即栈对 象),这不仅可以减少每次递归调用和返回时产生和释放nonstatic对象的开销, 而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

采用非递归的方式实现上述案例:

// 循环的方式实现 求n的阶乘 
int Fac(int n)
{
	int i = 0; 
	int ret = 1;
	for (i = 1; i <= n; i++)
		ret *= i;
	return ret;
}


// 循环方式求斐波那契数
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 1; // 若将 c 赋值 0,则 n =1 和 n = 2的情况无法计算,将 c 赋值为1,则可以解决这个问题。
	while (n > 2 )
	{
		c = a + b;
		a = b;
		b = c;
		n--;
	}
	return c;
}

什么时候用递归:

  1. 当解决一个问题递归和非递归都可以使用,且没有明显问题,那就可以使用递归。
  2. 当解决一个问题递归写起来很简单,非递归比较复杂,且递归没用明显问题,那就用递归。
  3. 如果说,用递归解决问题,写起来简单,但是有明显问题,那就不能使用递归。

函数递归的几个经典题目:

  1. 汉诺塔问题
  2. 青蛙跳台阶问题

希望可以对大家有所帮助,如果有什么不对的地方,还烦请指教,谢谢!

  • 10
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值