C语言初阶——2.函数

        上一篇博客我们提到了关于C语言的分支与循环,这一篇我来分享一下C语言函数的相关知识,那就请睁大眼睛,好好看(哈哈)。

preview

        说到函数,首先我们想到的就是数学上的函数,这个在数学领域非常有趣且实用的知识,也可以说是工具吧。那它在编程界会有同样的效果吗,那答案当时是:是的。不仅在数学中,而且在编程方面也是非常好用的工具,那废话不多说,开始我们接下来的内容。


目录

一、在C语言中的函数是什么

二、C语言函数的分类

        2.1 库函数

        2.2 自定义函数

三、函数的参数

        3.1 实际参数(实参):

        2.2 形式参数(形参):

四、函数的调用

        4.1 传值调用

        4.2传址调用

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

        5.1 函数的嵌套调用

        5.2链式访问

六、函数的声明和定义

        6.1 函数的声明

        6.2 函数的定义

 七、函数递归

                  7.1 递归的两个必要

         7.2 递归和迭代

结语:


一、在C语言中的函数是什么

        下面这段文字来自百度:

       “c语言定义函数是一段可以重复使用的代码,用来独立地完成某个功能,它可以接收用户传递的数据,也可以不接收。接收用户数据的函数在定义时要指明参数,不接收用户数据的不需要指明,根据这一点可以将函数分为有参函数和无参函数。 函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。函数可以提前保存起来,并给它起一个独一无二的名字,只要知道它的名字就能使用这段代码。”

        下面这段代码简单的使用函数打印了一个菜单,这里只是简单的说一下,关于函数怎样使用,它的形式,都会在后面讲到。(上面那段文字比较长,感觉不太好理解,下面也会根据我的理解详细介绍一下)        


 二、C语言函数的分类

        2.1 库函数

        首先,先举几个我们使用过的库函数

如:

1)在屏幕上打印一个"Hello World!",我们会用到(printf)

2)我们要输入"Hello World!",要用到(scanf)

3)我们要计算n的k次方时,要用到(pow)

        这几个都是我们常见的库函数,C语言为我们提供了一系列库函数,方便我们去使用。

        那到底有多少库函数呢,这里提到一个网站,懂得都懂。

http://www.cplusplus.com/reference/http://www.cplusplus.com/reference/

这里有很多我们会用到的库函数,所以是个非常好用的网站。

C语言常用的库函数有:
        IO函数,输入输出函数;
        字符串操作函数;
        字符操作函数;
        内存操作函数;
        时间/日期函数,比如时间戳;
        数学函数,如次方,开平方,绝对值;
        其他库函数等。

注意:

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


        2.2 自定义函数

 自定义函数与库函数不同的是,我们可以根据自己想要达成的效果,来自己设计函数。

        自定义函数的组成:
返回类型  函数名  (函数参数); (函数参数也是有类型的)(可以有一个也可以有多个)

函数是先定义,再调用的。

下面我们拿strlen函数(计算字符串的长度)来举例

size_t 代表无符号整型,计算字符串的长度,肯定不是负数

const,我们要计算该字符串的长度,不能让这个字符串改变 ,所以用const来修饰

char* 字符指针

 那下面我们自己写一个函数来计算两个数的最大值。

#include <stdio.h>
int Find_Max(int x, int y)
{
	if (x > y)
	{
		return x;
	}
	else
	{
		return y;
	}
    //还有一个更简单的写法,用三目操作符
    //return x > y ? x : y;
}
int main()
{
	int num1 = 0;
	int num2 = 0;
	scanf("%d %d", &num1, &num2);
	int max = Find_Max(num1, num2);
	printf("%d\n", max);
	return 0;
}


三、函数的参数

        3.1 实际参数(实参):

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

        2.2 形式参数(形参):

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

上面找两个函数的最大值的代码,num1,num2就是实参,x,y就是形参

        我们来看一下监视器,&取地址符号,取出形参和实参的地址看一下,显而易见,形参和实参开辟了两块不同的地址,所以形式参数相当于实际参数的一份临时拷贝


四、函数的调用

        4.1 传值调用

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


        4.2传址调用

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

下面我们写一个函数交换两个整型变量的内容

void Swap1(int x, int y)
{
	int tmp = 0;
	tmp = x;
	x = y;
	y = tmp;
}

void Swap2(int* px, int* py) {
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}
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;
}

        这里写了两个函数,我们可以对比一下Swap1和Swap2。

        上面我们也说到了,形参和实参是开辟两块不同的内存空间,对于Swap1的解释为传值调用,我们只是交换了形参,并没有改变实参,而形参只是实参的临时拷贝,等到函数调用完,形参就会自动销毁,所以Swap1不能实现我们想要的功能。

        而Swap2传过去的是实参的地址,函数参数部分用指针变量(指针变量就是存放地址的)来接收,这就是传址调用,形参和实参使用同一块内存空间,可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

通过上图我们可以看到a和px的地址是相同的,b和py的地址是形同的。


下面写几个用函数实现各种问题的代码

(1)

