C++中运行一个程序的内存分配情况及qt中的内存管理机制

编程日常/2023/11/30 14:45:48

一个由c/C++编译的程序占用的内存分为以下几个部分
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。- 程序结束后有系统释放
4、文字常量区—常量字符串就是放在这里的。程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。

例子程序

这是一个前辈写的,非常详细

 
  1. //main.cpp

  2. int a = 0; //全局初始化区

  3. int a = 0; //全局初始化区

  4. char *p1; //全局未初始化区

  5. main() {

  6.     int b; //栈

  7.     char s[] = "abc"; //栈

  8.     char *p2; //栈

  9.     char *p3 = "123456"; //123456\0在常量区,p3在栈上。

  10.     static int c = 0; //全局(静态)初始化区

  11.     p1 = (char *)malloc(10);

  12.     p2 = (char *)malloc(20);

  13.     //分配得来得10和20字节的区域就在堆区。

  14.     strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。

  15. }

二、堆和栈的理论知识

2.1申请方式

stack:
由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
p1 = (char *)malloc(10);
在C++中用new运算符
p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。

2.2 申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3 申请大小的限制

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4 申请效率的比较:

栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。

2.5 堆和栈中的存储内容

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

2.6 存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:

 
  1. #include

  2. void main() {

  3.     char a = 1;

  4.     char c[] = "1234567890";

  5.     char *p ="1234567890";

  6.     a = c[1];

  7.     a = p[1];

  8.     return;

  9. }

