C语言复习第3章 函数

目录

  • 一、函数介绍
    • 1.1 函数是什么
    • 1.2 C语言中函数的分类
    • 1.3 函数原型
    • 1.4 高内聚 低耦合
    • 1.5 C语言main函数的位置
  • 二、函数的参数
    • 2.1 实参和形参
    • 2.2 函数的参数(实参)可以是表达式
    • 2.3 传值与传址(swap函数)
    • 2.4 明确形参是实参的临时拷贝
    • 2.5 void(如果不写函数返回值 默认是int)
    • 2.6 有返回值的函数一定要都return
    • 2.7 一道关于不写返回值的例题
    • 2.8 在函数中 尽量少使用全局变量
    • 2.9 当参数是`数组名`的时候 不能在函数内部计算元素个数
    • 2.10 钻一个牛角尖:其实通过形参改变实参这句话是不严谨的
  • 三、函数的调用与练习
    • 3.1 传值调用和传址调用
    • 3.2 判断一个数是不是素数
    • 3.3 判断一年是不是闰年
    • 3.4 实现一个整形有序数组的二分查找
    • 3.5 写一个函数 每调用一次这个函数 就会将num的值增加1
  • 四、函数的嵌套调用和链式访问
    • 4.1 函数可以嵌套调用 但不能嵌套定义
    • 4.2 链式访问
    • 4.3 printf()函数的返回值
  • 五、函数的声明和定义
    • 5.1 函数声明和定义
    • 5.2 保证先声明(定义)后使用
    • 5.3 实际场景下的函数的声明和定义
    • 5.4 模块化编程的优势
  • 六、 函数递归与经典习题
    • 补充:int的取值范围
    • 6.1 什么是递归
    • 6.2 递归的两个必要条件
    • 6.3 死递归导致栈溢出(StackOverFlow)
    • 6.4 按顺序打印一个无符号整型值的每一位
    • 6.5 不允许创建临时变量 求字符串的长度
    • 6.6 怎么找递归出口
  • 七、迭代与递归
    • 7.1 求n的阶乘 注意0!=1
    • 7.2 求第n个斐波那契数
      • 递归法(容易栈溢出)
      • 迭代法
    • 7.3 如何选择递归与迭代
    • 7.4 函数栈帧简介
  • 八、两个经典例题
    • 8.1 青蛙跳台阶(动态规划的基本认识)
      • 问题描述
      • 递归
      • 动态规划
      • 补充:迭代法
    • 8.2 汉诺塔
      • 问题描述
      • 思路
      • 代码实现
      • 思考:怎么判断一共要移动几次?(时间复杂度?)
  • 九、函数栈帧的创建和销毁
    • 压栈和出栈
    • 9.1 说明
    • 9.2 寄存器及栈区的使用方式
    • 9.3 main函数其实也是被其他函数调用的
    • 9.4 F10之后 转到反汇编开始观察
    • S1:push ebp 压栈
    • S2:mov ebp,esp
    • S3:sub esp,0E4h->开辟main函数的函数栈帧
    • S4:三次push
    • S5:lea加载有效地址
    • S6:默认初始化main函数的栈帧空间
    • S7:开始给a分配空间(知道为什么局部变量不初始化是随机值了)
    • S8:继续给b c分配空间
    • 总结1:怎么创建局部变量的?
    • 9.5 开始观察Add()
    • A1:先传参b
    • A2:再传参a
    • 总结2:怎么传参的?(怎么和想象中不太一样?)
    • A3:call-调用函数(理解函数是怎么返回的)
    • 总结3 可以认为形参是在main函数的栈帧里创建的
    • A4:进入Add之后 类似main 先开辟栈帧 然后初始化
    • A5:给Add函数的局部变量z开辟空间
    • A6:执行z = x + y
    • A7:临时保存计算结果
    • A8:三次pop
    • A9:回收Add函数的栈帧
    • A10:继续往下执行(之前存放的call指令下一条指令的地址派上用场了)
    • A11:销毁形参
    • A12:把A7的计算结果真正给到c
    • 总结4:形参并不是是在Add函数内部创建的
    • 总结5:Add()函数的整个调用流程
    • 总结6:形参确实是实参的临时拷贝
    • 总结7:什么叫销毁?
    • 最后的总结

一、函数介绍

1.1 函数是什么

在计算机科学中 子程序(函数 function) 是一个大型程序中的某部分代码 由一个或多个语句块组成
它负责完成某项特定任务 而且相较于其他代码 具备相对的独立性
一般会有输入参数并有返回值 并封装了函数的过程 隐藏了实现细节
这些代码通常被集成为软件库(库函数)

1.2 C语言中函数的分类

  • 分为库函数 和 自定义函数

库函数的好处在这里插入图片描述

常见的库函数
在这里插入图片描述

1.3 函数原型

  • 函数名 参数 返回值
    在这里插入图片描述

1.4 高内聚 低耦合

  • 让函数的功能 尽量独立 解耦
  • 各模块之间尽量相互独立

1.5 C语言main函数的位置

  • 第一章说了 main函数必须要有 有且仅有1个
  • 对于位置是没有要求的
    在这里插入图片描述

二、函数的参数

2.1 实参和形参

  • 不管什么形式的形参 函数调用时 都要有确定的值
  • 函数调用过程中才会给形参分配内存空间 不调用函数的时候 他们就是一段代码
  • 形参是在函数调用的时候才会实例化 才开辟内存空间
  • 出函数的时候 形参就自动销毁了 形参只在函数中有效 和局部变量类似 所以他们都在栈区
  • 形参实例化之后 相当于是实参的一份临时拷贝

实际参数(实参)
在这里插入图片描述

形式参数(形参)
在这里插入图片描述

2.2 函数的参数(实参)可以是表达式

函数的参数可以是表达式 也就是()括起来的 所以下面只有四个参数
前面两个是逗号表达式 表达式的值才作为参数
在这里插入图片描述

2.3 传值与传址(swap函数)

  • 其实我可以说:都是传值 都是临时拷贝 只不过一种值是数值 一种值是地址

值传递:
a b是实际参数 x y是形式参数
形参会申请自己的内存空间 实参ab和形参xy使用的不是同一空间
本代码仅仅交换了形参x y
在这里插入图片描述
在这里插入图片描述

址传递:
其实形参仍然是实参的临时拷贝 pa就是&a的一份临时拷贝 只不过pa和&a 都指向同一块内存空间 所以会影响到a的值
在这里插入图片描述

2.4 明确形参是实参的临时拷贝

2.3的图解 x y的确是形参的临时拷贝
在这里插入图片描述

下图 传参传的是&a 也就是a的地址
形参pa 照样是实参的一份临时拷贝 如下图 pa里放的是一个地址
只不过因为pa是地址 所以通过pa 可以找到a
但是再怎么折腾形参pa 也不会影响到实参&a
解引用*pa其实是远程影响到了a
在这里插入图片描述

