c语言修炼秘籍【第二章】函数

c语言修炼秘籍【第二章】函数

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译



前言

本文会从函数是什么开始,逐一介绍函数的语法结构,如何声明和定义一个函数,函数的参数类型,调用函数时的传参类型,函数的调用方式,函数递归。请各位坐稳扶好,c语言函数部分的旅程马上开始。


一、函数是什么?

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

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

c语言中为什么要提供函数呢?
在使用c语言解决问题时,可能会遇到相同的功能逻辑需要反复使用的情况。例如,在程序中需要多次用到将两个变量中的内容交换的功能,没有函数的情况下,要实现这个功能,只能在需要使用的时候,将实现这个功能的代码拷贝过去,这使得代码变得非常的冗余、臃肿;有了函数,我们只需要调用该函数就行,大大简便了程序员的工作。

二、函数的分类

1.库函数

为什么会有库函数呢?

  • 在我们编程的过程中,会经常性的需要输入一些格式化的数据交给程序进行处理,这时会用到从键盘读取格式化的数据的功能(scanf),程序执行结束常常也需要输出执行的结果,这时会用到输出格式化的数据到屏幕上的功能(printf)。
  • 对字符串进行拷贝(strcpy)
  • 比较两字符串是否相等(strcmp)
  • 等等

像上述的代码是所有程序员都可能会用到的基础功能,并不是业务性代码。为了支持可移植性和提高程序的效率,所以c语言提供了一系列类似的库函数,方便程序员进行软件开发。

注:库函数并不需要记住,只需要知道怎么用即可
库函数的功能可以使用以下的查询工具查看:

MSDN(MicroSoft Develop Network)
www.cppreference.com(英文)
www.cppreference.com(中文)
www.cplusplus.com

2.自定义函数

库函数只是实现了一些编程中会用到的基础功能,编程中更重要的是自定义函数。

自定义函数与库函数一样,有函数名、返回值类型、函数参数。

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

ret_type -- 返回值类型
fun_name -- 函数名
para_type -- 函数参数类型
para1 -- 函数参数

一些函数的示例:

// 找出两数中的最大值
int get_max(int num1, int num2) // int是返回值类型,get_max是函数名,int是参数类型,num1、num2是函数参数
{
	return (num1 > num2) ? (num1) : (num2);
}

