简单不先于复杂,而是在复杂之后。
目录
函数这一篇章我会分三篇博客讲解,想要系统学习的朋友可以到我的主页C语言专栏进行阅读学习。
1.函数是什么
维基百科对于函数的定义:子程序
在计算机科学中,子程序(Subroutine,procedure, function, routine, method, subprogram, callable unit ), 是一个大型程序中的某部分代码, 由一个或多个语句块组成。 它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2.C语言中函数的分类
1.库函数
2.自定义函数
2.1 库函数
为什么会有库函数?
1. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待地想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁地使用一个功能:将信息按照一定的格式打印到屏幕上。(printf)。
2. 在编程的过程中我们会频繁地做一些字符串的拷贝工作(strcpy)。
3. 在编程时我们也计算,总是会计算n的次方这样的运算(pow)。
像上面我们描述的基础功能,他们不是业务性的代码。我们在开发的过程中每个程序员都有可能用得到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
那要怎么学习库函数呢?
这里我们简单地看看:C library
里面介绍了C语言的头文件,点开头文件就会有库函数的介绍,可以找到所有的库函数并且学习它们。
简单的总结,C语言常用的库函数都有:
- IO函数(input output)
- 字符串操作函数
- 字符操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
接下来简单介绍一下如何利用 C library 学习库函数,我们将以学习两个库函数的方式进行介绍,实践是最好的老师。
char * strcpy ( char * destination, const char * source );
注:
但是学习库函数必须要知道的是:使用库函数,必须包含 #include 对应的头文件。
这里对照文档来学习上面几个库函数,目的是掌握库函数的使用方法。
2.1.1 如何学会使用库函数
库函数不需要全部记住,但是需要学会查询工具的使用:
MSDN(Microsoft Developer Network) 可以在浏览器自行搜索下载。
https://en.cppreference.com/w/ (英文版)
https://zh.cppreference.com/w/%E9%A6%96%E9%A1%B5 (中文版)
英文是很重要的,最起码要看懂文献。
2.2 自定义函数
如果库函数可以干所有事情,那还要程序员干什么?
所以比库函数更重要的是自定义函数。
自定义函数和函数一样,有函数名、返回值类型和函数参数。
但不一样的是自定义函数由我们自己设计,给程序员一个很大的发挥空间。
库函数的组成:
ret_type fun_name(para1, *) { statement;//语句项 } ret_type//返回类型 fun_name//函数名 para1//函数参数
举个例子:
写个函数找出两个整数的最大值
写一个函数交换两个整型变量的内容:
但是这个是一个有问题的代码
事实上,a, b叫做实际参数,x, y是形式参数,当实参传给形参时,形参是实参的一份临时拷贝,对形参的修改不会影响实参。
以上是正确的版本。
如果需要改变实际参数变量的值,就要传变量地址
3.函数的参数
3.1 实际参数(实参)
真实传给函数的参数,叫实参
实参可以是常量、变量、表达式和函数等。
无论实参是何种类型的量,在函数调用时,它们都必须有确定的值,以便把这些值传给形参。
3.2 形式参数(形参)
形式参数指函数名后括号中的变量,因为形式参数只在函数被调用的时候才实例化(创建内存单元),所以叫形式参数。形式参数在函数调用完成后就自动销毁了。因此形式参数只有在函数中才有效。
4.函数的调用
4.1 传值调用
Swap(a, b);
函数的形参和实参分别占用不同的内存块,对形参的修改不会影响实参。
4.2 传址调用
Swap(&a, &b);
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
4.3 练习
1.写一个函数判断一个数是不是素数
2.写一个函数判断一年是不是闰年
3.写一个函数,实现一个整型有序数组的二分查找
4.写一个函数,每调用一次这个函数,就会使num的数增加1
4.3.1 判断素数
打印100 ~ 200 的素数并计数
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { int i = 0; int count = 0; //判断是否是素数,是素数就打印 //素数是只能被1和本身整除的数 //用 2 ~i-1的数试除i for (i = 100; i <= 200; i++) { int j = 0; int flag = 1; for (j = 2; j < i; j++) { if (i % j == 0) { flag = 0; break; } } if (flag == 1) { count++; printf("%d ", i); } } printf("\ncount = %d", count); return 0; }
这个程序还可以继续优化:
- 因为一个非素数,一定有两个相乘的因子,一定会有一个因子<= sqrt(i), 所以没有必要把2~i-1的数全部遍历一遍,如果2到开平方之前的数都不能整除,之后的数也不能整除,也说明这个数就是素数。
- 素数不可能是偶数,所以在生成数的时候,可以直接生成100~200之间的奇数。
下面是优化之后的程序:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<math.h> int main() { int i = 0; int count = 0; //判断是否是素数,是素数就打印 //素数是只能被1和本身整除的数 //用 2 ~i-1的数试除i for (i = 101; i <= 200; i+=2) { int j = 0; int flag = 1; for (j = 2; j <= sqrt(i); j++) { if (i % j == 0) { flag = 0; break; } } if (flag == 1)//flag为1是素数 { count++; printf("%d ", i); } } printf("\ncount = %d", count); return 0; }
以上是用循环在主函数里判断素数,接下来用一个函数再写一个程序:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<math.h> //是素数返回1 //不是素数返回0 int is_prime(int n) { int i = 0; for (i = 2; i <= sqrt(n); i++) { if (n % i == 0) return 0; } return 1; } int main() { int i = 0; int count = 0; for (i = 101; i < 200; i += 2) { if (is_prime(i)) { printf("%d ", i); count++; } } printf("\ncount = %d", count); return 0; }
4.3.2 判断闰年
打印1000~2000之间的闰年
上面的代码判断不完全,因为 if 和 else if 只能进去一个,如果满足第一个 if 不满足第二个,year 就不会再进入 else if 了,所以要想要让两个条件都判断,就要把第二个 else if 改成 if 。
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> //打印1000~2000之间的闰年 //1. 能被4整除且不能被100整除是闰年 //2. 能被四百整除是闰年 int main() { int year = 0; for (year = 1000; year <= 2000; year++) { //判断year是否是闰年 if (year % 4 == 0) { if (year % 100 != 0) { printf("%d ", year); } } if (year % 400 == 0) { printf("%d ", year); } } return 0; }
这是正确的写法。
还有一种简化的写法:
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> //打印1000~2000之间的闰年 //1. 能被4整除且不能被100整除是闰年 //2. 能被四百整除是闰年 int main() { int year = 0; for (year = 1000; year <= 2000; year++) { if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { printf("%d ", year); } } return 0; }
接下来改写成函数:
函数的功能一定要实现的足够单一,足够简单。
也就是高内聚,低耦合。
4.3.3 整型有序数组的二分查找
二分查找在我的C语言专栏中一些循环的练习代码中已经详细讲过,所以不再赘述,此处只介绍转换成函数的写法
下面是运行结果:
#define _CRT_SECURE_NO_WARNINGS 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 }; int k = 7; int sz = sizeof(arr) / sizeof(arr[0]); int ret = binary_search(arr, k, sz); //找到了,打印下标 //没找到,打印-1 if (ret == -1) { printf("找不到\n"); } else { printf("找到了,下标是:%d\n", ret); } return 0; }
形式参数和实际参数的名字可以相同也可以不同。
数组传参时传的是数组名。
数组传参时传的是首元素的地址就,而不是整个数组,所以在函数内部计算一个函数参数部分的数组元素个数是不靠谱的。
把整个数组传过去创建一份临时拷贝浪费空间,函数通过首元素地址就可以找到原来的数组,因为数组在内存中是连续存放的,所以只需要有数组首元素的地址,整个数组都可以找到。如果把计算数组大小这部分放到函数里,计算的是指针变量的大小,而不是数组元素的数量。
布尔类型
C99增加了一种内置类型,bool类型,用来判断真假。
#include<stdbool.h>
bool is_prime(int n)
{
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
{
return false;
}
}
return true;
}
bool类型的大小是一个字节。
4.4.4 每调用一次函数,将num的值增加1
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Add(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
//改变了num的值,传地址
Add(&num);
printf("%d\n", num);//1
Add(&num);
printf("%d\n", num);//2
return 0;
}