再来看这个案例 我图片上说的有些复杂了 别看
其实本质就在于:形参一直都是实参的临时拷贝 这里是把a和b的地址临时拷贝给pa和pb了
然后我只是交换了形参pa和形参pb的值 所以*pa找到b *pb找到a 函数里看上去是交换了 倒不如说我先打印的b 再打印的a
只要能理解"临时拷贝" 这里就很好理解 总感觉语言难以描述 但是想很容易想清楚
在这里插入图片描述

2.5 void(如果不写函数返回值 默认是int)

  • 写了void 就表示 不需要参数 不要传参;没有返回值 不需要返回!!
  • 不要去钻没有意义的牛角尖!!
  • 作为写函数的人 如果你希望函数没有返回值或者不需要传参 就写上void
  • 作为函数调用者 你看到了void 就遵守这个规则 不要钻牛角尖!!
  • 函数的返回类型不写的话 默认是int类型 如果不需要返回值请写上void
    在这里插入图片描述

下面我钻一下牛角尖:
下面的研究没啥意义 但凡规范一点 啥事没有 函数切忌写的模棱两可

如果函数没有规定形参的类型 不管传递什么 反正都不要 可以执行下去
在这里插入图片描述

如果函数规定形参是void 但还是传了个参数 不会报错 但是会报警告 无论如何 就算不报错 既然写了void 就不要传参给void
在这里插入图片描述

不规定函数的返回类型 不代表不返回任何东西 而默认返回int
当你不需要返回任何东西的时候 请明确定义为void test() 规范!!!
在这里插入图片描述

2.6 有返回值的函数一定要都return

  • 下面这个代码 假如我输入1 会打印yes 显然错误
  • 错误的原因就在于 如果是闰年 返回1 如果不是闰年呢? 没写返回值!(未定义行为!不一定会返回什么)
int is_leap_year(int year)
{if ((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0)))return 1;
}int main()
{int i = 0;scanf("%d", &i);int test = is_leap_year(i);if (is_leap_year(i) == 1)printf("yes\n");elseprintf("no\n");return 0;
}
  • 所以也要学会看警告
    在这里插入图片描述

  • 具体错误可能在于 编译器借用了是闰年的return 1 因为我没有定义不是闰年返回什么 所以他也给我返回1

  • 反正编译器报警告了 也要学会看警告
    在这里插入图片描述

2.7 一道关于不写返回值的例题

下面这个代码 不写返回值 默认返回的int 但是代码里并没有return(未定义行为)
这时候返回的值取决于编译器的实现 大多数时候返回的是该函数执行完最后一条指令产生的结果
最终test()返回的其实就是printf的返回值—>也就是打印的字符的个数(这是VS2022的测试结果)
在这里插入图片描述

2.8 在函数中 尽量少使用全局变量

  • 假如前面的2.3 把a b定义成全局变量 确实怎么都可以交换a b
  • 但是 全局变量本身很危险 谁都可以用 跨文件只要声明了 也能用 很危险
  • 其次 我们说函数有一个特点就是"模块化" 他仿佛是一个独立的模板 实现某个特点的功能 如果函数依赖全局变量 仿佛就失去了"独立性" 而依赖全局变量了

2.9 当参数是数组名的时候 不能在函数内部计算元素个数

如果在函数内部用sizeof(arr)/sizeof(char) 结果恒为4/8
因为不论传参的形式是下面哪一种 本质是就是一个指针
既然在函数里 ch一定是一个指针 那么sizeof(指针) 就一定是4或者8
在指针那一章节已经详细论述过
在这里插入图片描述

2.10 钻一个牛角尖:其实通过形参改变实参这句话是不严谨的

总之是有点钻牛角尖了
如果是出现在选择题中 心中有谱就行
主要就是区分传值和传址的区别和共同点

之前提到:
test(&a) 这种传址调用的方式
仿佛可以通过形参改变实参
下面这句话似乎是对的 但是不够严谨
在这里插入图片描述

但是:

  1. 这里的实参是&a 也就是a的地址 实参不是a!!
  2. 形参其实还是实参的一份临时拷贝 也就是形参是&a的临时拷贝
  3. 如此一来 我们说:通过形参和实参 都能够找到同一个a 于是,通过形参可以远程操作a(但a严谨说来并不是实参)
  4. 所以更严谨的说 通过形参可以改变实参所指向的对象 而不是实参本身
    test(&a) 通过形参也就是a的地址 怎么去改变&a? 难道能把a的地址给换了?
    在这里插入图片描述
    在这里插入图片描述

严格来说 形参一直都是实参的临时拷贝 两者是独立的 只不过有的时候拷贝的是值 有的时候是地址
形参不可能直接改变实参本身 他们都是独立的空间 严格来说应该是: 不管传址调用还是传值调用 改变形参不影响真正的实参本身
下面这句话肯定是对的 但不全面
在这里插入图片描述

三、函数的调用与练习

3.1 传值调用和传址调用

传值调用
在这里插入图片描述

传址调用
在这里插入图片描述
说明:
下面的练习的主要思路在考研C语言程序设计_编程题相关(持续更新)一文写过 本文主要是练习函数的写法

3.2 判断一个数是不是素数

  1. 一开始忘记#include<math.h>了 一直不打印 找了半天的错…
  2. return比break更彻底
#include<math.h>
#include<stdio.h>
#include<string.h>int isPrime(int num)
{//num是素数就返回1 否则返回0int j = 0;for (j = 2; j <= sqrt(num); j++){if (num % j == 0)return 0;}//因为这是return 所以能return1 肯定没有return0过return 1;
}
int main()
{int i = 0;for (i = 100; i <= 200; i++){if (isPrime(i))printf("%d ", i);}return 0;
}

3.3 判断一年是不是闰年

int isLeapYear(int year)
{return (year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0));
}
int main()
{int i = 0;for (i = 1000; i <= 2000; i++){if (isLeapYear(i))printf("%d ", i);}return 0;
}

3.4 实现一个整形有序数组的二分查找

这里犯了如下错误:

  1. mid = (left + right) / 2; 没有写在循环里!! 所以说 以后写while 别一上来就while 先把逻辑写好 再cv到while里!!!
  2. return下标mid 不是return k k要你return了干嘛的?
  3. 不能在函数里用sizeof(arr)/sizeof(int)求len 经典的错误 这个结果永远都是1或者2(取决于几位机器) 在函数里sizeof(arr)求的是一个指针变量的大小
    在这里插入图片描述
  4. 我的思路是 找到了返回下标 找不到就返回0 这个设计就是错的 假如我要找的下标正好是0呢?
    更正:既然找到了是返回下标 下标肯定是≥0 那找不到就返回-1
  5. 还有 前提一定是有序数组啊 我一开始又给了无序的测试数组....
  6. 数组传参也可以写成int arr[ ] 但是本质还是int* arr 而且arr[1]本质上是*(arr+1)