// 交换两个整型变量的值
void swap(int* num1, int* num2) // void是返回值类型,swap是函数名,int*是参数类型,num1、num2是函数参数
{
	int tmp = 0;
	tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

三、函数的参数

函数的参数可分为两类:

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

1.形式参数

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

2.实际参数

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

现在对一段代码进行分析

void swap1(int num1, int num2)
{
	int tmp = 0;
	tmp = num1;
	num1 = num2;
	num2 = tmp;
}

void swap2(int* num1, int* num2)
{
	int tmp = 0;
	tmp = *num1;
	*num1 = *num2;
	*num2 = tmp;
}

int main()
{
	int a = 1;
	int b = 2;

	swap1(a, b);
	printf("swap1: a == %d, b == %d\n", a, b);

	swap2(&a, &b);
	printf("swap2: a == %d, b == %d\n", a, b);

	return 0;
}

其中函数swap1()和函数swap2()中的参数num1、num2都是形式参数。
在main函数中调用swap1()和调用swap2()传入的参数a、b是实际参数。
运行结果:
在这里插入图片描述
可以看到函数swap1并没有成功交换a、b的内容。这是为什么呢?
通过调试,观察函数中num1、num2和a、b的地址,看看这两个函数有什么区别
在这里插入图片描述
可以看到swap1中,num1、num2的值虽然和实参a、b传入的值相同,但num1、num2的地址与a、b的地址不相同,即num1、num2是在调用函数时创建的一份临时拷贝,函数swap1只是将这个临时拷贝的值进行了交换,并没有真正的交换a、b的值。
在这里插入图片描述
而在函数swap2的参数类型为int*,main函数中调用swap2()时,传入的实参是a、b的地址&a&b,可以看到,此时num1、num2和&a、&b分别是指向同一片空间的,利用a、b的地址直接对a、b变量所在的内存空间进行修改,所以成功的交换了a、b的值。

四、函数的调用

1.传值调用

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

上面的swap1()就是传值调用

2.传址调用

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

上面的swap2()就是传址调用

五、函数的嵌套调用和链式访问

函数和函数之间是可以相互调用的,可以根据需求进行组合。

1.函数的嵌套调用

在函数中可以调用其他函数。

嵌套调用示例

#include <stdio.h>

void test()
{
	printf("test");
}

void test1()
{
	test();
}

void test2()
{
	test1();
}

int main()
{
	test2();

	return 0;
}

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

2.函数的链式访问

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

链式访问示例

#include <stdio.h>

int Add(int num1, int num2)
{
	return (num1 + num2);
}

int Sub(int num1, int num2)
{
	return (num1 - num2);
}

int main()
{
	Sub(Add(2, 3), 3);

	return 0;
}

六、函数的声明和定义

1.函数声明

  • 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但具体是否存在,函数声明无法决定。
  • 函数的声明一般出现在函数的使用之前,函数只有声明之后才能使用。
  • 函数的声明一般放在头文件中。

头文件中声明函数

// Add.h
// 方法一
#pragma once

int Add(int, int); // 函数声明中的参数只需要指明类型即可,无需形参名

// 方法二
#ifndef __TEST_H__
#define __TEST_H__

int Add(int, int);

#endif // __TEST_H__

2.函数定义

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

源程序中定义函数

// Add.c
int Add(int num1, int num2)
{
	return num1 + num2;
}

注:并非函数都需要显式声明,函数定义也能作为声明,但需注意函数在使用前必须先声明

以下两种代码效果相同

// 代码一
#include <stdio.h>
// 函数声明 -- 两个整型相减 -- 函数声明在前,定义在后
int Sub(int, int);

int main()
{
	int a = 2;
	int b = 1;
	int ret = Sub(a, b);
	printf("%d - %d == %d\n", a, b, ret);

	return 0;
}

// 函数定义 -- 两个整型相减
int Sub(int num1, int num2)
{
	return num1 - num2;
}
// 代码二
#include <stdio.h>
// 函数定义 -- 两个整型相减
int Sub(int num1, int num2)
{
	return num1 - num2;
}

int main()
{
	int a = 2;
	int b = 1;
	int ret = Sub(a, b);
	printf("%d - %d == %d\n", a, b, ret);

	return 0;
}

七、函数递归

1.什么是递归?

程序调用自己的编程技巧称为递归
它是一种将一个大型复杂的问题,层层转化为一个与原问题相似规模更小的问题进行求解。
递归只需要少量程序就能描述出解题过程中所需要的多次计算,大大地减少了代码量。

2.递归的两个必要条件

  • 存在限制条件,当满足限制条件时,递归将不再继续。
  • 每次递归调用之后都会越来越接近限制条件

3.递归实现的一些示例

接受一个整型值(无符号),按照顺序打印它的每一位
如:
输入:1234,输出1 2 3 4
在这里插入图片描述
从图中可以看出,要获取数字的最低位是最容易的。
想要顺序打印整型值num的每一位,可以拆解为打印num / 10的每一位,再打印num的最后一位,直到num / 10 == 0停止递归,它也就是该递归的限制条件。

#include <stdio.h>

void print_digit(unsigned int num)
{
	if (num / 10)
	{
		print_digit(num / 10);
	}
	printf("%d ", num % 10);
}

int main()
{
	print_digit(1234);

	return 0;
}

运行结果:
在这里插入图片描述

编写函数,不允许创建临时变量,求字符串的长度
如,
字符串“abcd”的长度,计算得4
在这里插入图片描述
从上图中,从字符串中获取第一个字符最容易,所以可以将计算"abcd"长度的问题,分解为计算"bcd"的长度再+1,直到变成""时递归停止。

#include <stdio.h>

int my_strlen(char* str)
{
	if (*str)
	{
		return (my_strlen(str + 1) + 1);
	}
	return 0;
}

int main()
{
	char arr[] = "abcd";
	printf("%s`s length == %d\n", arr, my_strlen(arr));

	return 0;
}

运行结果:
在这里插入图片描述

4.递归与迭代

计算n的阶乘 – 递归实现(不考虑溢出)

int factorial(int n)
{
	if (n)
	{
		return (n * factorial(n - 1));
	}
	return 1;
}

计算斐波那契数 – 递归实现(不考虑溢出)

int fib(int n)
{
	if (n <= 2)
	{
		return 1;
	}
	return (fib(n - 1) + fib(n - 2));
}

虽然上述能够实现各自所需的需求,但是它们仍存在问题:

  • 使用factorial这个函数求10000的阶乘时,程序会崩溃
  • 在使用fib这个函数计算第50个斐波那契数时,会耗费特别多的时间

这是为什么呢?

问题1:
当main函数中调用factorial(10000)时
在这里插入图片描述
出现了栈溢出。
下图简单的展示了c语言是如何使用内存空间
在这里插入图片描述
每次调用函数,都需要为它在栈区中分配一片空间,直到函数退出该空间才会还给系统,计算机的内存资源是有限的,计算factorial(10000)时,会调用它10000次,所以会导致栈溢出的问题。

问题2:
修改fib的代码

int count = 0;

int fib(int n)
{
	count++;
	if (n <= 2)
	{
		return 1;
	}
	return (fib(n - 1) + fib(n - 2));
}

int main()
{
	int n = 30;
	printf("%d`s fib == %d\n", n, fib(n));
	printf("fib %d`s count == %d\n", n, count);

	return 0;
}

运行结果:
在这里插入图片描述

可以看到,为了获得30的斐波那契数,程序总共进行了1664079次计算,这是一个很大的值了。
为什么会计算那么多次呢?
为了分析方便,仅对fib(5)进行分析
在这里插入图片描述
从上图中可以看出,计算fib(5)时,进行了许多次冗余的计算(fib(3),fib(2)…等重复计算了多次),这也就是为什么fib(50)的计算时间会非常久。

我们要如何解决上述的问题呢?
将递归改成非递归

上述两个问题的迭代代码

// 阶乘
int factorial(int n)
{
	int ret = 1;
	int i = 0;
	for (i = 1; i <= n; i++)
	{
		ret *= i;
	}
	return ret;
}

//斐波那契数
int fib(int n)
{
	int pre_ret = 1;
	int ret = 1;
	int next_ret = 0;
	if (n <= 2)
	{
		return ret;
	}
	else
	{
		int i = 0;
		for (i = 3; i <= n; i++)
		{
			next_ret = pre_ret + ret;
			pre_ret = ret;
			ret = next_ret;
		}
		return ret;
	}
}

提示

  1. 许多问题以递归的形式来解释,只是因为它比非递归形式更清晰,但这些问题的迭代实现往往比递归实现效率更高,仅仅只是代码可读性差一点。
  2. 当一个问题难以用迭代实现时,此时递归的简洁性就可以弥补它性能上的劣势。

5.函数递归的经典问题

1.汉诺塔

此处为了方便分析,仅分析4层的塔
在这里插入图片描述
为了将A中的4个盘子按顺序移动到C,可将步骤拆分为

  1. 将A中除最底部的盘子外的所有盘子按顺序移动到B
  2. 将A上最后一个盘子移到C
    在这里插入图片描述
  3. 将B中除最底部的盘子外的所有盘子按顺序移动到A
  4. 将B上最后一个盘子移到C
    在这里插入图片描述
  5. 将A中除最底部的盘子外的所有盘子按顺序移动到B
  6. 将A上最后一个盘子移到C
    在这里插入图片描述
  7. B此时只剩最后一个盘子,直接移动到C,递归结束。
#include <stdio.h>

#define STACK_MAX_SIZE 100

// 栈 -- 当作存放盘子的柱子
typedef struct
{
	int data[STACK_MAX_SIZE]; 
	int top; // 指向当前能插入的最低位置,栈为空时,指向下标0的位置
} Stack;

void initStack(Stack* s)
{
	(*s).top = 0;
}

// 将num压入s中
void Push(Stack* s, int num)
{
	(*s).data[(*s).top++] = num;
}

// 弹出s的栈顶元素
int Pop(Stack* s)
{
	if((*s).top)
		return (*s).data[--(*s).top];
	else
		printf("栈已空\n");
}

// 输出栈中的所有元素
void print_stack(Stack* s)
{
	while ((*s).top)
	{
		printf("%d ", Pop(s));
	}
}

// 将栈x中的栈顶元素移动到栈y
void move(Stack* x, Stack* y)
{
	Push(y, Pop(x));
}

void Hanoi(Stack* a, Stack* b, Stack* c, int n)
{
	if (n > 1)
	{
		Hanoi(a, c, b, n - 1); 
		move(a, c);
		Hanoi(b, a, c, n - 1);
	}
	else
	{
		move(a, c);
	}
}

int main()
{
	Stack A = { { 4, 3, 2, 1 }, 4 };
	Stack B;
	Stack C;

	initStack(&B);
	initStack(&C);

	Hanoi(&A, &B, &C, A.top);
	print_stack(&C);

	return 0;
}

2.青蛙跳台阶

为了分析简单,仅分析4阶台阶
在这里插入图片描述
此时青蛙需要跳4阶,可以拆分为,

  1. 先跳1阶,再跳3阶;或先跳2阶,再跳2阶;
    即有2种跳法:jump(3) + jump(2)
    在这里插入图片描述
  1. 跳3阶:先跳1阶,再跳2阶;或先跳2阶,再跳1阶
    即有2种跳法:jump(2) + 1
    在这里插入图片描述
  2. 跳2阶:先跳1阶,再跳1阶;或跳2阶。
    即有2种跳法:jump(1) + 1
    在这里插入图片描述
    4.跳1阶,只有一种跳法,递归结束。
    4阶台阶,青蛙总共有jump(3) + jump(2) = jump(2) + 1 + 1 + jump(1) = 1 + 1 + 1 + 1 + 1 = 5种
#include <stdio.h>

int jump(int n)
{
	if (n == 1)
	{
		return 1;
	}
	if (n == 2)
	{
		return 2;
	}
	return (jump(n - 1) + jump(n - 2));
}

int main()
{
	int n = 4;
	printf("%d阶台阶,青蛙有%d种跳法\n", n, jump(n));

	return 0;
}

运行结果:
在这里插入图片描述

3.8皇后

在一个8x8的国际象棋的棋盘中,放置8个皇后,且它们之间不能相互攻击(不能同行、同列、同斜线)
在这里插入图片描述
根据规则,这8行中,每行中有且仅有1个皇后。所以问题可以拆分为,

  1. 先放置第一行的皇后,再放置后七行的皇后。
    在这里插入图片描述
  2. 对放置后七行的皇后继续拆分:先放置第一行的皇后,再放置后六行的皇后。
    在这里插入图片描述
  1. 以此类推
    在这里插入图片描述
  2. 当放置第一行的皇后,出现没有放置的位置的情况时,该放置方法无效,退出此次放置
    在这里插入图片描述
  3. 将上一行的皇后往后移动,继续放置剩余的皇后
    在这里插入图片描述
    6.直到将最后一个皇后放入棋盘,才完成一次。

结束限制条件:直到剩余皇后数量为0时,放置成功。

完整代码

#include <stdio.h>

// 棋盘初始化
void initBoard(int (*board)[8])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 8; i++)
	{
		for (j = 0; j < 8; j++)
		{
			board[i][j] = 0;
		}
	}
}

