C语言 - 函数

或许,宇宙本身就是一个高复杂度的套娃。


前言

本篇笔记重点描述C语言中函数的分类以及如何去调用函数。


一、C语言中函数的分类

1.库函数

为什么会有库函数?

1、因为在使用C语言编程的时候,总是在代码编写完后迫不及待的想知道结果有没有问题,将信息按照一定格式打印到屏幕上,此时就需要用到我们的printf函数。
2、在编程的过程中频繁的做一些字符串拷贝此时就需要用到stcpy函数。
3、在编程需要计算时候,比如n的k次方这样的运算,此时就需要用到pow函数。

注:在使用库函数的时候,必须包含#include对应的头文件

以下是一些库函数的查询工具:

MSDN(Microsoft Developer Network)
www.cplusplus.com
www.cplusplus.com http://en.cppreference.com(英文版) http://zh.cppreference.com(中文版)


2.自定义与调用函数

当然,库函数也不是万能的,右时候我们需要自定义一个想要的效果,但库函数没有这样的效果时该这么办?
这里就需要使用自定义函数,自定义函数和库函数式一样的,有函数名,返回值类型和函数参数。
但是不一样的是这些都是自己来设计,给了程序猿很大的发挥空间。

函数的组成:

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

举个例子:

写一个函数可以找出两个整数中的最大值
#include <stdio.h>
	int get_max(int x, int y ){
		return ((x > y)?(x):(y));
}
	int main(){
	int num1 = 10;
	int num2 = 20;
	int max = get_max(num1, num2);
	printf("%d\n", max);
	return 0;
}

再举个例子:

写一个函数可以交换两个整型变量的内容
#include <stdio.h>
	void swap1(int x, int y){
	int z = 0;
	z = x;
	x = y;
	y = z;
}
	void swap2(int *px, int *py){
	//这里指针指向的是num1和num2的地址
	int tmp = 0;//创建一个初始变量存放数值
	tmp = *px;//tmp = 1
//把*px(num1)所指向地址的值,赋值给tmp
	*px = *py;//*px(num1) = 2
//把*py(num2)所指向地址的值,赋值给*px(num1)所指向地址的值
	*py = tmp;//*py(num2) = 1
//把之前tmp存放的值(num1)赋值给*py(num2)所指向地址的值
}
	int main(){
	int num1 = 1;
	int num2 = 2;
	swap1(num1, num2);
	printf("swap1:num1 = %d num2 = %d\n", num1, num2);
	//打印结果为swap1:num1 = 1 num2 = 2
	swap2(&num1, &num2);//取num1和num2地址传给swap2函数
	printf("swap1:num1 = %d num2 = %d\n", num1, num2);
	//打印结果为swap1:num1 = 2 num2 = 1
	return 0 ;
}

由此我们可知:函数定义和调用的时候所需要的元素分别是什么。

函数创建需要三个重要的元素:“类型” “函数名” “形参”。
其中,形参是指函数名中括号的变量,形式参数当函数调用完成后就会自动销毁,因此形式参数只在函数中有效。
调用函数需要两个重要的元素:“函数名” “实参”。
其中,这里的实参可以是常量,变量,表达式,函数等。

那么函数是如何调用的呢?
上述代码中的两个调用各不相同,第一个为传值调用,第二为传址调用。

传值调用:函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用:地址调用时把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

接下来我们进行一些函数的小练习

1、写一个函数可以判断素数
思路:

素数:也叫质数,即只能被1和自己本身整除的数。
函数内把2到n-1之间都除个遍,如果能余0则return 0,如果能全部都无法余0,则return 1;然后打印i。

自定义函数判断素数练习
#include <stdio.h>
#include <math.h>

	int is_prime(int n){
	2->n-1之间的数字
	int j = 0;
	for(j = 2; j <= sqrt(n); j++){
		if(n % j ==0)
			return 0;
	}
	return 1;
}
	int main(){
	int i = 0;
	int count = 0;
	for(i = 100; i <= 200; i++){
		if(is_prime(i) == 1){
			count++;
			printf("%d ", i);
		}
	}
	printf("\ncount = %d\n", count);
	return 0;
}

2、写一个函数判断一年是不是闰年

思路:

1、四年一闰百年不闰:即如果year能被4整除,但是不能被100整除,则year为闰年。
2、每四百年再一闰:如果year能被400整除,则year为闰年。

