🌕写在前面
- 🍊博客主页:kikoking的江湖背景
- 🎉欢迎关注🔎点赞👍收藏⭐️留言📝
- 🌟本文由 kikokingzz 原创,CSDN首发!
- 📆首发时间:🌹2021年11月21日🌹
- 🆕最新更新时间:🎄2021年11月21日🎄
- ✉️坚持和努力一定能换来诗与远方!
- 🙏作者水平很有限,如果发现错误,请留言轰炸哦!万分感谢感谢感谢!
🐉:本篇内容包含了C语言函数的全方位理解,同时配备了对应的例题理解,后续还会更新专门的习题,刷题板块!
L:大仙,终于来到函数板块了嘛!
🐉:yep!小子,这章内容很多,仔细看哦!
目录
🌕写在前面
🔥1.函数是什么?
🔥2.1 库函数
🌕2.1.1为什么会有库函数?
🌕2.1.2如何学会使用库函数?
🌙自学案例1-strcpy函数自学
🌙自学案例2-memset自学
🌙自学案例3-strlen
🔥2.2自定义函数
🌕2.2.1自定义函数的组成
🌕2.2.2典型案例
🌙1.写一个函数可以找出两个整数中的最大值
🌙2.写一个函数可以交换两个整形变量的内容
🔥3. 函数的参数
🌕3.1 实际参数(实参)
🌕3.2 形式参数(形参)
🔥4. 函数的调用
🌕4.1 传值调用
🌕4.2 传址调用
🔥函数调用案例
🌙1.判断一个函数是不是素数
🌙2.写一个函数判断是不是闰年
🌙3.写一个函数,实现一个整形有序数组的二分查找。
🌙4.写一个函数,每调用一次这个函数,就会将num的值增加1
🔥函数返回类型
🔥5. 函数的嵌套调用和链式访问
🌕5.1 嵌套调用
🌕5.2链式访问
🌕3.习题
🔥6. 函数的声明和定义
🌕6.1函数定义
🌕6.2函数声明
🌙公司写代码,会不会把所有代码都写在test.c中呢?
🔥7. 函数递归
🌕7.1 什么是递归?
🍊练习1.接收一个整型值(无符号),按照顺序打印它的每一位
🌕7.2 递归的两个必要条件
🍊练习2.编写函数不允许创建临时变量,求字符串的长度
🍊练习3. 求n的阶乘(不考虑溢出)
🍊练习4.求第n个斐波那契数(不考虑溢出)
🔥1.函数是什么?
- 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
- 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库
✨✨✨我是分割线✨✨✨
🔥2.1 库函数
🌕2.1.1为什么会有库函数?
- 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。
- 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。
·像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
🌕2.1.2如何学会使用库函数?
- 需要学会查询工具的使用:
- MSDN(Microsoft Developer Network)
- www.cplusplus.com
- http://en.cppreference.com(英文版)
- http://zh.cppreference.com(中文版)
🌙自学案例1-strcpy函数自学
·在MSDN中搜索strcpy
·了解函数的定义方法、返回值、参数等
#include<stdio.h> #include<string.h> int main() { //strlen--string length --字符串长度有关的 //strcpy--string copy--字符串拷贝 char arr1[] = "bit";//原数据 char arr2[] = "#########";//目的地字符串 // bit\0 打印时\0是结束标志 strcpy(arr2, arr1); printf("%s\n", arr2); return 0; }
🌙自学案例2-memset自学
·在MSDN中搜索memset
·了解函数的定义方法、返回值、参数等
#include<stdio.h> int main() { char arr[]="hello world"; memset(arr,'*',5); //arr-这个数组空间的地址 //这里放的'*',因为存进去的是ASCII值,也是整型值(int型) printf("%s\n",arr);//%s打印字符串 //***** world return 0; }
🌙自学案例3-strlen
·在www.cplusplus.com中搜索memset
·了解函数的定义方法、返回值、参数等
#include<stdio.h> #include<string.h> int main() { char arr[] = "abc"; size_t len = strlen(arr); printf("%u\n", len); //%d-有符号 //%u-无符号 return 0; }
✨✨✨我是分割线✨✨✨
🔥2.2自定义函数
L:库函数好好用啊,那是不是咱们只要熟练掌握库函数就好啦?
🐉:如果库函数能干所有的事情,那还要程序员干什么?所以更加重要的是自定义函数!
🌕2.2.1自定义函数的组成
·自定义函数和库函数一样,有函数名,返回值类型和函数参数。但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。
·函数的组成︰
🌕2.2.2典型案例
🌙1.写一个函数可以找出两个整数中的最大值
#include<stdio.h> int getmax(int x, int y) { if (x > y) return x; else return y; } int main() { int a = 10; int b = 20; //函数的使用 int max = getmax(a, b); int max1 = getmax(100, 300+1); printf("%d\n", max1); return 0; }
🌙2.写一个函数可以交换两个整形变量的内容
❌错误示范:
#include<stdio.h> void Swag(int x, int y)//void表示没有返回值 { int z = x; x = y; y = z; } int main() { int a = 10; int b = 20; int tmp; printf("a=%d b=%d\n", a, b); Swag(a, b); printf("a=%d b=%d\n", a, b); return 0; }
L:为什么这里明明Swag函数里面两个值交换了,而最后的结果没有交换呢?
🐉:小子,仔细看上图案例,Swag函数中的 形参x,y 的地址与 实参a、b 的地址不同,我们仅仅只是交换了Swag函数内的两值,对于a,b其实并未改变
🐉:也就是说:当函数调用时候,实参传给形参,形参其实是实参的一份临时拷贝;所以对形参的修改,不会影响实参
L:那么如何修改呢?
🐉:我们只要通过指针将实参a、b 地址传递给 形参x,y ,再通过解引用操作就可以成功啦
✅正解(传址):
#include<stdio.h> void Swag(int* pa, int* pb)//void表示没有返回值 { int tmp = 0; tmp = *pa;//用解引用操作 *pa = *pb; *pb = tmp; } int main() { int a = 10; int b = 20; int tmp; printf("a=%d b=%d\n", a, b); Swag(&a, &b); printf("a=%d b=%d\n", a, b); return 0; }
✨✨✨我是分割线✨✨✨
🔥3. 函数的参数
🌕3.1 实际参数(实参)
- 真实传给函数的参数,叫实参。
- 实参可以是:常量、变量、表达式、函数等。
- 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
🌕3.2 形式参数(形参)
- 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
- 形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
- 当实参传给形参的时候,形参其实是对实参的一份临时拷贝,对形参的修改是不会改变实参的(例如交换数字案例中的错解)
- 上面 Swap1 和 Swap2 函数中的参数 x、y、px、py 都是形式参数。
- 在main函数中传给 Swap1 的 num1 、num2 和传给Swap2 函数的 &num1 、&num2 是实际参数。
·这里可以看到 Swap1 函数在调用的时候, x 、 y 拥有自己的空间(传值调用),同时拥有了和实参一模一样的内容。 所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
✨✨✨我是分割线✨✨✨
🔥4. 函数的调用
🌕4.1 传值调用
- 函数的形参和实参分别占有不同内存块(内存地址不同),对形参的修改不会影响实参。
void Swag(int x, int y)//void表示没有返回值 { int z = x; x = y; y = z; }
🌕4.2 传址调用
- 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
- 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
void Swag(int* pa, int* pb)//void表示没有返回值 { int tmp = 0; tmp = *pa;//用解引用操作 *pa = *pb; *pb = tmp; }
🔥函数调用案例
🌙1.判断一个函数是不是素数
#include<stdio.h> #include<math.h> int is_prime(int n)//是素数返回1.不是素数返回0 { for (int j = 2; j <= sqrt(n); j++)//写出优化的方式sqrt(n) { if (n % j == 0)//被j整除 return 0; } return 1; } int main() {//打印100-200之间的素数 int i = 0; for (i = 100; i <= 200; i++)//产生100-200的数 { //判断i是否为素数 if (is_prime(i) == 1) printf("%d\n", i); } }
🌙2.写一个函数判断是不是闰年
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> //是闰年返回1,不是闰年返回0 int is_leap_year(int x) { if(((x % 4 == 0) && (x % 100 != 0))||(x % 400 == 0)) return 1; else return 0; } int main() { int count = 0; int y = 0; for (y = 1000; y <= 2000; y++) { //判断y是不是闰年 if (is_leap_year(y)) { count++; printf("%d\n", y); } } printf("%d个闰年\n", count); return 0; }
🌙3.写一个函数,实现一个整形有序数组的二分查找。
重点:数组传递到形参时,传递的是首元素地址
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int binary_search(int arr[], int k,int num) { //用折半查找 //int num = sizeof(arr) / sizeof(arr[0]); // 指针大小== 4 / 4 =1 int left = 0; int right = num - 1; do { int mid = (left + right) / 2; if (arr[mid] < k) { left = mid + 1; } else if (arr[mid] > k) { right = mid - 1; } else return mid;//找到了 }while(left<=right); return -1; } //如果找到了就返回下标 //找不到就返回-1 int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int k = 7; int num = sizeof(arr) / sizeof(arr[0]); //数组arr传给binary_search函数的时候,其实传的是arr首元素的地址 int ret =binary_search(arr,k,num); if (-1 == ret) printf("找不到\n"); else printf("找到了,下标是%d", ret); return 0; }
🌙4.写一个函数,每调用一次这个函数,就会将num的值增加1
·法1.需要改变实参——传址
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> void add(int* p) { ++*p; } int main() { int num = 0; add(&num); printf("%d", num); add(&num); printf("%d", num); add(&num); printf("%d", num); return 0; }
·法2.不使用传参,采取频繁赋值
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int add(int m) { return m + 1; } int main() { int num = 0; num=add(num);//频繁赋值 printf("%d", num); num = add(num); printf("%d", num); num = add(num); printf("%d", num); return 0; }
✨✨✨我是分割线✨✨✨
🔥函数返回类型
- void-无返回(可以使用return;来结束void函数)
- char int float-返回值为其对应类型
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> void test1() { int n = 5; printf("hehe\n"); if (n == 5) return;//void里用return;来返回 //可以提前使函数终止 printf("haha\n"); } int test2() { return 1; } int main() { test1(); return 0; }
✨✨✨我是分割线✨✨✨
🔥5. 函数的嵌套调用和链式访问
🌕5.1 嵌套调用
·函数可以嵌套调用;不可以嵌套定义//函数不能嵌套定义 void test1() { void test2() { } } //但是可以嵌套调用 void test1() { test2(); }
🌕5.2链式访问
·把一个函数的返回值作为另外一个函数的参数
int main() { int len = strlen("abc"); printf("%d\n", len); printf("%d\n", strlen("abc")); //把strlen函数的返回值作为printf函数的参数 return 0; }
🌕3.习题
·考察链式访问——把一个函数的返回值作为另外一个函数的参数
int main() { printf("%d", printf("%d", printf("%d", 43))); //结果是啥? //注:printf函数的返回值是打印在屏幕上字符的个数 return 0; }
前情提要:了解printf函数的返回值
🔥6. 函数的声明和定义
🌕6.1函数定义
·函数的定义是指函数的具体实现,交待函数的功能实现。·解决办法:(添加一个函数声明)
ADD(int x, int y);//函数声明 int main() { int a = 10; int b = 20; int ret = ADD(a, b); printf("%d", ret); return 0; } int ADD(int x, int y) { int z = x + y; return z; }
🌕6.2函数声明
1.告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。
2.函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3.函数的声明一般要放在头文件中(.h文件中)的。
·本质上的应用:剥离出来
//add.h 放函数声明 #include<stdio.h> int ADD(int x, int y);
//add.c 放函数定义 #include"add.h" int ADD(int x, int y) { int z = x + y; return z; }
//test.c 主函数 #include"add.h" int main() { int a = 10; int b = 20; int ret = ADD(a, b); printf("%d", ret); return 0; }
上述中把 add.c 和 add.h 合在一起合称加法模块
🌙公司写代码,会不会把所有代码都写在test.c中呢?
1.分函数模块来写!效率大大提高
2.可以使用静态库,出售函数
#define _CRT_SECURE_NO_WARNINGS 1 #include"add.h" #include<stdio.h> #pragma comment(lib,"add.lib")//使用静态库 int main() { int a = 10; int b = 20; printf("%d", ADD(10, 20)); return 0; }
✨✨✨我是分割线✨✨✨
🔥7. 函数递归
🌕7.1 什么是递归?
·程序调用自身的编程技巧称为递归( recursion)
·函数递归:函数自己调用自己
·递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
·递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
🍊练习1.接收一个整型值(无符号),按照顺序打印它的每一位
·例如: 输入:1234 ;输出 1 2 3 4
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> Print(unsigned int n) { if (n > 9) { Print(n / 10);//Print(123) 1 2 3 } printf("%d\t", n % 10);// } int main() { unsigned int num = 0; scanf("%d", &num);//顺序打印每一位 Print(num); }
作图理解:
🌕7.2 递归的两个必要条件
1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
🍓Tip1.每一次函数调用都会在内存的栈区申请一块内存空间
·系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出
🍓Tip2.stackoverflow-程序员版的知乎
🍊练习2.编写函数不允许创建临时变量,求字符串的长度
·解法1.使用临时变量(不满足本题要求)
int my_strlen(char* s)//s相当于指向字符串中的 a { int count = 0;//临时变量 while (*s != '\0') { count++; s++;//char* 的指针是一个字节 //地址序列+1就可以跳到下一个字符 //如果是一个int型的指针 跳到下一个字符要+4 } return count; } int main() { //求字符串长度 char arr[] = "abc"; int len = my_strlen(arr); //arr里放着 a b c \0 //arr是数组名 数组名是数组首元素的地址 printf("%d", len); return 0; }
·解法2.使用函数递归
思路: my_strelen("abc"); 1+my_strelen("bc"); 1+1+my_strelen("c"); 1+1+1+my_strelen("\0"); 1+1+1+0=3
int my_strlen(char* s)//s相当于指向字符串中的 a { int count = 0; while (*s != '\0') { count++; s++;//char* 的指针是一个字节 //地址序列加1就可以变到下一个字符 } return count; } int my_strlen(char* s)//s里面放的是a的地址 { if (*s == '\0') return 0; else return 1 + my_strlen(s + 1);//s+1就是b的地址
笔记理解:
🍊练习3. 求n的阶乘(不考虑溢出)
·解法1.循环解法
int main() { int n = 0; int ret = 1; scanf("%d", &n); for (int i = 1; i <= n; ++i) { ret *= i; } printf("%d", ret); return 0; }
·解法2.递归法
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", ret); return 0; }
🍊练习4.求第n个斐波那契数(不考虑溢出)
·解法1.递归
int count = 0; int fib(int x) { if (x == 3)//看看计算3这步需要多少次 count++; if (x <= 2) return 1; else return fib(x - 1) + fib(x - 2); } int main() { int n = 0; scanf("%d", &n); int ret = fib(n); printf("第%d个斐波那契数是%d\n", n,ret); return 0; }
·解法2.循环方法(从前往后)
int fib(int x) { int a = 1; int b = 1; int c = 1; while (x>2) { c = a + b; a = b; b = c; x--; } return c; } int main() { int n = 0; scanf("%d", &n); int ret = fib(n); printf("第%d个斐波那契数是%d\n", n,ret); return 0; }
🍓Tip3.补码数的正溢出