函数是什么?

数学中我们常见到函数的概念比如y=kx+b。但是你了解C语言中的函数吗?
维基百科中对函数的定义:子程序

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

上面的这些解释初识可能不理解,下面来一一介绍函数。

C语言中函数的分类:

1. 库函数

为什么会有库函数?

. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf函数)。
求字符串的长度(strlen函数)
判断字符串的大小(strcmp函数)
向上面这些函数会经常用,早期的互联网公司每个公司会把函数封装好,形成库函数,方便使用时调用。但是每个公司函数名称用法不同,后来形成了国际标准。库函数需要引用头文件才能使用。
它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

库函数怎么学习呢

这里提供一个库函数学习网站:cplusplus.这是一个官方的网站但都是英文。
在这里插入图片描述
简单的总结,C语言常用的库函数都有:

  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期函数
  • 数学函数
  • 其他库函数
    我们参照文档,学习几个库函数:
    strcpy.
    在这里插入图片描述

代码应用

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[20] = { 0 };
	strcpy(arr, "*****");
	printf("%s", arr);
	return 0;
}

最终结果是输出*****

menset.
在这里插入图片描述

代码应用

#include<stdio.h>
#include<string.h>
int main()
{
	char str[] = "hello world";
	memset(str, '*', 5);
	printf("%s", str);
	return 0;
}

最终结果是输出***** world

但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
不需要全部记住库函数,需要学会查询工具的使用:

自定义函数

是不是有了库函数就能干所有的事情了?
如果库函数能干所有的事情,那还要程序员干什么?所有更加重要的是自定义函数。自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

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

写一个求和的函数

#include<stdio.h>
//函数定义
int Sum(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	//函数调用
	int sum = Sum(a, b);
	printf("%d", sum);
	return 0;
}

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

两个变量无法通过相互赋值来交换,需要借助第三个变量
在这里插入图片描述

#include<stdio.h>
void swap1(int x,int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前a = %d   b = %d\n", a, b);
	swap1(a, b);
	printf("交换后a = %d   b = %d\n", a, b);
	return 0;
}

上面的程序输出结果交换前后都不变。显然代码是错误的。

在这里插入图片描述

上图是vs2019对该程序监视的结果,形参和实参的地址不同
当实参传递给形参的时候,形参是实参的一份临时拷贝
对形参的修改不会影响实参

对于解决此问题我们引出了指针,传递变量的地址,临时拷贝地址变量,可以通过拷贝的这个地址变量来找到变量改变变量的内容。

#include<stdio.h>
void swap2(int* x, int* y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	printf("交换前a = %d   b = %d\n", a, b);
	swap2(&a, &b);
	printf("交换后a = %d   b = %d\n", a, b);
	return 0;
}

函数的参数

实际参数(实参)

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

int Max(int x, int y)
{
	return x > y ? x : y;
}
int main()
{
	int a = 10;
	int b = 20;
	int max1 = Max(10, 20);//常量
	int max2 = Max(a, b);//变量
	int max3 = Max(a + b, b - a);//表达式
	int max4 = Max(Max(a, b), Max(a + b, b - a));//函数
	return 0;
}

形式参数(形参)

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
上面 Swap1 和 Swap2 函数中的参数 x,y,px,py 都是形式参数。在main函数中传给 Swap1 的 num1 ,num2 和传给 Swap2 函数的 &num1 , &num2 是实际参数。

在这里插入图片描述

这里可以看到 Swap1 函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。Swap2中px,py存储了num1,num2的地址,也可以当成对num1,num2的地址的临时拷贝,可以通过地址找到变量num1和num2变量,
所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。

关于主函数参数的小知识

主函数是有参数的,
int main(int argc, char* argv[], char *envp[])
遇到主函数有参数也不要奇怪啦。

函数的调用

传值调用

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参.
这中传值调用就是上面的Swap1,Max值传递,变量值未发生改变,

传址调用

Swap2函数传址调用,传递的是变量的地址,通过地址找到变量对变量的内容改变。

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

练习

  1. 写一个函数可以判断一个数是不是素数,并调用函数实现输出100–200间的素数。(质数又称素数。一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数;)
    一个数N若有公约数必定有一个大于等于另一个,相等的时候为

    N

    \sqrt{N}

    N
    ,不等的时候必有一个小于

    N

    \sqrt{N}

    N
    ,另一个大于

    N

    \sqrt{N}

    N
    ,所以写函数的时候素数的约数的判定从2------

    N

    \sqrt{N}

    N
    ,
    C语言中求一个数的开方用sqrt函数,头文件<math.h>