//把找到的下标返回 找不到就返回-1
//也可以写成int arr[] 但是本质上是int* arr
int BinarySearch(int* arr, int k, int len)
{int left = 0;int right = len - 1;int mid = 0;//能不能= 想不通 举个例子不就想通了 //都指向同一个元素的时候 肯定还要再求一个mid去判断一下的 肯定要写=while (left <= right){mid = (left + right) / 2;if (arr[mid] < k)left = mid + 1;else if (arr[mid] > k)right = mid - 1;elsereturn mid;//返回的是下标 不是k}return -1;
}int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int len = sizeof(arr) / sizeof(arr[0]);int k = 10;int index = BinarySearch(arr, k, len);if (index != -1)printf("找到了 下标是:%d\n", index);elseprintf("查无此人\n");return 0;
}

3.5 写一个函数 每调用一次这个函数 就会将num的值增加1

本解法主要就是要知道传址调用的意义 此外还要注意一下优先级的问题

  • *pnum = *pnum + 1; ok
  • *(pnum)++; ok
  • *pnum++;no no no
  • *pnum++ 等价于 *(pnum++) 因为++的优先级高 先应用于pnum 而且是后置++ 这么写相当于每次进去函数就解引用pnum 拿到num 然后啥也不干 再对pnum自增 让他成为野指针
  • 关于优先级的问题 在第5章操作符会详细讨论
void func(int * pnum)
{//*pnum++ err(*pnum)++;
}int main()
{int num = 10;func(&num);printf("%d\n", num);func(&num);printf("%d\n", num);func(&num);printf("%d\n", num);return 0;
}

还有一种思路 可以不用指针
在这里插入图片描述

四、函数的嵌套调用和链式访问

4.1 函数可以嵌套调用 但不能嵌套定义

  • 下面就是嵌套调用
#include <stdio.h>void new_line()
{printf("hehe\n");
}void three_line()
{int i = 0;for(i=0; i<3; i++){new_line();}
}int main()
{three_line();return 0;
}

函数可以嵌套调用 但是不能嵌套定义
main函数也是个函数
在这里插入图片描述

4.2 链式访问

  • 链式访问:把一个函数的返回值作为另外一个函数的参数
  • 这就是一种链式访问 把strlen的返回值 作为printf的参数
  • 这里要注意 先执行里面一层的函数(strlen) 再执行外面的
    在这里插入图片描述

4.3 printf()函数的返回值

  • printf的返回值是打印的字符的个数 比如用%d打印34 34有俩字符 3和4 返回值就是2
    在这里插入图片描述

  • 4.2说到了 先执行里面一层的函数 再执行外面的

  • 所以打印顺序是:图上面写错了写错了 是打印4321
    在这里插入图片描述

五、函数的声明和定义

5.1 函数声明和定义

  1. 声明可以告诉编译器 有一个函数叫什么 参数是什么 返回类型是什么 但是具体是不是真的存在这个函数 函数声明决定不了
  2. 函数的声明一般出现在函数的使用之前 要满足先声明后使用
  3. 函数的声明一般要放在头文件中
  4. 函数的定义是指函数的具体实现 交待函数的功能实现

5.2 保证先声明(定义)后使用

  • 编译的时候 是从前往后扫描代码的 先用后定义 会报一个警告
    在这里插入图片描述

  • 声明一下 就不会报警告了 满足先声明后使用

  • 函数声明里的x y 可以省略
    在这里插入图片描述

  • 其实函数的定义也算是一种特殊的声明 既然说要先声明后使用 那建议定义的时候直接定义在使用之前
    在这里插入图片描述

  • 变量的声明和定义 在第一章已经提到了 也是类似的规则
    在这里插入图片描述

  • 虽然规则允许 但是为什么要这样写代码??? 这不是**吗 直接定义在前面不就行了
    在这里插入图片描述

5.3 实际场景下的函数的声明和定义

● add.h:函数的声明
● add.c:函数的实现(定义)
● test.c:测试
在这里插入图片描述

5.4 模块化编程的优势

  • 每个程序员分别去写自己的模块 模块之间相互独立 需要用到的时候调用一下即可
  • 其实 之前已经提到过 我在test.c里extern int Add(int x,int y); 就可以跨文件使用add.c里的Add()了 为什么非要写一个add.h 然后include<“add.h”>呢?->这样写可以做到代码的隐藏

假设我希望把我写的程序卖给别人用 但是不希望别人知道我的源码
以add这个功能为例 我就可以写add.h+add.c 两个文件 然后编译生成一个静态库add.lib
在这里插入图片描述
这个文件就是.h和.c编译所生成的 是无法通过.lib看到源码的
然后就把add.lib和add.h卖给别人 别人只知道函数长什么样 怎么用 但是不知道函数的具体实现
在这里插入图片描述

买家导入静态库 就可以使用这个函数的功能了 但是不知道源代码
在这里插入图片描述

六、 函数递归与经典习题

补充:int的取值范围

  • 这个知识点会在后续第九章:数据的存储 详细介绍
  • 这里就暂时记住如果是有符号int最大是21亿多 如果是无符号int最大是42亿多
  • 下面的题目暂时不考虑计算结果超过了42亿这种情况 而是学习递归的思想

6.1 什么是递归

  • 程序或函数直接或间接调用自身的编程技巧称为递归(recursion)
  • 递归通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
  • 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算 大大地减少了程序的代码量
  • 递归的主要思考方式在于:大事化小 找到子问题+递归函数的出口

这就是一个递归 但是是死递归
在这里插入图片描述

6.2 递归的两个必要条件

  • 存在限制条件 当满足这个限制条件的时候 递归便不再继续 即递归要有出口
  • 每次递归调用之后 越来越接近这个限制条件
  • 我个人理解的递归是:
    1. 能判断出一个问题确实是有N个类似的子问题组成 明确子问题是怎么样的
    1. 假设出递归函数实现的功能 并顺着思路利用这个函数 写出代码
    1. 能确定递归出口(明确在什么条件下才继续"递" 否则就开始"归")
    1. 每次函数调用都会更接近出口 在这种条件下 就可以用递归求解

6.3 死递归导致栈溢出(StackOverFlow)

在这里插入图片描述

6.4 按顺序打印一个无符号整型值的每一位

接收一个无符号整型值
按照顺序打印它的每一位
例如:
输入:1234
输出:1 2 3 4

  • 确定子问题 假设PrintfNum()的功能是打印参数的每一位
  • PrintfNum(1234) =
    PrintfNum(123) + 打印4 =
    PrintfNum(12) + 打印3 + 打印4 =
    PrintfNum(1) + 打印2 + 打印3 + 打印4(达到递归出口) =
    打印1 + 打印2 + 打印3 + 打印4
  • 确定递归结束条件:要打印的数字只剩下个位了 也就是参数<10
    也就是说参数>=10(或者>9)的时候 继续递归
void PrintNum(int num)
{//printf("%d ", num % 10); 写在前面就是倒序打印if (num > 9)//or if(num / 10 > 0)PrintNum(num / 10);//子问题//写在这里才是顺序打印printf("%d ", num % 10);
}
int main()
{int num = 2311516;PrintNum(num);return 0;
}
  • 如果尝试迭代法的话 发现:只能倒序打印
    在这里插入图片描述

  • 看一下这道题一个错误的写法 多写了else:
    在这里插入图片描述

