嵌入式C语言自我修养《内存堆栈管理》学习笔记

目录

一、Linux环境下的内存管理

二、栈的管理

三、堆内存管理

四、mmap映射区

五、内存泄漏与防范

六、常见的内存错误及检测


        C程序中定义的函数、全局变量、静态变量经过编译链接后,分别以section的形式存储在可执行文件的代码段、数据段和BSS段中。当程序运行时,可执行文件首先被加载到内存中,各个section分别加载到内存中对应的代码段、数据段和BSS段中。需要动态链接的动态库也被加载到内存中,完成代码的链接和重定位操作,以保证程序的正常运行。一个可执行文件被加载到内存中运行时,它在内存空间的分布如下图所示。

一、Linux环境下的内存管理

        在Linux环境下运行的程序,在编译时链接的起始地址都是相同的,而且是一个虚拟地址。Linux操作系统需要CPU内存管理单元的支持才能运行,Linux内核通过页表和MMU硬件来管理内存,完成虚拟地址到物理地址的转换、内存读写权限管理等功能

        可执行文件在运行时,加载器将可执行文件中的不同section加载到内存中读写权限不同的区域,如代码段、数据段、.bss段、.rodata段等。

        计算机上运行的程序主要分为两种:操作系统和应用程序。每一个应用程序进程都有4GB大小的虚拟地址空间。为了系统的安全稳定,0~4GB的虚拟地址空间一般分为两部分:用户空间和内核空间。0~3GB地址空间给应用程序使用,而操作系统一般运行在3~4GB内核空间。通过内存权限管理,应用程序没有权限访问内核空间,只能通过中断或系统调用来访问内核空间,这在一定程度上保障了操作系统核心代码的稳定运行。

        在Linux环境下,虽然所有的程序编译时使用相同的链接地址,但在程序运行时,相同的虚拟地址会通过MMU转换,映射到不同的物理内存区域各个可执行文件被加载到内存不同的物理页上。如下图所示,每个进程都有各自的页表,用来记录各自进程中虚拟地址到物理地址的映射关系。

二、栈的管理

        栈有两种基本操作:入栈(push)和出栈(pop)。入栈是把一个栈元素压入栈中,而出栈则是从栈中弹出一个栈元素。入栈和出栈都靠栈指针(Stack Pointer,SP)来维护,SP会随着入栈和出栈在栈顶上下移动。如下图所示,根据栈指针SP指向栈顶元素的不同,栈可分为满栈和空栈;根据栈的生长方向不同,栈又分为递增栈和递减栈

        满栈的栈指针SP总是指向栈顶元素,而空栈的栈指针则指向栈顶元素上方的可用空间;一个栈元素入栈时,递增栈的栈指针从低地址往高地址增长,而递减栈的栈指针则从高地址往低地址增长

        在栈的初始化过程中,栈在内存中的起始地址还是有点讲究的。ARM处理器使用的是满递减栈,在Linux环境下,栈的起始地址一般就是进程用户空间的最高地址,紧挨着内核空间,栈指针从高地址往低地址增长。为了防止黑客栈溢出攻击,新版本的Linux内核一般会将栈的起始地址设置成随机的,如下图所示,每次程序运行,栈的初始化起始地址都会基于用户空间的最高地址有一个随机的偏移,每次栈的起始地址都不一样。

        Linux默认给每一个用户进程栈分配8MB大小的空间。栈的容量如果设置得过大,则会增加内存开销和启动时间;如果设置得过小,则程序超出栈设置的内存空间又容易发生栈溢出(Stack Overflow),产生段错误

        在设置栈大小时,我们要根据程序中的变量、数组对栈空间的实际需求,设置合理的栈大小。用户在编写程序时,为了防止栈溢出,可以参考下面的一些原则。
        ● 尽量不要在函数内使用大数组,如果确实需要大块内存,则可以使用malloc申请动态内存。
        ● 函数的嵌套层数不宜过深
        ● 递归的层数不宜太深

        全局变量定义在函数体外,其作用域范围为从声明处到文件结束。其他文件如果想使用这个全局变量,则在自己的文件内使用extern声明后即可使用。全局变量的生命周期在整个程序运行期间都是有效的。

        局部变量定义在函数内,其作用域只能在函数体内使用函数只有在被调用的时候才会在内存中开辟一个栈帧空间,在这个栈空间里存储局部变量及传进来的函数实参等。函数调用结束,这个栈帧空间就被销毁释放了,变量也就随之消失,因此局部变量的生命周期仅仅存在于函数运行期间。每一次函数被调用,临时开辟的栈帧空间可能不相同,局部变量的地址也不相同。

        编译器在编译程序时,其实是根据一对大括号{}来限定一个变量的作用域的,示例:

