本章分为:函数的分类、函数的参数、函数的调用、嵌套调用和链式访问、函数的声明和定义、函数递归。
第一节:函数的分类
库函数:老师推荐的库函数学习工具是MSDN。使用库函数必须包含#include对应的头文件。
自定义函数:返回类型+函数名+函数参数
第二节:函数的参数
实参:传递给函数的参数。可以是常量、变量、表达式、函数等。
形参:形参只有在函数被调用时才实例化,并且只在函数内部有效。形参是实参的一份临时拷贝。
第三节:函数的调用
传值调用:因为函数的形参和实参分别占有不同的内存空间,所以修改形参不能影响实参。
传址调用:这种调用方式是将变量的地址传给函数,因此函数修改了变量后会影响实参。
练习
1. 写一个函数可以判断100~200之间的数是不是素数。
#include <math.h>
int is_prime(int n)
{
int i = 0;//除数
for (i = 2; i <= sqrt(n); i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int n = 0;//100-200之间的数
for (n = 101; n < 200; n+=2)
{
if (is_prime(n) == 1)
printf("%d ", n);
}
return 0;
}
2. 写一个函数判断一年是不是闰年。
int is_leap_year(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
return 1;
else
return 0;
}
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
if (is_leap_year(year) == 1)
printf("%d ", year);
}
return 0;
}
3. 写一个函数,实现一个整形有序数组的二分查找。
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;//此方法求平均值不会越界
if (arr[mid] < k)//如果中间元素比要找的元素小,说明范围在右边
left = mid + 1;//此时左下标left要变成mid+1
else if (arr[mid] > k)//如果中间元素比要找的元素大,说明范围在左边边
right = mid - 1;//此时右下标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);
if (ret == -1)
printf("找不到\n");
else
printf("找到了,下标是:%d\n", ret);
return 0;
}
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
//传址调用
void Add(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
Add(&num);
printf("%d\n", num);
Add(&num);
printf("%d\n", num);
return 0;
}
//传值调用
int Add2(int n)
{
return ++n;
}
int main()
{
int num = 0;
num = Add2(num);
printf("%d\n", num);
num = Add2(num);
printf("%d\n", num);
return 0;
}
第四节:嵌套调用和链式访问
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
第五节:函数的声明和定义
函数的声明:
- 声明就告诉编译器有一个什么样的函数(名字、参数、返回类型)。但是否存在声明决定不了。
- 声明要在函数使用之前,先声明后使用。
- 声明一般要放在头文件中
函数的定义:就是函数功能的具体实现。
第六节:函数递归
函数递归其实分两个动作,先完成所有的递,再全部归。并且是最后一次调用的自己是第一次返回,也就是深度优先。
void print(unsigned int n)
{
//如果这里没有if这个限制条件,这个递归会无限调用形成栈溢出
//内存中的栈区存放着局部变量、函数的形参。函数的每一次调用也会在栈区申请空间
if (n > 9) //1234 123 12 1
{
print(n / 10); //123 12 1
}
printf("%d ", n % 10);
}
练习1:接受一个整型值(无符号),按照顺序打印它的每一位。
递归分 递和归,先执行完递延,最后一起回归。
1.n = 1234,大于9,继续递归,n变为1234 / 10 = 123。
2.n = 123,大于9,继续递归,n变为123 / 10 = 12。
3.n = 12,大于9,继续递归,n变为12 / 10 = 1。
4.n = 1,不大于9,结束递归,打印n % 10,即打印1。
5.回溯到n = 12,打印12 % 10,即打印2。
6.回溯到n = 123,打印123 % 10,即打印3。
7.回溯到n = 1234,打印1234 % 10,即打印4。
所以,最后打印的结果是1 2 3 4。
一开始学习递归函数时,总是搞不清调用自己以后函数执行到哪了,并且返回的时候也比较迷,通过画图的形式才一步一步搞清楚递归的整个过程。
练习2:编写函数不允许创建临时变量,求字符串的长度
思路:使用递归方式的核心就是拆解问题,将规模减小,直到最简单的情况。这题可以把问题看成查看第一个字符是否是\0,如果不是就+1,并向后移动一位。
去查看第一个字符是不是\0,如果不是下一步。具体如下:
my_strlen("abc")
1+my_strlen("bc")
1+1+my_strlen("c")
1+1+1+my_strlen("")
1+1+1+0
int my_strlen(char* str)
{
if (*str != 0)
return 1 + my_strlen(str + 1);//这里不能使用str++,因为先使用后++,每次都把相同的地址传到下一次
else
return 0;
}
作业
1. 以下关于函数设计不正确的说法是:( )
A.函数设计应该追求高内聚低耦合
B.要尽可能多的使用全局变量
C.函数参数不易过多
D.设计函数时,尽量做到谁申请的资源就由谁来释放
答案:B
2. 以下叙述中不正确的是:( )
A.在不同的函数中可以使用相同名字的变量
B.函数中的形式参数是在栈中保存
C.在一个函数内定义的变量只在本函数范围内有效
D.在一个函数内复合语句中定义的变量在本函数范围内有效(复合语句指函数中的成对括号构成的代码)
答案:D
3. 关于C语言函数描述正确的是:( )
A.函数必须有参数和返回值
B.函数的实参只能是变量
C.库函数的使用必须要包含对应的头文件
D.有了库函数就不需要自定函数了
答案:C
4. C语言规定,在一个源程序中,main函数的位置( )
A.必须在最开始
B.必须在库函数的后面
C.可以任意
D.必须在最后
答案:C
5. 关于实参和形参描述错误的是:( )
A.形参是实参的一份临时拷贝
B.形参是在函数调用的时候才实例化,才开辟内存空间
C.改变形参就是改变实参
D.函数调用如果采用传值调用,改变形参不影响实参
答案:C
6. 函数调用exec((vl,v2),(v3,v4),v5,v6); 中,实参的个数是:( )
A.3
B.4
C.5
D.6
答案:B
7. 关于函数的声明和定义说法正确的是:( )
A.函数的定义必须放在函数的使用之前
B.函数必须保证先声明后使用
C.函数定义在使用之后,也可以不声明
D.函数的声明就是说明函数是怎么实现的
答案:B
8. 在函数调用时,以下说法正确的是:( )
A.函数调用后必须带回返回值
B.实际参数和形式参数可以同名
C.函数间的数据传递不可以使用全局变量
D.主调函数和被调函数总是在同一个文件里
答案:B
9. 关于函数调用说法不正确的是:( )
A.函数可以传值调用,传值调用的时候形参是实参的一份临时拷贝
B.函数可以传址调用,传址调用的时候,可以通过形参操作实参
C.函数可以嵌套定义,但是不能嵌套调用
D.函数可以嵌套调用,但是不能嵌套定义
答案:C
10. 函数判断素数
实现一个函数is_prime,判断一个数是不是素数。
利用上面实现的is_prime函数,打印100到200之间的素数。
#include <math.h>
int is_prime(int n)
{
int i = 0;//除数
for (i = 2; i <= sqrt(n); i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int n = 0;//100-200之间的数
int count = 0;
for (n = 101; n < 200; n+=2)
{
if (is_prime(n) == 1)
{
printf("%d ", n);
count++;
}
}
printf("\n%d\n", count);
return 0;
}
11. 函数判断闰年
int is_leap_year(int year)
{
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
return 1;
else
return 0;
}
int main()
{
int year = 0;
for (year = 1000; year <= 2000; year++)
{
if (is_leap_year(year) == 1)
printf("%d ", year);
}
return 0;
}
12. 写一个函数,实现一个整形有序数组的二分查找。
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;//此方法求平均值不会越界
if (arr[mid] < k)//如果中间元素比要找的元素小,说明范围在右边
left = mid + 1;//此时左下标left要变成mid+1
else if (arr[mid] > k)//如果中间元素比要找的元素大,说明范围在左边边
right = mid - 1;//此时右下标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);
if (ret == -1)
printf("找不到\n");
else
printf("找到了,下标是:%d\n", ret);
return 0;
}
13. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
//传址调用
void Add1(int* p)
{
(*p)++;
}
int main()
{
int num = 0;
Add1(&num);
printf("%d\n", num);
Add1(&num);
printf("%d\n", num);
return 0;
}
//传值调用
int Add2(int n)
{
return ++n;
}
int main()
{
int num = 0;
num = Add2(num);
printf("%d\n", num);
num = Add2(num);
printf("%d\n", num);
return 0;
}
14. 函数交换两个整数
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = 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;
}
15. 乘法口诀表
实现一个函数,打印乘法口诀表,口诀表的行数和列数自己指定
如:输入9,输出9 * 9口诀表,输出12,输出12 * 12的乘法口诀表。
void multi_table(int n)
{
int i = 0;
int j = 0;
for (i = 1; i <= n; i++)
{
for (j = 1; j <= i; j++)
{
printf("%-2d*%2d = %-2d ", i, j, i * j);
}
printf("\n");
}
}
int main()
{
int n = 0;
scanf("%d", &n);
multi_table(n);
return 0;
}
16. 能把函数处理结果的2个数据返回给主调函数,在下面的方法中不正确的是:( )
A.return 这2个数
B.形参用数组
C.形参用2个指针
D.用2个全局变量
答案:A
17. 根据下面递归函数:调用函数Fun(2),返回值是多少( )
int Fun(int n)
{
if (n == 5)
return 2;
else
return 2 * Fun(n + 1);
}
//A.2
//B.4
//C.8
//D.16
//答案:D
18. 关于递归的描述错误的是:( )
A.存在限制条件,当满足这个限制条件的时候,递归便不再继续
B.每次递归调用之后越来越接近这个限制条件
C.递归可以无限递归下去
D.递归层次太深,会出现栈溢出现象
答案:C
19. 打印一个数的每一位
递归方式实现打印一个整数的每一位
void print(int n)
{
if (n > 9)
print(n / 10);
printf("%d ", n % 10);
}
int main()
{
int n = 1234;
print(n);
return 0;
}
20. 求阶乘
递归和非递归分别实现求n的阶乘(不考虑溢出的问题)
递归版本
思路:用递归求阶乘首先找到最简单的情况,即求1的阶乘。1的阶乘就是1。2的阶乘是用2乘以1的阶乘。那么n的阶乘就是n*(n-1)的阶乘
非递归版本
思路:非递归版本求阶乘也是用当前的数乘以上一个数的阶乘。在程序实现上需要考虑1的阶乘情况。这里创建一个阶乘的变量,并初始化为1,这样1*1还是等于1。这样就得到了1的阶乘,并保存起来。利用for循环让数字每次+1,这样每次都是乘以上一个数的阶乘。
//非递归
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d\n", ret);
return 0;
}
//递归
int fac(int n)
{
if (n == 1)
return 1;
else
return n * fac(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
printf("%d\n", fac(n));
return 0;
}
21. strlen的模拟(递归实现)
递归和非递归分别实现strlen
非递归版本
思路:将字符数组的地址传过去,用指针变量接收。利用循环判断当前位置是否是\0,如果不是那么计数变量+1,并且让指针向后移动一位。直到遇到\0。
递归版本
思路:同样将字符数组传过去,用指针变量接收,判断当前位置是否为\0。如果不是那么返回1,并且继续递归。同时,让指针+1也就是让指针让后移动一位。
//非递归
#include <assert.h>
size_t my_strlen1(const char* str)
{
assert(str);
size_t count = 0;
while (*str!='\0')
{
str++;
count++;
}
return count;
}
//递归
size_t my_strlen2(const char* str)
{
assert(str);
if (*str != '\0')
return 1 + my_strlen2(str + 1);
else
return 0;
}
int main()
{
char arr[] = "abcdef";
size_t ret1 = my_strlen1(arr);
printf("%zu\n", ret1);
size_t ret2 = my_strlen2(arr);
printf("%zu\n", ret2);
return 0;
}
22. 字符串逆序(递归实现)
编写一个函数 reverse_string(char* string)(递归实现)
实现:将参数字符串中的字符反向排列,不是逆序打印。
要求:不能使用C函数库中的字符串操作函数。
比如 :
char arr[] = "abcdef";
逆序之后数组的内容变成:fedcba
非递归版本
//循环非指针版本
//思路:逆置字符串,循环的方式实现非常简单
//确定左边下标(第一个元素)和右边下标(最后一个元素)。
//交换两个元素,每次左下标+1,有下标-1,直到左右下标相等说明只有一个元素,此时不需要再交换。
#include <string.h>
void reverse_string(char arr[])
{
int sz = strlen(arr);
int left = 0;
int right = sz - 1;
while (left < right)
{
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
//循环指针版本
//1. 给两个指针,left放在字符串左侧,right放在最后一个有效字符位置
//2. 交换两个指针位置上的字符
//3. left指针往后走,right指针往前走,只要两个指针没有相遇,继续2,两个指针相遇后,逆置结束
void reverse_string(char* arr)
{
char* left = arr;
char* right = arr + strlen(arr) - 1;
while (left <= right)
{
char tmp = *left;
*left = *right;
*right = tmp;
left++;
right--;
}
}
int main()
{
char arr[] = "abcdef";
reverse_string(arr);
printf("%s\n", arr);
return 0;
}
递归版本
思路:交换第一个和最后一个,再交换第二个和倒数第二个,以此类推
只能逆序元素个数为奇数的数组
//递归版本(只能逆序元素个数为奇数的数组)
void reverse_string(char arr[], int left, int right)
{
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
//如果if条件在这个位置,遇到元素个数为偶数的的数组会有以下情况
//当逆序到最后两个字符时,虽然字符被交换了,但下标还未 + 1,(调用自己时才 + 1)
//所以此时的左下边仍然小于右下标,所以又再次调用自己,导致已经被正确交换的两个字符又被交换了。
//下面函数为正确版本,即使交换也要左下标小于右下标。
if (left < right)
reverse_string(arr, left + 1, right - 1);
}
//递归版本(正确版本)
void reverse_string(char arr[], int left, int right)
{
if (left < right)
{
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
reverse_string(arr, left + 1, right - 1);
}
}
int main()
{
char arr[] = "abcdef";
int left = 0;
int right = strlen(arr) - 1;
reverse_string(arr, left, right);
printf("%s\n", arr);
return 0;
}
23. 计算一个数的每位之和(递归实现)
写一个递归函数DigitSum(n),输入一个非负整数,返回组成它的数字之和
例如,调用DigitSum(1729),则应该返回1 + 7 + 2 + 9,它的和是19
输入:1729,输出:19
思路:这题和顺序打印每一位类似。首先要拆分每一位,在获取每一位之后再相加。
//DigitSum(1234)
//DigitSum(123)+4 等效于DigitSum(1234/10)+ 1234%10
//DigitSum(12)+3+4
//DigitSum(1)+2+3+4
//自己的版本
int DigitSum(int n)
{
int sum = 0;
if (n > 9)
sum += DigitSum(n / 10);
return sum += n % 10;
}
//GPT版本
int DigitSum(int n)
{
if (n == 0)
return 0;
return n % 10 + DigitSum(n / 10);
}
//老师版本
int DigitSum(unsigned int n)
{
if (n > 9)
return DigitSum(n / 10) + n % 10;
else
return n;
}
int main()
{
int n = 1234;
int ret = DigitSum(n);
printf("%d\n", ret);
return 0;
}
24. 递归实现n的k次方
编写一个函数实现n的k次方,使用递归实现。
思路:从指数入手,分3种情况:
k=0,无论n是几,都等于1(此为最简单的情况)
k>0,例如3^3可以理解为3*3^2(即n*n^k-1)
k<0,其实就是k>0时再取倒数
//Pow(n, k) -> n*Pow(n, k-1)
//k=0, 1
//k>0, Pow(n, k)->n* Pow(n, k - 1)
//k<0, 1/(Pow(n, -k))
//n的k次方可以看作,n乘以n的k-1次方,直到k=0
double Pow(int n, int k)
{
if (k == 0)
return 1;
else if (k > 0)
return n * Pow(n, k - 1);
else
//如果k是负数则进入下面代码,假设k是-2,那么-k=-(-2),结果就是2
//然后进去k>0的部分,最后才返回到 1.0 / Pow(n, -k);
return 1.0 / Pow(n, -k);
}
int main()
{
int n = 0;
int k = 0;
scanf("%d%d", &n, &k);
double ret = Pow(n, k);
printf("%lf\n", ret);
return 0;
}
25. 计算斐波那契数
递归和非递归分别实现求第n个斐波那契数
例如:
输入:5 输出:5
输入:10, 输出:55
输入:2, 输出:1
1,1,2,3,5,8,13,21,34,55,89
//非递归
//自己的方法
int fib(int n)
{
int a = 1;
int b = 1;
int c = 0;
if (n <= 2)
return 1;
n--;
while (--n)
{
c = a + b;
a = b;
b = c;
}
return c;
}
//老师方法
//思路:第1个数+第2个数=第3个数。第2个数+第3个数=第四个数。
//又因为最开始两个数都是1,所以创建3个变量分别是第1、2个数(初始化为1),和第3个数。
//每次加完之后,把第3个数的值赋给第2个数;第2个数的值赋给第1个数。
int fib(int n)
{
int a = 1;
int b = 1;
int c = 0;
if (n <= 2)
return 1;
int i = 0;
for ( i = 3; i <= n; i++)
{
c = a + b;
a = b;
b = c;
}
return c;
}
//递归版本
//根据上方非递归可以总结出,任何一个数都是往前移动1和2的两个数相加。即n=(n-1)+(n-2)
//最简单的情况是最开始的两个数,都是1。
int fib(n)
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = fib(n);
printf("%d\n", ret);
return 0;
}
26. 将数组A的内容和数组B的内容进行交换。(数组一样大)
int main()
{
int arr1[] = { 1,3,5,7,9 };
int arr2[] = { 2,4,6,8,0 };
//不可以使用数组名进行交换,因为数组名是地址
int i = 0;
int sz = sizeof(arr1) / sizeof(arr1[0]);
for (i = 0; i < sz; i++)
{
int tmp = arr1[i];
arr1[i] = arr2[i];
arr2[i] = tmp;
}
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
printf("\n");
for (i = 0; i < sz; i++)
{
printf("%d ", arr2[i]);
}
printf("\n");
return 0;
}
27. 冒泡排序
思路:假设有10个数(n=10),那么每个数都要交换到指定位置需要9轮(n-1),因为9个数交换完,最后一个数也在指定位置了。
第一轮交换的次数是9次(n-1)。
每一轮交换的次数比上一次-1,增加几轮少几次。
void bubble(int* arr, int sz)
{
int i = 0;//i是需要交换多少轮
for (i = 0; i < sz - 1; i++)
{
//j是元素下标,从下标为0的元素(也就是第一个元素)开始逐一比较
//第1轮:第1个元素需要交换 元素个数-1次,10-1=9
//第2轮:第2个元素需要交换 元素个数-2次,10-2=8
//....
//第9轮:第9个元素需要交换 元素个数-9次,10-9=1
//结束,总共交换 元素个数-1轮,10-1=9
int j = 0;//元素下标
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
28. 小乐乐上课需要走n阶台阶,因为他腿比较长,所以每次可以选择走一阶或者走两阶,那么他一共有多少种走法?
思路1:
假设n=10,fib(n)表示n个台阶的走法。
如果第一步走1个台阶,那么还剩9个台阶,剩余台阶走法是fib(9)
如果第一步走2个台阶,那么还剩8个台阶,剩余台阶走法是fib(8)
思路2:
假设台阶有5级
n 跳法
1 1
2 2
3 3
4 5
5 8
如果青蛙站在第1级台阶,那么剩下的4级台阶总共有5种跳法
如果青蛙站在第2级台阶,那么剩下的3级台阶总共有3种跳法
所以5级台阶的跳法:3级台阶的跳法+4级台阶跳法(3+5=8)
也就是当n<=2时,有几级台阶就只有几种跳法
当n>2时,跳法总共有:(n-1)+(n-2)
每次求n时,都是青蛙站在第1级,或第2级作为起点(动态规划:用上一步的结果,来计算下一步的结果)
int fib(int n)
{
if (n <= 2)
return n;
else
return fib(n - 1) + fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int m = fib(n);
printf("%d\n", m);
}