// 判断将皇后放在(row,col)位置是否合法; 1 - 合法,0 - 非法
int islegal(int(*board)[8], int row, int col)
{
	int i = 0;
	// 判断同一行
	for (i = 0; i < 8; i++)
	{
		if(board[row][i])
			return 0;
	}
	// 判断同一列
	for (i = 0; i < 8; i++)
	{
		if(board[i][col])
			return 0;
	}

	// 主对角线方向
	if (row > col) // 左下
	{
		int i = row - col;
		int j = 0;
		for (; i < 8; i++, j++)
		{
			if(board[i][j])
				return 0;
		}
	}
	else if (row < col) // 右上
	{
		int i = 0;
		int j = col - row;
		for (; j < 8; i++, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
	else // 主对角线
	{
		int i = 0;
		int j = 0;
		for (; j < 8; i++, j++)
		{
			if (board[i][j])
				return 0;
		}
	}

	// 副对角线方向
	if (row + col < 7) // 左上
	{
		int i = row + col;
		int j = 0;
		for (; i >= 0; i--, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
	else if (row + col > 7) // 右下
	{
		int i = 7;
		int j = col + row - 7;
		for (; j < 8; i--, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
	else // 副对角线
	{
		int i = 7;
		int j = 0;
		for (; j < 8; i--, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
	return 1;
}

// 在row,col位置放置皇后
int setQueen(int(*board)[8], int row, int col)
{
	if (islegal(board, row, col))
	{
		board[row][col] = 1;
		return 1;
	}
	else
	{
		return 0;
	}
}

void printBoard(int (*board)[8])
{
	int i = 0;
	int j = 0;
	for (i = 0; i < 8; i++)
	{
		for (j = 0; j < 8; j++)
		{
			if(board[i][j])
				printf("Q ");
			else
				printf("* ");
		}
		printf("\n");
	}
}

// n表示还有n行需要放置皇后
void put_queen(int (*board)[8], int n)
{
	static count = 0; // 记录8皇后成功放置的次数
	// 棋盘

	int i = 0;
	int flag = 0; // 当前行是否被修改过。1 - 是,0 - 否
	for (i = 0; i < 8; i++)
	{
		if (setQueen(board, 8 - n, i))
		{
			flag = 1;
			if (n == 1) // 最后一个需放置的皇后
			{
				count++;
				printf("第%d种方法:\n", count);
				printBoard(board);
			}
			else
			{
				put_queen(board, n - 1);
			}
		}
		if (flag)
		{
			// 当前状态的所有可能情况处理完成
			board[8 - n][i] = 0; // 回溯当前行
			flag = 0;
		}
	}
}

int main()
{
	int board[8][8]; // 1表示皇后,0表示空
	initBoard(board);

	put_queen(board, 8);

	return 0;
}

运行结果:
在这里插入图片描述

此代码中的几个难点:

  1. 如何判断一个位置能否放置皇后?
  2. 递归程序如何写?

  1. 判断皇后放置位置的合法性
    要放置一个皇后需满足,同行、同列、它所处的斜线都不能存在其他皇后。
    同行、同列的判断很简单,循环判断所处的行和列即可,它所处的斜线我们要如何判断呢?
    观察下图
    在这里插入图片描述
    绿线方向表示主对角线方向,它将棋盘分为了左下右上主对角线上三个部分。
    左下:该部分中格子的行号是大于它的列号的;
    右上:该部分中格子的行号是小于它的列号的;
    主对角线上:该部分中格子的行号是等于它的列号的;
    每个相邻元素的行号和列号都相差1


    蓝线方向表示副对角线方向,它将棋盘分为了左上右下主对角线上三个部分。
    该方向上的行号和列号之和相等
// 主对角线方向
if (row > col) // 左下
{
	int i = row - col;
	int j = 0;
	for (; i < 8; i++, j++)
	{
		if(board[i][j])
			return 0;
	}
}
else if (row < col) // 右上
{
	int i = 0;
	int j = col - row;
	for (; j < 8; i++, j++)
	{
		if (board[i][j])
			return 0;
	}
}
else // 主对角线
{
	int i = 0;
	int j = 0;
	for (; j < 8; i++, j++)
	{
		if (board[i][j])
			return 0;
	}
}
	// 副对角线方向
	if (row + col < 7) // 左上
	{
		int i = row + col;
		int j = 0;
		for (; i >= 0; i--, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
	else if (row + col > 7) // 右下
	{
		int i = 7;
		int j = col + row - 7;
		for (; j < 8; i--, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
	else // 副对角线
	{
		int i = 7;
		int j = 0;
		for (; j < 8; i--, j++)
		{
			if (board[i][j])
				return 0;
		}
	}
  1. 递归的实现
    当函数进入到下一层时,当前行的皇后处于已经放置的状态,当下一层递归函数返回时,需要将当前行的皇后的状态回溯到未放置的状态,并将其放置位置往后移动。

即以下这段代码的逻辑

int flag = 0; // 当前行是否被修改过。1 - 是,0 - 否
for (i = 0; i < 8; i++)
{
	if (setQueen(board, 8 - n, i))
	{
		flag = 1;
		if (n == 1) // 最后一个需放置的皇后
		{
			count++;
			printf("第%d种方法:\n", count);
			printBoard(board);
		}
		else
		{
			put_queen(board, n - 1);
		}
	}
	if (flag)
	{
		// 当前状态的所有可能情况处理完成
		board[8 - n][i] = 0; // 回溯当前行
		flag = 0;
	}

总结

函数部分,需要大家理解形参、实参分别是什么,函数的两种调用方式 - - 传值、传址,学会如何自定义函数实现自己所需功能,充分理解递归的思想,并且自己要实际动手写几个递归的代码,真正掌握该解决问题的办法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值