#include <iostream>void functionWithStatic() {static int staticVar = 0; // 带有 static 修饰的局部变量int normalVar = 0;       // 普通局部变量staticVar++;normalVar++;std::cout << "Static Variable: " << staticVar << std::endl;std::cout << "Normal Variable: " << normalVar << std::endl;
}int main() {for (int i = 0; i < 3; i++) {functionWithStatic();}return 0;
}

运行结果:

最后我们对变量的作用域做下小结。
全局变量的作用域如下
● 全局变量的作用域由文件来限定。
● 可使用extern进行扩展,被其他文件引用。
● 也可以使用static进行限制,只能在本文件中被引用。
局部变量的作用域如下
● 局部变量的作用域由{}限定
● 可以使用static修饰局部变量来改变它们的存储属性(生命周期),但不能改变其作用域。

三、堆内存管理

        我们使用malloc()/free()函数申请/释放的动态内存就属于堆内存,如下图所示,堆是Linux进程空间中一片可动态扩展或缩减的内存区域,一般位于BSS段的后面

堆内存与栈相比,有相同点,也有区别。
● 堆内存是匿名的,不能像变量那样使用名字直接访问,一般通过指针间接访问。
● 在函数运行期间,对函数栈帧内的内存访问也不能像变量那样通过变量名直接访问,一般通过栈指针FP或SP相对寻址访问。
● 堆内存由程序员自己申请和释放,函数退出时,如果程序员没有主动释放,就会造成内存泄漏。
● 栈内存由编译器维护,函数运行时开辟一个栈帧空间,函数运行结束,栈帧空间随之销毁释放。

        malloc()/free()函数的底层实现,其实就是通过系统调用brk向内核的内存管理系统申请内存。内核批准后,就会在BSS段的后面留出一片内存空间,允许用户进行读写操作。申请的内存使用完毕后要通过free()函数释放,free()函数的底层实现也是通过系统调用来归还这块内存的。

        当用户要申请的内存比较大时,如大于128KB,一般会通过mmap系统调用直接映射一片内存,使用结束后再通过ummap系统调用归还这块内存。mmap区域是Linux进程中比较特殊的一块区域,主要用于程序运行时动态共享库的加载和mmap文件映射。早期的Linux内核将该区域设置在0x40000000附近,Linux 2.6以后的内核将该区域移到了栈附近,打印mmap映射区域的地址,你会发现大部分地址都在0xBxxxxxxx范围内,紧挨着进程的用户栈

示例代码:

运行结果:

        根据程序的打印结果,我们可以看到:对于用户申请的小块内存,Linux内存管理子系统会在BSS段的后面批准一块内存给用户使用。当用户申请的内存大于128KB时,Linux系统则通过mmap系统调用,映射一片内存给用户使用,映射区域在用户进程栈附近。两次申请的不同大小
