主要掌握函数的基本使用和递归
[toc]

函数是什么

==子程序==,负责完成某项特定任务,具备相对独立性。

分类

  • 库函数
  • 自定义函数

库函数

方便调用频繁大量使用的功能,推动标准化、模块化

printf 打印
strcpy 字符串拷贝
pow 计算n的k次方

MSDN(Microsoft Developer Network)
c语言文档网站cpp

常用的库函数有

  • IO函数
    • printf scanf getchar putchar
  • 字符串操作函数
    • strlen strcmp
  • 字符操作函数
    • toupper小写转大写
  • 内存操作函数
    • memcpy memcmp memset
  • 时间日期函数
    • time
  • 数学函数
    • sqrt pow
  • 其他库函数

以下例子:
strcpy
拷贝字符串

1
2
char * strcpy (char * destination, const char * source)
返回类型 (参数 )
1
2
3
4
5
6
7
8
9
10
11
12
13
#define _CRT_SECURE_NO_WARNINGS 1
//放在代码第一行,取消报错显示
#include <stdio.h>
#include <string.h>

int main()
{
char arr1[20] = { 0 };
char arr2[] = "Hello";
strcpy(arr1, arr2);
printf("%s", arr1); //打印arr1这个字符串,%s是以字符串的格式打印
return 0;
}

memset
内存设置memory set

1
void * memset (void * ptr, int value, size_t num)//size_t 无法整形的类型

ptr指向的所指向的内存前num的内容全部设置成value值

1
2
3
4
5
6
7
int main()
{
char arr[] = "Hello bit";
memset(arr, 'x', 5);
printf("%s\n", arr);
return 0;
}

效果是 把arr前5个字符设置成x

自定义函数

一些案例
基本组成形式

1
2
3
4
5
6
7
ret_type fun_name(paral, *)
{
statement; //语句项
}
ret_type 返回类型
fun_name 函数名
paral 函数参数

获取较大值

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
//自定义get_max函数
int get_math(int x, int y)
{
int z = 0;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;//返回z这个较大值
}

int main()
{
int a = 10;
int b = 20;

int max = get_max(a, b);//调用get_max函数

printf("max= %d\n", max);
return 0;
}

交换2个整型变量的值

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
void Swap1(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
//这样写的结果是在调用Swap时,实质上只调换了x和y的地址,应该使用指针
}

//函数返回类型的地方写void表示这个函数不返回任何值,也不需要返回
void Swap2(int* pa, int* pb) //由于交换的部分不在main函数内部,需要通过调用pa、pb实现远程对main函数里的a、b的修改
{
int z = 0;
z = *pa; //借助*pa把a的值放到z里
*pa = *pb; //把b的值放进a里
*pb = z; //把z的值放进b里
}

int main()
{
int a = 10;
//int* pa = &a; //pa是一个指针变量,*pa就找到了a,因此对a、b的修改应该对pa、pb进行
int b = 20;
//int* pb = &b;

printf("交换前:a=%d b=%d\n", a, b);
Swap1(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}

函数的参数

实参

真实传递给函数的参数,可以是常量、变量、表达式。函数调用时,必须要有确定的值,以便把这些值传递给形参

例如调用函数时int max = get_max(2 + 5, get_max(4, 7))传递给函数的,是确定的、真实存在的值

形参

函数名后括号内的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元)所以叫形式参数。形式参数在函数调用完以后就自动销毁,因此形参只在函数中有效(生命周期)。

例如定义一个函数,函数名后面的就是形参,下面int x就是一个形参

1
2
3
4
5
6
7
void Swap1(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}

形参实例化后,是实参的一份临时拷贝

函数的调用

传值调用

实参形参占有不同的内存块,对形参的修改不会影响实参,例如上面的Swap1

传址调用

  • 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。例如上面的Swap2
  • 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。