6.5 不允许创建临时变量 求字符串的长度

解法1:迭代法
遗憾的是 用到了临时变量count

int MyStrlen(char* str)
{int count = 0;while (*str != '\0'){count++;str++;}return count;
}

解法2:递归法 但是还是创建了临时变量
这是我一开始想到的思路 但是没注意到不创建临时变量
其次 我一开我把int len = 0;定义在了下面注释的地方 导致每次长度都会被清零 最终结果永远是1

int len = 0;
int StrLen(char* str)
{//int len = 0;if (*str != '\0'){StrLen(++str);len++;}return len;
}

解法3:递归法 而且不创建临时变量
在这里插入图片描述

注意 不要忘记return 0
如果没有这个return 0 当最后一次计算MyStrLen(‘\0’)的时候 发现根本无路可走 成了未定义行为了
而且不写return 0 不仅是语法上的错误 更是逻辑上的错误
本身本题的递归出口就是:遇到\0 就要返回0了
也就说每次传参都离\0越来越近 直到真的传了\0的地址进去 解引用发现是\0 就返回0 并从此刻开始"归"
在这里插入图片描述

参考代码:

int MyStrlen(char* ch)
{//hello 1+ello 1+1+llo 1+1+1+lo 1+1+1+1+o 1+1+1+1+1+0if (*ch == '\0')//递归出口return 0;elsereturn 1 + MyStrlen(++ch);
}int main()
{char ch[] = "hello";printf("%d ", MyStrlen(ch));return 0;
} 

或者这么写:
只要每次都传入str+1 离递归出来越来越近就ok
千万不可以写成后置++了 不然每次传递的都是首地址 死递归了

int MyStrLen(char* str)
{if (*str != '\0')return 1 + MyStrLen(str+1);elsereturn 0;	 
}
  • 写成后置++ 如下图 就死递归了(看下面的函数栈帧)
  • 第一次调用MyStrlen(ch++) 后置++ 先使用 再++ 但是+的是这个函数栈帧的ch
  • 第二次调用MyStrlen(ch++) 这个ch接收的是第一次先使用传进来的地址 虽然参数的名字是一样的 但是各自在不同的函数栈帧 也是后置++ 也是先使用 本质上都是同一个地址
  • 每次递归调用的时候 虽然形参名字都叫ch 但其实是不同的ch 都是一份临时拷贝
  • 所以每次都是首地址 永远不会接近递归出口 导致死递归

在这里插入图片描述

6.6 怎么找递归出口

我认为这个思想很重要 所以写了一小节
如果用递归
那么我每次都会把原问题 转换成一个相同逻辑的子问题+其他操作
我必须要思考 什么时候这个子问题就成最小的 无法再分裂出子问题了?
6.4:当只剩下个位数的时候 无法再分裂了 所以递归出口就是参数是个位数
6.5:当成空字符串了 就无法再分裂了 所以递归出口就是*str指向\0 这个时候要返回0 并开始"归"

七、迭代与递归

7.1 求n的阶乘 注意0!=1

  • 一个注意点:0!=1
  1. 函数从哪里调用 return就回到那里去继续往下执行
  2. 理解代码执行流程:函数A里在SP处调用函数B 从SP处开始函数A就"暂停"了 直到函数B执行完 再回到SP处继续执行A
  3. 假如求factorial(10000) 递归会栈溢出 迭代算的不对(超过int范围)
  4. 假如递归的重复计算太多了 导致栈溢出 可以考虑用迭代 迭代最多算的不对 栈溢出可是程序崩溃了
int factorial_rec(int num)//递归
{//5!=5*4!=5*4*3!.... //分裂到1! 不可以再分出子问题 就可以归了if (num == 1 || num == 0)//or if(num<=1)return 1;elsereturn  num * factorial_rec(num - 1);
}int factorial_ite(int num)//迭代
{int i = 0;int ret = 1;for (i = 1; i <= num; i++){ret *= i;}return ret;
}int main()
{//递归求n的阶乘int num = 0;printf("%d ", factorial_rec(num));return 0;
}

7.2 求第n个斐波那契数

递归法(容易栈溢出)

有了前面的铺垫 可以轻易写出这个的递归算法 但是代码简单就一定好吗?
NO!! 递归的写法 假如算fib(50) 算的特别慢 但并没有栈溢出 一直在算 因为发生了大量的重复计算
如下图 递归求第四十个fib数 第三个fib数被重复计算了快四千万次!!
在这里插入图片描述

int fib_rec(int index)
{if (index == 1 || index == 2)return 1;else //也就是求第3+个fibreturn fib_rec(index - 1) + fib_rec(index - 2);
}

迭代法

迭代法的思路:
要自己画图 找到关键步骤:
1 1 2 3 5 8 13 假设我要求第6个fib 也就是8
想象一下 一开始的时候有两个指针指向1和1 然后每次循环都让他们往前移动一位数
这两个指针指向的内容变化过程是:
1 1->1 2->2 3->3 5->5 8 这个时候 8就出现了(假如是一个人口算 那他肯定也是这么算的 逐渐往后加)
也就是第二个指针所指向的内容 那么直接把他返回即可

迭代法 求fib(10000) 虽然结果不对 因为结果用int接收的 但是算的很快
下面提供三种不同的写法 思路都是一样的 就是实现过程不一样

  • 写法1 for循环:
  • 这是我的写法 自己画图找规律发现 求第3个 要循环1次 求第index个 要循环index-2次
  • 能确定循环次数 于是决定用for循环
int fib_ite1(int index)
{//前两个直接定义出int first = 1;int second = 1;//计算得出第index个fib数 index>=3int index_num = 0;//循环结束条件是画图找规律得出for (int i = 1; i <= index - 2; i++){index_num = first + second;first = second;second = index_num;//second放的就是第index个fib}//直接返回就行了 如果index<3 返回的是我定义好的1和1//否则循环计算出来的return second;
}

写法2 while循环:
好像突然感觉到while和for的适用场景了
确定循环次数 用for; 当…时需要循环 用while 因为while不是还可以翻译成当....时候
这里就顺着我的思路写代码:前两个都初始化成1 当(while)要求第三个开始的fib数的时候 就进入循环计算得出 同时确定一下出循环的条件 就ok了
注意这里返回的是c 所以c必须初始化成1
返回b也一样 毕竟最后一次总会把b=c 但是如果返回b的话 c就不一定要初始化成1了
在这里插入图片描述

7.3 如何选择递归与迭代

  • 在调试factorial函数的时候 如果你的参数比较大 那就会报错:stack overflow(栈溢出)
  • 系统分配给程序的栈空间是有限的 但是如果出现了死循环 或者死递归 这样有可能导致一直开辟栈空间 最终产生栈空间耗尽的情况 这样的现象我们称为栈溢出
    在这里插入图片描述