#include<stdio.h>
#include<math.h>
int  is_prime(int x)
{
	int i = 2;
	for (i = 2; i <= sqrt(x); i++)
	{
		if (x % i == 0)
		{
			return 0;
		}
	}
	return 1;
}

int main()
{
	int i = 101;
	for (i = 101; i <= 200; i += 2)
	{
		if (is_prime(i))
		{
			printf("%d ", i);
		}
	}
	return 0;
}
  1. 写一个函数判断一年是不是闰年
    打印1000~2000年之间的闰年
    闰年判断的规则:
    1. 能被4整除,并且不能被100整除是闰年
    1. 能被400整除是闰年
  • 判断函数函数
  • 是闰年返回1
    非闰年返回0
#include<stdio.h>
int Is_Leap_Year(int year)
{
	if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
	{
		return 1;
	}
	return 0;
}
int main()
{
	int year = 1000;
	for (year = 1000; year <= 2000; year++)
	{
		if (Is_Leap_Year(year))
		{
			printf("%d ", year);
		}
	}
	return 0;
}
  1. 写一个函数,实现一个整形有序数组的二分查找()二分查找在前面已经讲过,这里不再讲述)
    找到了,返回下标
    找不到,返回-1
#include<stdio.h>
int binary_search(int arr[], int key, int sz)
{
	int left = 0;
	int right = sz - 1;
	while (left <= right)
	{
		int mid = left + (right - left) / 2;
		if (arr[mid] < key)
		{
			left = mid + 1;
		}
		else if (arr[mid] > key)
		{
			right = 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]);
	int ret = binary_search(arr, key, sz);
	if (ret == -1)
	{
		printf("没找到\n");
	}
	else
	{
		printf("找到了,下标是:%d\n", ret);
	}
}

有的人想要把sz在自定义函数内部,这样会出现错误,因为形参int arr[]看着像数组但其本质是指针。数组传参实际上传递的是数组首元素的地址而不是整个数组,所以在函数内部计算一个函数参数部分的数组的元素个数是不靠谱的。

4 写一个函数,每调用一次这个函数,就会将 num 的值增加1。(用mun从1加到4)

ex1:传址函数

#include<stdio.h>
void Add1(int* pa)
{
	(*pa)++;
}
int main()
{
	int num = 1;
	printf("%d\n", num);
	Add1(&num);
	printf("%d\n", num);
	Add1(&num);
	printf("%d\n", num);
	Add1(&num);
	printf("%d\n", num);
	Add1(&num);
	printf("%d\n", num);
	return 0;
}

ex1:利用函数返回值

#include<stdio.h>
int Add2(int num)
{
	return ++num;//注意num++不可以因为他是先返回后++,返回的还是原来的数
}
int main()
{
	int num = 1;
	printf("%d\n", num);
	num = Add2(num);
	printf("%d\n", num);
	num = Add2(num);
	printf("%d\n", num);
	num = Add2(num);
	printf("%d\n", num);
	num = Add2(num);
	printf("%d\n", num);
	return 0;
}

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

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

函数的嵌套调用

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

嵌套调用

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

嵌套定义(是错误的,C语言不能出现)

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

链式访问

链式访问就是把一个函数的返回值作为另外一个函数的参数。

#include <stdio.h>
#include <string.h>
int main()
{
	char arr[20] = "hello";
	int ret = strlen(strcat(arr, "bit"));
	printf("%d\n", ret);
	printf("%s\n", arr);
	return 0;
}

strcat(a,b);函数就是把b字符串接到a字符串最后一个字符后面,并加上’\0‘
上面的int ret = strlen(strcat(arr, “bit”));就是strlen链式访问了strcat。

来做一个练习题
结果是啥?
注:printf函数的返回值是打印在屏幕上字符的个数

#include <stdio.h>
int main()
{
  printf("%d", printf("%d", printf("%d", 43)));
  return 0;
}

最终屏幕输出4321

一些不好的函数书写习惯

1.有返回类型但无返回值

int Add(int x, int y)//不推荐
{
	printf("hehe\n");
}

2.无形参的函数传递实参

void test(void)
{
	printf("hehe\n");
}

int main()
{
	test(100);//不推荐的
	return 0;
}

该程序不会报错和警告,如果test函数改成test(void);这样会警告,但还是会运行过去。