函数是什么?

在维基百科中对函数的定义:子程序

什么是子程序呢?

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

函数的分类

  1. 库函数
  2. 自定义函数

库函数

当我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到屏幕上。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf

在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy

在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。

像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。

常用的库函数

C语言常见的库函数有:

  • IO函数 (输入输出函数) - printfscanfgetcharputchar
  • 字符串操作函数 - strcmpstrlen
  • 字符操作函数 - toupper(小写转大写)
  • 内存操作函数 - memcpymemcmpmemset
  • 时间/日期函数 - time
  • 数学函数 - sqrtpow
  • 其他库函数

例子:

strcpy

1
2
3
4
5
6
7
8
9
10
11
12
//strcpy() - char *strcpy(char *dest, const char *src) 需要引用头文件 string.h
//dest - 指向用于存储复制内容的目标数组 src - 要复制的字符串
//把 src 所指向的字符串复制到 dest
//如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况
int main()
{
char arr1[20] = { 0 };
char arr2[] = "hello world";
strcpy(arr1,arr2);
printf("%s", arr1); //hello world
return 0;
}

memset

1
2
3
4
5
6
7
8
9
10
11
12
//memset() - void *memset(void *str, int c, size_t n) 需要引用头文件 string.h
//str - 指向要填充的内存块。
//c - 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
//n - 要被设置为该值的字符数。
int main()
{
//把hello变为xxxxx
char arr1[] = "hello world";
memset(arr1, 'x', 5);
printf("%s", arr1); //xxxxx world
return 0;
}

自定义函数

自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是这些都是我们自己来设计

1
2
3
4
ret_type fun_name(para1, * )
{
statement;//语句项
}

ret_type - 返回类型

fun_name - 函数名

para1 - 函数参数

{} - 函数体

注意:如果一个函数不写返回类型,则返回类型默认是int类型

例子:

写一个函数可以找出两个整数的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//写一个函数可以找出两个整数的最大值
int Max(int a,int b)
{
int max = a > b ? a : b;
return max;
}
int main()
{
int a = 0;
int b = 0;
printf("请输入两个整数:");
scanf("%d %d", &a, &b);
//调用函数Max
int max = Max(a, b);
printf("最大值为:%d",max);
return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//写一个函数,可以交换两个整型变量的内容
//函数返回类型写 void 表示这个函数不返回任何值,也不需要返回
void Swap(int x, int y)
{
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);
Swap(a, b);
printf("a = %d,b = %d\n", a, b);
return 0;
}

以上代码是否正确?

当我们运行之后,会发现,他们的值并没有被交换

为什么呢?

通过查看监视我们发现

a和b的地址与x和y的地址并不一样,即x和y有了自己的空间

Swap(a, b)传过去的参数也仅仅只是他们的值

所以我们可以把传过去的参数改为他们的地址,然后通过指针变量拿到他们的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Swap(int* pa, int* pb)
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = 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;
}

这样他就可以进行数值的交换了

函数的参数

实际参数(实参)

真实传给函数的参数,叫实参。

实参可以是:常量、变量、表达式、函数等。

无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。

形式参数(形参):

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单 元),所以叫形式参数。

形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。

用一下上面交换两个整型变量的例子来说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Swap1(int x, int y) //函数定义
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int* pa, int* pb) //函数定义
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap1(a, b); //函数调用
printf("a = %d,b = %d\n", a, b);
Swap2(&a, &b); //函数调用
printf("a = %d,b = %d\n", a, b);
return 0;
}

主函数内的Swap1(a, b)Swap2(&a, &b)中的ab&a&b是有真实传给函数的参数,所以是实参。

而在Swap1(int x, int y)Swap2(int* pa, int* pb)他们后面的函数参数xbpapb只有在函数被调用的过程中才实例化,所以是形参。

注:形参只有再函数被调用的时候才会分配空间,且形参只在函数内有效

前面我们说到,swap1(int x, int y)在被调用时,它的形参xy和传过去的实参ab的地址并不同

xy拥有自己的空间,同时拥有了和实参一模一样的内容

我们可以认为:形参实例化之后其实相当于实参的一份临时拷贝,改变形参,并不能改变实参

函数的调用

传值调用:

函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。

传址调用:

传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。

这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void Swap1(int x, int y) //函数定义
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
void Swap2(int* pa, int* pb) //函数定义
{
int tmp = 0;
tmp = *pa;
*pa = *pb;
*pb = tmp;
}
int main()
{
int a = 10;
int b = 20;
Swap1(a, b); //函数调用
printf("a = %d,b = %d\n", a, b);
Swap2(&a, &b); //函数调用
printf("a = %d,b = %d\n", a, b);
return 0;
}

用上面交换两个整型变量的例子来说

Swap1(a, b)就是传值调用,而Swap2(&a, &b)就是传址调用

练习

  1. 写一个函数可以判断一个数是不是素数。
  2. 写一个函数判断一年是不是闰年。
  3. 写一个函数,实现一个整形有序数组的二分查找。
  4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。

题目一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//写一个函数可以判断一个数是不是素数。
#include <math.h>
int prime(int num)
{
int i = 0;
int flag = 1;
for ( i = 2; i <=sqrt(num); i++)
{
if (num%i == 0) //num%i == 0 num不是素数
{
flag = 0;
}
}
return flag;
}
int main()
{
int a = 0;
printf("请输入一个数:\n");
scanf("%d",&a);
if (prime(a) == 1)
{
printf("%d 是素数\n",a);
}
else
{
printf("%d 不是素数\n", a);
}
return 0;
}

题目二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 写一个函数判断一年是不是闰年。
int year(int num)
{
if ((num % 4 == 0 && num % 100 != 0) || (num % 400 == 0))
{
return 1;
}
return 0;
//也可以直接返回,不需要使用if语句
//return ((num % 4 == 0 && num % 100 != 0) || (num % 400 == 0));
}

int main()
{
int a = 0;
printf("请输入一个年份:\n");
scanf("%d",&a);
if (year(a) == 1)
{
printf("%d 是闰年\n", a);
}
else
{
printf("%d 不是闰年\n", a);
}
return 0;
}

题目三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//写一个函数,实现一个整形有序数组的二分查找。
int search(int arr[], int a,int s)
{
int left = 0;
int right = s - 1;
while (left<=right)
{
int mid = (left + right) / 2;
if (a > arr[mid])
{
left = mid + 1;
}
else if (a < arr[mid])
{
right = mid - 1;
}
else
{
return mid;
}
}
return -1;
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int a = 0;
printf("请输入要查找的数字:\n");
scanf("%d",&a);
//注意:arr数组传参,实际传递的不是数组本身
//只传过去了数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int num = search(arr, a, sz);
if (num != -1)
{
printf("%d在数组的位置下标为%d\n",a,num);
}
else
{
printf("找不到\n", a, num);
}
return 0;
}

题目四:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//写一个函数,每调用一次这个函数,就会将 num 的值增加1。
void Add(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
return 0;
}