那如何解决上述的问题:

  1. 将递归改写成非递归(迭代)
  2. 使用static对象替代nonstatic局部对象 在递归函数设计中 可以使用static对象替代nonstatic局部对象(即栈对象) 就是把本来占用栈空间的数据放到静态区 一定程度上可以缓解栈区的压力 但是这种操作的帮助是微乎其微的(而且static对象还可以保存递归调用的中间状态)
  • 递归用栈区开销换简洁性和可读性
    在这里插入图片描述

7.4 函数栈帧简介

  • 配合着本文的6.5思考:为什么MyStrlen(ch++)会导致死递归?
  • 函数栈帧的底层细节十分复杂 本文暂时不做详细论述 只需要知道一些基本的原理 帮助理解递归即可

问题描述:
也许会疑问:当n=2 最后一次调用Fac(n-1) 去执行Fac(1)
然后1返回给Fac(n-1) 继续执行return n*Fac(n-1)的时候 n为什么还是2?
在这里插入图片描述

因为每次函数调用 都会在栈区开辟各自的函数栈帧 他们都有各自的独立形参 互不影响
当Fac(1)调用结束(就是return了) 它的栈帧就销毁了
然后返回到Fac(2)的栈帧继续执行 它的栈帧里的n自然还是2 当Fac(2)调用结束 它的栈帧也销毁了
以此类推 栈帧会依次销毁
main函数执行完 也会销毁
所以说死递归的时候 一直在向内存的栈区申请函数栈帧 就栈溢出了
如下图 最终会开辟这么多函数栈帧 然后"归"的时候开始销毁
在这里插入图片描述

八、两个经典例题

8.1 青蛙跳台阶(动态规划的基本认识)

问题描述

一只青蛙
他可以一次跳一级台阶
或者一次跳两级台阶
假设青蛙要跳上n级台阶
请问一共有多少种跳法?
在这里插入图片描述

递归

  • 这道题和斐波那契数列十分类似 只不过斐波那契数列我们直接知道 第三个数开始 每个数就是前两个数之和
  • 而这道题 青蛙跳上一级台阶 只有1种跳法 ;跳上二级台阶 有2种跳法
  • 思考:跳上第n级(n>=3)呢?
  • 假设我给定一个函数 frogJumps(int steps) 假设它的功能就是:可以计算出青蛙跳到steps级台阶有多少种跳法
  • 假设青蛙第一次跳了1级 那么剩下的跳法就是frogJumps(n-1)
  • 假设青蛙第一次跳了2级 那么剩下的跳法就是frogJumps(n-2)
  • 所以所有的跳法就是frogJumps(n-1) + frogJumps(n-2) 当n>=3
  • 只要有了递推公式 递归的代码其实很容易写出来
int frogJumps(int steps)
{if (steps <= 2)return steps;elsereturn frogJumps(steps - 1) + frogJumps(steps - 2);
}int main()
{printf("%d ", frogJumps(1));printf("%d ", frogJumps(2));printf("%d ", frogJumps(3));printf("%d ", frogJumps(4));printf("%d ", frogJumps(5));return 0;
}

动态规划

利用本题正好可以介绍一下动态规划的基本思想 博主在后续的C语言专栏 会持续更新八大算法的思想和最基本的例题 希望对大家有所帮助~

本文在前面多次提到:
不管是用递归计算n!还是递归求第n个斐波那契数量
递归代码虽十分简洁 但是有一个很严重的问题:递归很容易产生大量的重复计算
但是实际上:

  • 在计算10!的时候就已经产生9! 8! 7!..了
  • 在计算第40个斐波那契数的时候 前面的斐波那契数其实也都已经计算过了
  • 那么 该怎么把这些已经计算过的子问题 给利用起来 帮助计算下一个子问题呢?

这就引出了动态规划的一个典型的思想:利用上一次的计算结果 快速计算出下一步的结果
下面可以看看动态规划会怎么计算这道题:

  • 台阶的级数n和几种跳法 其实是一 一对应的 --> 数组
    在这里插入图片描述
#define N 10
int main()
{int arr[N] = { 0 };arr[0] = 1;arr[1] = 2;//前两个子问题的解直接给出//从第三个子问题开始(i=2) 计算得出for (int i = 2; i < N; i++)arr[i] = arr[i - 1] + arr[i - 2];//当循环结束 其实数组里已经放好了1~N级台阶对应的跳法for (int i = 0; i < N; i++)printf("青蛙跳上%d级台阶有%d种跳法\n", i + 1, arr[i]);return 0;
}

补充:迭代法

  • 本文7.2类似
  • 只不过前两个数是1 2 所以无法直接return tmp 需要再加一个if判断
    参考代码:
int Fib(int index)
{//如果index 1 2 直接返回即可if (index <= 2)return index;//如果能走到这里 就说明index>=3了 那就计算求出第index个数(也就是index级台阶有几种跳法)int a = 1;int b = 2;int tmp = 0;while (index >= 3)//3级 循环1次 把tmp返回;4级 循环2次 把tmp返回....{tmp = a + b;a = b;b = tmp;index--;}return tmp;
}int main()
{printf("%d\n", Fib(1));printf("%d\n", Fib(2));printf("%d\n", Fib(3));printf("%d\n", Fib(4));printf("%d\n", Fib(5));printf("%d\n", Fib(6));printf("%d\n", Fib(7));printf("%d\n", Fib(8));printf("%d\n", Fib(9));printf("%d\n", Fib(10));return 0;
}

8.2 汉诺塔

问题描述

将塔A的柱子移动到塔C
要求:

  1. 大的柱子只能在小的柱子下面
  2. 一次只能移动一个柱子
    在这里插入图片描述

思路

这里的思路和本文6.2我个人理解的递归非常类似 首先核心思路是:
想把A上的n个柱子移动到C
S1:先把A上面n-1个柱子移动到B
S2:再把A剩下的1个柱子移动到C
S3:最后把B上的n-1个柱子移动到C

  1. 本题显然可以分裂出子问题 移动n个柱子从A到C移动n-1个柱子从B到C是类似的问题
    在这里插入图片描述
  2. 假设 void Hanoi(int n, char source, char auxiliary, char target)的功能是:
    把在source(源头塔)上的n个长方体 利用auxiliary(辅助塔) 移动到target(目标塔)
    下面看代码的时候 不要受参数名影响(B有时候作为目标塔 有时候作为辅助塔 这取决于传参的顺序)
    就这么理解:Hanoi()的功能是把第二个参数上的n个柱子 利用第三个参数 移动到第四个参数上去
  3. 递归的出口:只有一个柱子的时候 一个柱子直接移动就行了
  4. 每次调用都是一层一层往上 越来越接近1个柱子的情况
  5. move函数只是模拟了移动这个行为 move(‘A’,‘C’)就表示把A塔上第一个柱子移动到C塔上去

代码实现

假设出函数的功能 同时有了子问题如何分裂的思路之后
直接顺着这个思路写下去 那就对了!! 正如我6.2所说的!!

