1. 函数是什么
数学中我们常见到函数的概念。但是你了解C语言中的函数吗? 维基百科中对函数的定义:子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
C语言中函数的分类:
- 库函数
- 自定义函数
2. 库函数
为什么会有库函数?
- 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格
式打印到屏幕上(printf)。 - 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
- 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,
为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
简单来说如果么有库函数意味着很多代码都需要程序员个人来写出,就会有各种各样的代码会出现来实现他们想要的功能,所以说库函数帮助程序员解决了很多的麻烦。
那如何学习库函数呢?
这里我们简单的看看:http://www.cplusplus.com/reference/
可以看得到很多的库函数在图上有详细的说明,点进去可以有一些解释和例子,非常适合程序员进行查询以及解惑。
而C语言常用的库函数就有以下这些:
- IO函数 : input/output, printf, scanf, getchar, putchar…
- 字符串操作函数 : strlen, strcmp, strcpy, strcat…
- 字符操作函数 : tolower, toupper…
- 内存操作函数 : memcpy, memset, memmove, memcmp…
- 时间/日期函数 : time…
- 数学函数 : sqrt, abs, fabs, pow…
- 其他库函数
这么多的库函数需要全部记住吗,其实不然,大多数程序员都是需要用的时候去查这些函数的用法,所以也无需太过于压迫自己全部背完。但是库函数必须知道的一个秘密就是:使用库函数,必须包含 #include 对应的头文件。
有些人可能觉得自己的英语不好怎么办,当今的翻译软件已经做的不错,我们也可以利用翻译软件来帮我们解决问题。
接下来我们来学习几个库函数的使用:
来看我如何使用文档来学习库函数。
比如说strcpy
上面的解释非常的简洁容易理解。
char * strcpy ( char * destination, const char * source )
source作为一个地址指向了一个字符串,而destination作为一个地址应该指向一个空间。所以意思是把source指向的地址放到destination所指向的空间里面
而且笛一段解释中也说明了\0作为结束字符也会被拷贝过去。
接下来我们再来看parameters形参destination所指向的那里的目标数组将会被覆盖。
source解释的是要被拷贝的字符串。
而返回的值是destination目标空间的起始地址。
接下来我们来用代码来实践理解。
int main()
{
char arr1[20] = { 0 };//目标空间
char arr2[] = "hello";//注意目标空间的数组空间一定要足够,不然没法拷贝
strcpy(arr1, arr2);//第一个放目标空间
printf("%s\n", arr1);//打印的时候只要放目标空间的起始地址就可以了
return 0;
}
打印结果:注意看文档的strcpy的头文件 #include <stdio.h>
看了这些你应该就知道如何使用库函数了
接下来我们再把视线看看\0
提示:之前的代码的初始空间我们设置的是0,但是\0本身的ASCII码值就是0所以我们无法判断到底它读取了\0没有。
而有个方法可以很好地验证那就是把目标空间全部换成字符形式存放。
int main()
{
char arr1[20] = "xxxxxxxxxxx";//目标空间
char arr2[] = "hello";//注意目标空间的数组空间一定要足够,不然没法拷贝
strcpy(arr1, arr2);//第一个放目标空间
printf("%s\n", arr1);//打印的时候只要放目标空间的起始地址就可以了
return 0;
}
我们来看看监视,只要看第五个是否为0就可知道\0是否被读取进去了
通过这张图就可以知道序号五确确实实被读取了\0, 成功的拷入。
接下来读取完毕之后返回的就是目标空间,
int main()
{
char arr1[20] = "xxxxxxxxxxx";//目标空间
char arr2[] = "hello";//注意目标空间的数组空间一定要足够,不然没法拷贝
char* ret = strcpy(arr1, arr2);
printf("%s\n", arr1);//打印的时候只要放目标空间的起始地址就可以了
printf("%s\n", ret);
//因为返回的是目标空间所以ret存放的也是其初始地址,而arr1本身就是初始地址,
//这两个的结果是一样的
return 0;
}
注意
char arr2[5] = "hello";
//这样的写法是错误的本身他就要存放五个字符外加一个\0
//如果这样设置就会导致溢出
\0就是结束标志,遇到\0就会停止后面的打印。
下一个函数memset翻译过来是设置内存
void * memset ( void * ptr, int value, size_t num );
意思是:
把ptr指向的空间的前num字节的内容,填充成我们想要的value,填充的时候是以字节为单位。大概就是这个意思
来个代码来看看如何使用?
int main()
{
char arr[] = "hello google";//然后我们想要把前面的hello变成五个x
//改成xxxxx google
//所以我们如何使用这个函数?
char *ret = memset(arr,'x', 5);
//我们要改变的那块空间的起始地址是不是就是我们arr数组名吗;
//数组名表示的就是首元素地址所以第一个参数就是arr
//第二个参数就是我们的value也就是我们设置的数,我们想设置的数是字符x
//第三个参数就是设置多少个字节,因为我们想要设置前五个所以写5就行了
//这个函数最终返回的是ptr也就是其起始地址
//让后我们用ret来接收其地址
printf("%s\n", ret);
return 0;
}
这个5也可以是变量。
int main()
{
char arr[] = "hello google";//然后我们想要把前面的hello变成五个x
//改成xxxxx google
//所以我们如何使用这个函数?
int n = 5;
char *ret = memset(arr,'x', n);
//我们要改变的那块空间的起始地址是不是就是我们arr数组名吗;
//数组名表示的就是首元素地址所以第一个参数就是arr
//第二个参数就是我们的value也就是我们设置的数,我们想设置的数是字符x
//第三个参数就是设置多少个字节,因为我们想要设置前五个所以写5就行了
//这个函数最终返回的是ptr也就是其起始地址
//让后我们用ret来接收其地址
printf("%s\n", ret);
return 0;
}
注意:设置char *ret = memset(arr,‘x’, 5);的时候可能会报错,所以我们需要强制转换一下。char ret = (char)memset(arr,‘x’, 5);
附加内容:
int main()
{
int arr[10] = { 0 };
memset(arr, 1, 5*sizeof(int));//这个代码的效果是什么?
//这样设置会出现错误
//首先memset是以字节为单位来改变那你想改变的值,
//以上这样设置就会出现以每四个字节是不可能变为1.过为分散的设置就会出现问题
//这样分散的设置就会导致出现严重错误
//原先的函数就是把以arr为起始位置向后初始化后面这么多字节,把每个字节设置为自己想要的值。
return 0;
}
3. 自定义函数
用函数求两个数最大值
int get_max(int x, int y)
{
return (x > y ? x : y);
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
int max = get_max(a, b);
printf("max=%d\n", max);
return 0;
}
写一个函数可以交换两个整形变量的内容。
void swap(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);
swap(a, b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
有些人可能会这样写,但会发现打印出来的结果和理想状态下不一样
为什么会出现这样的情况?
我们来调试一下看看
x y 和a b 的地址完全不相同, 而我们知道内存地址不相同那变量一定不是相同的,所以可以判断xy在ab上空间上是完全独立的
尽管x和y互相交换,但并不影响ab的数目变化。
也就是说实参a和b,传给形参x, y的时候,形参将是实参的一份临时拷贝。
改变形参变量x,y,是不会影响实参a和b的。
所以如何解决这个问题?
int main()
{
int a = 10;
int* pa = &a;
//pa就能找到a
*pa = 20;
printf("%d\n", a);
return 0;
}
看到这份代码你有什么想法吗?
答案也就是我下面那份代码了
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int *px, int *py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap1(a, b);
printf("Swap1::a = %d b = %d\n", a, b);
Swap2(&a, &b);//地址传到函数进行储存,也就不会产生新的地址来。
printf("Swap2::a = %d b = %d\n", a, b);
return 0;
}
void swap(int* px, int* py)
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 20;
swap(&a, &b);
printf("交换后:a = %d b = %d", a, b);
return 0;
}
打印结果
4. 函数参数
实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配
内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在
函数中有效。
上面Swap1和Swap2函数中的参数 x,y,px,py 都是形式参数。在main函数中传给Swap1的a,
b和传给Swap2函数的&a,&b是实际参数
而且swap1的传参方法叫传值,而swap2叫传址。
5. 函数调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量。
写函数的时候是建议单功能,独立的函数,这样也好被其他人运用。
练习
- 写一个函数可以判断一个数是不是素数。
#include <math.h>
int is_prime(int n)
{
//2~n-1;
//2~sqrt(n)
int j = 0;
for (j = 2; j <= sqrt(n); j++)
{
if (n % j == 0)
return 0;
}
return 1;
}
int main()
{
int i = 0;
for (i = 100; i <= 200; i++)
{
if (is_prime(i) == 1)
{
printf("%d", i);
}
}
return 0;
}
- 写一个函数判断一年是不是闰年。
int is_leap_year(int y)
{
if ((y % 400 == 0) || (y % 4 == 0 && y % 100 != 0))
{
return 1;
}
else
return 0;
}
int main()
{
int y = 0;
for (y = 1000; y <= 2000; y++)
{
//判断y是否闰年
//闰年返回一,不是则是0
if (is_leap_year(y) != 0)
printf("%d ", y);
}
return 0;
}
- 写一个函数,实现一个整形有序数组的二分查找。
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int m = (left + right) / 2;
if (arr[m] < k)
{
left = m + 1;
}
else if (arr[m] > k)
{
right = m - 1;
}
else
return m;
}
return -1;
}
int main()
{
int arr[10] = { 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);
//找到了返回下标
//找不到返回-1
if (ret == -1)
{
printf("zhaobudao\n");
}
else
printf("zhaodaole: %d\n", ret);
return 0;
}
int binary_search(int arr[], int k, int left, int right)
{
while (left <= right)
{
int m = (left + right) / 2;
if (arr[m] < k)
{
left = m + 1;
}
else if (arr[m] > k)
{
right = m - 1;
}
else
return m;
}
return -1;
}
int main()
{
int arr[10] = { 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, 5, 9);//这个地方也可以设置范围,使得函数的灵活性变得更高
//找到了返回下标
//找不到返回-1
if (ret == -1)
{
printf("zhaobudao\n");
}
else
printf("zhaodaole");
return 0;
}
- 写一个函数,每调用一次这个函数,就会将num的值增加1。
void Add(int* p)
{
*p = *p + 1;
}
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
Add(&num);
printf("%d\n", num);//4
return 0;
}
方法二
int Add(int n)
{
return n + 1;
}
int main()
{
int n = 0;
n = Add(n);
printf("%d\n", n);//1
n = Add(n);
printf("%d\n", n);//2
n = Add(n);
printf("%d\n", n);//3
n = Add(n);
printf("%d\n", n);//4
return 0;
}
函数的嵌套调用和链式访问
函数和函数之间可以有机的组合的。
嵌套调用
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 = strlen("abc");
printf("%d\n", len);
printf("%d\n", strlen("abc"));
char arr1[20] = "xxxxxxxx";
char arr2[20] = "abc";
strcpy(arr1, arr2);
printf("%s\n", arr1);
printf("%s\n", strcpy(arr1, arr2));
return 0;
}
附加题:
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
结果是4321