练习

  1. 打印100~200之间的素数
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
int is_prime(int n)//返回类型是整形
{
//2到n-1之间的数字
int j = 0;
for (j = 2; j < n; j++)
{
if (n % j == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
int count = 0;
for (i = 100; i <= 200; i++)
{
if (is_prime(i) == 1)//判断i是不是素数
{
count++;
printf("%d ", i);
}
}
printf("%d ", count);
return 0;
}

注意到这个地方把判断打印分成两个部分来完成。
实际写代码时,这是体现了模块化的思想。


  1. 写一个函数,判断是不是闰年
    写法一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int is_leap_year(int x)
{
if ((x % 4 == 0) && (x % 100 != 0) || (x % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int y = 0;
for (y = 1000; y < 2000; y++)
{
if (is_leap_year(y) == 1)
{
printf("%d ", y);
}
}
return 0;
}

判断闰年部分还可以这样写

1
2
3
4
int is_leap_year(int x)
{
return ((n % 4 && n % 100 != 0) || (n % 400 == 0));
}

  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
31
32
33
34
35
36
37
38
39
int binary_search(int a[], int k, int s)
{
int left = 0;
int right = s - 1;
while (left <= right)
{
int mid = (left + right) / 2;
if (a[mid] > k)
{
right = mid - 1;
}
else if (a[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 key = 7;//目标查找值
int sz = sizeof(arr) / sizeof(arr[0]);//数组元素个数,`sizeof(arr)`是整个数组的大小,`sizeof(arr[0])`是一个元素的大小,相除就可以得出整个数组含有的元素个数
//找到就返回找到位置的下标,否则返回-1
int ret = binary_search(arr, key, sz);
if (ret == -1)
{
printf("找不到\n");
}
else
{
printf("找到了,下标是:%d\n", ret);
}
return 0;
}

  1. 写一个函数,每调用一次这个函数,就会将num的值增加1

思路:传值调用

1
2
3
4
5
6
7
8
9
10
11
12
13
void Add(int*p)
{
(*p)++;
}

int main()
{
int num = 0;
printf("%d", num);
Add(&num);
printf("%d", num);
return 0;
}

函数的嵌套调用和链式访问

嵌套调用

函数不能嵌套定义,但函数和函数之间是可以嵌套调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int test3()
{
printf("卢本伟牛逼");
}

int test1()
{
test3();
}

int main()
{

return 0;
}

链式访问

一个函数的返回值作为另一个函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
#include <string.h>
int main()
{
int len = strlen("abc");
printf("%d\n", len);
printf("%d\n", strlen("abc"));//也可以直接这样写,是链式访问
char arr1[20] = 0;
char arr2[] = "bit";
printf("%s\n", strcpy(arr1, arr2));//strcpy的返回值作为printf的参数
printf("%d", printf("%d", printf("%d", 43)));//打印4321,因为最内部打印了43,printf返回值是int类型,返回值是打印在屏幕上的字符的个数,所以第二层printf打印2,返回值就是1,第一层就打印1
return 0;
}

要注意链式调用是从上到下、从左到右,所以假如调用一个自定义函数,有两种方式:

  • 把自定义函数放在调用语句所在的函数之前,相当于先定义
  • 在调用语句前先进行声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
int a = 1;
int b = 2;
//函数的声明
int Add(int x, int y);
Add(a, b);
return 0;
}

int Add(int x, int y);
{
return x+y;
}

函数的声明和定义

函数声明

  1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但具体是不是存在无关紧要
  2. 函数的声明一般出现在函数的使用之前。要满足先声明再使用
  3. 函数的声明一般要放在头文件中的

函数定义

函数的具体实现,交代函数的功能实现

可以认为定义是一种强有力的声明

函数递归

函数调用梓森的编程技巧称为递归。递归作为一种算法在程序语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化成一个与原问题相似的规模较小的问题来求解,递归策略只需要少量的程序就可以描述出解题过程需要的多次重复计算,大大减少了程序的代码量。递归的主要思考方式在于:把大事化小

递归有两个必要限制条件

  • 存在限制条件,当满足这个限制条件时候,递归便不再继续
  • 每次递归调用之后越来越接近这个限制条件
  1. 接受一个整型值(无符号),按照顺序打印它的每一位,例如:输入1234,输出1 2 3 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
printf("%d ", n % 10);
}

int main()
{
unsigned int num = 0;
scanf("%u", &num);

print(num);

return 0;
}

原理是:
进行四次 “递”(向下传递),分别得到4321,最后到1时跳出if,开始 “归” 返回去各层打印

递归过程一直入栈直到递归结束才出栈,因此可能会遇到栈溢出的问题,因此在此处补充关于栈区的知识

区域 放的东西
栈区 局部变量、函数形参
堆区 动态内存分配的malloc/free/calloc/realloc
静态区 全局变量、静态变量

编写递归时要注意:

  • 不能死递归,要有跳出条件,每次递归逼近跳出条件
  • 递归层次不能太深