void move(char ch1, char ch2)
{printf("%c----->%c\n", ch1, ch2);
}void Hanoi(int n, char source, char auxiliary, char target)
{//如果只有1个柱子 直接移动到目标塔就行if (n == 1){move(source, target);}//不止1个柱子 接收到的参数是A B Celse//n>=2{//S1:先把A上面n-1个柱子移动到BHanoi(n - 1, source, target, auxiliary);//S2:再把A剩下的1个柱子移动到Cmove(source, target);//S3:最后把B上的n-1个柱子移动到CHanoi(n - 1, auxiliary, source, target);}
}int main()
{Hanoi(3, 'A', 'B', 'C');return 0;
}

思考:怎么判断一共要移动几次?(时间复杂度?)

  • 根据子问题的递推规则发现:
    n个柱子移动次数是1+2*(n-1个柱子的移动次数)
  • 进一步找规律发现:
    n个柱子需要移动2^n-1
  • 也就是说时间复杂度是O(2^n) 其中n是盘子的数量
    在这里插入图片描述

九、函数栈帧的创建和销毁

压栈和出栈

● push 压栈 在栈顶放一个元素
● pop esh 出栈 把栈顶的元素赋给esh
在这里插入图片描述

9.1 说明

在学习前面的内容 可能仍然存在很多困惑:

  • 局部变量是如何创建的?
  • 为什么局部变量的值不初始化就是随机值?
  • 函数是怎么传参的?传参的顺序?
  • 一直说形参是实参的临时拷贝 具体怎么实现的?
  • 调用一个函数 具体过程是什么?
  • 函数调用完 是怎么返回的?

在深入了解函数栈帧的创建和销毁之后 以上问题都会迎刃而解

注意:
在这里插入图片描述

本小节使用如下代码讲解:
在这里插入图片描述

9.2 寄存器及栈区的使用方式

  • 寄存器 是集成在CPU上的 和硬盘 内存之间是独立的
  • ebp esp看作指针变量 里面放的是地址
  • 正在调用哪个函数 ebp和esp维护的就是该函数的函数栈帧
    在这里插入图片描述
  • main函数被调用执行 也需要main函数的栈帧空间
  • 栈区的使用方式:先使用高地址 再使用地地址(仿佛从栈顶往里东西 沉到栈底了)
  • esp:栈顶指针
  • ebp:栈底指针
    在这里插入图片描述

9.3 main函数其实也是被其他函数调用的

  • 然后下面就要调用Add()函数了
    在这里插入图片描述

9.4 F10之后 转到反汇编开始观察

在这里插入图片描述

下面的Sn 就是在观察这部分代码怎么一步一步走的
可以对比观察
在这里插入图片描述

S1:push ebp 压栈

刚开始的样子:
在这里插入图片描述

push ebp:把ebp这个指针变量压栈 放在栈顶
与此同时 由于esp永远都指向栈顶
当ebp被压栈之后 esp也要往上走(也就是地址值会变低/变小) 直到他指向栈顶
在这里插入图片描述

S2:mov ebp,esp

也就是把esp的值赋给ebp 也就说这俩指针变量暂时指向同一块空间
在这里插入图片描述

S3:sub esp,0E4h->开辟main函数的函数栈帧

也就是esp = esp - 0E4h(八进制的228)
地址变小了 在图上 就是往上移动了 在这里插入图片描述

于是又维护了一个全新的函数栈帧 这其实就是main函数的栈帧
这其实就相当于:在栈区为main函数预开辟了一块空间
在这里插入图片描述

S4:三次push

之前说过了 esp永远指向栈顶
所以每次push一个元素到栈顶 esp就会往上移(其实是地址值变小)
在这里插入图片描述

S5:lea加载有效地址

先看看这个地址是啥 勾选上这个选项
在这里插入图片描述

原来是这样 下图圈起来的不是同一个意思吗?

在这里插入图片描述

就是把S3esp指向的地址赋给edi
在这里插入图片描述

S6:默认初始化main函数的栈帧空间

从edi这个位置开始(刚好到ebp结束)
把39h这么多个 dword(相当于四个字节)
全都初始化成CCCCCCCCh
在这里插入图片描述

相当于把main预开辟的空间都改成0xCCCCCCCh
在这里插入图片描述

S7:开始给a分配空间(知道为什么局部变量不初始化是随机值了)

这表明把0Ah这个值 也就是10么 放到ebp-8的位置
在这里插入图片描述
在这里插入图片描述

也就是说 不赋值的话 a那块空间放的就是S6默认初始化的CCCCCCC
CCCCCCCvs2013的初始化方式
不同的编译器可能不一样
所以我们说是 “随机值”
在这里插入图片描述

S8:继续给b c分配空间

有可能有的编译器就是连着分配的 vs2013中间就隔了八个字节
在这里插入图片描述
在这里插入图片描述

如下图:
在这里插入图片描述

总结1:怎么创建局部变量的?

调用函数
先给函数栈帧开辟空间
然后给这部分栈帧全都初始化成CCCCCCCCC
然后再给函数里的局部变量分配空间 如果不初始化 就是CCCCCCCC

9.5 开始观察Add()

类似9.4的操作 下面开始观察下图的过程
在这里插入图片描述

A1:先传参b

把ebp-14h里放的值(也就是十进制的20) 赋给eax
在这里插入图片描述

然后push eax 也就是把eax压栈
从栈顶放一个eax 也就是20
同时esp往上移动
在这里插入图片描述

A2:再传参a

和A1同理
把ebp-8这个位置的值 也就是十进制的10 也就是a了 赋给ecx
在这里插入图片描述

然后push exc
把10压栈
同时esp往上移动
在这里插入图片描述

总结2:怎么传参的?(怎么和想象中不太一样?)

A1 A2 就是在传参!
而且好像是b先传 a后传 也就是从右向左传!
和我们想象中的好像不太一样?
没错!! 继续往下看!!
在这里插入图片描述

A3:call-调用函数(理解函数是怎么返回的)

请记住下面这个地址

在这里插入图片描述

然后按F11
下图是按F11之前esp的值:
在这里插入图片描述
按完F11:
在这里插入图片描述

还记得前面让记住一个地址了吗
所以我按完F11 也就是执行call指令
不仅跳进去Add函数了 而且把call指令的下一条指令的地址 给压栈了
压栈之后 esp肯定要继续往上移的
在这里插入图片描述

思考:F11跳到Add函数里 执行完怎么回来?
所以才会把call执行下一条指令的地址先压栈
待会就通过它 等函数执行完毕 可以找回来
然后继续往下执行

总结3 可以认为形参是在main函数的栈帧里创建的

其实是先push
下图的exc eax 就可以看做形参
然后push完 esp又跑到上面去了
这个时候仍然可以认为esp和ebp维护的还是main的栈帧
所以main的栈帧貌似扩大了一点(从ebp到esp)
形参是在main的栈帧里的
可以这么理解 但是也可以认为形参是独立的空间
总之明白形参本质上不是在Add()函数的栈帧里创建的就对了! 因为这个时候还没调用Add呢!!
在这里插入图片描述

A4:进入Add之后 类似main 先开辟栈帧 然后初始化

在这里插入图片描述