的内存,其地址分别位于内存中两个不同的区域:heap区和mmap区

        让a.out进程先不退出,一直死循环运行,以方便我们通过cat命令查看a.out进程的内存布局。

        在32位X86平台下我们可以看到,heap区域在.bss段的后面,而mmap区域则紧挨着stack,mmap区域包括进程动态链接时加载到内存的动态链接器ld-2.23.so、动态共享库、使用mmap申请的动态内存

        使用kill命令杀掉a.out进程再重新运行,你会发现&mem_100和&mem_256K的地址打印值发生了变化,每次程序运行的地址可能都不相同。这是因为heap区和mmap区的起始地址和stack一样,也不是固定不变的。为了防止黑客攻击,每次程序运行时,它们都会以一个随机偏移作为起始地址。

        通过a.out进程的内存布局我们看到,栈的起始地址并不紧挨着内核空间0xc0000000,而是从0xbf9a2000作为起始地址,中间有一个大约6MB的偏移heap区也不紧挨着.bss段,它们之间也有一个offset;mmap区也是如此,它和stack区之间也有一个offset

        大量的系统调用会让处理器和操作系统在不同的工作模式之间来回切换:操作系统要在用户态和内核态之间来回切换,CPU要在普通模式和特权模式之间来回切换,每一次切换都意味着各种上下文环境的保存和恢复,频繁地系统调用会降低系统的性能。系统调用还有一个不人性化的地方是不支持任意大小的内存分配,有的平台甚至只支持一个或数倍物理页大小的内存申请,这在一定程度上会造成内存的浪费。举个通俗的例子,内存申请有点类似你去银行存取款。当你需要用钱时,如果每次都是用多少取多少,用1块取1块,用10块取10块,那么估计你天天都得往银行跑,花费在交通、排队上的时间开销无疑是巨大的。同样的道理,当你往银行存钱时,如果只要口袋里有钱,就往银行里存,有1块存1块,有10块存10块,那么估计你也得天天往银行跑。正确的做法应该是:每次取钱时多取一些,放到自己的钱包里,可以多次使用;存钱时也是如此,先存放到钱包里,等攒够了一定数额,再存到银行里,这样就可以大大减少去银行的次数。

        为了提高内存申请效率,减少系统调用带来的开销,我们可以参考上面的钱包模式,在用户空间层面对堆内存介入管理。如在glibc中实现的内存分配器(allocator)可以直接对堆内存进行维护和管理。如图5-27所示,内存分配器通过系统调用brk()/mmap()向Linux内存管理子系统“批发”内存,同时实现了malloc()/free()等API函数给用户使用,满足用户动态内存的申请与释放请求。
        当用户使用free()释放内存时,释放的内存并不会立即返回给内核,而是被内存分配器接收,缓存在用户空间。内存分配器将这些内存块通过链表收集起来,等下次有用户再去申请内存时,可以直接从链表上查找合适大小的内存块给用户使用,如果缓存的内存不够用再通过brk()系统调用去内核“批发”内存。内存分配器相当于一个内存池缓存,通过这种操作方式,大大减少了系统调用的次数,从而提升了程序申请内存的效率,提高了系统的整体性能。

四、mmap映射区

        当用户使用malloc申请大于128KB的堆内存时,内存分配器会通过mmap系统调用,在Linux进程虚拟空间中直接映射一片内存给用户使用。

        当我们运行一个程序时,需要从磁盘上将该可执行文件加载到内存。将文件加载到内存有两种常用的操作方法,一种是通过常规的文件I/O操作,如read/write等系统调用接口;一种是使用mmap系统调用将文件映射到进程的虚拟空间,然后直接对这片映射区域读写即可

        文件I/O操作使用文件的API函数(open、read、write、close)对文件进行打开和读写操作。文件存储于磁盘中,我们通过指定的文件名打开一个文件,就会得到一个文件描述符,通过该文件描述符就可以找到该文件的索引节点inode,根据inode就可以找到该文件在磁盘上的存储位置。然后我们就可以直接调用read()/write()函数到磁盘指定的位置读写数据了。文件的读写流程如图5-34左侧图所示。

        磁盘属于机械设备,程序每次读写磁盘都要经过转动磁盘、磁头定位等操作,读写速度较慢。为了提高读写效率,减少I/O读盘次数以保护磁盘,Linux内核基于程序的局部原理提供了一种磁盘缓冲机制。如图5-34所示,在内存中以物理页为单位缓存磁盘上的普通文件或块设备文件。当应用程序读磁盘文件时,会先到缓存中看数据是否存在,若数据存在就直接读取并复制到用户空间;若不存在,则先将磁盘数据读取到页缓存(page cache)中然后从页缓存中复制数据到用户空间的buf中。当应用程序写数据到磁盘文件时,会先将用户空间buf中的数据写入page cache,当page cache中缓存的数据达到设定的阈值或者刷新时间超时,Linux内核会将这些数据回写到磁盘中。

        当动态库第一次被链接器加载到内存参与动态链接时,如图5-43所示,动态库映射到了当前进程虚拟空间的mmap区域动态链接和重定位结束后,程序就开始运行。当程序访问mmap映射区域,去调用动态库的一些函数时,发现此时还没有为这片虚拟空间分配物理内存,就会产生一个请页异常。内核接着会为这片映射内存区域分配物理内存,将动态库文件libtest.so加载到物理内存,并将虚拟地址和物理地址之间的映射关系更新到进程的页表项,此时动态库才真正加载到物理内存,程序才可以正常运行。

        对于已经加载到物理内存的文件,Linux内核会通过一个radix tree的树结构来管理这些页缓存对象。在图5-44中,当进程B运行也需要加载动态库libtest.so时,动态链接器会将库文件libtest.so映射到进程B的一片虚拟内存空间上,链接重定位完成后进程B开始运行。当通过映射内存地址访问libtest.so时也会触发一个请页异常,Linux内核在分配物理内存之前会先从radix tree树中查询libtest.so是否已经加载到物理内存,当内核发现libtest.so库文件已经加载到内存后就不会给进程B分配新的物理内存,而是直接修改进程B的页表项,将进程B中的这片映射区域直接映射到libtest.so所在的物理内存上。

        通过上面的分析我们可以看到,动态库libtest.so只加载到物理内存一次,后面的进程如果需要链接这个动态库,直接将该库文件映射到自身进程的虚拟空间即可,同一个动态库虽然被映射到了多个进程的不同虚拟地址空间,但是通过MMU地址转换,都指向了物理内存中的同一块区域。此时动态库libtest.so也被多个进程共享使用,因此动态库也被称作动态共享库

