一、函数是什么
一个大型程序中的某部分代码 由一个或多个语句块组成
负责完成某项特定任务 具有独立性
一般会有输入参数和返回值 提供对过程的分装和细节的隐藏
二、C语言中函数的分类
- 库函数
- 自定义函数
1、如何学会使用库函数
1.1、strcpy
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = { 0 }; // 目标空间
char arr2[] = "hello";
strcpy(arr1, arr2);
printf("%s\n", arr1);
return 0;
}
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = "xxxxxxxxx"; // 目标空间
char arr2[] = "hello";
char* ret = strcpy(arr1, arr2);
printf("%s\n", arr1); // hello
printf("%s\n", ret); // hello
// \0 字符串的结束标志
return 0;
}
1.2、memset
#include <stdio.h>
#include <string.h>
// memset
int main()
{
char arr[] = "hello bit";
char* ret = (char*)memset(arr, 'x', 5);
printf("%s\n", ret); // xxxxx bit
return 0;
}
memset以字节为单位初始化
//memset( arr,1,5 * sizeof(int) ); // 把前5个整形 20个字节设计成1
2、自定义函数
2.1、与库函数的区别
- 与库函数一样 有函数名,返回值,函数参数
- 不一样的是这些都是我们自己来设计 有很大的发挥空间
// 函数的组成
// ret_type fun_name(paral, *)
// {
// statement; // 语句项
// }
// 返回类型 函数名 函数参数
2.2、写一个函数可以找出两个整数的最大值
#include <stdio.h>
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;
}
2.3、写一个函数可以交换两个整形变量的内容
无函数
int main()
{
int a = 10;
int b = 20;
int c = 0;
printf("交换前:a=%d b=%d\n", a, b);
c = a;
a = b;
b = c;
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
err 传值
void Swap(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
int c = 0;
printf("交换前:a=%d b=%d\n", a, b);
Swap(a, b);
// 实参a和b 穿给形参x,y时,形参将是实参的一份临时拷贝
// 改变实参变量x,y不会影响实参a,b
printf("交换后: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;
int c = 0;
printf("交换前:a=%d b=%d\n", a, b);
// 函数
Swap(&a, &b);
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
三、函数参数
1、实际参数
实参可以是:常量 变量 表达式 函数
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, get_max(3, 5));
//int max = get_max(b, 20-1);
printf("max = %d\n", max);
return 0;
}
2、形式参数
只有在函数调用的过程中才会分配空间
调用完成后销毁 因此形式参数只在函数内有效
四、函数调用
传值、传址
取以上“写一个函数可以交换两个整形变量的内容”的两个Swap例子
Swap1是传值调用,因为形参是实参的一份临时拷贝,对形参的修改不会影响实参,所以不能对a和b进行交换
Swap2是传址调用,函数和函数外的变量可以建立真正的联系
传值调用
// 形参是实参的一份临时拷贝,对形参的修改不会影响实参
传址调用
// 把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式
// 可以让函数和函数外的变量建立真正的联系,也就是函数内部可以直接操作函数外部的变量
void Swap1(int x, int y)
{
int z = 0;
z = x;
x = y;
y = z;
}
void Swap2(int* px, int* py)
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 10;
int b = 20;
Swap1(a, b); // 传值调用
Swap2(&a, &b); // 传址调用
return 0;
}
五、练习
1、写一个函数判断一个数是不是素数
#include <stdio.h>
#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;
int cnt = 0;
for (i = 100; i <= 200; i++)
{
if (is_prime(i) == 1)
{
printf("%d ", i);
cnt++;
}
}
printf("\ncnt = %d\n", cnt);
return 0;
}
2、写一个函数判断是不是闰年
int is_leap_year(int y)
{
if ((y % 4 == 0) && (y % 100 != 0) || (y % 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);
}
}
}
3、写一个函数 实现一个整数有序数组的二分查找
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; // 0有可能是下标
}
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); // TDD - 测试驱动开发
if (ret == -1)
printf("找不到\n");
else
printf("找到了,下标是:%d\n", ret);
return 0;
}
限制范围 找3找不到
int binary_search(int arr[], int k, int left, int right)
{
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 = 3;
int sz = sizeof(arr) / sizeof(arr[0]);
int ret = binary_search(arr, k, 5, 9);
if (ret == -1)
printf("找不到\n");
else
printf("找到了,下标是:%d\n", ret);
return 0;
}
4、写一个函数 每调用一次这个函数 就会将num的值增加一次
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
return 0;
}
int Add(int* n)
{
return n + 1;
}
int main()
{
int num = 0;
num = Add(num);
printf("%d\n", num); // 1
num = Add(num);
printf("%d\n", num); // 2
num = Add(num);
printf("%d\n", num); // 3
return 0;
}
六、函数的嵌套调用和链式访问
1、函数可以嵌套调用,但不能嵌套定义
int main()
{
// 函数嵌套定义 - err
/*void test()
{
printf("hehe\n");
}*/
return 0;
}
2、链式访问
- 把一个函数的返回值作为另一个函数的参数
int main()
{
int len = strlen("abc");
printf("%d\n", len);
printf("%d\n", strlen("abc")); // 链式访问
return 0;
}
int main()
{
char arr1[20] = "xxxxxxxxx";
char arr2[] = "abc";
// strcpy(arr1, arr2);
printf("%s\n", strcpy(arr1, arr2));
return 0;
}
3、打印什么
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
// printf 函数返回的是打印在屏幕上的字符的个数
// 如果发生错误 将返回负数
//
printf("%d", printf("%d", 2)); // 43是2位数
// 43
printf("%d", 1);
// 432
// 4321
// 结果是4321
七、函数的声明和定义
1、函数声明add.h
函数的声明一般出现在函数的使用之前,要满足先声明后使用
函数声明放在头文件里
新建头文件add.h
// add.h
#pragma once
// 函数的声明
extern int Add(int x, int y);
2、函数实现add.c
函数的实现在对应的.c文件里
新建add.c文件
// add.c
int Add(int x, int y)
{
int z = x + y;
return z;
}
3、引头文件test.c
引用头文件"add.h"
就可以使用了
#include <stdio.h>
// 函数的声明一般出现在函数的使用之前,要满足先声明后使用
#include "add.h"
int main()
{
int a = 10;
int b = 20;
int ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
4、引用头文件问题
引用头文件"add.h",而不需要引用对应的"add.c":
test.c与add.c经过各自编译后,产生对应的.obj文件,链接产生test.exe可执行程序
5、#pragma once
- 头文件出现的 #pragma once
- 它的作用主要是:防止头文件被重复多次引用
- 等价于:
#ifndef __ADD_H__
#define __ADD_H__
#endif
// #pragma once
#ifndef __ADD_H__
#define __ADD_H__
extern int Add(int x, int y);
#endif
6、分快去写的好处:
多人协作
封装和隐藏
// add.c和add.h 编译生成一个add.lib文件(点头文件右击属性- 常规- 配置类型改为静态库.lib 在debug里生成一个add.lib文件)
// add.h 和add.lib
// 在test.c里加上#pragma comment(lib, “add.lib”)导入静态库就可以使用
八、函数递归
一个过程或函数在其定义或说明过程中,有直接或间接调用自身的一种方法,多次重复计算,减少了程序的代码量
递归的主要思考方式:把大事化小
1、练习1 - 接受一个整型值(无符号),按照顺序打印它的每一位
#include <stdio.h>
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10); // 123
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
scanf_s("%u", &num);
print(num);
return 0;
}
分析:
// 1. 1234>9 1234/10=123 调用print函数
// 2. 123>9 123/10=12 调用print函数
// 3. 12>9 12/10=1 调用print函数
// 4. 1<9 执行printf打印 n%10=1 打印 返回(函数从哪里调用就返回哪里)
// 5. 往下执行printf打印 此时n是12 n%12=2 打印2
// 6. 往下执行printf打印 此时n是123 n%123=3 打印3
// 7. 往下执行printf打印 此时n是1234 n%1234=4 打印4
// 8. 调用完 返回main中print 往下执行 return 0
// 1_2_3_4_
2、递归的两个必要条件
- 存在限制条件,当满足这个限制条件时,递归不再继续
- 每次递归调用之后,越来越接近这个限制条件
如果没有判断条件 -> 死递归:
每一次函数调用都要在内存中开辟空间
为main print函数各自开辟的空间 - 函数栈帧
3、练习2 - 编写函数不允许创建临时变量,求字符串的长度
变量求:
#include <stdio.h>
int main()
{
char arr[] = "abcdef";
int len = strlen(arr);
printf("%d\n", len); // 6
return 0;
}
用函数,还是有临时变量count
#include <stdio.h>
my_strlen(char* s)
{
int count = 0;
while (*s != '\0')
{
count++;
s++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
// 数组名arr是数组首元素的地址 数组每个元素都是char类型 数组首元素也是char类型 数组首元素的地址是char*类型 - char*
int len = my_strlen(arr); // 数组名传过去
printf("%d\n", len);
return 0;
}
函数递归解决
#include <stdio.h>
my_strlen(char* s)
{
if (*s != '\0')
{
return 1 + my_strlen(s + 1); // 函数递归
}
else
{
return 0;
}
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
分析:
// 'a' != '\0' 进入if strlen递归 s+1 bcdef\0
// 'b' != '\0' 进入if strlen递归 s+1 cdef\0
// 'c' != '\0' 进入if strlen递归 s+1 def\0
// 'd' != '\0' 进入if strlen递归 s+1 ef\0
// 'e' != '\0' 进入if strlen递归 s+1 f\0
// 'f' != '\0' 进入if strlen递归 s+1 \0
// '\0' = '\0' 执行else return 0 返回
// return 1 + 0 = 1 返回1
// 1+1=2 返回2
// 1+2=3 返回3
...
// 1+5=6 返回6
// 返回main my_strlen 往下执行
// 往下执行 printf打印6
6
指针不同类型
// 字符指针+1 - 向后跳1个字节
// char *p;
// p+1 -> 向后跳1个字节
//
// 整形指针+1 - 向后跳4个字节
// int *p;
// p+1 -> 向后跳4个字节
//
// 指针加一都是指向下一个元素地址,指针类型不同,跳过的字节也不同
4、递归与迭代
4.1、练习3 - 求n的阶乘(不考虑溢出)
用循环
#include <stdio.h>
int Fac(int n)
{
int i = 0;
int ret =1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
return ret;
}
int main()
{
int n = 0;
scanf_s("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
递归求
// 求n的阶乘递归公式:
// n<=1时 Fac(n) = 1
// n>1时 Fac(n) = n*fac(n-1)
#include <stdio.h>
int Fac(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * Fac(n - 1);
}
}
int main()
{
int n = 0;
scanf_s("%d", &n);
int ret = Fac(n);
printf("%d\n", ret);
return 0;
}
分析:
// 例求3的阶乘 调用Fac函数
// 3>1 else 调用Fac(2)
// 2>1 else 调用Fac(1)
// 1<=1 if return 0 返回1
// 2*1=2 返回2
// 3*2=6 返回6
4.2、练习4 - 求第n个斐波那契数
什么是斐波那契数:
// 1 1 2 3 5 8 13 21 34 55 …
// 前2个数字之和等于第3个数字
// 求第n个斐波那契数递归公式:
// Fib(n)
// 当n<=2时 Fib(n) = 1
// 当n>2时 Fib(n) = Fib(n-1) + Fib(n-2)
用递归求
#include <stdio.h>
int count = 0;
int Fib(int n)
{
// 第三个斐波那契数被重复计算多少次
if (n == 3)
{
count++;
}
if (n <= 2)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0;
scanf_s("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
printf("count = %d\n", count);
return 0;
}
迭代(循环)
这个问题用函数递归可以使用,但是会很慢。
如果求第50个斐波那契数,需要很长时间才能算出
因为函数递归就是调用自身,多次重复计算,
计算50,就需要49和48;而49需要48和47;48需要47和46…
那么会重复多少次:
定义全局变量count,看看第三个斐波那契数被重复计算多少次
假设求第50个斐波那契数,结果count = 39088169,重复了这么多次,
所以我们要使用更加有效率的方法计算
分析:
// a = 1
// b = 1
// c = a + b
// 求完3
// 把b放进a 把c放进a
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n > 2)
{
// 当n=3时 开始计算
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf_s("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
5、什么时候用递归
- 当解决一个问题递归和非递归都可以使用,且没有名下那问题,那就可以使用递归
- 当解决一个问题写起来很简单,非递归比较复杂,且递归没有明显问题,那就用递归
- 如果说用递归解决问题,写起来简单,但是有明显问题,那就不能使用递归,得写出非递归方式来解决
函数递归几个经典题目:
1.汉诺塔问题
2.青蛙跳台问题
汉诺塔
#include <stdio.h>
```c
void Move(char pos1, char pos2)
{
printf(" %c - %c ", pos1, pos2);
}
// N:代表盘子的个数
// pos1:起始位置
// pos2:中转位置
// pos3:目的位置
void Hanoi(int n, char pos1, char pos2, char pos3)
{
if (n == 1)
{
Move(pos1, pos3);
}
else
{
Hanoi(n - 1, pos1, pos3, pos2);
Move(pos1, pos3);
Hanoi(n - 1, pos2, pos1, pos3);
}
}
int main()
{
Hanoi(4, 'A', 'B', 'C');
return 0;
}
九、分支与循环getchar补充
1、getchar
getchar - 函数 - 从键盘读取字符的(单个字符)
- getchar 是读取字符,为甚么返回类型是int?
- getchar 既然返回的是字符 其实返回的是找私服的ASCII码值 因为ASCII是整数 所以返回int
- getchar 在读取结束或失败的时候 会返回EOF
EOF - end of file -> -1
-1是整数 而ASCII码范围没有整数
结束代码 -> Ctrl+Z 回车
#include <stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)
{
putchar(ch);
}
return 0;
}
2、getchar读取密码
2.1、缓冲区
请确认密码(Y/N):>确认失败
// err
#include <stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf_s("%s", password, 20);
printf("请确认密码(Y/N):>");
int ch = getchar();
if (ch == 'Y')
{
printf("确认成功\n");
}
else
{
printf("确认失败\n");
}
return 0;
}
输入函数scanf和getchar 首先在缓冲区里看 如果有 就不用键盘输入
缓冲区是空的 键盘输入字符到缓冲区 此时缓冲区有abcdef\n
scanf读取 abcdef
getchar 读取\n
2.2、getchar清理\n
把缓冲区的\n清理掉
#include <stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf_s("%s", password, 20);
// 把缓冲区的\n清理掉
getchar();
printf("请确认密码(Y/N):>");
int ch = getchar();
if (ch == 'Y')
{
printf("确认成功\n");
}
else
{
printf("确认失败\n");
}
return 0;
}
一个getchar 只能清除一个\n
如果输入abc def 还是会出现错误“请确认密码(Y/N):>确认失败”
因为scanf只读取到abc 遇到空格就会停止
2.3、清除缓冲区
#include <stdio.h>
int main()
{
char password[20] = { 0 };
printf("请输入密码:>");
scanf_s("%s", password, 20);
// 把缓冲中的内容
int tmp = 0;
while (tmp = getchar() != '\n')
{
;
}
printf("请确认密码(Y/N):>");
int ch = getchar();
if (ch == 'Y')
{
printf("确认成功\n");
}
else
{
printf("确认失败\n");
}
return 0;
}
如果密码有空格,可以使用gets - 直接读一行
3、ch = getchar()) != EOF
// 只打印数字
#include <stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)
{
if (ch < '0' || ch>'9')
continue;
putchar(ch);
}
return 0;
}
十、练习
1、能把函数处理结果的二个数据返回给主调函数
1.1、return 2个值(err)
一个函数只能返回一个结果 return a,b 结果是20 -> 逗号表达式
int test()
{
int a = 10;
int b = 20;
return a, b; // err
}
int main()
{
int ret = test();
printf("%d\n", ret);
return 0;
}
1.2、形参用数组
void test(int arr[])
{
int a = 10;
int b = 20;
arr[0] = a;
arr[1] = b;
}
int main()
{
int arr[2] = { 0 };
test(arr);
printf("%d %d\n", arr[0], arr[1]);
return 0;
}
1.3、形参用2个指针
void test(int* px, int* py)
{
int a = 10;
int b = 20;
*px = a;
*py = b;
}
int main()
{
int x = 0;
int y = 0;
test(&x, &y);
printf("%d %d\n", x, y);
return 0;
}
1.4、用2个全局变量
int x = 0;
int y = 0;
void test()
{
int a = 10;
int b = 20;
x = a;
y = b;
}
int main()
{
test();
printf("%d %d\n", x, y);
return 0;
}
高内聚低耦合:自己把自己的事情做好,尽量不和别人产生关联
2、复合语句定义的变量在本函数范围内有效
在一个函数内复合语句中定义的变量在本函数范围内有效(复合语句指函数中的成对括号构成的代码)
void test()
{
{
int a = 10;
}
printf("a=%d\n", a); // err
}
int main()
{
return 0;
}
3、实现一个函数,打印乘法口诀表,口诀表的行数和列数自己指定
#include <stdio.h>
void print_table(int n)
{
int i = 0;
for (i = 1; i <= n; i++)
{
int j = 0;
for (j = 1; j <= i; j++)
{
printf("%d*%d=%-2d ", i, j, i * j);
}
printf("\n");
}
}
int main()
{
int n = 0;
scanf_s("%d", &n);
print_table(n);
return 0;
}
4、字符串逆序
要求:
1.递归实现
2.不能使用C函数库中的字符串操作函数
4.1、用函数循环
#include <stdio.h>
#include <string.h>
void reverse_string(char* str)
{
// 用指针
// char* left = str;
// char* right = str + len - 1;
// 用下标
int len = strlen(str);
int left = 0; //'a'
int right = len - 1; //'f'
while (left < right)
{
char tmp = str[left];
str[left] = str[right];
str[right] = tmp;
left++;
right--;
}
}
int main()
{
char arr[20] = "abcdef";
//数组名是首元素地址
//首元素就是char类型
//char*
reverse_string(arr);
printf("%s\n", arr); // fedcba
return 0;
}
4.2、递归版本
#include <stdio.h>
int my_strlen(char* str)
{
// 不能使用库函数 用my_strlen函数计数器求字符串
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return count;
}
void reverse_string(char* str) // char str[]
{
int len = my_strlen(str); // 求字符串长度
char tmp = str[0]; // *str 把a 放进tmp
str[0] = str[len - 1]; // 把f 放进a
str[len - 1] = '\0'; // 将\0 放进f
if (my_strlen(str + 1) >= 2) //str+1 指向b 从第二个元素开始
reverse_string(str + 1);
str[len - 1] = tmp; // 出递归 把tmp 放进\0
}
int main()
{
char arr[20] = "abcdef";
reverse_string(arr);
printf("%s\n", arr);
return 0;
}
分析:
5、计算一个数的每位之和
(递归实现 非负整数)
#include <stdio.h>
int DigitSum(unsigned int n)
{
if (n > 9)
{
return DigitSum(n / 10) + n % 10;
}
else
{
return n;
}
}
int main()
{
unsigned int num = 1729;
int ret = DigitSum(num);
printf("%d\n", ret);
return 0;
}
6、递归实现n的k次方
// Pow(n, k)
// 当k>0 时 -> n*Pow(n, k-1)
// 当k=0时 -> 1
// 当k<0时 -> 1.0/n^-k -> 1.0/Pow(n, -k)
#include <stdio.h>
double Pow(int n, int k)
{
if (k > 0)
return n * Pow(n, k - 1);
else if (k == 0)
return 1.0;
else
return 1.0 / Pow(n, -k);
}
int main()
{
int n = 2;
int k = -3;
double ret = Pow(n, k);
printf("%lf\n", ret);
return 0;
}