此时epb还在维护main的栈底呢
所以一上来先push 把main函数的ebp压栈
同时esp肯定要继续往上移
这个动作就很像:我要开始开辟全新的栈帧了!!
在这里插入图片描述

后续的过程不再详述 和前面S1~S6完全一致 就是给Add函数开辟栈帧 完成初始化的
最终如下图:
在这里插入图片描述

A5:给Add函数的局部变量z开辟空间

下面的步骤和main函数里一开始int a b c是十分类似的
同样都是在ebp-8的位置开始开辟
不过这个时候的ebp已经被更新过了 现在维护的是Add函数的栈帧
都是在各种的空间开辟局部变量的内存空间

把ebp-8这个空间初始化成0 也就是z了 在这里插入图片描述
在这里插入图片描述

A6:执行z = x + y

x y是什么呢?? 先回看A1和A2
结合A1 A2 和 A5的图
ebp+8和ebp+12 就是往下找 正好找到了A1 A2的exc和eax

  1. 找到ebp+8 也就是A2的a(也就是10) 把10给eax
  2. 然后再给eax add上ebp+0Ch里的东西(也就是ebp+12) 也就找到了A1的b 也就是12
  3. 然后再把eax的值放到ebp-8里去 其实就是把相加的结果放到了abp-8(其实就是z的位置) 在这里插入图片描述
    最终如下图:
    z已经放好了计算结果30
    在这里插入图片描述

A7:临时保存计算结果

前面已经把eax里的计算结果放到ebp-8里了 其实就是z
然后又把ebp-8的值 也就是z的值(30)
临时放到了eax寄存器里(全局的寄存器) 在这里插入图片描述

A8:三次pop

注意pop出栈之后
esp由于一直要指向栈顶
也会一直往下移(其实是地址值增大了)
在这里插入图片描述
在这里插入图片描述

A9:回收Add函数的栈帧

在这里插入图片描述

执行完mov:
把ebp的值赋给了esp 这样Add的esp和ebp都指向了main之前的ebp
在这里插入图片描述

执行完pop:
也就是把栈顶的元素(main之前的ebp) 赋给了Add的ebp
每次pop完之后 esp也要下移
这样一来
Add的ebp走到一开始main的ebp的位置了
Add的esp恰好也是一开始main的esp的位置
总结来说就是 现在的esp和ebp 维护的又是之前main函数的栈帧了
在这里插入图片描述

A10:继续往下执行(之前存放的call指令下一条指令的地址派上用场了)

ret这个指令会:

  1. 先pop一下(也就是把栈顶的元素弹出 栈顶那个元素 就是call指令的下一条指令的地址) 同时esp下移
  2. 然后跳到刚刚pop的地址去 也就跳到call下面的执行 开始继续执行了
  3. 也就是说Add函数调用完毕 还能找到回去的路 继续往下执行
    在这里插入图片描述
    请看A9的图 栈顶上正放着call的下一条指令的地址呢!
    在这里插入图片描述

A11:销毁形参

找回call 继续往下执行
把esp+8
也就是在A9的图的基础上 esp往下移动两个元素 正好跳过了两个形参
相当于把形参的空间还给操作系统了
在这里插入图片描述
在这里插入图片描述

A12:把A7的计算结果真正给到c

  • 请回头看A7 计算结果 临时放到了eax寄存器里(全局的寄存器)
  • 然后这里把eax的结果 又给了ebp-20h 其实就是c!
    在这里插入图片描述

总结4:形参并不是是在Add函数内部创建的

从总结3可以看出
a和b应该属于扩大之后的main函数的栈帧
而Add()函数在计算z = x + y的时候 是直接去访问到下面的a和b进行计算的 所以形参并不是在Add的函数栈帧创建的
不难看出:
Add的栈帧里只创建了一个z
然后利用扩大之后的main函数的栈帧里的a和b
计算出x+y的值 赋给z
再把z临时保存在寄存器eax中
在这里插入图片描述

总结5:Add()函数的整个调用流程

再来看这个代码

  1. 开辟main栈帧
  2. 分配 abc的空间 完成初始化
  3. 在进入Add之前 已经把a和b压栈了 而且先压的是b 再压的a
  4. 进入Add之后 开辟add的栈帧 初始化局部变量z
  5. 计算x+y的时候 往下找到之前已经压栈的a b 利用他们的值 计算出x+y的值(a给x用 b给y用 ) 然后赋给z
  6. 然后把z里的计算结果 临时保存在全局寄存器eax之中
  7. 至此 就要开始销毁Add函数的栈帧了 Add函数调用结束
  8. 继续找到call的下一条指令继续执行
  9. 先销毁形参 然后把eax中的计算结果赋给c
  10. 再往后就是打印c 然后开始销毁main函数的栈帧 和Add的过程非常相似 在这里插入图片描述

总结6:形参确实是实参的临时拷贝

虽然a’ b’不是在Add内部创建的
但是确实是给了形参x y使用的
所以就可以把a’ b’看做a b的形参
显然是临时拷贝

而且a’ b’ 和 a b完全是独立的空间
所以修改a’ b’肯定不会影响a b
在这里插入图片描述

总结7:什么叫销毁?

所以销毁不是真的给他毁了啊! 叫做操作系统回收更好!
计算机的销毁就是:这块内存空间 可以被别人使用/覆盖了
就在这里而言:如果不再被esp和ebp维护 就可能会被分配给别人使用 就算是被销毁/回收了

最后的总结

让我们回到一开始的问题

  • 局部变量是如何创建的?:
    先给函数分配完栈帧空间 然后初始化这块空间(这里是都初始化成CCCCCCCC) 然后给局部变量分配空间 如果不初始化 那就是随机值
  • 为什么局部变量的值不初始化就是随机值?
    上面已经回答
  • 函数是怎么传参的?传参的顺序?
    A1 A2的两次push 其实就是在传参(在创建两个临时变量) 而且是从右往左push的
  • 一直说形参是实参的临时拷贝 具体怎么实现的? 看总结6
  • 调用一个函数 具体过程是什么? 看总结5
  • 函数调用完 是怎么返回的?
    因为一开始就记住了call执行的下一条指令 所以执行ret指令的时候 pop一下就可以找回去
    而返回值其实是利用eax全局寄存器返回的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/452616.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 【日期】函数大全(六)

目录 1、TIME_FORMAT() 按照指定的格式格式化时间。 2、TIME_TO_SEC() 将指定的时间值转为秒数。 3、TIMEDIFF() 返回两个时间之间的差值。 4、TIMESTAMP() 累加所有参数并将结果作为日期时间值返回。 5、TIMESTAMPADD() 将指定的时间间隔加到一个日期时间值上并返回结果。…

sql注入盲注详解以及绕过waf方法

盲注 mysql函数普及 exists(): 用于判度条件是否满足&#xff0c;满足返回True&#xff0c;否则返回False if(条件,为真返回的值,为假返回的值)&#xff1a;根据条件真假返回指定的值 length(string)&#xff1a;计算出长度string 可以是任何字符串字面量或者字段名。 substr(…