五、内存泄漏与防范

 什么是内存泄漏?

        在一个C函数中,如果我们使用malloc()申请的内存在使用结束后没有及时被释放,则C标准库中的内存分配器ptmalloc和内核中的内存管理子系统都失去了对这块内存的追踪和管理。失去管理和追踪的这块内存,一直孤零零地躺在内存的某片区域,用户、内存分配器和内存管理子系统都不知道它的存在,它就像内存中的一块漏洞,我们称这种现象为内存泄漏。

预防内存泄漏:

        预防内存泄漏最好的方法就是:内存申请后及时地释放,两者要配对使用,内存释放后要及时将指针设置为NULL,使用内存指针前要进行非空判断

内存泄漏检测工具:

        MTrace是Linux系统自带的一个工具,它通过跟踪内存的使用记录来动态定位用户代码中内存泄漏的位置。使用MTrace很简单,在代码中添加下面的接口函数就可以了。

        mtrace()函数用来开启内存使用的记录跟踪功能muntrace()函数用来关闭内存使用的记录跟踪功能。如果想检测一段代码是否有内存泄漏,则可以把这两个函数添加到要检测的程序代码中。 

        开启跟踪功能后,MTrace会跟踪程序代码中使用动态内存的记录,并把跟踪记录保存在一个文件里,这个文件可以由用户通过MALLOC_TRACE来指定。接下来我们编译、运行这个程序,并使用MTrace来定位内存泄漏的位置。

        通过生成的日志文件mtrace.log来定位内存泄漏在程序中的位置。

        根据动态内存的使用记录,我们可以很快定位到内存泄漏发生在mcheck.c文件中的第11行代码。 

六、常见的内存错误及检测

        如图5-47所示,内存管理子系统将一个进程的虚拟空间划分为不同的区域,如代码段、数据段、BSS段、堆、栈、mmap映射区域、内核空间等,每个区域都有不同的读、写、执行权限

        通过内存管理,每个区域都有具体的访问权限,如只读、读写、禁止访问等。数据段、BSS段、堆栈区域都属于读写区,而代码段则属于只读区,如果你往代码段的地址空间上写数据就会发生一个段错误。在Linux用户进程的4GB虚拟空间上,除了上面我们熟悉的区域,还剩下很多区域,如代码段之前的区域、堆和mmap区域之间的进程空间、内核空间等。这部分内存空间是禁止用户程序访问的。当一个用户进程试图访问这部分空间时,就会被系统检测到,在Linux下系统会向当前进程发送一个信号SIGSEGV,终止该进程的运行。

        对于应用程序来说,常见的内存错误一般主要分为以下几种类型内存越界、内存踩踏、多次释放、非法指针

内存越界:导致段错误

内存踩踏:如果一个进程中有多个线程,多个线程都申请堆内存,这些堆内存就可能彼此相邻,使用时需要谨慎,提防越界。在内核驱动开发中,驱动代码运行在特权状态,对内存访问比较自由,多个驱动程序申请的物理内存也可能彼此相邻。如果你的程序代码经常莫名其妙地崩溃,而且每次出错的地方也不一样,在确保自己的代码没问题后,也可以大胆地去怀疑一下是不是内存踩踏的问题。