//打印100~200之间的素数
#include <stdio.h>
#include <math.h>
int is_prime(int n)
{
	//2~n-1的数试除
	//2~sqrt(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++)
	{
		//判断是否为素数
		//返回值为1,表示是素数
		//返回值为0,表示不是素数
		if (is_prime(i) == 1)
		{
			printf("%d ", i);
		}
	}
	return 0;
}


 (2)

//写一个函数判断是不是闰年
#include <stdio.h>
int Is_leap_year(int n)
{
	//判断闰年的规则:
	//(1)能被4整除,但不能被100整除
	//(2)能被400整除
	if (n % 4 == 0 && n % 100 != 0 || n % 400 == 0)
	{
		return 1;
	}
	return 0;
    //简洁方法
    //return (((n % 4 == 0) && (n % 100 != 0)) || (n % 400 == 0));
}
int main()
{
	int y = 0;
	scanf("%d", &y);
	int ret = Is_leap_year(y);
	//如果是闰年,就返回1
	//如果不是闰年,就返回0
	if (ret == 1)
	{
		printf("%d年是闰年\n", y);
	}
	else
	{
		printf("%d年不是闰年\n", y);
	}
	return 0;
}


 (4)

//写一个函数,实现一个整形有序数组的二分查找
//找到了就返回下标
//找不到就返回-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 - left) / 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 = 0;
	scanf("%d", &k);//要查找的数
	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;
}

 


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

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

        5.1 函数的嵌套调用

#include <stdio.h>
void new_line()
{
	printf("Hello\n");
}
void three_line()
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		new_line();
	}
}
int main()
{
	three_line();
	return 0;
}
注意:  函数可以嵌套调用,但不可以嵌套定义。

嵌套定义:

 所以记住这样是错的。


        5.2链式访问

把一个函数的返回值作为另外一个函数的参数。
这是正常的代码

 这是链式访问的代码

有一个典型的链式访问的代码

 屏幕上打出4321,那这个是为什么呢?

        这是printf函数的返回值,意思是:这些函数中的每一个都返回打印的字符数,如果发生错误,则返回负值。所以从里向外,先打印43,43是两个字符数,下一个打印2,2是一个字符数,所以最后打印1,最后屏幕上打印4321。


六、函数的声明和定义

        6.1 函数的声明

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

        6.2 函数的定义

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

        下面这段代码和上面的不同,上面的代码会把自定义函数放到main函数的上面,但这样写,就会报一个警告。

 所以要注意的是上面提到的先声明后使用。(但不常像下面这样写,即使它是正确的写法)

 


 七、函数递归

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

递归策略:只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小

简单说函数递归就是函数自己调用自己

        7.1 递归的两个必要

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

举一个例子:

//接收一个整型值(无符号),按照顺序打印它的每一位
void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	unsigned int num = 0;
	scanf("%d", &num);//1234
	print(num);//print函数可以把num的每一位按照顺序打印出来

	return 0;
}

画了一个简单的图,来熟悉一下这个递归过程:

 

        这里提一下栈区,在程序每次调用函数都会在内存的栈区上开辟一块内存空间 ,例如刚才的代码,n = 1234时开辟一块空间,以此类推,如果我们没有设置一个限制条件,并且随着递归下去,没有越来越接近这个限制条件,是不是递归就会一直重复下去,形成死递归的情况,但栈区的空间是有限的,无限制的开辟下去,就会造成栈溢出。下面这个图利用上面的代码简单说明一下。

 


         7.2 递归和迭代

关于递归,上面我们也介绍到了,那迭代呢,可以简单的理解为循环。

下面还是写出几个例子:

(1)

//用递归计算n的阶乘
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;
}


(2)

//用递归求第n个斐波那契数。(不考虑溢出)
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;
}

         这里是用递归的方法写的,但是用递归的方法会有一些小的问题,如果我们想计算第40个斐波那契数,屏幕上并不会马上打印出来,那是因为计算第40个数需要比较大的计算量,就算是计算机都得算一会儿,可想而知计算量有多大。

要想计算第40个斐波那契数,就如上图,需要从40算起,算到第一个数。如果是第50个斐波那契数,那就得需要更长的时间。

但如果不用递归的方法,会不会有更好的方法可以提高效率呢?

int fib(int n)
{
	int a = 1;
	int b = 1;
	int c = 0;
	int i = 0;
	if (n <= 2)
	{
		return 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;
}

        上面这段代码没有用到递归的方法,但还可以计算第n个斐波那契数,并且代码的效率大大的提高了,相比于用递归的方法从第n个算到第1个,这种非递归的方法从第1个算到第n个的方法计算的速度是非常快的,所以知识是活灵活现的,不能说今天学了递归就舍弃了其他的方法,我们还是要找到最优的解法。


结语:

        那关于C语言函数的相关知识就分享到这里,这一篇博客就写到这里了,回想一下写博客的感觉还是非常棒的,感觉每一天过的很充实,我很期待下一篇博客啊。(哈哈,完结撒花)

猜你喜欢

转载自blog.csdn.net/m0_64607843/article/details/122540152