对应的汇编代码

 
  1. 10: a = c[1];

  2. 00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]

  3. 0040106A 88 4D FC mov byte ptr [ebp-4],cl

  4. 11: a = p[1];

  5. 0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]

  6. 00401070 8A 42 01 mov al,byte ptr [edx+1]

  7. 00401073 88 45 FC mov byte ptr [ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了。

2.7小结:

堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

三 、windows进程中的内存结构

在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。

接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。

首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码:

 
  1. #include <stdio.h>

  2. int g1=0, g2=0, g3=0;

  3. int main()

  4. {

  5.     static int s1=0, s2=0, s3=0;

  6.     int v1=0, v2=0, v3=0;

  7.     //打印出各个变量的内存地址    

  8.     printf("0x%08x\n",&v1); //打印各本地变量的内存地址

  9.     printf("0x%08x\n",&v2);

  10.     printf("0x%08x\n\n",&v3);

  11.     printf("0x%08x\n",&g1); //打印各全局变量的内存地址

  12.     printf("0x%08x\n",&g2);

  13.     printf("0x%08x\n\n",&g3);

  14.     printf("0x%08x\n",&s1); //打印各静态变量的内存地址

  15.     printf("0x%08x\n",&s2);

  16.     printf("0x%08x\n\n",&s3);

  17.     return 0;

  18. }

编译后的执行结果是:

 
  1. 0x0012ff78

  2. 0x0012ff7c

  3. 0x0012ff80

  4. 0x004068d0

  5. 0x004068d4

  6. 0x004068d8

  7. 0x004068dc

  8. 0x004068e0

  9. 0x004068e4

输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

 
  1. ├———————┤低端内存区域

  2. │ …… │

  3. ├———————┤

  4. │ 动态数据区 │

  5. ├———————┤

  6. │ …… │

  7. ├———————┤

  8. │ 代码区 │

  9. ├———————┤

  10. │ 静态数据区 │

  11. ├———————┤

  12. │ …… │

  13. ├———————┤高端内存区域

堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码:

 
  1. #include <stdio.h>

  2. void __stdcall func(int param1,int param2,int param3)

  3. {

  4.     int var1=param1;

  5.     int var2=param2;

  6.     int var3=param3;

  7.     printf("0x%08x\n",param1); //打印出各个变量的内存地址

  8.     printf("0x%08x\n",param2);

  9.     printf("0x%08x\n\n",param3);

  10.     printf("0x%08x\n",&var1);

  11.     printf("0x%08x\n",&var2);

  12.     printf("0x%08x\n\n",&var3);

  13.     return;

  14. }

  15. int main() {

  16.     func(1,2,3);

  17.     return 0;

  18. }

编译后的执行结果是:

 
  1. 0x0012ff78

  2. 0x0012ff7c

  3. 0x0012ff80

  4. 0x0012ff68

  5. 0x0012ff6c

  6. 0x0012ff70

 
  1. ├———————┤<—函数执行时的栈顶(ESP)、低端内存区域

  2. │ …… │

  3. ├———————┤

  4. │ var 1 │

  5. ├———————┤

  6. │ var 2 │

  7. ├———————┤

  8. │ var 3 │

  9. ├———————┤

  10. │ RET │

  11. ├———————┤<—“__cdecl”函数返回后的栈顶(ESP)

  12. │ parameter 1 │

  13. ├———————┤

  14. │ parameter 2 │

  15. ├———————┤

  16. │ parameter 3 │

  17. ├———————┤<—“__stdcall”函数返回后的栈顶(ESP)

  18. │ …… │

  19. ├———————┤<—栈底(基地址 EBP)、高端内存区域

上图就是函数调用过程中堆栈的样子了。首先,三个参数以从右到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码:

 
  1. ;--------------func 函数的汇编代码-------------------

  2. :00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间

  3. :00401003 8B442410 mov eax, dword ptr [esp+10]

  4. :00401007 8B4C2414 mov ecx, dword ptr [esp+14]

  5. :0040100B 8B542418 mov edx, dword ptr [esp+18]

  6. :0040100F 89442400 mov dword ptr [esp], eax

  7. :00401013 8D442410 lea eax, dword ptr [esp+10]

  8. :00401017 894C2404 mov dword ptr [esp+04], ecx

  9. ……………………(省略若干代码)

  10. :00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间

  11. :00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间

  12. ;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复

  13. ;-------------------函数结束-------------------------

  14. ;--------------主程序调用func函数的代码--------------

  15. :00401080 6A03 push 00000003 //压入参数param3

  16. :00401082 6A02 push 00000002 //压入参数param2

  17. :00401084 6A01 push 00000001 //压入参数param1

  18. :00401086 E875FFFFFF call 00401000 //调用func函数

  19. ;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C”

聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码:

 
  1. #include <stdio.h>

  2. #include <string.h>

  3. void __stdcall func() {

  4.     char lpBuff[8]="\0";

  5.     strcat(lpBuff,"AAAAAAAAAAA");

  6.     return;

  7. }

  8. int main() {

  9.     func();

  10.     return 0;

  11. }

编译后执行一下回怎么样?哈,“”0x00414141”指令引用的”0x00000000”内存。该内存不能为”read”。”,“非法操作”喽!”41”就是”A”的16进制的ASCII码了,那明显就是strcat这句出的问题了。”lpBuff”的大小只有8字节,算进结尾的\0,那strcat最多只能写入7个”A”,但程序实际写入了11个”A”外加1个\0。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要这个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。

 
  1. ├———————┤<—低端内存区域

  2. │ …… │

  3. ├———————┤<—由exploit填入数据的开始

  4. │ │

  5. │ buffer │<—填入无用的数据

  6. │ │

  7. ├———————┤

  8. │ RET │<—指向shellcode,或NOP指令的范围

  9. ├———————┤

  10. │ NOP │

  11. │ …… │<—填入的NOP指令,是RET可指向的范围

  12. │ NOP │

  13. ├———————┤

  14. │ │

  15. │ shellcode │

  16. │ │

  17. ├———————┤<—由exploit填入数据的结束

  18. │ …… │

  19. ├———————┤<—高端内存区域

windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码:

 
  1. #include <stdio.h>

  2. #include <iostream.h>

  3. #include <windows.h>

  4. void func()

  5. {

  6.     char *buffer=new char[128];

  7.     char bufflocal[128];

  8.     static char buffstatic[128];

  9.     printf("0x%08x\n",buffer); //打印堆中变量的内存地址

  10.     printf("0x%08x\n",bufflocal); //打印本地变量的内存地址

  11.     printf("0x%08x\n",buffstatic); //打印静态变量的内存地址

  12. }

  13. void main() {

  14.     func();

  15.     return;

  16. }

程序执行结果为:

 
  1. 0x004107d0

  2. 0x0012ff04

  3. 0x004068c0

可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数:

 
  1. - HeapAlloc 在堆中申请内存空间

  2. - HeapCreate 创建一个新的堆对象

  3. - HeapDestroy 销毁一个堆对象

  4. - HeapFree 释放申请的内存

  5. - HeapWalk 枚举堆对象的所有内存块

  6. - GetProcessHeap 取得进程的默认堆对象

  7. - GetProcessHeaps 取得进程所有的堆对象

  8. - LocalAlloc

  9. - GlobalAlloc

当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间:

 
  1. HANDLE hHeap=GetProcessHeap();

  2. char *buff=HeapAlloc(hHeap,0,8);

其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧:

 
  1. #pragma comment(linker,"/entry:main") //定义程序的入口

  2. #include <windows.h>

  3. _CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf

  4. /*---------------------------------------------------------------------------

  5.  写到这里,我们顺便来复习一下前面所讲的知识:

  6.  (*注)printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。

  7.  由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是__stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。

  8.  ---------------------------------------------------------------------------*/

  9. void main()

  10. {

  11.     HANDLE hHeap=GetProcessHeap();

  12.     char *buff=HeapAlloc(hHeap,0,0x10);

  13.     char *buff2=HeapAlloc(hHeap,0,0x10);

  14.     HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");

  15.     printf=(void *)GetProcAddress(hMsvcrt,"printf");

  16.     printf("0x%08x\n",hHeap);

  17.     printf("0x%08x\n",buff);

  18.     printf("0x%08x\n\n",buff2);

  19. }

执行结果为:

 
  1. 0x00130000

  2. 0x00133100

  3. 0x00133118

hHeap的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。

最后来说说内存中的数据对齐所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86 CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果:

 
  1. #include <stdio.h>

  2. int main()

  3.     {

  4.     int a;

  5.     char b;

  6.     int c;

  7.     printf("0x%08x\n",&a);

  8.     printf("0x%08x\n",&b);

  9.     printf("0x%08x\n",&c);

  10.     return 0;

  11. }

这是用VC编译后的执行结果:

 
  1. 0x0012ff7c

  2. 0x0012ff7b

  3. 0x0012ff80

变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。

这是用Dev-C++编译后的执行结果:

 
  1. 0x0022ff7c

  2. 0x0022ff7b

  3. 0x0022ff74

变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。

这是用lcc编译后的执行结果:

 
  1. 0x0012ff6c

  2. 0x0012ff6b

  3. 0x0012ff64

变量在内存中的顺序:同上。

三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存。

/***************************************************************************

MFC与Qt的内存管理

最近在做MFC向Qt的移植,在内存管理方面遇到了很头疼的问题,虽然不知道问题到底出在哪,先了解下这两个库的内存管理方式。于是转载两篇关于内存管理的文章。

一. Qt内存管理:

在Qt的程序中经常会看到只有new而不delete的情况,其实是因为Qt有一套回收内存的机制,主要的规则如下:

1.所有继承自QObject类的类,如果在new的时候指定了父亲,那么它的清理时在父亲被delete的时候delete的,所以如果一个程序中,所有的QObject类都指定了父亲,那么他们是会一级级的在最上面的父亲清理时被清理,而不用自己清理;

2. 程序通常最上层会有一个根的QObject,就是放在setCentralWidget()中的那个QObject,这个QObject在 new的时候不必指定它的父亲,因为这个语句将设定它的父亲为总的QApplication,当整个QApplication没有时它就自动清理,所以也 无需清理。这里Qt4和Qt3有不同,Qt3中用的是setmainwidget函数,但是这个函数不作为里面QObject的父亲,所以Qt3中这个顶 层的QObject要自行销毁)。

3.这是有人可能会问那如果我自行delete掉这些Qt接管负责销毁的指针了会出现什么情况呢,如果 这么做的话,正常情况下被delete的对象的父亲会知道这件事情,它会知道它的儿子被你直接delete了,这样它会将这个儿子移出它的列表,并且重新 构建显示内容,但是直接这样做是有风险的!也就是要说的下一条。

4.当一个QObject正在接受事件队列时如果中途被你DELETE掉 了,就是出现问题了,所以Qt中建议大家不要直接DELETE掉一个QObject,如果一定要这样做,要使用QObject的 deleteLater()函数,它会让所有事件都发送完一切处理好后马上清除这片内存,而且就算调用多次的deletelater也不会有问题。

5.Qt 不建议在一个QObject对象的父亲的范围之外持有对这个对象的指针,因为如果这样外面的指针很可能不会察觉这个QObject被释放,会出现错误。如 果一定要这样,就要记住你在哪这样做了,然后抓住那个被你违规使用的QObject的destroyed()信号,当它没有时赶快置零你的外部指针。当然 我认为这样做是及其麻烦也不符合高效率编程规范的,所以如果要这样在外部持有QObject的指针,建议使用引用或者用智能指针,如Qt就提供了智能指针 针对这些情况,见最后一条。

6.Qt中的智能指针封装为QPointer类,所有QObject的子类都可以用这个智能指针来包装,很多用法与普通指针一样,可以详见Qt assistant

通过调查这个Qt的内存管理功能,发现了很多东西,现在觉得虽然这个Qt弄的有点小复杂,但是使用起来还是很方便的,最后要说的是某些内存泄露的检测工具会认为Qt的程序因为这种方式存在内存泄露,发现时大可不必理会~

原帖地址:http://blog.csdn.net/leonwei/archive/2009/01/04/3703598.aspx

二. MFC内存分配方式与调试机制

1 内存分配

1.1 内存分配函数

     MFCWin32或者C语言的内存分配API,有四种内存分配API可供使用。

Win32的堆分配函数

每一个进程都可以使用堆分配函数创建一个私有的堆──调用进程地址空间的一个或者多个页面。DLL创建的私有堆必定在调用DLL的进程的地址空间内,只能被调用进程访问。

HeapCreate用来创建堆;HeapAlloc用来从堆中分配一定数量的空间,HeapAlloc分配的内存是不能移动的;HeapSize可以确定从堆中分配的空间的大小;HeapFree用来释放从堆中分配的空间;HeapDestroy销毁创建的堆。

Windows传统的全局或者局部内存分配函数

由于Win32采用平面内存结构模式,Win32下的全局和局部内存函数除了名字不同外,其他完全相同。任一函数都可以用来分配任意大小的内存(仅仅受可用物理内存的限制)。用法可以和Win16下基本一样。

Win32下保留这类函数保证了和Win16的兼容。

C语言的标准内存分配函数

C语言的标准内存分配函数包括以下函数:

malloc,calloc,realloc,free,等。

这些函数最后都映射成堆API函数,所以,malloc分配的内存是不能移动的。这些函数的调式版本为

malloc_dbg,calloc_dbg,realloc_dbg,free_dbg,等。

Win32的虚拟内存分配函数

虚拟内存API是其他API的基础。虚拟内存API以页为最小分配单位,X86上页长度为4KB,可以用GetSystemInfo函数提取页长度。虚拟内存分配函数包括以下函数:

该函数用来分配一定范围的虚拟页。参数1指定起始地址;参数2指定分配内存的长度;参数3指定分配方式,取值 MEM_COMMINT或者MEM_RESERVE;参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、 PAGE_READWRITE或者PAGE_NOACCESS。

该函数功能类似于VirtualAlloc,但是允许指定进程process。VirtaulFree、VirtualProtect、VirtualQuery都有对应的扩展函数。

该函数用来回收或者释放分配的虚拟内存。参数1指定希望回收或者释放内存的基地址;如果是回收,参数2可以指向虚 拟地址范围内的任何地方,如果是释放,参数2必须是VirtualAlloc返回的地址;参数3指定是否释放或者回收内存,取值为 MEM_DECOMMINT或者MEM_RELEASE。

该函数用来把已经分配的页改变成保护页。参数1指定分配页的基地址;参数2指定保护页的长度;参数3指定页的保护属性,取值PAGE_READ、PAGE_WRITE、PAGE_READWRITE等等;参数4用来返回原来的保护属性。

该函数用来查询内存中指定页的特性。参数1指向希望查询的虚拟地址;参数2是指向内存基本信息结构的指针;参数3指定查询的长度。

该函数用来锁定内存,锁定的内存页不能交换到页文件。参数1指定要锁定内存的起始地址;参数2指定锁定的长度。 

参数1指定要解锁的内存的起始地址;参数2指定要解锁的内存的长度。

1.2 C++的new 和 delete操作符

    MFC定义了两种作用范围的new和delete操作符。对于new,不论哪种,参数1类型必须是size_t,且返回void类型指针。

全局范围内的new和delete操作符

原型如下:

void _cdecl ::operator new(size_t nSize);

void __cdecl operator delete(void* p);

调试版本:

void* __cdecl operator new(size_t nSize, int nType,

LPCSTR lpszFileName, int nLine)

类定义的new和delete操作符

原型如下:

类的operator new操作符是类的静态成员函数,对该类的对象来说将覆盖全局的operator new。全局的operator new用来给内部类型对象(如int)、没有定义operator new操作符的类的对象分配内存。

new操作符被映射成malloc或者malloc_dbg,delete被映射成free或者free_dbg。

2 调试手段

    MFC应用程序可以使用C运行库的调试手段,也可以使用MFC提供的调试手段。两种调试手段分别论述如下。

2.1 C运行库提供和支持的调试功能

    C运行库提供和支持的调试功能如下:

调试信息报告函数

用来报告应用程序的调试版本运行时的警告和出错信息。包括:

_CrtDbgReport 用来报告调试信息;

_CrtSetReportMode 设置是否警告、出错或者断言信息;

_CrtSetReportFile 设置是否把调试信息写入到一个文件。

条件验证或者断言宏:

断言宏主要有:

assert 检验某个条件是否满足,不满足终止程序执行。

验证函数主要有:

_CrtIsValidHeapPointer 验证某个指针是否在本地堆中;

_CrtIsValidPointer 验证指定范围的内存是否可以读写;

_CrtIsMemoryBlock 验证某个内存块是否在本地堆中。

内存(堆)调试:

malloc_dbg 分配内存时保存有关内存分配的信息,如在什么文件、哪一行分配的内存等。有一系列用来提供内存诊断的函数:

_CrtMemCheckpoint 保存内存快照在一个_CrtMemState结构中;

_CrtMemDifference 比较两个_CrtMemState;

_CrtMemDumpStatistics 转储输出一_CrtMemState结构的内容;

_CrtMemDumpAllObjectsSince 输出上次快照或程序开始执行以来在堆中分配的所有对象的信息;

_CrtDumpMemoryLeaks 检测程序执行以来的内存漏洞,如果有漏洞则输出所有分配的对象。

2.2 MFC提供的调试手段

    MFC在C运行库提供和支持的调试功能基础上,设计了一些类、函数等来协助调试。

MFC的TRACE、ASSERT

ASSERT

使用ASSERT断言判定程序是否可以继续执行。

TRACE

使用TRACE宏显示或者打印调试信息。TRACE是通过函数AfxTrace实现的。由于AfxTrace函数使用了cdecl调用约定,故可以接受个数不定的参数,如同printf函数一样。它的定义和实现如下:

在程序源码中,可以控制是否显示跟踪信息,显示什么跟踪信息。如果全局变量afxTraceEnabled为 TRUE,则TRACE宏可以输出;否则,没有TRACE信息被输出。如果通过afxTraceFlags指定了跟踪什么消息,则输出有关跟踪信息,例如 为了指定“Multilple Application Debug”,令AfxTraceFlags|=traceMultiApp。可以跟踪的信息有:

这样,应用程序可以在需要的地方指定afxTraceEnabled的值打开或者关闭TRACE开关,指定AfxTraceFlags的值过滤跟踪信息。

Visual C++提供了一个TRACE工具,也可以用来完成上述功能。

为了显示消息信息,MFC内部定义了一个AFX_MAP_MESSAG类型的数组allMessages,储存了Windows消息和消息名映射对。例如:

MFC内部还使用函数_AfxTraceMsg显示跟踪消息,它可以接收一个字符串和一个MSG指针,然后,把该字符串和MSG的各个域的信息组合成一个大的字符串并使用AfxTrace显示出来。

allMessages和函数_AfxTraceMsg的详细实现可以参见AfxTrace.cpp。

MFC对象内容转储

对象内容转储是CObject类提供的功能,所有从它派生的类都可以通过覆盖虚拟函数DUMP来支持该功能。在讲述CObject类时曾提到过。

虚拟函数Dump的定义:

在使用Dump时,必须给它提供一个CDumpContext类型的参数,该参数指定的对象将负责输出调试信 息。为此,MFC提供了一个预定义的全局CDumpContext对象afxDump,它把调试信息输送给调试器的调试窗口。从前面AfxTrace的实 现可以知道,MFC使用了afxDump输出跟踪信息到调试窗口。

CDumpContext类没有基类,它提供了以文本形式输出诊断信息的功能。

例如:

MFC对象有效性检测

对象有效性检测是CObject类提供的功能,所有从它派生的类都可以通过覆盖虚拟函数AssertValid来支持该功能。在讲述CObject类时曾提到过。

虚拟函数AssertValid的定义:

使用ASSERT_VALID宏判断一个对象是否有效,该对象的类必须覆盖了AssertValid函数。形式为:ASSERT_VALID(pObject)。

另外,MFC提供了一些函数来判断地址是否有效,如:

AfxIsMemoryBlock,AfxIsString,AfxIsValidAddress。

3 内存诊断

MFC使用DEBUG_NEW来跟踪内存分配时的执行的源码文件和行数。

把#define new DEBUG_NEW插入到每一个源文件中,这样,调试版本就使用_malloc_dbg来分配内存。MFC Appwizard在创建框架文件时已经作了这样的处理。

AfxDoForAllObjects

MFC提供了函数AfxDoForAllObjects来追踪动态分配的内存对象,函数原型如下:

void AfxDoForAllObjects( void (*pfn)(CObject* pObject,

void* pContext), void* pContext );

其中:

参数1是一个函数指针,AfxDoForAllObjects对每个对象调用该指针表示的函数。

参数2将传递给参数1指定的函数。

AfxDoForAllObjects可以检测到所有使用new分配的CObject对象或者CObject类派生的对象,但全局对象、嵌入对象和栈中分配的对象除外。

内存漏洞检测

仅仅用于new的DEBUG版本分配的内存。

完成内存漏洞检测,需要如下系列步骤:

调用AfxEnableMemoryTracking(TRUE/FALSE)打开/关闭内存诊断。在调试版本下,缺省是打开的;关闭内存诊断可以加快程序执行速度,减少诊断输出。

使用MFC全局变量afxMemDF更精确地指定诊断输出的特征,缺省值是allocMemDF,可以取如下值或者这些值相或:

afxMemDF,delayFreeMemDF,checkAlwaysMemDF

其中:allocMemDF表示可以进行内存诊断输出;delayFreeMemDF表示是否是在应用程序结束时 才调用free或者delete,这样导致程序最大可能的分配内存;checkAlwaysMemDF表示每一次分配或者释放内存之后都调用函数 AfxCheckMemory进行内存检测(AfxCheckMemory检查堆中所有通过new分配的内存(不含malloc))。

这一步是可选步骤,非必须。

创建一个CMemState类型的变量oldMemState,调用CMemState的成员函数CheckPoint获得初次内存快照。

执行了系列内存分配或者释放之后,创建另一个CMemState类型变量newMemState,调用CMemState的成员函数CheckPoint获得新的内存快照。

创建第三个CMemState类型变量difMemState,调用CMemState的成员函数Difference比较oldMemState和newMemState,结果保存在变量difMemState中。如果没有不同,则返回FALSE,否则返回TRUE。

如果不同,则调用成员函数DumpStatistics输出比较结果。

/**********************************************************

Qt内存管理机制

前言

内存管理,是对软件中内存资源的分配与释放进行有效管理的方法和理论。

众所周知,内存管理是软件开发的一个重要的内容。软件规模越大,内存管理可能出现的问题越多。如果像C语言一样手动地管理内存,一会给开发人员带来巨大的负担,二是手动管理内存的可靠性较差。

Qt为软件开发人员提供了一套内存管理机制,用以替代手动内存管理。

下面开始逐条讲述Qt中的内存管理机制。

一脉相承的栈与堆的内存管理

了解C语言的同学都知道,C语言中的内存分配有两种形式:栈内存、堆内存。

栈内存

栈内存的管理是由编译器来做的,栈上申请的内存变量,生存期由所在作用域决定,超出作用域的栈内存变量会被编译器自动释放。

值得一提的是,作用域的显著标志是一对大括号,大括号内部即为作用域内部,大括号外部即为作用域外部。

参考下列代码:

 
int main()
{
int a = 0;
return 1;
}

变量a在栈内存上,main函数返回时,作用域结束,a的内存自动被释放。

从以上描述也可以看出,栈内存的使用是在编译器严密监管之下进行的,遵循严格的作用域规则,所以栈内存的大小、申请时机、释放时机都能在编译的时候确定。

堆内存

堆内存是另外一种管理方式。堆内存最大的特点是可以动态分配,即在运行时可以根据需要进行申请。当然随之而来的弊端也显而易见:需要开发人员对堆内存的释放进行严格管理,稍有疏漏会导致内存泄漏,甚至软件崩溃等问题。

参考下列代码:

 
int main()
{
// 申请堆内存
int *intArray = (int *)malloc(100);
// 使用堆内存...
// 释放堆内存
free(intArray);
return 1;
}

如上述代码,堆内存分配的写法区别于栈内存。C语言中,堆内存使用malloc分配,使用free释放。C++中可以使用new分配,使用delete释放。

至此,我们介绍了C语言中的内存管理方式。我们知道Qt是C++的框架,C++是对C语言的扩展,所以C语言中的内存管理方式(堆、栈)和动态内存管理(堆内存释放问题)存在的问题,在C++中仍然存在。所以Qt中自然而然也有相同的问题。说起来可能有点乱,下面用一张图来说明它们的关系:

请添加图片描述

那么,Qt是如何为我们解决动态内存管理问题的呢?下面开始正式讲解。

使用对象父子关系进行内存管理

使用对象父子关系进行内存管理的原理,简述为:

在创建类的对象时,为对象指定父对象指针。当父对象在某一时刻被销毁释放时,父对象会先遍历其所有的子对象,并逐个将子对象销毁释放。

为了直观理解上述过程,以如下代码为例进行说明:

 
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 创建主窗口
QWidget mainWidget;
mainWidget.resize(400, 300);
// 创建文字标签
QLabel *label = new QLabel("Hello World!", &mainWidget);
// 显示主窗口
mainWidget.show();
return a.exec();
}

运行结果如下:

请添加图片描述

上述代码中,mainWidget为主窗口对象,类型为QWidgetlabel为子窗口对象,类型为QLabel *。

注意代码第13行,在创建label文本标签窗口对象时,new QLabel的第二个参数即为父对象地址(参考Qt Assistant中QLabel的说明文档),这里给的值是主窗口的地址。

main函数退出时,mainWidget超出main函数作用域会析构,析构时会自动删除label窗口对象,所以这里,我们不需要再写一行:delete label; 来释放label的内存,很方便而且又能节省时间精力。

使用引用计数对内存进行管理

引用计数

引用计数可以说是软件开发人员必知必会的知识点,它在内存管理领域的地位是数一数二的。

引用计数的原理,还是力所能及地用最简单的话来描述:

引用计数需要从三个方面来全面理解:

  1. 使用场景:一个资源,多处使用(使用即引用)。

  2. 问题:到底谁来释放资源。

  3. 原理:使用一个整形变量来统计,此资源在多少个地方被使用,此变量称为引用计数。当某处使用完资源以后,将引用计数减1。当引用计数为0时,即没有任何地方再使用此资源时,真正释放此资源。这里的资源,在动态内存管理中就是指堆内存。

用一句话描述就是:谁最后使用资源,谁负责释放资源

我们很容易联想到现实中的例子,就是日常生活中的刷碗问题的解决方案,即谁最后吃完谁刷碗。

需要说明的是,引用计数不仅仅是在内存管理中使用,它是一个通用的机制,凡是涉及到资源管理的问题,都可以考虑使用引用计数。

下面将要介绍基于引用计数原理的两种衍生的机制:显式共享和隐式共享。

显式共享

显式共享,是仅仅使用引用计数控制资源的生命周期的一种共享管理机制。这种机制下,无论资源在何处被引用,自始至终所有引用指向资源都是同一个。

之所以叫显式共享,是因为这种共享方式很直接,没有隐含的操作,如:Copy on Write写时拷贝(见隐式共享的相关说明)。如果想要拷贝并建立新的引用计数,必须手动调用detach()函数。

从使用者的角度看,从头到尾资源只有一份,一个地方修改了,另一个地方就能读取到修改后的资源。

**相关Qt类:**QExplicitlySharedDataPointer,更加深入的用法和编码,需要参考Qt文档中的相关说明及Demo。

隐式共享

隐式共享,也是一种基于引用计数的控制资源的生命周期的共享管理机制。

隐式共享,对不同的操作有不同的处理:

  • 读取时,在所有引用的地方使用同一个资源;

  • 在写入、修改时自动复制一份资源出来做修改,自动脱离原始的引用计数,因为是新的资源,所以要建立新的引用计数。这种操作叫Copy on Write写时复制技术,是自动隐含进行的。

从使用者的角度看,每个使用者都像是拥有独立的一份资源。在一个地方修改,修改的只是原始资源的拷贝,不会影响原始资源的内容,自然就不会影响到其他使用者。所以这种共享方式称为隐式共享。

相关Qt类有QString、QByteArray、QImage、QList、QMap、QHash等。

推荐阅读:Qt文档中的Implicit Sharing专题。

智能指针

智能指针是对C/C++指针的扩展,同样基于引用计数。

智能指针和显示共享和隐式共享有何区别?它们区别是:智能指针是轻量级的引用计数,它将显式共享、隐式共享中的引用计数实现部分单独提取了出来,制作成模板类,形成了多种特性各异的指针。

例如,QString除了实现引用计数,还实现了字符串相关的丰富的操作接口。QList也实现了引用计数,还实现了列表这种数据结构的各种操作。可以说,显式共享和隐式共享一般是封装在功能类中的,不需要开发者来管理。

智能指针将引用计数功能剥离出来,为Qt开发者提供了便捷的引用计数基础设施。

强(智能)指针

Qt中的强指针实现类是:QSharedPointer,此类是模板类,可以指向多种类型的数据,主要用来管理堆内存。关于QSharedPointer在Qt Assistant中有详细描述。

它的原理和显式共享一样:最后使用的地方负责释放删除资源,如类对象、内存块。

强指针中的“强”,是指每多一个使用者,引用计数都会老老实实地**+1**。而弱指针就不同,下面就接着讲解弱指针。

弱(智能)指针

Qt中的弱指针实现类是QWeakPointer,此类亦为模板类,可以指向多种类型的数据,同样主要用来管理堆内存。关于QWeakPointer在Qt Assistant中有详细描述。

弱指针只能从强指针QSharedPointer转化而来,获取弱指针,不增加引用计数,它只是一个强指针的观察者,观察而不干预。只要强指针存在,弱指针也可以转换成强指针。可见弱指针和强指针是一对形影不离的组合,通常结合起来使用。

局部指针

局部指针,是一种超出作用域自动删除、释放堆内存、对象的工具。它结合了栈内存管理和堆内存管理的优点。

Qt中的实现类有:QScopedPointer,QScopedArrayPointer,具体可以参考Qt Assistant。

观察者指针

上面说弱指针的时候,讲到过观察者。观察者是指仅仅做查询作用的指针,不会影响到引用计数。

Qt中的观察者指针是QPointer,它必须指向QObject的子类对象,才能对对象生命周期进行观察。因为只有QObject子类才会在析构的时候通知QPointer已失效。

QPointer是防止悬挂指针(即野指针)的有效手段,因为所指对象一旦被删除,QPointer会自动置空,在使用时,判断指针是否为空即可,不为空说明对象可以使用,不会产生内存访问错误的问题。

总结

本篇文章讲解了Qt中的各种内存管理机制,算是做了一个比较全面的描述。

之所以说是必读,是因为笔者在工作中发现,内存管理确实非常重要。Qt内存管理机制是贯穿整个Qt中所有类的核心线索之一,搞懂了内存管理

  • 能在脑海中形成内存中对象的布局图,写代码的时候才能下笔如有神,管理起项目中众多的对象才能游刃有余,提高开发效率;
  • 能够减少bug的产生。有经验的开发者应该知道,内存问题很难调试定位到具体的位置,往往导致奇怪的bug出现。
  • 能够帮助理解Qt众多类的底层不变的逻辑,学起来更容易。

本文只是对Qt中内存管理进行了梳理,无法涵盖很多细节问题,读者需要花一些时间去详细阅读Qt助手文档,最好是写几个demo测试验证。花时间是值得的,因为技术是日新月异的,但是核心的原理变化是不大的。Qt中的内存管理思想和方法,在很多语言、框架中(Python、Objective C、JavaScript等等)都有类似的应用。

值得一提的是,之所以Qt中具有各种各样的内存管理方式,是因为它能够减轻开发者的负担,更加专注于业务代码的实现,而不是被内存问题折腾的焦头烂额。不使用Qt中的内存管理,只用C的手动内存管理仍然可以写可以运行的代码!前提是不考虑成本问题,并假设开发者在内存问题上不会犯错。总之一句话,不要对立各种技术,每种技术都有适用的场景,抛开场景谈方法都是不理智的。

/*********************************************************************

内存为程序分配空间的四种分配方式

存储器是个宝贵但却有限的资源。一流的操作系统,需要能够有效地管理及利用存储器。

内存为程序分配空间有四种分配方式:

  • 1、连续分配方式
  • 2、基本分页存储管理方式
  • 3、基本分段存储管理方式
  • 4、段页式存储管理方式

连续分配方式

首先讲连续分配方式。连续分配方式出现的时间比较早,曾广泛应用于20世纪60~70年代的OS中,但是它至今仍然在内存管理方式中占有一席之地,原因在于它实现起来比较方便,所需的硬件支持最少。连续分配方式又可细分为四种:单一连续分配、固定分区分配、动态分区分配和动态重定位分区分配

其中固定分区的分配方式,因为分区固定,所以缺乏灵活性,即当程序太小时,会造成内存空间的浪费(内部碎片)程序太大时,一个分区又不足以容纳,致使程序无法运行(外部碎片)。但尽管如此,当一台计算机去控制多个相同对象的时候,由于这些对象内存大小相同,所以完全可以采用这种内存管理方式,而且是最高效的。这里我们可以看出存储器管理机制的多面性:没有那种存储器管理机制是完全没有用的,在适合的场合下,一种被认为最不合理的分配方案却可能称为最高效的分配方案。一切都要从实际问题出发,进行设计。

为了解决固定分区分配方式的缺乏灵活性,出现了动态分配方式。动态分配方式采用一些寻表(Eg:空闲链表)的方式,查找能符合程序需要的空闲内存分区。但代价是增加了系统运行的开销,而且内存空闲表本身是一个文件,必然会占用一部分宝贵的内存资源,而且有些算法还会增加内存碎片。

可重定位分区分配通过对程序实现成定位,从而可以将内存块进行搬移,将小块拼成大块,将小空闲“紧凑”成大空闲,腾出较大的内存以容纳新的程序进程。

基本分页存储管理方式

连续分配方式会形成许多“碎片”,虽然可以通过“紧凑”方式将许多碎片拼接成可用的大块空间,但须为之付出很大开销。所以提出了“离散分配方式”的想法。如果离散分配的基本单位是页,则称为分页管理方式;如果离散分配的基本单位是段,则称为分段管理方式

分页存储管理是将一个进程的逻辑地址空间分成若干个大小相等的片,称为页面


https://www.daipet.cn/news/4639.html

相关文章

LeetCode链表练习(上)

文章目录前言1.反转链表1.题目分析2.代码示例2.力扣203. 移除链表元素1.题目分析2.代码示例3.力扣876. 链表的中间结点1.题目分析2.代码示例4.链表倒数第k个节点1.题目分析2.代码示例5.总结前言 之前介绍了链表的实现&amp;#xff0c;为了更好巩固所学的知识&amp;#xff0c;刷题是很有…

【springboot】你了解@Autowired 和 @Resource吗?@Autowired 和 @Resource深入分析

Autowired 和 Resource深入分析“认祖归宗”--Autowired 和 Resource来源“通过现象看本质”--Autowired 和 Resource作用和区别1.现象一&amp;#xff1a;一个业务接口只对应一个业务实现类2.现象二&amp;#xff1a;一个业务接口 对应 两个或多个业务实现类我们在开发中&amp;#xff0c;一直…

【数据结构】链表其实并不难 —— 手把手带你实现双向链表

文章目录0. 前言1. 双向链表的概念2. 双向链表的实现2.1 结构设计2.2 接口总览2.3 初始化2.4 创建新节点2.5 尾插2.6 头插2.7 尾删2.8 头删2.9 查找2.10 在pos位置之前插入2.11 在pos位置删除2.12 打印2.13 销毁3. 完整代码List.hList.ctest.c4. 结语0. 前言 之前&amp;#xff0c;…

【路径规划】局部路径规划算法——DWA算法(动态窗口法)|(含python实现)

文章目录参考资料1. DWA算法原理1.1 简介1.2 算法原理1. 速度采样2. 轨迹预测&amp;#xff08;轨迹推算&amp;#xff09;3. 轨迹评价2. Python实现2.1 参数配置2.2 机器人运动学模型2.3 DWA算法类实现2.4 画图2.5 主函数3. 总结参考资料 The Dynamic Window Approach to Collision Avo…

【Python百日进阶-WEB开发】Day175 - Django案例:07状态保持

文章目录五、状态保持5.1 Django中状态保持5.1.1 状态保持概述5.1.2 Cookie5.1.2.1 Cookie的用处&amp;#xff1a;5.1.2.1 Cookie的特点&amp;#xff1a;5.1.2.1 Cookie的操作&amp;#xff1a;5.1.3 session5.1.3.1 Session的特点&amp;#xff1a;5.1.3.2 Session依赖于Cookie5.1.3.3 存储方式5…

网页数据抓取-网页实时数据抓取软件

网页数据抓取&amp;#xff0c;随着社会的发展&amp;#xff0c;互联网的普及&amp;#xff0c;不管是企业还是个人都意识到数据的重要性。今天给大家分享一款免费的网页数据抓取软件。只要点点鼠标就能轻松采集你想要的内容不管是导出还是自动发布都支持&amp;#xff01;详细参考图片&amp;#xff01;…

Qlib股票数据获取与查看(Qlib学习1)

文章目录Qlib基本信息数据使用方法1. 借助Qlib下载数据2. 查看相关数据参考链接Qlib基本信息 Qlib Github主页&amp;#xff1a;https://github.com/microsoft/qlib Qlib quickstart&amp;#xff1a;https://qlib.readthedocs.io/en/latest/introduction/quick.html#introduction 基本…

二分查找的模板

这篇博客的二分用的都是左闭右闭的区间&amp;#xff0c;对于二分来说还是我还是习惯这样写 最传统的二分查找&amp;#xff0c;用左闭右闭写 int search(vector&lt;int&gt;&amp; nums, int target) {int left 0;int right nums.size() - 1; // 定义target在左闭右闭的区间里&amp;#xff0…

LeetCode刷题---142. 环形链表 II(双指针-快慢指针)

文章目录一、编程题&amp;#xff1a;142. 环形链表 II&amp;#xff08;双指针-快慢指针&amp;#xff09;1.题目描述2.示例1&amp;#xff1a;3.示例2&amp;#xff1a;4.示例3&amp;#xff1a;5.提示&amp;#xff1a;6.提示&amp;#xff1a;二、解题思路1.思路2.复杂度分析&amp;#xff1a;3.算法图解三、代码实现总结…

Mybatis学习之动态Sql

目录 1. 什么是动态Sql 2. 动态Sql需要学习什么 3. 动态Sql之《if》 4. 动态Sql之《where》 5. 动态Sql之《foreach》 6. 动态Sql之《sql》 7. PageHelper分页插件的使用 1. 什么是动态Sql 答案&amp;#xff1a;动态Sql指的是&amp;#xff0c;Sql语句是变化的&amp;#xff0c;不是固…