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.mzph.cn/news/882412.shtml

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

相关文章

python 爬虫 入门 三、登录以及代理。

目录 一、登录 &#xff08;一&#xff09;、登录4399 1.直接使用Cookie 2.使用账号密码进行登录 可选观看内容&#xff0c;使用python对密码进行加密&#xff08;无结果代码&#xff0c;只有过程分析&#xff09; 二、代理 免费代理 后续&#xff1a;协程&#xff0c;…

企业级调度器 LVS

集群和分布式基础知识 系统性能的扩展方式 当一个系统&#xff0c;或一个服务的请求量达到一定的数量级的时候&#xff0c;运行该服务的服务器的性能和资源上限&#xff0c; 很容易成为其性能瓶颈。除了性能问题之外&#xff0c;如果只部署在单台服务器上&#xff0c;在此服务…

gitee建立/取消关联仓库

目录 一、常用指令总结 二、建立关联具体操作 三、取消关联具体操作 一、常用指令总结 首先要选中要关联的文件&#xff0c;右击&#xff0c;选择Git Bash Here。 git remote -v //查看自己的文件有几个关联的仓库git init //初始化文件夹为git可远程建立链接的文件夹…

uniapp uni.uploadFile errMsg: “uploadFile:fail

uniapp 上传后一直显示加载中 1.检查前后端上传有无问题 2.检查失败信息 await uni.uploadFile({url,filePath,name,formData,header,timeout: 30000000, // 自定义上传超时时间fail: async function(err) {$util.hideAll()// 失败// err 返回 {errMsg: "uploadFile:fai…

SpringCloud学习:Openfeign组件实现服务调用和负载均衡

OpenFeign&#xff1a;服务调用与负载均衡&#xff08;服务端接口&#xff09; 是什么&#xff1a;通过OpenFeign可以实现服务调用和负载均衡 OpenFeign是一个声明性web服务客户端&#xff0c; 怎么用&#xff1a;服务提供者提取公共接口用FrignClient标注&#xff0c;服务调…

kernel32.dll下载地址:如何安全地恢复系统文件

关于从网络上寻找kernel32.dll的下载地址&#xff0c;这通常不是一个安全的做法&#xff0c;而且可能涉及到多种风险。kernel32.dll是Windows操作系统的核心组件之一&#xff0c;负责内存管理、进程和线程管理以及其他关键系统功能。因为kernel32.dll是系统的基础文件&#xff…

信息安全工程师(57)网络安全漏洞扫描技术与应用

一、网络安全漏洞扫描技术概述 网络安全漏洞扫描技术是一种可以自动检测计算机系统和网络设备中存在的漏洞和弱点的技术。它通过使用特定的方法和工具&#xff0c;模拟攻击者的攻击方式&#xff0c;从而检测存在的漏洞和弱点。这种技术可以帮助组织及时发现并修补漏洞&#xff…

【数据结构与算法】链表(上)

记录自己所学&#xff0c;无详细讲解 无头单链表实现 1.项目目录文件 2.头文件 Slist.h #include <stdio.h> #include <assert.h> #include <stdlib.h> struct Slist {int data;struct Slist* next; }; typedef struct Slist Slist; //初始化 void SlistI…

C++20中头文件ranges的使用

<ranges>是C20中新增加的头文件&#xff0c;提供了一组与范围(ranges)相关的功能&#xff0c;此头文件是ranges库的一部分。包括&#xff1a; 1.concepts: (1).std::ranges::range:指定类型为range&#xff0c;即它提供开始迭代器和结束标记(it provides a begin iterato…

系统托盘图标+快捷启动(Python)

QkStart 我把这个程序命名为QkStart 代码 # -*- coding: utf-8 -*- # Environment PyCharm # File_name QkStart |User Pfolg # 2024/10/19 22:06 import threading import time import pystray from PIL import Image from pystray import MenuItem, Menu import o…

leetcode.204.计数质数

#中等#枚举 给定整数 n &#xff0c;返回 所有小于非负整数 n 的质数的数量 。 埃氏筛 枚举没有考虑到数与数的关联性&#xff0c;因此难以再继续优化时间复杂度。接下来我们介绍一个常见的算法&#xff0c;该算法由希腊数学家厄拉多塞&#xff08;Eratosthenes&#xff09;提…

文字跑马灯:实现文字自动滚动策略的原理分析

一. 背景 在前端开发中&#xff0c;不少网站和应用都会运用到动态效果来吸引用户的注意&#xff0c;并提升用户体验。文字跑马灯是一种常见的动态效果&#xff0c;通过文字不断滚动来展示内容&#xff0c;吸引用户的注意力。 最近的一个项目就需要实现文字跑马灯效果&#xf…

【消息队列】RabbitMQ实现消费者组机制

目录 1. RabbitMQ 的 发布订阅模式 2. GRPC 服务间的实体同步 2.1 生产者服务 2.2 消费者服务 3. 可靠性 3.1 生产者丢失消息 3.2 消费者丢失消息 3.3 RabbitMQ 中间件丢失消息 1. RabbitMQ 的 发布订阅模式 https://www.rabbitmq.com/tutorials/tutorial-three-go P 生…

基于SpringBoot+Vue+uniapp微信小程序的乡村政务服务系统的详细设计和实现(源码+lw+部署文档+讲解等)

项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…

【软件测试】JUnit

Junit 是一个用于 Java 编程语言的单元测试框架&#xff0c;Selenium是自动化测试框架&#xff0c;专门用于Web测试 本篇博客介绍 Junit5 文章目录 Junit 使用语法注解参数执行顺序断言测试套件 Junit 使用 本篇博客使用 Idea集成开发环境 首先&#xff0c;创建新项目&#…

VUE 仿神州租车-开放平台

项目背景&#xff1a; 神州租车是一家提供汽车租赁服务的公司&#xff0c;其API开放平台为开发者提供了访问神州租车相关服务和数据的接口。用VUE技术来仿照其开发平台。 成果展示&#xff1a; 首页&#xff1a; API文档&#xff1a; 关于我们&#xff1a;

牛只行为及种类识别数据集18g牛只数据,适用于多种图像识别,目标检测,区域入侵检测等算法作为数据集。数据集中包括牛只行走,站立,进食,饮水等不同类型的数据

18g牛只数据&#xff0c;适用于多种图像识别&#xff0c;目标检测&#xff0c;区域入侵检测等算法作为数据集。 数据集中包括牛只行走&#xff0c;站立&#xff0c;进食&#xff0c;饮水等不同类型的数据&#xff0c;可以用于行为检测 数据集中包含多种不同种类的牛只&#xff…

黑盒测试 | 挖掘.NET程序中的反序列化漏洞

通过不安全反序列化漏洞远程执行代码 今天&#xff0c;我将回顾 OWASP 的十大漏洞之一&#xff1a;不安全反序列化&#xff0c;重点是 .NET 应用程序上反序列化漏洞的利用。 &#x1f4dd;$ _序列化_与_反序列化 序列化是将数据对象转换为字节流的过程&#xff0c;字节流可以…

基于SpringBoot+Vue+uniapp的诗词学习系统的详细设计和实现

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…

Maxwell 底层原理 详解

Maxwell 是一个 MySQL 数据库的增量数据捕获&#xff08;CDC, Change Data Capture&#xff09;工具&#xff0c;它通过读取 MySQL 的 binlog&#xff08;Binary Log&#xff09;来捕获数据变化&#xff0c;并将这些变化实时地发送到如 Kafka、Kinesis、RabbitMQ 或其他输出端。…