本章分为:函数的分类、函数的参数、函数的调用、嵌套调用和链式访问、函数的声明和定义、函数递归。
第一节:函数的分类
库函数:老师推荐的库函数学习工具是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的阶乘(不考虑溢出的问题)
//非递归
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
//非递归
#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
非递归版本
//循环非指针版本
#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--;
}
}
//循环指针版本
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
//自己的版本
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次方,使用递归实现。
double Pow(int n, int k)
{
if (k == 0)
return 1;
else if (k > 0)
return n * Pow(n, k - 1);
else
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;
}
//老师方法
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;
}
//递归版本
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;
}