函数 —— 上

158 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

一、什么是函数

数学中我们常见到函数的概念,但是你了解C语言中的函数吗?维基百科中对函数的定义是子程序。

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

二、C语言中函数的分类

1.库函数

  • 为什么会有库函数 1.我们都知道在写完一段代码时,总是想很快的知道结果,这时就会频繁的使用一个功能:将信息按照一定的格式打印在屏幕上(printf) 2.在编程过程中会频繁做一些字符串的拷贝工作(strcpy) 3.或是经常计算n的k次方这样的运算(pow) 像上面这些的基础功能,它们不是业务性代码。我们在以后编写代码时都有可能用到,为了支持可移值性和提高程序的效率,所以C语言提供了一系的库函数,开发者只要调用即可
  • 怎么学习库函数(以下提供文件和网站方便查询) 1.文件版:MSDN、Linux下的C函数手册(这个是中文版) 链接:pan.baidu.com/s/1MkMOXJZk… 提取码:1999 2.网页版 www.cplusplus.com en.cppreference.com(c/c++官网;将en更改… 注:英语虽然不是学习编程的必需品,但是学好英语无疑是一个很好的加持
  • 需要记住全部库函数吗 当然不用,也记不完。需要学会面向查询工具编程,但是不能面向CTRL+C -> CTRL+V编程

2.自定义函数

  • 既然C语言已经提供了这么多的库函数,为什么还要自己写函数呢? 如果库函数能干所有的事情,那还要程序员干什么?

  • 库函数和自定义函数的区别 自定义函数和库函数一样,有函数名、返回值类型和函数参数。但是不一样的是这些是由我们自己来设计,这无疑是增加自己的发挥空间

通过MSDN,查询并使用1个库函数(strcpy)

通过查询我们发现strcpy的基本信息。功能是拷贝字符串、所在头文件string,以及函数的返回值类型(char*)、参数(两个char*类型的参数) 在这里插入图片描述

#include<stdio.h>
#include<string.h>
int main()
{
	char arr1 [20] = {0};
	char arr2 [] = "abc";
	strcpy(arr1, arr2);
	printf("%s\n", arr1);//abc
	return 0;
}

自定义函数:找出两个数的较大值 注:需要自己定义函数名(最好是有意义的名字,让别人一看就能知道这个函数是干什么的)、函数的返回值、函数的参数、自己写功能

#include<stdio.h>
//函数的定义
//参数的类型必须匹配 
//函数的返回值类型必须匹配,不返回值就写void
//函数不写返回值时,默认是int类型
int get_max(int x, int y)
{
	 int z = 0;
	 if(x > y)
	 	z = x;
	 else
	 	z = y;
	 return z;//返回整型z
}
int main()
{
	int a = 10;
	int b = 20;
	//函数的调用
	//返回的值存放在max里
	int max = get_max(a,b);
	printf("max = %d\n", max);
	return 0;
}

例如: main -> 我 get_max -> 张三 我请求张三帮我带份饭 -> (饭的种类,饭的价格)不详,带不了 我请求张三帮我带份饭 -> (蛋炒饭,20元)带的了。至于张三是如何带饭我们不用关心 我请求张三帮我带份饭 -> (蛋炒饭,20元)张三拿着钱跑路了,没有带回饭;或者张三带错饭了。说明这个程序出bug了


自定义函数:交换2个整型变量的值

#include<stdio.h>
//这里只是交换变量,不需要返回值(void)
void Swap1(int x, int y)
{
	int temp = 0;
	temp = x;
	x = y;
	y = temp;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d  b = %d\n", a,b);//10 20
	Swap1(a,b);
	printf("交换后:a = %d  b = %d\n", a,b);10 20
	return 0;
}

例如: 在这里插入图片描述

运行代码发现并不是我们想要的结果 在这里插入图片描述

经调试:在main函数内创建了a和b的空间,在Swap函数内创建了x和y的空间; x和y是2块独立的空间,在Swap函数内借助了temp把x和y交换是不会对main函数内的a和b有影响的

在这里插入图片描述


先回忆下指针:

#include<stdio.h>
int main()
{
	int a = 10; //4个字节的空间
	int* pa = &a;//pa就是一个指针变量,它存放a的地址
	*pa = 20;//通过指针访问a,并对其操作
	printf("%d\n", a);//20
}

改正:使用指针的方式:

#include<stdio.h>
//这里只是交换变量,不需要返回值(void)
void Swap2(int* pa, int* pb)
{
	int temp = 0;
	temp = *pa;
	*pa = *pb;
	*pb = temp;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d  b = %d\n", a,b);//10 20
	Swap2(&a, &b);
	printf("交换后:a = %d  b = %d\n", a,b);//10 20
	return 0;
}

注:pa就是a的地址,pb就是b的地址;*pa找到a的值,*pb找到b的值 在这里插入图片描述 结果: 在这里插入图片描述

在什么情况下要传地址? 在外部函数(Swap)要改变内部函数(main)里面的变量时。

三、函数的参数和调用

  • 实际参数(实参): 真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
  • 形式参数(形参): 形式参数是指函数名括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数,当函数调用完成之后形式参数就自动销毁了。因此形式参数只在函数内部有效。
//函数的定义 -> 形式参数 - 形参(x, y)
void Swap1(int x, int y)
{
	... ...
}
//函数的定义 ->形式参数  - 形参(pa, pb)
void Swap2(int* pa, int* pb)
{
	... ...
}
int main()
{
	//函数的调用  -> 实际参数 - 实参
	Swap1(a, b);
	Swap2(&a, &b);
}
  • 传值调用: 函数的形参和实参分别占有不同的内存块,对形参的修改不会影响实参
  • 传址调用 : 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 这种传参方式可以让函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
void Swap1(int x, int y)
{
	... ...
}

void Swap2(int* pa, int* pb)
{
	... ...
}
int main()
{
	//传值
	Swap1(a, b);
	//传址
	Swap2(&a, &b);
}

四、小试牛刀

1.写一个函数判断一个数是不是素数

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int is_prime(int n)
{
	 int i = 0;
	 for (i = 2; i < n; i++)
	 {
		if (n % i == 0)
			break;
	 }
	 if (n == i)
			return 1;//素数
		else 
			return  0;//还是素数
}
int main()
{ 
	int num = 0;
	printf("请输入一个数>:");
	scanf("%d", &num);
	if (1 == is_prime(num))
	{
		printf("这个数是素数\n");
	}
	else
	{
		printf("这个数不是素数\n");
	}
	return 0;
}

看代码:虽然这种写法也能实现我们想要的功能,但是这种代码是非常戳的。 在我们设计函数的时候,不建议在一个拥有独立功能的函数里面再加其它功能。比如这里is_prime只是想要判断是否为素数,而不是打印(如果有人只是为了判断,而不是打印,那么就不能使用这个函数),这个函数的功能是不够单一、不够独立的。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int is_prime(int n)
{
	 int i = 0;
	 for (i = 2; i < n; i++)
	 {
		if (n % i == 0)
			break;
	 }
	 if (n == i)
			printf("这个数是素数\n");
		else 
			printf("这个数不是素数\n");
	  return 0;
}
int main()
{ 
	int num = 0;
	printf("请输入一个数>:");
	scanf("%d", &num);
	is_prime(num);
	return 0;
}

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

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

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

先看一个错误写法:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//如果传上来的是数组首元素地址,那么也可以写为:
//int binary_search(int* a,int k);而int a[]是挂羊头,卖狗身
int binary_search(int a[], int k)
{
	//sizeof(地址)/sizeof(a[0]) = 1
	int sz = sizeof(a)/sizeof(a[0]);
	int left = 0;
	int right = sz-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 = 0;
	scanf("%d", &key);
	//找到了就返回找到的位置的下标;找不到返回-1
	//数组传参,实际上传递的不是数组本身。而是数组首元素的地址
	int ret = binary_search(arr,key);
	if (-1 == ret)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}

改正:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int binary_search(int a[], int k, int s)
{
	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 sz = sizeof(arr)/sizeof(arr[0]);
	int key = 0;
	scanf("%d", &key);
	//找到了就返回找到的位置的下标;找不到返回-1
	//数组传参,实际上传递的不是数组本身。而是数组首元素的地址
	int ret = binary_search(arr,key,sz);
	if (-1 == ret)
	{
		printf("找不到\n");
	}
	else
	{
		printf("找到了,下标是%d\n", ret);
	}
	return 0;
}

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

#include<stdio.h>
void Add(int* p)
{
	(*p)++;
}
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;
}

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

  • 函数的嵌套调用

