Bootstrap

C语言笔记:函数

目录

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

1.1 库函数

1.2 自定义函数

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

例2:写一个函数可以交换两个整形变量的内容

1.2.1***需要学会找bug的过程:调试( F10) 

2.函数的参数

2.1 实际参数(实参)

2.2 形式参数(形参)

3.函数的调用:

3.1 传值调用

3.2 传址调用

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

4.1 嵌套调用

4.2 链式访问

5.函数的声明和定义

5.1 函数声明

5.2 函数定义

6.函数递归

6.1 递归的两个必要条件

补充:在内存中的区域划分

6.1.1 练习1:接受一个整型值(无符号),按照顺序打印它的每一位。(用递归解决)

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

6.1.3 练习3:用递归求n的阶乘(不考虑溢出)。

6.1.4 练习4:求第n个斐波那契数。(不考虑溢出)。


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

1.库函数

2.自定义函数

1.1 库函数

为了支持可移植性和提高程序的效率,C语言的基础库中提供了一系列类似的库函数(由常用的功能(如打印数据,求字符串长度,输入数据 等)用C语言封装而成),方便程序员进行软件开发。

推荐一个学习库函数的网站:www.cplusplus.com

打开后可查询每个头文件包含的库函数 以及库函数的用法、示例等

如:查询strcpy函数:

 注: 但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件

简单的总结,C语言常用的库函数都有:

IO函数

字符串操作函数

字符操作函数

内存操作函数

时间/日期函数

数学函数

其他库函数

库函数需要全部记住吗?

并不是。但需要学会查询工具的使用

MSDN(Microsoft Developer Network)

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

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

1.2 自定义函数

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

函数的基本组成:

ret_type fun_name(para1, * )
{
    statement;          //语句项
}
//ret_type 返回类型
//fun_name 函数名
//para1    函数参数

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

#include <stdio.h>

int get_max(int x, int y)
  {
	 return (x>y)? (x):(y);
  }
/*或者
int get_max(int x, int y)
{
	int z = 0;
	if (x > y)
		z = x;
	else
		z = y;
	return z;
*/


int main()
{	
	int num1 = 0;
	int num2 = 0;

	scanf("%d %d", &num1, &num2);

	//get_max为自己创建的自定义函数
	int max = get_max(num1, num2);
	
	printf("max = %d\n", max);

	return 0;
}

例2:写一个函数可以交换两个整形变量的内容

先看一个错误实例:

//错误实例
#include <stdio.h>
//bug:结果并没有交换
//找bug的过程叫:调试 F10 按F10一步一步向下走,遇到函数时按F11进入函数内部
void Swap(int a, int b)
 {
    int temp = 0;
    temp = a;
    a = b;
    b = temp;
 }
  

int main()
{	
    int num1 = 0;
	int num2 = 0;

	scanf("%d %d", &num1, &num2); 

	Swap(num1, num2);
	
	printf("num1 = %d num2 = %d\n",num1,num2);  /num1与num2的值并未发生交换

	return 0;
}

1.2.1***需要学会找bug的过程:调试( F10) 

按F10一步一步向下走,遇到函数时按F11进入函数内部

在错误实例中:

可见,当执行swap函数后,a与b的值交换了,而num1和num2的值并未交换

再看a,b,num1,num2的地址情况:

当把参数num1,num2传给a,b时,a和b独立开辟了空间,swap函数确实交换了a与b的值,但由于a,b与num1,num2并不在同一个空间,所以num1和num2的值没有随着ab改变。

 示例中的num1和num2 被称为实参 - 实际参数 a和b被称为形参 - 形式参数

当实参传给形参的时候,形参是实参的一份临时拷贝  ==》对形参的修改不会影响实参

那么如何正确完成该示例呢? 

用指针: 通过swap函数传入num1,num2的地址。pnum1存入的num1的地址,从而使pnum1指向num1;同理,pnum2指向num2。由此使swap函数内外产生联系。

//正确示例
#include <stdio.h>

void Swap(int* pnum1, int* pnum2)
{
	int temp = *pnum1;
	*pnum1 = *pnum2;
	*pnum2 = temp;
}

int main()
{	
	int num1 = 0;
	int num2 = 0;

	scanf("%d %d", &num1, &num2);

	Swap(&num1, &num2);
	
  	printf("num1 = %d num2 = %d\n",num1,num2);

	return 0;
}

总结:当在函数内部需要改变外部的某些变量的时候,需要使用指针(函数传入变量的地址进入)

2. 函数的参数

2.1实际参数(实参)

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

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

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

2.2形式参数(形参)

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

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

形参实例化之后其实相当于实参的一份临时拷贝。

练习:用函数实现二分查找

//实现函数在arr数组中查找k
//若找到返回下标,找不到返回-1(因为数组下标可以为0,所以查找失败返回值采用-1)

#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)
			right = mid - 1;
		else if (arr[mid] < k)
			left = mid + 1;
		else
			return mid;     
	}
	return -1;
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int k = 7; //k为被查找的数字
	int ret = binary_search(arr, k, sz);
	
	if (ret == -1)
		printf("未找到指定元素");
	else
		printf("找到了,该元素下标是:%d", ret);
	return 0;
}

需注意的是,如果在binary_search函数内部用sz = sizeof(arr) / sizeof(arr[0])来计算数组长度,会查找失败:

#include <stdio.h>

int binary_search(int arr[], int k) 
{											
	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)
			right = mid - 1;
		else if (arr[mid] < k)
			left = mid + 1;
		else
			return mid;     
	}
	return -1;
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int k = 7; //k为被查找的数字
	int ret = binary_search(arr, k);
	
	if (ret == -1)
		printf("未找到指定元素");
	else
		printf("找到了,该元素下标是:%d", ret);
	return 0;
}                          //未找到指定元素

查找失败的原因在于,当在向binary_search函数传入arr时,实际上只是向该函数中传入了arr数组中第一个元素的地址,此时sizeof(arr) = 4,所以导致了查找失败

当数组作为传参时,传入的是数组首字符的地址

所以当函数中需要求数组元素个数的时候,在函数外部求好后,再传入函数中

3.函数的调用:

3.1 传值调用

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

3.2 传址调用

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

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

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

#include <stdio.h>

void Add(int* p)
{
	*p = *p + 1 ;
}

int main()
{
	int num = 0;

	Add(&num);
	printf("num = %d\n", num);  //1

	Add(&num);
	printf("num = %d\n", num);  //2
	return 0;
}

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

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

4.1 嵌套调用

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

#include <stdio.h>
void new_line()
{
    printf("hehe\n");
}
void three_line()
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        new_line();
    }
}
int main()
{
    three_line();
    return 0;
}

4.2链式访问

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

#include <stdio.h>

int main()
{
    printf("%d", printf("%d", printf("%d", 43)));  //4321

    return 0;

 在MSDN中,可以查到,printf函数返回值打印的字符的个数,发生错误的时候返回负值

5.函数的声明和定义

5.1 函数声明

1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。

2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用

3. 函数的声明一般要放在头文件中的。

教科书中的写法:(一般不采用该写法)

#include <stdio.h>

//声明一下

//声明函数 - 告诉编译器有一个函数,名叫ADD,2个参数都是int类型,函数返回类型是int

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

工程中的写法:

5.2 函数定义

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

6.函数递归

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

递归做为一种算法在程序设计语言中广泛应用。

一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法。

它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

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

6.1递归的两个必要条件

基准情形存在限制条件,当满足这个限制条件的时候,递归便不再继续。

不断推进:每次递归调用之后越来越接近这个限制条件。

补充:在内存中的区域划分

每一次函数调用都需要分配空间。

所以在写递归函数的时候一定要注意两个必要条件,否则可能出现栈溢出的现象。

6.1.1 练习1:接受一个整型值(无符号),按照顺序打印它的每一位。(用递归解决)

分析,当输入1234时,需要用函数输出1 2 3 4 ,那么用递归的化简思想:

假设自定义打印函数为print,则:   

1.当传入数字大于等于两位(>9),继续划分     

2.当划分至个位,打印              

图示为:

                                                                    

代码如下:

#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);
    return 0;
}

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

创建临时变量(count)的版本:

#include <stdio.h>

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

int main()
{
    char arr[] = "abcdef";
    int len = my_strlen(arr);//传入的是首字符的地址,所以用字符指针接收
    printf("%d\n", len);  //6
    return 0;
}

不创建临时变量的版本(递归版本)

递归思路:

#include <stdio.h>

int my_strlen(char* str)
{
    if (*str != '\0')
        return 1 + my_strlen(str + 1);
    else
        return 0;
}

int main()
{
    char arr[] = "abcdef";
    int len = my_strlen(arr);//传入的是首字符的地址,所以用字符指针接收
    printf("%d\n", len);
    return 0;
}

6.1.3 练习3:用递归求n的阶乘(不考虑溢出)。

递归思路:

n! = n * (n-1) , n>= 2;

n! = 1            , n<= 1;

int Fac(int a)
{
    if (a > 1)
        return a * Fac(a - 1);
    else
        return 1;
}

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

6.1.4 练习4:求第n个斐波那契数。(不考虑溢出)。

斐波那契数列:1 1 2 3 5 8 13 21 34 55...

规律:第n个数字等于n前两个数字之和

Fib(n) = 1,                                   n <= 2

Fib(n) = Fib(n - 1) + Fib(n - 2),   n <= 2

递归思路:

#include <stdio.h>

int Fib(int a)
{
    if (a > 2)
        return Fib(a - 1) + Fib(a - 2);
    else
        return 1;
}

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

无减枝操作的斐波那契递归代码效率很低下,如果参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。

系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,即栈溢出。

那如何解决上述的问题:

1. 将递归改写成非递归。

2. 使用static对象替代 nonstatic 局部对象

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

非递归版本:

第一次:

a = 1,b = 1,c = a + b

第二次:

a = b,b = c,c = a + b

所以非递归代码为:

//更简洁的代码
#include <stdio.h>

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;
    scanf("%d", &n);
    int ret = Fib(n);
    printf("%d\n", ret);
    return 0;
}


//初始构思的代码
#include <stdio.h>

int Fib(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    int i = 0;
    if (n <= 2)
        return 1;
    for (i = 0; i < n - 2; i++) //因为逻辑上当n > 2时,才开始循环,所以n=3对应第1次循环 所以 
                          
                                //判断条件为i < n - 2
    {
        a = b;          
        b = c;
        c = a + b;
    }
    return c;
}

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

小结:

1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。

2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。

3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

;