C语言函数超详细讲解上篇
前言
本文主要学习函数的相关内容。
1、函数是什么?
维基百科中对函数的定义:子程序
- 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
- 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2、C语言中函数的分类
- 库函数
- 自定义函数
2.1 库函数
- 在学习C语言编程时,总是在一个代码编写完成之后,想把这个结果打印到屏幕上看看。这时使用一个功能:将信息按照一定的格式打印到屏幕上(printf)。
- 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)
- 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)
上面的函数不用自己编写,直接可以调用。为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 字符操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
使用库函数,必须包含 #include 对应的头文件。
2.1.1 如何学会使用库函数
推荐查询工具官网:
MSDN(Microsoft Developer Network)
http://en.cppreference.com(英文版)
http://zh.cppreference.com(中文版)
2.1.2 自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数。这些都是我们自己来设计,函数的组成:
ret_type fun_name(para1, * ) { statement;//语句项 } //ret_type 返回类型 //fun_name 函数名 //para1 函数参数
//举例:写一个函数可以找出两个整数中的最大值 //get_max函数的设计 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("max = %d\n", max); return 0; }
3、函数的参数
3.1 实际参数(实参)
- 真实传给函数的参数,叫实参
- 实参可以是:常量、变量、表达式、函数等
- 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参
3.2 形式参数(形参)
- 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数
- 形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效
//举例2:写一个函数可以交换两个整形变量的内容 //实现成函数,但是不能完成任务 int exchange1(int x, int y) {//当实参传给形参时候,形参是实参的一份临时拷贝, //对形参的修改不会影响实参 int temp = x; x = y; y = temp; } //正确的版本 int exchange2(int* pa, int* pb)//定义指针,接收地址 { int temp = *pa; *pa = *pb; *pb = temp; } int main() { int a = 3; int b = 5; exchange1(a, b);//传参是值 printf("exchange1::a = %d b = %d\n", a, b);//交换前 exchange2(&a, &b);//传参是地址 printf("exchange2::a = %d b = %d\n", a, b);//交换后的 //传入地址,自定义的形参和实参联系更加紧密,能改变地址存储的数值 //此时,形参的地址与实参的地址是一样的 return 0; }
上面代码中, exchange1和 exchange2函数中的参数 x,y,pa,pb 都是形式参数。在main函数中传给 exchange1的 a ,b 和传给 exchange2函数的 &a ,&b是实际参数。
运行结果如下所示,exchange1并没有起到预想的交换数值的作用,exchange2可以。
通过监视变量发现:
名称 | 值 | 意义 |
---|---|---|
a | 5 | 数值为5 |
&a | 0x113f8e4 | 数值a的地址 |
x | 5 | 形参x数值为5 |
&x | 0x113f800 | 形参x的地址 |
pa | 0x113f8e4 | 形参pa数值为a的地址 |
- 变量a在内存中开辟了空间,地址是0x113f8e4
- 函数exchange1将实参a传递给形参x,x的数值也是5,但此时形参x另外重新再内存中开辟了空间,地址 0x113f800,形参和实参的地址是不一样的
- 函数exchange2将实参a传递给形参pa,pa的值是变量a的地址0x113f8e4,这个地址里面存放变量a的数值5。
这里可以看到 exchange1函数在调用的时候, x , y 拥有自己的空间,同时拥有了和实参一模一样的内容。所以我们可以简单的认为:
形参实例化之后其实相当于实参的一份临时拷贝。形参x、y的值发生交换,但是不影响实参a、b的值。一般性的只是使用数值大小,利用形参传值就可以了,传值表明,形参和实参的关系肤浅,仅限于表面数值的拷贝。
如果需要对主函数的实参值进行操作,比如交换,此时形参需传地址,功能更为强大。传地址表明,形参和实参的关系更深一步,直接可以通过地址修改实参的数值。
4、函数的调用
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2 传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量
4.3 练习
4.3.1 判断一个数是不是素数
写一个函数可以判断一个数是不是素数
//返回1 表示是素数 //返回-1 表示不是素数 //写法1 2-n-1 试除法 int is_sushu(int a) {//素数就是除1和自身外,不能被其他数整除 for (int i = 2; i < a-1; i++) {//用2-n-1的数一一试除 if (a%i==0) {//若有能除的,直接返回-1,跳出后面的循环了 return -1;//表明不是素数, } }//break退出循环会调到这里 //return返回时直接退出这个函数了,返回到主函数了,级别更高 return 1; } int main() { int a = 0; scanf("%d", &a); int num = is_sushu(a); if (num==1) { printf("%d: 是素数", a); } else { printf("%d:不是素数", a); } return 0; } //写法2 2-sqrt(n) 试除法,速度更快 #include<math.h> int is_sushu(int num) { for (int i = 2; i <=sqrt(num); i++) { if (num%i == 0) { return -1; } } return 1; } int main() { int num = 0; for (int num = 100; num < 201; num++) { if ((is_sushu(num)) == 1) { printf("%d ", num); } } return 0; }
4.3.2 判断一年是不是闰年
写一个函数判断一年是不是闰年
//闰年时是4的倍数且不是100的倍数,或者是400的倍数 int is_run_nian(int y) { if (((num%4 == 0)&& (num%100 != 0))|| (num % 400 == 0)) { return 1; } //return ((num%4 == 0)&& (num%100 != 0))|| (num % 400 == 0);//简写 } int main() { int num = 0; for (int num = 1000; num <= 2000; num++) { if ((is_run_nian(num)) == 1) { printf("%d ", num); } } return 0; }
4.3.3 二分查找
写一个函数,实现一个整形有序数组的二分查找
//找到了就返回下标 //找不到返回-1 int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int k = 0; int i = 0; int sz = sizeof(arr) / sizeof(arr[0]); int left = 0;//左下标 int right = sz - 1;//右下标 scanf("%d", &k); while (left<=right) {//每次left right mid都要更新 int mid = left + (right - left) / 2; if (arr[mid] > k) {//寻找的数值在左半边,所以左不动,右边动 right = mid - 1; } else if (arr[mid] < k) {//寻找的数值在右半边,所以左边动,右不动 left = mid + 1; } else { printf("找到了:%d ", mid); break; } } if (left>right)//循环结束了 { printf("找不到"); } return 0; }
可以将二分法进行改进,写成独立的函数,让函数功能单一化:
int is_erfen(int arr[],int k, int sz) {//数组传递参数进来就是数组首元素的地址,并不是整个数组 //地址的大小是4或8个字节,前面有讲到过的 //x86平台中,地址占4个字节,sizeof(arr)是4, 而不是40了 //int sz = sizeof(arr) / sizeof(arr[0]);//sz要在主函数计算, int left = 0; int right = sz - 1; while (left <= right) {//每次left right mid都要更新 int mid = left + (right - left) / 2; if (arr[mid] > k) { right = mid - 1; } else if (arr[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 k = 0; int i = 0; scanf("%d", &k); //主函数中 按数组名找到整个数组,求出占用字节是40 int sz = sizeof(arr) / sizeof(arr[0]); int num = is_erfen(arr, k, sz); if (num ==-1) { printf("找不到"); } else { printf("找到了:%d ", num); } return 0; }
在上面的函数中需要注意,数组的大小必须在主函数中计算,下面代码可说明在主函数中和其他函数中求数组长度的区别:
int erfen(int arr[]) {//数组传递进来,只能是首地址,这个arr数组里只有{1},长度为4 int sz1 = sizeof(arr) / sizeof(arr[0]);,//4/4 = 1求出只有1个元素 printf("erfen求数组大小,sz1:%d\n ", sz1); } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //数组长度为40 int sz = sizeof(arr) / sizeof(arr[0]);//40/4 = 10 求出数组10个元素 printf("main中求数组大小,sz:%d\n ", sz); erfen(arr); return 0; }
按F10进入调试界面,按F11,可观察到:
- 主函数中sizeof(arr)长度是40,数组中包含10个元素。
- 而在函数erfen中,sizeof(arr)长度是4,数组中包含1个元素,就是首元素{ 1 }。
由此,可知道,参数里传递数组时,实际传递的数组就是数组的地址,也是数组首元素的地址。数组名传递进来,只能是首地址,这个arr数组里只有首元素{1},长度为4。整个数组是传递不了的。因此,必须在主函数里求取数组的长度。
因为传递的参数是地址,所以erfen中也可以定义指针来接受数组:
//int erfen(int arr[])//接受数组,只能接受一个首元素 int erfen(int* arr) {//数组传递进来,只能是首地址,这个arr数组里只有{1},长度为4 int sz1 = sizeof(arr) / sizeof(arr[0]);,//4/4 = 1求出只有1个元素 printf("erfen求数组大小,sz1:%d\n ", sz1); } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; //数组长度为40 int sz = sizeof(arr) / sizeof(arr[0]);//40/4 = 10 求出数组10个元素 printf("main中求数组大小,sz:%d\n ", sz); erfen(arr); return 0; }
运行结果一样
4.3.4 数值自增增加1
写一个函数用一次这个函数,就会将 num 的值增加1
int add(int* p) { (*p)++;//参数传地址,可以操作实参的值 } int main() { int a = 10; add(&a);//传地址 printf("%d\n", a); add(&a); printf("%d\n", a); add(&a); printf("%d\n", a); add(&a); printf("%d\n", a); return 0; }
5、函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
5.1 嵌套调用
//套娃 void newline() { printf("hehe\n"); } void threeline() { int i = 0; for (int i = 0; i < 3; i++) { newline(); } } int main() { threeline(); newline; return 0; }
5.2 链式访问
把一个函数的返回值作为另外一个函数的参数。
int main() { char arr[20] = "hello"; int ret = strlen(strcat(arr, "bit")); printf("%d\n", ret); return 0; } int main() { printf("%d", printf("%d", printf("%d", 43)));//输出4321, //前面是打印43 2是返回两个字符(因为4 、3是两个字符) //1是返回1个字符(2是1个字符) return 0; }
输出结果见下图:
总结
本文主要介绍了函数相关的知识,下一篇接着介绍函数的内容。
上一篇:C++四种case的详细介绍小结
栏 目:C代码
下一篇:C++实现SLR文法分析法
本文标题:C语言函数超详细讲解上篇
本文地址:http://www.codeinn.net/misctech/220962.html