函数定义
函数就是一段可以重复调用的、功能相对独立完整的程序段。
//一个只完成加法的自定义函数自定义函数
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 10;
int b = 20;
int sum = Add(a,b);
printf("%d\n",sum);
return 0;
}
函数的分为两类:
- 库函数
- 自定义函数
库函数
像打印函数(printf)、字符串的拷贝函数(strcpy)、计算n的k次方的运算函数(pow),这样每个程序员在开发中都可能频繁用到的编译器自带的函数,叫库函数。库函数的发明是为了支持可移植性和提高程序的效率,方便开发者进行软件开发。
简单的总结,C语言常用的库函数都有:
- IO函数
- 字符串操作函数
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
使用库函数,必须包含#include对应的头文件。库函数的学习必须对应文档来学习。
学习strcpy函数
strcpy - string copy - 字符串拷贝
看完cplusplus网站上关于strcpy的解释了解到,这个库函数的用途是:把含有’\0’结束符的字符串复制到另一个地址空间,返回值的类型为char*。
题目:把内容为“bit”的字符串arr1拷贝到长度为20内容为“######”的字符串arr2中,并打印结果
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "bit";
char arr2[20] = "######";
strcpy(arr2, arr1);
printf("%s\n", arr2);
return 0;
}
运行结果
为什么只把“bit”拷贝过去,“#”去那里了呢,为什么不打印呢?
打开监视观察两个字符串的变化
看到\0也被拷贝过来了,不光印证了函数解释里的含有’\0’结束符的字符串的意思是复制字符串时连带\0一起复制,还印证了对于printf函数打印字符串时,遇到\0就停止打印,所以只输出了“bit”。
学习memset函数
memset - menory set - 内存设置
看过解释,这个库函数的用途是:将ptr所指向的内存块的前num个字节设置为指定的值(unsigned char)。
题目:将hello world前五个字符变成“*”
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "hello world";
memset(arr, '*', 5);
printf("%s\n",arr);
return 0;
}
运行结果
自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计的。
函数的组成:
ret_type fun_name(para1, * )
{
statenebt; //语句项
}
//ret_type 返回类型
//fun_name 函数名
//para1 函数参数
题目:写一个函数可以找出两个整数中的最大值
这道题重点思考函数传参和返回值的过程
//定义函数
int get_max(int x, int y) //注释2 注释4
{
if (x > y) //注释3
return x;
else
return y;
}
int main()
{
int a = 10;
int b = 20;
//函数的使用
int max = get_max(a, b); //注释1
printf("max = %d\n", max);
return 0;
}
注释1:在主函数里封装一个函数get_max(),并将a和b两个参数传过去,get_max(a, b)这个操作叫传参,a和b被称为实参。
注释2:主函数上方定义get_max(int x, int y)函数,括号里写上int x和int y是为了让get_max函数有能力接收主函数传来的参数,x和y被称为形参。
注释3:函数里面用if else来比较大小,返回x,y中较大的值。
注释4:函数返回的都是整形所以在get_max(int x, int y)前面定义返回类型是int。
运行结果
当然get_max函数也可简化成
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
题目:写一个函数可以交换两个整型变量的内容
这道题目重点思考传值与传址的不同
void Swap1(int x, int y) //void表示函数没有返回值,函数前面那个表示函数的返回值类型
{
int tmp = 0;
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);
return 0;
}
运行结果
并没有交换,为什么呢?
打开监视观察变量a和b的值,还有a和b的地址,以及Swap1函数里面的那两个形参x,y以及它们的地址也要观察
能看到a、b的地址和x、y的地址并不一样,说明它们各自有独立的空间,所以形参其实是实参的一份临时拷贝,我们把临时拷贝x和y交换了。并没有影响到主函数里的实参a和b。
这种主函数将实参的值传递到自定义函数的形参的行为叫值传递简称传值。
在值传递中x和y接受了a和b的值,但x和y有自己独立的空间,它们和外面的a和b没有直接的联系,并没有影响到a和b。要想对它们产生影响应该将a和b的地址交换。
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起真的联系,也就是函数内部可以直接操作函数外部的变量。
传址用到了指针的知识点,指针变量是专门用来存放地址的。
指针变量的定义和解引用操作:
int main()
{
int a = 10;
int *pa = &a; //pa指针变量
*pa = 20; //解引用操作
printf("%d\n",a);
return 0;
}
解引用
“ * ” 的作用是引用指针指向的变量值,引用其实就是引用该变量的地址,“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。也就是说,解引用是返回内存地址中对应的对象。
运行结果
使用传址调用改写后的正确代码
void Swap2(int *pa,int *pb)
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap2(&a, &b);
printf("a=%d b=%d\n", a, b);
return 0;
}
运行结果
函数的参数
实际参数(实参)
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有函数被调用的过程才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式此参数只在函数中有效。
int get_max(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int a = 10;
//实参可以是:常量、变量、表达式、函数等
int max1 = get_max(a, 100); //实参是变量与常量
int max2 = get_max(100, 300+1); //实参是常量与表达式
int max3 = get_max(100, get_max(3,7)); //实参是常量与函数
printf("max = %d\n", max1);
printf("max = %d\n", max2);
printf("max = %d\n", max3);
return 0;
}
运行结果
函数的调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方法。
这种传参方式可以让函数和函数外的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
练习
练习1:写出一个函数可以判断一个数是不是素数,是素数返回1,不是素数返回0。
#include <math.h>
int is_prime(int n)
{
int j;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
return 0;
}
return 1;
}
int main()
{
//打印100—200之间的素数
int i = 0;
for (i = 100; i <= 200; i++)
{
//判断i是否为素数
if (is_prime(i) == 1)
printf("%d",i);
}
}
练习2:判断一年是否是闰年。
int is_leap_year(int y)
{
if ((y % 4 == 0 && 100 != 0)|| (y % 400 == 0))
return 1;
else
return 0;
}
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
if (1 == is_leap_year(year)) //判断是否为闰年
{
printf("%d", year);
}
}
return 0
}
这里编写的is_leap_year()函数只返回1和0,而不是直接打印“是闰年”或者“不是闰年”,这是一种很简洁的写法,函数的编写要具有可复用性。
练习3:实现一个整型数组的二分查找,在一个有序数组中查找具体的某个数,如果找到了返回这个数的下标,找不到的返回-1。
int binary_search(int arr[], int k,int sz)
{
//算法实现
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = (left + right) / 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,10 };
int k = 7;
int sz = sizeof(arr) / sizeof(arr[0]);
int ret = binary_search(arr, k, sz); //传递过去的是数组arr首元素的地址
if (ret == -1)
{
printf("找不到指定的数字\n");
}
else
{
printf("找到了,下标是:%d\n",ret);
}
return 0;
}
练习4:写一个函数,每调用一个这个函数,就会将num的值增加1。
int Add(int *p)
{
(*p)++; //p是地址,*p是num的间接引用
}
int main()
{
int num = 0;
Add(&num);
printf("num = %d\n", num);
Add(&num);
printf("num = %d\n", num);
Add(&num);
printf("num = %d\n", num);
return 0;
}
这道题要注意“++”的优先级比“*”高,所以千万不要把“(*p)++”写成“p++”。
函数的嵌套调用和链式访问
函数和函数之间可以有机的组合的。
嵌套调用
嵌套调用就是某个函数调用另外一个函数(即函数嵌套允许在一个函数中调用另外一个函数)。
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for (i = 0; i < 3; i++)
{
new_line();
}
}
int main( )
{
three_line();
return 0;
}
链式访问
链式访问就是把一个函数的返回值作为另一个函数的参数
求字符串的长度
int main()
{
int len = 0;
len = strlen("abc");
printf("%d\n",len);
}
如果使用链式访问的思想改写代码如下
int main()
{
int len = 0;
printf("%d\n",strlen("abc"));
}
题目:下面代码结果是什么?
int main()
{
printf("%d",printf("%d",printf("%d",43)));
return 0;
}
运行结果
为什么是4321呢,最里层的printf打印43后返回一个值,被中间的printf调用打印,然后返回一个值被最外层的函数打印。那么printf的返回值是什么呢?
printf函数返回的是打印字符的个数。所以最里层打印43返回2被中间的printf打印并返回1,被最外层printf打印。
函数的声明和定义
函数的声明
告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,无关紧要。
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
函数的声明一般要放在头文件中的。
函数定义
函数的定义是指函数的具体实现,交待函数的功能实现。
test.h的内容 放置函数的声明
#ifndef __TEST_H__ //如果没有定义过__TEST_H__
#define __TEST_H__ //结果为真定义__TEST_H__ ,结果为假运行结束
//函数的声明
int Add(int x, int y);
#endif
test.c的内容 放置函数的实现
#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
return x+y;
}
以加法函数为例
为什么函数的声明里面要写“ #ifndef TEST_H 、#define TEST_H 和 #endif ”呢?
我们经常在头文件里写“#include<stdio.h>”来包含一个头文件,编译器在运行#include<stdio.h>时,会把stdio.h里的所有内容都拷贝到我们的代码里。
在一个大型工程里我引一个这样的头文件,别人如果也引用了这样的头文件,这个头文件里的内容就会重复拷贝,造成空间浪费。
为了避免同一个头文件被引用多次,C语言提供了“ #ifndef TEST_H 、#define TEST_H 和 #endif ”。在函数的声明前面写上“ #ifndef TEST_H 和 #define TEST_H ”后面写上“ #endif ”。
下一节函数递归