系列文章目录
C语言学习入门第三节——函数(下)
函数
- 函数是什么
- 库函数
- 自定义函数
- 函数参数
- 函数调用
- 函数的嵌套调用和链式访问
- 函数的声明和定义
- 函数递归
一、函数调用
1.传值调用: 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
2.传址调用: 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
#include<stdio.h>
void Swap1(int x, int y)//传值调用:在这种设计中形参是实参的一份临时拷贝,对形参的修改不会影响实参
{
int tmp = x;
x = y;
y = tmp;
}
void Swap2(int* pa, int* pb)//传值调用:通过形参的指针就能够访问到函数外部的变量,并进行操作
{
int tmp = *pa;//tmp = a
*pa = *pb;//a = b
*pb = tmp;//b = tmp
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap1(a, b);//传值调用:在这种设计中形参是实参的一份临时拷贝,对形参的修改不会影响实参
printf("交换后:a=%d b=%d\n", a, b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b);//传值调用:通过形参的指针就能够访问到函数外部的变量,并进行操作
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
函数练习
1.写一个函数可以判断一个数是不是素数:
// 写一个函数可以判断一个数是不是素数。
// 是素数返回1,不是素数返回0:
#include <stdio.h>
#include <math.h>
int is_prime(int n)
{
int j = 0;
for ( j = 2; j <= sqrt(n); j++ )
{
if (n % j == 0)
{
return 0; // 能被整除说明是素数,返回0
}
}
return 1; // 整个循环中都没有被整除,说明是素数
}
int main()
{
int i = 0;
int count = 0;
for ( i = 100; i <= 200; i++)
{
// 判断i是否为素数
if (is_prime(i) == 1) // 调用函数返回值是1,说明是素数
{
printf("%d\n", i);
count++;
}
}
printf("\ncount = %d\n", count);//打印100-200的素数个数
return 0;
}
2.写一个函数判断一年是不是闰年:
// 自定义判断是不是闰年的函数:
int is_leap_year(int y)
{
return ((y % 4 ==0) && (y % 100 != 0) || (y % 400 == 0));
// 因为判断结果是1或0,所以可以直接放在return后
}
int main()
{
int y = 0;
int count = 0;
for ( y = 1000; y <= 2000; y++ )
{
if (is_leap_year(y) == 1)
{
printf("%d ", y);
count++;
}
}
printf("\ncount = %d\n", count);
return 0;
}
3.写一个函数,实现一个整形有序数组的二分查找:
//写一个函数,实现一个整形有序数组的二分查找:
#include <stdio.h>
int binary_search(int arr[], int k, int sz)
{
int left = 0;
int right = sz - 1;
while (left <= right)
{
// 如果left和right过大超过4个字节,那就不是想要的结果了
// int mid = (left + right) / 2;
// left是小的一边,(right - left)是两者差值,
// (right - left) / 2,差值的一半赋给小的一边
// 两边一样,这时任意一边都是平均值(中间值)
int mid = left + (right - left) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid; // 找到了
}
}
}
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); // 返回下标
// 自定义函数参数:数组,要找的值,元素个数
// 找到了,返回下标 ; 未找到,返回-1
if (ret == -1)
{
printf("找不到\n");
}
else
{
printf("找到了,下标是:%d\n", ret);
}
return 0;
}
4.写一个函数,每调用一次这个函数,就会将num的值增加1
方法一:使用指针变量
//改变了实参的值,要使用地址变量
#include <stdio.h>
void test(int* p)
{
(*p)++; // p是指针变量
// 此时 *p 去取num的地址,所以相当于num
}
int main()
{
int num = 0;
test(&num); // 调用一次test()这个函数,将num的值增加1
test(&num); // 函数参数为变量地址,用于在函数中改变实参的值
printf("%d\n", num);
return 0;
}
方法二:使用return返回值
#include <stdio.h>
int test(int n)
{
return (n + 1);
}
int main()
{
int num = 0;
num = test(num);
num = test(num);
printf("%d\n", num);
return 0;
}
二、函数的嵌套调用和链式访问
函数和函数之间是可以根据实际的需求进行组合的,也就是互相调用。
1.嵌套调用:在函数定义中调用别的已声明定义函数
#include <stdio.h>
int test()
{
int a = 5;
int b = 6;
return a + b;
}
void fun()
{
int c = test();//函数的嵌套调用
printf("%d\n",c);
}
int main()
{
fun();
return 0;
}
注意:可以嵌套调用,但是不能嵌套定义(在函数定义中声明定义并使用别的函数)
#include <stdio.h>
int test()
{
int a = 5;
int b = 6;
return a + b;
void fun()
{
int c = test();//c语言不支持函数的嵌套定义
printf("%d\n", c);
}
}
int main()
{
fun();
return 0;
}
2. 链式访问:把一个函数的返回值作为另外一个函数的参数( * * * )
例一:
#include <stdio.h>
#include <string.h>
int main()
{
printf("%zd\n", strlen("abcdef"));//strlen函数的返回值作为printf函数的参数
return 0;
}
例二:
#include <stdio.h>
int main()
{
// printf()函数的返回值是 字符串的个数
printf("%d", printf("%d", printf("%d", 43)));//所以打印结果为4321
return 0;
}
例三
#include <stdio.h>
int main()
{
// printf()函数的返回值是 字符串的个数
printf("%d ", printf("%d ", printf("%d ", 43)));//多打印了个空格,空格也被printf函数识别为字符串
//所以打印结果为43 3 2
return 0;
}
三、函数的声明和定义(用途广泛)
1.函数声明
1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是
存在,函数声明决定不了。
2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中的
补充:声明时形参也可以不写,只写参数类型和个数即可
函数定义在主函数下面,这时如果主函数上面没有该函数的声明,运行时系统会报警告
//函数声明:告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。
//但是具体是不是存在(可能只声明了但未定义),函数声明决定不了。
#include <stdio.h>
//函数的声明一般出现在函数的使用之前。要满足先声明后使用。
int Add(int x, int y);//函数的声明
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//自定义函数:
int c = Add(a, b);//函数调用
//打印
printf("%d\n", c);
return 0;
}
//函数的定义:
int Add(int x, int y)
{
return x + y;
}
函数的声明一般要放在头文件里。
分模块编程:
1.分模块写方便协助作,最后再整合。
2.可以把代码的实现和声明分离
//add.h:
//函数的 声明 放在头文件中xiezuuo
int Add(int x, int y);
//add.c:
//函数 定义 放在对应的c文件里
int Add(int x, int y)
{
return x + y;
}
// 主程序:
//分模块去编程,方便协作,最后做整合
//可以把代码的实现和声明分离
#include <stdio.h>
#include "add.h" // 调用对应的头文件,
//相当于对相应的函数进行了声明
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//加法函数:
int c = Add(a, b);
//其它源文件内定义的函数具有 外部连接属性 ,
//可直接调用 ,(之前staic讲过)
//打印
printf("%d\n", c);
return 0;
}
设置静态库(代码保密):
设置完后,执行程序,会生成一个对应的.lib文件,是一个二进制文件,包含函数定义的内容,文件点开后显示的是乱码。进行交易时可以提供给买家 .lib文件 和 .h头文件,头文件告诉有什么函数和函数怎么使用,买家只知道怎么使用里面的函数,但无法直接知道具体是怎么实现的。
导入静态库:
// .lib - 静态库
// 导入静态库:(假设静态库名称为"add.lib")
#pragma comment(lib, "add.lib")
2.函数定义
- 函数的定义是指函数的具体实现,交待函数的功能实现。
- 函数定义也是一种特殊的声明。(把函数定义写在主函数之前)
#include <stdio.h>
int Add(int x, int y);//函数的声明
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//加法函数:
int c = Add(a, b);//函数调用
//打印
printf("%d\n", c);
return 0;
}
//函数的定义:
int Add(int x, int y)
{
return x + y;
}
#include <stdio.h>
//函数的定义在主函数之前:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 0;
int b = 0;
//输入
scanf("%d %d", &a, &b);
//加法函数:
int c = Add(a, b);//函数调用
//打印
printf("%d\n", c);
return 0;
}
四、函数递归(难使用,会导致栈溢出)
1.什么是递归
程序调用自身的编程技巧称为递归( recursion)。 递归做为一种算法在程序设计语言中广泛应用。
一个过程或函数在其定义或说明中有直接或间接 调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问
题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程 序的代码量。 递归的主要思考方式在于:把大事化小
递归的主要思考方式在于:把大事化小
(递归和循环类似,但递归每次都要开辟空间,而循环每次都是使用固定的数据,所以循环不会让程序崩溃,而递归可能会让程序崩溃)
2.递归的两个必要条件( * * * )
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
(两个条件有了不一定对,没了一定错。)
满足这两个条件
#include <stdio.h>
void print(unsigned int n)
{
if (n > 9)//如果是两位数-->限制条件,当n不满足这个条件时,递归结束
{
print(n/10);//每次递归结束都越来越接近这个条件
}
printf("%d ", n % 10);
}
int main()
{
unsigned int num = 0;
//输入
scanf("%d", &num);
print(num);
return 0;
}
满足这两个条件(易导致栈溢出)
#include <stdio.h>
void print()
{
printf("haha\n");
print(); // 递归:函数自己调用自己
}
int main()
{
print();
return 0;
}
接受一个整型值(无符号),按照顺序打印它的每一位(用递归实现)
#include <stdio.h>
//1234 % 10 --》4
//1234 / 10 ---》123
//123 % 10 --》3
//123 / 10 ---》12
//12 % 10 --》2
//12 / 10 ---》1
//1 % 10 ---》 1
void print(unsigned int n)
{
if (n > 9)//如果是两位数
{
print(n/10);
}
printf("%d\n", n % 10);
}
int main()
{
unsigned int num = 0;
//输入
scanf("%d", &num);//1234
print(num);
return 0;
}
编写函数不允许创建临时变量,求字符串的长度。
使用临时变量的解法:
#include <stdio.h>
#include <string.h>
int my_strlen(char* s) // 返回字符串长度,所以返回类型是int
// 因为实参是数组名称,是地址,所以形参使用 char*
// 数组末尾包括 \0 , 求字符串长度是求 \0 前有多少个字符(注意)
{
int count = 0; // 临时变量
while (*s != '\0') // 如果没有到\0(结束标志),说明数组还有值,就继续循环
// 使用 s指针 往后遍历字符串 ,字符串放在数组中是连续的
{
count++; // 加入循环说明有值,计数加1
s++; // 让指针往后偏移,判断下一位
}
return count;
}
int main()
{
char arr[] = "abc";
// 这个数组相当于 {a b c \0]
int len = my_strlen(arr);
// 调用自定义函数
printf("%d\n", len);
// 字符数组的数组名是首元素的地址
return 0;
}
无临时变量解法(使用递归):
// 使用递归
// my_strlen("abc") --> 1 + my_strlen("bc")
// --> 1 + 1 + my_strlen("c") --> 1 + 1 + 1 + my_strlen("\0")
// --> 1 + 1 + 1 + 0 (\0 不计数 ,记为0)
// -> 把字符一个一个剥离下来(大事化小)
int my_strlen(char* p)
{
if (*p == '\0')
{
return 0; // 首地址数为\0,说明为空数组
}
else
{
// 进入else说明首地址有值,所以先给个1
return 1 + my_strlen(p + 1); // 使用递归
// (s + 1)相当于让指针往后偏移,判断下一位
}
}
int main()
{
char arr[] = "abc";
// 这个数组相当于 {a b c \0]
int len = my_strlen(arr);
// 调用自定义函数
printf("%d\n", len);
// 字符数组的数组名是首元素的地址
return 0;
}
3.递归与迭代(循环就是一种迭代)
许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
求n的阶乘。(不考虑溢出)
使用(循环)迭代解法:
#include <stdio.h>
// 循环(迭代)
int Fac(int n)
{
int r = 1;
int i = 0;
for ( i = 1; i <= n; i++ ) // 产生1-n的数
{
r = r * i;
}
return r;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fac(n); // 调用自定义函数
printf("%d\n", ret);
return 0;
}
使用递归:
#include <stdio.h>
// 递归
int Fac(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * Fac(n - 1);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fac(n); // 调用自定义函数
printf("%d\n", ret);
return 0;
}
求第n个斐波那契数。(不考虑溢出)
使用递归解法:
#include <stdio.h>
int Fib(int n)
{
if (n <= 2)
{
return 1; // 前两个斐波那契数都是1
}
else
{
return Fib(n - 1) + Fib(n - 2); //Fit(4)
//= Fit(3)+Fit(2)
//=Fit(2)+Fit(1)+1
//=1 + 1 +1
}
}
int main()
{
int n = 0;
scanf("%d", &n);//4
int ret = Fib(n);
printf("%d\n", ret);//3
return 0;
}
上面解法有弊端(递归反倒使计算量变多了,递推出去成千上万个分支,再回归成千上万个分支,效率太低了)当要求的斐波那契数比较大时,电脑算不过来(算的速度很慢)。
使用迭代(循环)的解法:
// 1 1 2 3 5 8 13 21 34 55 ...
// 前2个的数的和是第三个数
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1; //n < 3 时c直接取1(前两个斐波那契数都是1)
while (n >= 3)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n = 0;
scanf("%d", &n);//50
int ret = Fib(n); // 调用自定义函数
printf("%d\n", ret);//算的速度很快但是不考虑溢出所以结果不正确
return 0;
}