内存践踏检测API:mprotect()页(page)是Linux内存管理的基本单元,在32位系统中,一个页通常是4096字节,mprotect()要保护的内存单元通常要以页地址对齐,我们可以使用memalign()函数申请一个以页地址对齐的一片内存

内存检测神器:Valgrind,包含一套工具集,其中一个内存检测工具Memcheck可以对我们的内存进行内存覆盖、内存泄漏、内存越界检测

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

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

相关文章

【Zabbix】Zabbix学习笔记

现在Zabbix Server存在的问题&#xff1a; 问题1&#xff1a; Zabbix server: Utilization of discoverer processes over 75% 问题2&#xff1a; Zabbix server: Utilization of icmp pinger processes over 75% 优化的解决办法是修改配置文件把Discovery和Pinger进程数量调大…

04-RocketMQ源码解读

目录汇总&#xff1a;RocketMQ从入门到精通汇总 上一篇&#xff1a;03-RocketMQ高级原理 这一部分&#xff0c;我们开始深入RocketMQ的源码。源码的解读是个非常困难的过程&#xff0c;每个人的理解程度都会不一样&#xff0c;也不太可能通过讲解把其中的细节全部讲明白。我们今…

panads操作excel

panads简介 pandas是基于Numpy创建的Python包&#xff0c;内置了大量标准函数&#xff0c;能够高效地解决数据分析数据处理和分析任务&#xff0c;pandas支持多种文件的操作&#xff0c;比如Excel&#xff0c;csv&#xff0c;json&#xff0c;txt 文件等&#xff0c;读取文件之…

unity发布微信小游戏,未找到 game.json报错原因

unity发布微信小游戏&#xff0c;未找到 game.json报错原因 同一个问题相隔一年遇到两次&#xff0c;两次原因都不一样&#xff0c;记录一下&#xff0c;以后不要再掉坑里 原因一&#xff1a;申请的appID是小程序不是小游戏 解决方法&#xff1a;需要在程序平台修改服务类目 如…

哈希应用之布隆过滤器

文章目录 1.介绍1.1百度搜索1.2知乎好文1.3自身理解 2.模拟实现2.1文档阅读2.2代码剖析 3.误判率的研究4.布隆过滤器的应用4.1如何找到两个分别有100亿个字符串的文件的交集[只有1G内存].分别给出精确算法和近似算法4.2如何扩展BloomFilter使得它支持删除元素的操作 5.整体代码…

pytorch中nn.DataParallel多次使用

pytorch中nn.DataParallel多次使用 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader# 定义模型 class MyModel(nn.Module):def __init__(self):super(MyModel, self).__init__()self.fc nn.Linear(10, 1)def forwa…

ROS为机器人装配激光雷达

移动机器人在环境中获取障碍物的具体位置、房间的内部轮廓等信息都是非常必要的&#xff0c;这些信息是机器人创建地图、进行导航的基础数据&#xff0c;除上面所讲的Kinect&#xff0c;还可以使用激光雷达作为这种场景应用下的传感器。 激光雷达可用于测量机器人和其他物体之间…

3.简单场景构建

在新建的项目中&#xff0c;默认存在 Main Camera 和 Directional Light两个对象。若是缺失&#xff0c;可通过选择菜单中的 Game Object->Camera 和 Geme Object->Light->Directional Light进行创建。 1.添加地形及底图 通过在Cesium面板中选择 Cesium World Terrai…

批量文件重命名软件 A Better Finder Rename 11汉化for mac

A Better Finder Rename 11是一款功能强大的文件重命名工具&#xff0c;可在Mac操作系统上使用。它提供了简单而直观的界面&#xff0c;帮助用户快速批量重命名文件和文件夹&#xff0c;提高文件管理和组织效率。 以下是A Better Finder Rename 11可能提供的一些主要功能和特点…

设计模式 - 结构型模式考点篇:适配器模式(类适配器、对象适配器、接口适配器)

目录 一、适配器模式 一句话概括结构式模式 1.1、适配器模式概述 1.2、案例 1.2.1、类适配器模式实现案例 1.2.2、对象适配器 1.2.3、接口适配器 1.3、优缺点&#xff08;对象适配器模式&#xff09; 1.4、应用场景 一、适配器模式 一句话概括结构式模式 教你将类和对…