轻量级可视化数据分析报表,分组汇总表!

什么是可视化分组汇总表&#xff1f; 可视化分组汇总表&#xff0c;是一种结合了数据分组、聚合计算与视觉呈现功能的数据分析展示功能。它能够按照指定的维度&#xff08;如时间、地区、产品类型等&#xff09;对数据进行分组&#xff0c;还能自动计算各组的统计指标&#xf…

JavaWeb 24.Vue3的简介和快速体验

目录 一、Vue3介绍 Vue的两个核心功能 ① 声明式渲染&#xff1a; ② 响应性: 什么是声明式响应 什么是编程式响应 什么是渐进式框架 特点&#xff1a; 二、Vue3快速体验 三、关于JavaScript和TypeScript的选择问题 春风若有怜花意&#xff0c;可否许我再少年 —— 24.10.19 一…

mysql 备份与恢复

目录 一、备份分类与方法 1.1 备份类型 1.2 备份策略 1.3 备份工具 二、完全备份与恢复 2.1 物理冷备 2.2 mysqldump逻辑热 备 &#xff08;1&#xff09;完全备份一个或多个完整的库&#xff08;包括其中所有的表&#xff09; &#xff08;2&#xff09;完全备份 My…

【排序】——2.快速排序法(含优化)

快速排序法 递归法 霍尔版本(左右指针法) 1.思路 1、选出一个key&#xff0c;一般是最左边或是最右边的。 2、定义一个begin和一个end&#xff0c;begin从左向右走&#xff0c;end从右向左走。&#xff08;需要注意的是&#xff1a;若选择最左边的数据作为key&#xff0c;则…

第十六周:机器学习笔记

第十六周周报 摘要Abstratc一、机器学习1. Pointer Network&#xff08;指针网络&#xff09;2. 生成式对抗网络&#xff08;Generative Adversarial Networks | GAN&#xff09;——&#xff08;上&#xff09;2.1 Generator&#xff08;生成器&#xff09;2.2 Discriminator&…

React 子组件调用父组件的方法,以及互相传递数据

<script type"text/babel" data-type"module"> import React, { StrictMode, useState } from react; import { createRoot } from react-dom/client;const ParentComponent () > {const [message, setMessage] useState("")//父组件…

C语言函数实现:深入理解strcpy

文章目录 一、strcpy函数的基本用法二、strcpy函数的实现原理三、strcpy函数的应用场景四、strcpy函数的安全性问题五、结论 C语言函数实现&#xff1a;深入理解strcpy 在C语言编程中&#xff0c;字符串处理是一项基础且重要的任务。 strcpy函数作为C标准库中的一个基本函数&a…

计算生物学与生物信息学漫谈-2-测序深度/读长质量和Fasta处理

上一篇文章中我们介绍了测序技术的由来与发展&#xff0c;那么在介绍第三代测序的时候&#xff0c;我们提到了关于测序深度和读长的问题&#xff0c;那么本篇文章就详解介绍一下。 计算生物学与生物信息学漫谈-1-测序一路走来-CSDN博客 目录 1.测序深度SEQUENCING DEPTH &…

现代物流管理:SpringBoot技术突破

3系统分析 3.1可行性分析 通过对本智能物流管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本智能物流管理系统采用SSM框架&#xff0c;JAVA作为开发语…

【SQL|大数据|数据清洗|过滤】where条件中 “ != “ 和 “ NOT IN() ” 对NULL的处理

对数据进行清洗过滤的时候&#xff0c;NULL往往是一个很特殊的存在&#xff0c;对NULL值的存在通常有以下三种方式 1、保留NULL 2、过滤掉NULL 3、将NULL替换为其他符合业务需求的默认常量 下面是一些常用处理NULL的方式&#xff1a; 如下图所示数据源&#xff1a; car_vin&…

嵌入式入门学习——6Protues点亮数码管,认识位码和段码,分辨共阴还是共阳(数字时钟第一步)

0 系列文章入口 嵌入式入门学习——0快速入门&#xff0c;Let‘s Do It&#xff01; 首先新建基于Arduino UNO的protues工程&#xff0c;见本系列第3篇文章 1 点“P”按钮找器件 2 输入“seg”或“digit”查找数码管器件 3 找到我们想要的6位7段数码管 4如图A、B…DP都是段码…

ESP32-C3 入门笔记04:gpio_key 按键 (ESP-IDF + VSCode)

1.GPIO简介 ESP32-C3是QFN32封装&#xff0c;GPIO引脚一共有22个&#xff0c;从GPIO0到GPIO21。 理论上&#xff0c;所有的IO都可以复用为任何外设功能&#xff0c;但有些引脚用作连接芯片内部FLASH或者外部FLASH功能时&#xff0c;官方不建议用作其它用途。 通过开发板的原…

自由学习记录(11)

Surface Effector 2D Platform Effector 2D 向上跳跃穿过天花板的功能 平台效应器不用变Trigger&#xff0c;因为本来就是要有碰撞的 use one way grouping是让所有相关联的碰撞器都可以单面跳墙 默认不勾选&#xff0c;左右两边没有摩擦力和弹力&#xff0c;要自己先设置sid…

三菱PLC伺服-停止位置不正确故障排查

停止位置不正确时&#xff0c;请确认以下项目。 1)请确认伺服放大器(驱动单元)的电子齿轮的设定是否正确。 2&#xff09;请确认原点位置是否偏移。 1、设计近点信号(DOG)时&#xff0c;请考虑有足够为0N的时间能充分减速到爬行速度。该指令在DOG的前端开始减速到爬行速度&…

计算机毕业设计 基于Web的景区管理系统的设计与实现 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

&#x1f34a;作者&#xff1a;计算机编程-吉哥 &#x1f34a;简介&#xff1a;专业从事JavaWeb程序开发&#xff0c;微信小程序开发&#xff0c;定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事&#xff0c;生活就是快乐的。 &#x1f34a;心愿&#xff1a;点…

市场上几个跨平台开发框架?

跨平台桌面应用开发框架是一种工具或框架&#xff0c;它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上&#xff0c;开发者需要为每个操作系统编写不同的代码&#xff0c;使用不同的开发工具和语言。而跨平台桌面应用开发框架通过…

【网络】IP协议的地址管理

【网络】IP协议的地址管理 一. IP协议格式二. 地址管理1.动态分配IP地址2.NAT机制2.1 NAT机制下网络的请求/响应 3. 网段划分3.1 特殊的IP地址 4.路由选择5.DNS域名解析系统 一. IP协议格式 4位版本号(version): 指定IP协议的版本&#xff08;IPv4/IPv6&#xff09;, 对于IPv4来…

一起搭WPF架构之livechart的MVVM使用介绍

一起搭WPF架构之livechart使用介绍 前言ModelViewModelView界面设计界面后端 效果总结 前言 简单的架构搭建已经快接近尾声了&#xff0c;考虑设计使用图表的形式将SQLite数据库中的数据展示出来。前期已经介绍了livechart的安装&#xff0c;今天就详细介绍一下livechart的使用…