写一个函数判断闰年
#include <stdio.h>
	int is_leap_year(int n){
	return((n % 4 == 0 && n % 100 != 0) || (n % 400 == 0));
//返回,判断n模4是否余0和n模100是否不等于0或者n模400等于0的数
//为真返回1,为假返回0。
	}
	int main(){
	int y = 0;
	for(y = 1000; y <= 2000; y++){
	//y从1000到2000逐个进入循环判断
		if(is_leap_year(y) == 1){
	//调用函数is_leap_year
	//如果返回的值为1,则打印y的数字
			printf("%d ",y);
		}
	}
	return 0;
}

3、写一个函数,实现一个整型有序数组的二分查找

思路:

之前写过一次,不过并没有运用自定义函数。
详细思路看回之前的C语言程序练习题(一)

函数实现有序数组二分查找
int binary_search(int a[], int k, int s){
//用int a[]数组接受,形参和实参名字可以相同也可以不同
	int left = 0;
	int right = s - 1;
	while(left <= right){
		int mid = (left + right) / 2;
		if(a[mid] > k){
			right = mid - 1;
		}
		else if(a[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 key = 7;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//找到了就返回找到的位置的下标
	//找不到返回-1
	//数组arr传参,实际传递的不是数组的本身
	//仅仅传过去了数组首元素的地址
	int ret = binary_search(arr, key, sz);
	
	if(-1 == ret){
		printf("找不到\n");
	}
	else{
		printf("找到了,下标是%d\n", ret);
	}
	
	return 0;
}

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

思路:

调用函数num就增加1,利用ADD函数

写一个函数,每调用一次这个函数num值增加1
#include <stdio.h>
	void Add(int * p){
	(*p)++;
	//把指针指向的数值+1;
}
	int main(){
	int num = 0;
	Add(&num);
	printf("%d\n", num);//1

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

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

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();
    //打印3个hehe
	return 0; 
}


2.函数的链式访问

函数的链式访问其实就是把一个函数的返回值作为另一个函数的参数来使用。

#include <stdio.h>
	int main() 
	{
	printf("%d", printf("%d", printf("%d", 43))); 
	//结果是啥? 
	//注:printf函数的返回值是打印在屏幕上字符的个数 
	return 0;
}

代码解剖:

第一个printf需要调用第二个printf,第二个printf需要调用第三个printf,printf函数返回的是打印在屏幕上的字符的个数。所以第三个printf返回值为2,此时第二个printf打印2,第二个printf的返回值为1,此时第一个printf打印1,最终的打印结果为4321


三、函数的声明和定义

1.函数声明

函数声明在C语言中:

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


2.函数定义

test.h的内容
放置函数的声明

#ifndef __TEST_H__ #define __TEST_H__ //函数的声明
int Add(int x, int y);
#endif //__TEST_H__

test.c的内容
放置函数的实现

#include "test.h"
//函数Add的实现
int Add(int x, int y){
return x+y; 
}

四、函数递归

1.函数递归的两个必要条件

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

递归的主要思想:把大的问题化成小的问题

练习
接受一个整型值,按照顺序打印它的每一位:

#include <stdio.h>
void print(unsigned int n){
	if(n > 9){
	//如果数字大于9就一直循环
		print(n / 10);
	}
	printf("%d ", n % 10);
	//打印
}
int main(){
	unsigned int num = 0;
	scanf("%u ", &num);//1234
	//递归 - 函数自己调用自己
	print(num);//把num的数字传给函数print
	//print函数可以打印参数部分数字的每一位

}

同样在递归中也会出现两种我们不想看见的情况,一种叫死递归,一种叫栈溢出。

先来说说死递归
死递归的示例:

int main(){
	printf("hehe\n");
	main();
//这里就会发生死递归,是个错误的递归
	return 0;
}

接下来说一下栈溢出
栈溢出的示例:

void test(int n){
	if(n < 10000){
		test(n + 1);
	}
}
int main(){
	test(1);
//这里就会出现栈溢出的情况
	return 0;
}

要说栈溢出,我们先来说说栈区,堆区,还有静态区

栈区存放的是:局部变量,函数形参
堆区存放的是:动态内存分配的malloc/free,calloc,realloc
静态区存放的是:全局变量,静态变量

而每一个函数调用,都需要在栈区分配空间

所以上述代码中main的栈帧空间,main里面发现要调用test,又要给一块区域test,test内部又要调用test,上次的test还没结束,又分配了一块区域给test,如此往复下去,每次都给test开辟一个空间,栈区空间被耗光后,就会出现栈溢出的现象。


总结

写递归代码的时候:
1、不能死递归,都有跳出条件,每次递归逼近跳出条件
2、递归层次不能太深(递归层次太深的时候也会出现栈溢出的情况)

同时推荐两个网站:
github.com
https://www.stackoverflow.com/

程序猿的知乎

  • 56
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值