多线程入门

1 创建线程 下面的程序&#xff0c;我们可以用它来创建一个 POSIX 线程&#xff1a; #include <pthread.h> pthread_create (myThread, attr, start_routine, arg) 在这里&#xff0c;pthread_create 创建一个新的线程&#xff0c;并让它可执行。下面是关于参数的说明…

QT自制软键盘 最完美、最简单、跟自带虚拟键盘一样

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 QT自制软键盘 最完美、最简单、跟自带虚拟键盘一样 Chapter1 QT自制软键盘 最完美、最简单、跟自带虚拟键盘一样一、本自制虚拟键盘特点二、windows打开系统自带软键盘三、让…

3、在docker 容器中安装tomcat

&#xff11;、在服务器上查找tomcat镜像,查看前5条 docker search tomcat --limit 5​​​​​​​ 2、拉取镜像到本地 拉取官方的tomcat到本地 docker pull tomcat:9.0.34-jdk8 3、查看本地镜像 docker images |grep tomcat 4、启动tomcat 服务 使用默认配置 docker ru…

如何选择一个向量数据库:Elastic Cloud 和 Zilliz Cloud 面面观

随着以 Milvus 为代表的向量数据库在 AI 产业界越来越受欢迎&#xff0c;诸如 Elasticsearch 之类的传统数据库和检索系统也开始行动起来&#xff0c;纷纷在快速集成专门的向量检索插件方面展开角逐。 例如&#xff0c;在提供类似插件的传统数据库中&#xff0c;Elasticsearch …

VAE模型(详细推导+实例代码)

文章目录 EM算法思路E步M步直观感觉 GMM模型VAEVAE思想从GMM到VAE公式推导重参数VAE神经网络另一个视角的VAE思想为什么引入encoder为什么要重参数噪声与重建 Discrete VAE 本文会从EM算法&#xff0c;GMM模型一步一步的的推导&#xff0c;在过渡到VAE模型&#xff0c;如果有熟…

棱镜七彩参编!开源领域4项团体标准正式发布

近日&#xff0c;中电标2023年第27号团体标准公告正式发布&#xff0c;《T/CESA 1270.2-2023 信息技术 开源治理 第 2 部分&#xff1a;企业治理评估模型》、《T/CESA 1270.3-2023 信息技术 开源治理 第 3 部分&#xff1a;社区治理框架》、《T/CESA 1270.5-2023 信息技术 开源…

信创办公–基于WPS的EXCEL最佳实践系列 (单元格与行列)

信创办公–基于WPS的EXCEL最佳实践系列 &#xff08;单元格与行列&#xff09; 目录 应用背景操作步骤1、插入和删除行和列2、合并单元格3、调整行高与列宽4、隐藏行与列5、修改单元格对齐和缩进6、更改字体7、使用格式刷8、设置单元格内的文本自动换行9、应用单元格样式10、插…

STM32F4X I2C LM75

STM32F4X I2C LM75 I2C协议讲解I2C接线I2C协议波形I2C起始信号I2C停止信号I2C应答信号I2C寻址I2C地址格式 I2C数据传输 LM75ALM75A介绍LM75A引脚说明LM75A地址LM75A寄存器LM75A I2C协议写配置寄存器读配置寄存器写Tos和Thyst寄存器读Tos Thyst Temp寄存器LM75A温度计算 LM75A例…

力扣(LeetCode)2578. 最小和分割(C++)

哈希集合 请读者思考&#xff0c;num拆分成num1和num2&#xff0c;要使得num1 num2最小&#xff0c;应满足两条性质&#xff1a; num1和num2位数相同&#xff0c;或最多差一位。num1和num2应按数值从小到大在num中取数。 想到统计num的位数&#xff0c;以实现性质1的需要&a…

淘宝天猫商品历史价格API接口

获取淘宝商品历史价格接口的步骤如下&#xff1a; 注册淘宝开放平台&#xff1a;首先在淘宝开放平台上注册一个账号&#xff0c;并进行登录。创建应用&#xff1a;在淘宝开放平台上创建一个应用&#xff0c;并获取该应用的App Key和App Secret&#xff0c;用于后续的接口调用。…