函数是不能嵌套定义的

void test1()
{
	void test2()
	{
		
	}
}
int main()
{
	return 0;
}

但是函数可以嵌套调用

void test2()
{
	printf("hehe\n");
}
int test1()
{
	test2()
	return 0;
}
int main()
{
	//在main函数里调用了test1,在test1函数里调用了test2 
	test1();
	return 0;
}

  • 函数的链式访问
#include<stdio.h>
#include<string.h>
int main()
{
	int len = strlen("abc");
	
	printf("%d\n", len);
	//这里strlen的返回值就做了printf的参数->这就叫链式访问
	printf("%d\n", strlen("abc"));
	//---------------------------------------------------------
	char arr1 [20] = {0};
	char arr2 [] = "bit";
	
	strcpy(arr1, arr2);
	printf("%s\n", arr1);
	//这里strcpy的返回值(arr1的地址),就做了printf的参数
	printf("%s\n", strcpy(arr1,arr2));
	return 0;
}

看代码,输出结果是多少

#include<stdio.h>
int main()
{
	printf("%d", printf("%d", printf("%d", 43)));//4321
	//printf的返回值是打印在屏幕上的字符的个数
	//第1次打印43 -> 返回值是2 -> 2作为第2次打印的参数,返回值是1 -> 1作为第3次打印的参数
	return 0;
}

六、函数的声明和定义

  • 函数声明 1.告诉编译器有一个函数叫什么,参数是什么(参数名可加可不加),返回类型是什么。但是具体是否存在,无关紧要 2.函数的声明一般出现在函数的使用之前。要满足先声明后使用 3.函数的声明一般要放在头文件中
  • 函数定义 1.函数的定义是指函数的具体实现,交待函数的功能实现
  • 如果函数定义在main函数后面,需要先声明;如果函数定义在main函数之前,则不用声明

在main函数里使用定义在main函数后面的函数 warning:Add未定义 解决方法:声明

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	//函数的声明 - extern可加可不加
	extern int Add(int, int);
	int c = Add(a,b);
	printf("%d\n", c);
	return 0;
}
//函数的定义
int Add(int x, int y)
{
	return x + y;
}

但其实在以后的编码中这种写法是很少见的;具体场景看以下代码:

写一个计算器

在这里插入图片描述 在这里插入图片描述