一文搞懂C/C++内存管理原理与实现

news2024/9/23 17:24:01

C 语言内存管理指对系统内存的分配、创建、使用这一系列操作。在内存管理中,由于是操作系统内存,使用不当会造成毕竟麻烦的结果。本文将从系统内存的分配、创建出发,并且使用例子来举例说明内存管理不当会出现的情况及解决办法。

一、内存

在计算机中,每个应用程序之间的内存是相互独立的,通常情况下应用程序 A 并不能访问应用程序 B,当然一些特殊技巧可以访问,但此文并不详细进行说明。例如在计算机中,一个视频播放程序与一个浏览器程序,它们的内存并不能访问,每个程序所拥有的内存是分区进行管理的。

在计算机系统中,运行程序 A 将会在内存中开辟程序 A 的内存区域 1,运行程序 B 将会在内存中开辟程序 B 的内存区域 2,内存区域 1 与内存区域 2 之间逻辑分隔。

1.1 内存四区

在程序 A 开辟的内存区域 1 会被分为几个区域,这就是内存四区,内存四区分为栈区、堆区、数据区与代码区。

栈区指的是存储一些临时变量的区域,临时变量包括了局部变量、返回值、参数、返回地址等,当这些变量超出了当前作用域时将会自动弹出。该栈的最大存储是有大小的,该值固定,超过该大小将会造成栈溢出。

堆区指的是一个比较大的内存空间,主要用于对动态内存的分配;在程序开发中一般是开发人员进行分配与释放,若在程序结束时都未释放,系统将会自动进行回收。

数据区指的是主要存放全局变量、常量和静态变量的区域,数据区又可以进行划分,分为全局区与静态区。全局变量与静态变量将会存放至该区域。

代码区就比较好理解了,主要是存储可执行代码,该区域的属性是只读的。

1.2 使用代码证实内存四区的底层结构

由于栈区与堆区的底层结构比较直观的表现,在此使用代码只演示这两个概念。首先查看代码观察栈区的内存地址分配情况:

#include<stdio.h>
int main()
{
 int a = 0;
 int b = 0;
 char c='0';
 printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);

}

运行结果为:

我们可以观察到变量 a 的地址是 2293324 变量 b 的地址是 2293320,由于 int 的数据大小为 4 所以两者之间间隔为 4;再查看变量 c,我们发现变量 c 的地址为 2293319,与变量 b 的地址 2293324 间隔 1,因为 c 的数据类型为 char,类型大小为 1。在此我们观察发现,明明我创建变量的时候顺序是 a 到 b 再到 c,为什么它们之间的地址不是增加而是减少呢?那是因为栈区的一种数据存储结构为先进后出,如图:

首先栈的顶部为地址的“最小”索引,随后往下依次增大,但是由于堆栈的特殊存储结构,我们将变量 a 先进行存储,那么它的一个索引地址将会是最大的,随后依次减少;第二次存储的值是 b,该值的地址索引比 a 小,由于 int 的数据大小为 4,所以在 a 地址为 2293324 的基础上往上减少 4 为 2293320,在存储 c 的时候为 char,大小为 1,则地址为 2293319。由于 a、b、c 三个变量同属于一个栈内,所以它们地址的索引是连续性的,那如果我创建一个静态变量将会如何?在以上内容中说明了静态变量存储在静态区内,我们现在就来证实一下:

#include<stdio.h>
int main()
{
 
 int a = 0;
 int b = 0;
 char c='0';
 static int d = 0;
 
 printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
 
 printf("静态变量d的地址是:%d\n", &d);

}

运行结果如下:

以上代码中创建了一个变量 d,变量 d 为静态变量,运行代码后从结果上得知,静态变量 d 的地址与一般变量 a、b、c 的地址并不存在连续,他们两个的内存地址是分开的。那接下来在此建一个全局变量,通过上述内容得知,全局变量与静态变量都应该存储在静态区,代码如下:

#include<stdio.h>
int e = 0;
int main()
{
 
 int a = 0;
 int b = 0;
 char c='0';
 static int d = 0;
 
 printf("变量a的地址是:%d\n变量b的地址是:%d\n变量c的地址是:%d\n", &a, &b, &c);
 
 printf("静态变量d的地址是:%d\n", &d);
 printf("全局变量e的地址是:%d\n", &e);

}

运行结果如下:

从以上运行结果中证实了上述内容的真实性,并且也得到了一个知识点,栈区、数据区都是使用栈结构对数据进行存储。

在以上内容中还说明了一点栈的特性,就是容量具有固定大小,超过最大容量将会造成栈溢出。查看如下代码:

#include<stdio.h>

int main()
{
 char arr_char[1024*1000000];
    arr_char[0] = '0';
}

以上代码定义了一个字符数组 arr_char,并且设置了大小为 1024*1000000,设置该数据是方便查看大小;随后在数组头部进行赋值。运行结果如下:

这是程序运行出错,原因是造成了栈的溢出。在平常开发中若需要大容量的内存,需要使用堆。

堆并没有栈一样的结构,也没有栈一样的先进后出。需要人为的对内存进行分配使用。代码如下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int main()
{
 char *p1 = (char *)malloc(1024*1000000);
 strcpy(p1, "这里是堆区");
 printf("%s\n", p1);
}

以上代码中使用了strcpy 往手动开辟的内存空间 p1 中传数据“这里是堆区”,手动开辟空间使用 malloc,传入申请开辟的空间大小 1024*1000000,在栈中那么大的空间必定会造成栈溢出,而堆本身就是大容量,则不会出现该情况。随后输出开辟的内存中内容,运行结果如下:

在此要注意p1是表示开辟的内存空间地址。

二、malloc 和 free

在 C 语言(不是 C++)中,malloc 和 free 是系统提供的函数,成对使用,用于从堆中分配和释放内存。malloc 的全称是 memory allocation 译为“动态内存分配”。

2.1 malloc 和 free 的使用

在开辟堆空间时我们使用的函数为 malloc,malloc 在 C 语言中是用于申请内存空间,malloc 函数的原型如下:

void *malloc(size_t size);

在 malloc 函数中,size 是表示需要申请的内存空间大小,申请成功将会返回该内存空间的地址;申请失败则会返回 NULL,并且申请成功也不会自动进行初始化。

细心的同学可能会发现,该函数的返回值说明为 void *,在这里 void * 并不指代某一种特定的类型,而是说明该类型不确定,通过接收的指针变量从而进行类型的转换。在分配内存时需要注意,即时在程序关闭时系统会自动回收该手动申请的内存 ,但也要进行手动的释放,保证内存能够在不需要时返回至堆空间,使内存能够合理的分配使用。

释放空间使用 free 函数,函数原型如下:

void free(void *ptr);

free 函数的返回值为 void,没有返回值,接收的参数为使用 malloc 分配的内存空间指针。一个完整的堆内存申请与释放的例子如下:

#include<stdio.h>
#include<string.h>
#include <malloc.h>

int main() {
    int n, *p, i;
    printf("请输入一个任意长度的数字来分配空间:");
    scanf("%d", &n);
    
    p = (int *)malloc(n * sizeof(int));
 if(p==NULL){
  printf("申请失败\n");
  return 0;
 }else{
  printf("申请成功\n");
 } 
 
 memset(p, 0, n * sizeof(int));//填充0 
 
 //查看 
    for (i = 0; i < n; i++)
        printf("%d ", p[i]);
    printf("\n");

    free(p);
    p = NULL;
    return 0;
}

以上代码中使用了 malloc 创建了一个由用户输入创建指定大小的内存,判断了内存地址是否创建成功,且使用了 memset 函数对该内存空间进行了填充值,随后使用 for 循环进行了查看。

最后使用了 free 释放了内存,并且将 p 赋值 NULL,这点需要主要,不能使指针指向未知的地址,要置于 NULL;否则在之后的开发者会误以为是个正常的指针,就有可能再通过指针去访问一些操作,但是在这时该指针已经无用,指向的内存也不知此时被如何使用,这时若出现意外将会造成无法预估的后果,甚至导致系统崩溃,在 malloc 的使用中更需要需要。

2.2 内存泄漏与安全使用实例与讲解

内存泄漏是指在动态分配的内存中,并没有释放内存或者一些原因造成了内存无法释放,轻度则造成系统的内存资源浪费,严重的导致整个系统崩溃等情况的发生。

内存泄漏通常比较隐蔽,且少量的内存泄漏发生不一定会发生无法承受的后果,但由于该错误的积累将会造成整体系统的性能下降或系统崩溃。特别是在较为大型的系统中,如何有效的防止内存泄漏等问题的出现变得尤为重要。

例如一些长时间的程序,若在运行之初有少量的内存泄漏的问题产生可能并未呈现,但随着运行时间的增长、系统业务处理的增加将会累积出现内存泄漏这种情况;这时极大的会造成不可预知的后果,如整个系统的崩溃,造成的损失将会难以承受。由此防止内存泄漏对于底层开发人员来说尤为重要。

C 程序员在开发过程中,不可避免的面对内存操作的问题,特别是频繁的申请动态内存时会及其容易造成内存泄漏事故的发生。

如申请了一块内存空间后,未初始化便读其中的内容、间接申请动态内存但并没有进行释放、释放完一块动态申请的内存后继续引用该内存内容;如上所述这种问题都是出现内存泄漏的原因,往往这些原因由于过于隐蔽在测试时不一定会完全清楚,将会导致在项目上线后的长时间运行下,导致灾难性的后果发生。

如下是一个在子函数中进行了内存空间的申请,但是并未对其进行释放:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
void m() { 
 char *p1; 
 p1 = malloc(100); 
 printf("开始对内存进行泄漏...");
}
 
int main() {
    m();
    return 0;
}

如上代码中,使用 malloc 申请了 100 个单位的内存空间后,并没有进行释放。假设该 m 函数在当前系统中调用频繁,那将会每次使用都将会造成 100 个单位的内存空间不会释放,久而久之就会造成严重的后果。理应在 p1 使用完毕后添加 free 进行释放:

free(p1);

以下示范一个读取文件时不规范的操作:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) { 
 FILE* f;
 int key; 
 f = fopen(filename, "r"); 
 fscanf(f, "%d", &key); 
 return key; 
}
 
int main() {
    m("number.txt");
    return 0;
}

以上文件在读取时并没有进行 fclose,这时将会产生多余的内存,可能一次还好,多次会增加成倍的内存,可以使用循环进行调用,之后在任务管理器中可查看该程序运行时所占的内存大小,代码为:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *filename) { 
 FILE* f;
 int key; 
 f = fopen(filename, "r"); 
 fscanf(f, "%d", &key); 
 return key; 
}
 
int main() {
 int i;
 for(i=0;i<500;i++) {
     m("number.txt");
 }
    return 0;
}

可查看添加循环后的程序与添加循环前的程序做内存占用的对比,就可以发现两者之间添加了循环的代码将会成本增加占用容量。

未被初始化的指针也会有可能造成内存泄漏的情况,因为指针未初始化所指向不可控,如:

int *p;
*p = val;

包括错误的释放内存空间:

pp=p;
free(p); 
free(pp);

释放后使用,产生悬空指针。在申请了动态内存后,使用指针指向了该内存,使用完毕后我们通过 free 函数释放了申请的内存,该内存将会允许其它程序进行申请;但是我们使用过后的动态内存指针依旧指向着该地址,假设其它程序下一秒申请了该区域内的内存地址,并且进行了操作。

当我依旧使用已 free 释放后的指针进行下一步的操作时,或者所进行了一个计算,那么将会造成的结果天差地别,或者是其它灾难性后果。所以对于这些指针在生存期结束之后也要置为 null。查看一个示例,由于 free 释放后依旧使用该指针,造成的计算结果天差地别:

#include<stdio.h>
#include<string.h>
#include <malloc.h>
int m(char *freep) { 
 int val=freep[0];
 printf("2*freep=:%d\n",val*2);
 free(freep);
 val=freep[0];
 printf("2*freep=:%d\n",val*2);
}
 
int main() {
 int *freep = (int *) malloc(sizeof (int));
 freep[0]=1;
 m(freep);
    return 0;
    
}

以上代码使用 malloc 申请了一个内存后,传值为 1;在函数中首先使用 val 值接收 freep 的值,将 val 乘 2,之后释放 free,重新赋值给 val,最后使用 val 再次乘 2,此时造成的结果出现了极大的改变,而且最恐怖的是该错误很难发现,隐蔽性很强,但是造成的后顾难以承受。运行结果如下:

三、 new 和 delete

C++ 中使用 new 和 delete 从堆中分配和释放内存,new 和 delete 是运算符,不是函数,两者成对使用(后面说明为什么成对使用)。

资料直通车:最新Linux内核源码资料文档+视频资料icon-default.png?t=N176https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈icon-default.png?t=N176https://ke.qq.com/course/4032547?flowToken=1040236

new/delete 除了分配内存和释放内存(与 malloc/free),还做更多的事情,所有在 C++ 中不再使用 malloc/free 而使用 new/delete。

3.1 new 和 delete 使用

new 一般使用格式如下:

  • 指针变量名 = new 类型标识符;
  • 指针变量名 = new 类型标识符(初始值);
  • 指针变量名 = new 类型标识符[内存单元个数];

在C++中new的三种用法包括:plain new, nothrow new 和 placement new。

plain new 就是我们最常使用的 new 的方式,在 C++ 中的定义如下:

void* operator new(std::size_t) throw(std::bad_alloc);  
void operator delete( void *) throw();

plain new 在分配失败的情况下,抛出异常 std::bad_alloc 而不是返回 NULL,因此通过判断返回值是否为 NULL 是徒劳的。

char *getMemory(unsigned long size)   
{    
    char * p = new char[size];   
    return p; 
}   
void main(void)   
{
    try{   
        char * p = getMemory(1000000);    // 可能发生异常
        // ...   
        delete [] p;   
    }   
    catch(const std::bad_alloc &amp; ex)   
    {
        cout &lt;&lt; ex.what();
    }   
}

nothrow new 是不抛出异常的运算符new的形式。nothrow new在失败时,返回NULL。定义如下:

void * operator new(std::size_t, const std::nothrow_t&) throw();
void operator delete(void*) throw();

void func(unsinged long length)   
{
    unsinged char * p = new(nothrow) unsinged char[length];   
    // 在使用这种new时要加(nothrow) ,表示不使用异常处理 。
 
    if (p == NULL)  // 不抛异常,一定要检查
        cout << "allocte failed !";   
        // ...   
    delete [] p;
}

placement new 意即“放置”,这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:

void* operator new(size_t, void*);
void operator delete(void*, void*);

palcement new 的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组。placement new构造起来的对象或其数组,要显示的调用他们的析构函数来销毁,千万不要使用delete。

void main()   
{  
    using namespace std;   
    char * p = new(nothrow) char [4];   
    if (p == NULL)   
    {
        cout << "allocte failed" << endl;  
        exit( -1 );
    }   
    // ...   
    long * q = new (p) long(1000);   
    delete []p;    // 只释放 p,不要用q释放。
}

p 和 q 仅仅是首址相同,所构建的对象可以类型不同。所“放置”的空间应小于原空间,以防不测。当”放置new”超过了申请的范围,Debug 版下会崩溃,但 Release 能运行而不会出现崩溃!

该运算符的作用是:只要第一次分配成功,不再担心分配失败。

void main()   
{
    using namespace std;   
    char * p = new(nothrow) char [100];   
    if (p == NULL)   
    {  
        cout << "allocte failed" << endl;
        exit(-1);
    }   
    long * q1 = new (p) long(100);   
    // 使用q1  ...   
    int * q2 = new (p) int[100/sizeof(int)];   
    // 使用q2 ...   
    ADT * q3 = new (p) ADT[100/sizeof(ADT)];   
    // 使用q3  然后释放对象 ...   
    delete [] p;    // 只释放空间,不再析构对象。
}

注意:使用该运算符构造的对象或数组,一定要显式调用析构函数,不可用 delete 代替析构,因为 placement new 的对象的大小不再与原空间相同。

void main()   
{  
    using namespace std;   
    char * p = new(nothrow) char [sizeof(ADT)+2];   
    if (p == NULL)   
    {  
        cout << "allocte failed" &lt;&lt; endl;
        exit(-1); 
    } 
    // ... 
    ADT * q = new (p) ADT; 
    // ... 
    // delete q; // 错误
    q->ADT::~ADT();  // 显式调用析构函数,仅释放对象
    delete [] p;     // 最后,再用原指针来释放内存
}

placement new 的主要用途就是可以反复使用一块已申请成功的内存空间。这样可以避免申请失败的徒劳,又可以避免使用后的释放。

特别要注意的是对于 placement new 绝不可以调用的 delete, 因为该 new 只是使用别人替它申请的地方。释放内存是 nothrow new 的事,即要使用原来的指针释放内存。free/delete 不要重复调用,被系统立即回收后再利用,再一次 free/delete 很可能把不是自己的内存释放掉,导致异常甚至崩溃。

上面提到 new/delete 比 malloc/free 多做了一些事情,new 相对于 malloc 会额外的做一些初始化工作,delete 相对于 free 多做一些清理工作。

class A
{
 public:
     A()
     {
        cont<<"A()构造函数被调用"<<endl;
     }
     ~A()
     {
        cont<<"~A()构造函数被调用"<<endl;
     }
}

在 main 主函数中,加入如下代码:

A* pa = new A();  //类 A 的构造函数被调用
delete pa;        //类 A 的析构函数被调用

可以看出:使用 new 生成一个类对象时系统会调用该类的构造函数,使用 delete 删除一个类对象时,系统会调用该类的析构函数。可以调用构造函数/析构函数就意味着 new 和 delete 具备针对堆所分配的内存进行初始化和释放的能力,而 malloc 和 free 不具备。

2.2 delete 与 delete[] 的区别

c++ 中对 new 申请的内存的释放方式有 delete 和 delete[] 两种方式,到底这两者有什么区别呢?

我们通常从教科书上看到这样的说明:

  • delete 释放 new 分配的单个对象指针指向的内存
  • delete[] 释放 new 分配的对象数组指针指向的内存 那么,按照教科书的理解,我们看下下面的代码:
int *a = new int[10];
delete a;        //方式1
delete[] a;     //方式2
  1. 针对简单类型 使用 new 分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:
int *a = new int[10];
delete a;
delete[] a;

此种情况中的释放效果相同,原因在于:分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数。

它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体 _CrtMemBlockHeader 中,具体情况可参看 VC 安装目录下 CRTSRCDBGDEL.cpp)。

  1. 针对类 Class,两种方式体现出具体差异

当你通过下列方式分配一个类对象数组:

class A
   {
    private:
      char *m_cBuffer;
      int m_nLen;

   `` public:
      A(){ m_cBuffer = new char[m_nLen]; }
      ~A() { delete [] m_cBuffer; }
   };

   A *a = new A[10];
   delete a;         //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
   delete[] a;      //调用使用类对象的析构函数释放用户自己分配内存空间并且   释放了a指针指向的全部内存空间

所以总结下就是,如果 ptr 代表一个用new申请的内存返回的内存空间地址,即所谓的指针,那么:

delete ptr 代表用来释放内存,且只用来释放 ptr 指向的内存。delete[] rg 用来释放rg指向的内存,!!还逐一调用数组中每个对象的destructor!!

对于像 int/char/long/int*/struct 等等简单数据类型,由于对象没有 destructor ,所以用 delete 和 delete []是一样的!但是如果是 C++ 对象数组就不同了!

关于 new[] 和 delete[],其中又分为两种情况:

  • (1) 为基本数据类型分配和回收空间;
  • (2) 为自定义类型分配和回收空间;

对于 (1),上面提供的程序已经证明了 delete[] 和 delete 是等同的。但是对于 (2),情况就发生了变化。

我们来看下面的例子,通过例子的学习了解 C++ 中的 delete 和 delete[] 的使用方法

#include <iostream>
using namespace std;

class Babe
{
public:
    Babe()
    {
        cout << \"Create a Babe to talk with me\" << endl;
    }

    ~Babe()
    {
        cout << \"Babe don\'t Go away,listen to me\" << endl;
    }
};

int main()
{
    Babe* pbabe = new Babe[3];
    delete pbabe;
    pbabe = new Babe[3];
    delete[] pbabe;
    return 0;
}

结果是:

Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Create a babe to talk with me
Create a babe to talk with me
Create a babe to talk with me
Babe don\'t go away,listen to me
Babe don\'t go away,listen to me
Babe don\'t go away,listen to me

大家都看到了,只使用 delete 的时候只出现一个 Babe don’t go away,listen to me,而使用 delete[] 的时候出现 3 个 Babe don’t go away,listen to me。不过不管使用 delete 还是 delete[] 那三个对象的在内存中都被删除,既存储位置都标记为可写,但是使用 delete 的时候只调用了 pbabe[0] 的析构函数,而使用了 delete[] 则调用了 3 个 Babe 对象的析构函数。

你一定会问,反正不管怎样都是把存储空间释放了,有什么区别。

答:关键在于调用析构函数上。此程序的类没有使用操作系统的系统资源(比如:Socket、File、Thread等),所以不会造成明显恶果。如果你的类使用了操作系统资源,单纯把类的对象从内存中删除是不妥当的,因为没有调用对象的析构函数会导致系统资源不被释放,这些资源的释放必须依靠这些类的析构函数。

所以,在用这些类生成对象数组的时候,用 delete[] 来释放它们才是王道。而用 delete 来释放也许不会出问题,也许后果很严重,具体要看类的代码了。

最后祝各位保持良好的代码编写规范降低严重错误的产生。

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

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

相关文章

[python入门㊽] - 自定义异常 raise 关键字

目录 ❤ 自定义抛出异常关键字 - raise ❤ 使用raise主动引发异常 ❤ raise 关键字的用法 ❤ 触发异常 ❤ 自定义异常类 在前面我们学过异常三个关键字分别是try、except 以及 finally 在编程过程中合理的使用异常可以使得程序正常的执行。有直接抛出异常的形式&…

【HTML】HTML 表格总结 ★★★ ( 表格标签 | 行标签 | 单元格标签 | 表格标签属性 | 表头单元格标签 | 表格标题标签 | 合并单元格 )

文章目录一、表格标签组成 ( 表格标签 | 行标签 | 单元格标签 )二、table 表格属性 ( border 属性 | align 属性 | width 属性 | height 属性 )三、表头单元格标签四、表格标题标签五、合并单元格1、合并单元格方式2、合并单元格顺序3、合并单元格流程六、合并单元格示例1、原始…

Linux之ping\kill\killall命令

参考视频&#xff1a;linux命令&#xff08;ping&#xff09;linux命令&#xff08;kill、killall&#xff09;Linux ping 命令用于检测主机。执行 ping 指令会使用 ICMP 传输协议&#xff0c;发出要求回应的信息&#xff0c;若远端主机的网络功能没有问题&#xff0c;就会回应…

Redis进阶:布隆过滤器(Bloom Filter)及误判率数学推导

1 缘起 有一次偶然间听到有同事在说某个项目中使用了布隆过滤器&#xff0c; 哎呦&#xff0c;我去&#xff0c;我竟然不知道啥是布隆过滤器&#xff0c; 这我哪能忍&#xff1f;其实&#xff0c;也可以忍&#xff0c;但是&#xff0c;可能有的面试官不能忍&#xff01;&#…

简介JVM

目录 一、内存分区 1、程序计数器 2、栈 3、堆 4、方法区 二、类加载 1、Loading 2、Linking Verification Preparation Resolution 3、Initializing 4、双亲委派模型 三、垃圾回收 1、如何判断为垃圾&#xff1f; 引入引用计数 可达性分析 2、如…

Transformer论文阅读:Swin Transformer算法笔记

标题&#xff1a;Swin Transformer: Hierarchical Vision Transformer using Shifted Windows 会议&#xff1a;ICCV2021 论文地址&#xff1a;https://ieeexplore.ieee.org/document/9710580/ 官方代码&#xff1a;https://github.com/microsoft/Swin-Transformer 作者单位&am…

[安装之2] 台式计算机加固态硬盘,台式机添加固态硬盘教程_台式主机固态硬盘怎么安装

固态硬盘是用固态电子存储芯片阵列制成的硬盘&#xff0c;也是电脑中比较常见的内存硬件&#xff0c;有些用户在使用电脑时候&#xff0c;由于内存不足导致系统运行较卡的情况&#xff0c;往往会选择添加固态硬盘来解决&#xff0c;那么台式主机固态硬盘怎么安装呢&#xff1f;…

shell脚本内调用另外一个shell脚本的几种方法

有时会在一个shell脚本(如test_call_other_shell.sh)中调用另外一个shell脚本(如parameter_usage.sh)&#xff0c;这里总结几种可行的方法&#xff0c;这些方法在linux上和windows上(通过Git Bash)均适用&#xff1a; 1.通过source: 运行在相同的进程&#xff0c;在test_…

CCIE重认证-300-401-拖图题全

拖图 拖图题 编程 snippet&#xff1b;192.168.5.0&#xff0c;mask 255.255.255.0&#xff1b;number是192.168.5.0&#xff1b;mask是255.255.255.0 snippets&#xff1b;edit-config对config&#xff0c;loopback对name 100&#xff0c;address对primary&#xff0c;mask…

广度优先搜索算法 - 迷宫找路

广度优先搜索算法1 思考问题1.1 这个迷宫需不需要指定入口和出口&#xff1f;2 先粗略实现2.1 源码2.2 源码解释3 优化代码3.1 优化读取文件部分3.2 增加错误处理4 再优化-让程序变得更加灵活4.1 用户外部可以循环输入入口和出口5 完整代码这是一个提问者的提出的问题&#xff…

制造业的云ERP在外网怎么访问?内网服务器一步映射到公网

随着企业信息化、智能化时代的到来&#xff0c;很多制造业企业都在用云ERP。用友U 9cloud通过双版本公有云专属、私有云订阅、传统软件购买三种模式满足众多制造业企业的需求&#xff0c;成为一款适配中型及中大型制造业的云ERP&#xff0c;是企业数智制造的创新平台。 用友U 9…

python 面向对象利用selenium【获取某东商品信息】

用python程序和谷歌selenium插件获取某东商品详细信息【商品名称、商品简介&#xff0c;超链接】利用selenium自动化程序 中的css页面结构索取来获取详细数据关于谷歌selenium的安装方法和使用方法第一步检查自己谷歌浏览器的版本1.1 找到设置&#xff1a;并鼠标点击进入1.2进入…

排序评估指标——NDCG和MAP

在搜索和推荐任务中&#xff0c;系统常返回一个item列表。如何衡量这个返回的列表是否优秀呢&#xff1f; 例如&#xff0c;当我们检索【推荐排序】&#xff0c;网页返回了与推荐排序相关的链接列表。列表可能会是[A,B,C,G,D,E,F],也可能是[C,F,A,E,D]&#xff0c;现在问题来了…

使用canvas写一个flappy bird小游戏

简介 canvas 是HTML5 提供的一种新标签&#xff0c;它可以支持 JavaScript 在上面绘画&#xff0c;控制每一个像素&#xff0c;它经常被用来制作小游戏&#xff0c;接下来我将用它来模仿制作一款叫flappy bird的小游戏。flappy bird&#xff08;中文名&#xff1a;笨鸟先飞&am…

XSS注入进阶练习篇(一)XSS-LABS通关教程

XSS注入进阶练习篇1.常用标签整理2. XSS-LABS 练习2.1 level 1 无限制2.2 level 2 双引号闭合2.3 level 3 源码函数书写不全&#xff0c;单引号绕过2.4 level 4 无尖括号绕过2.5 level 5 a标签使用2.6 level 6 大小写绕过2.7 level 7 置空替换绕过2.8 level 8 URL编码绕过 - 重…

安全—07day

Tomcat AJP 文件包含漏洞&#xff08;CVE-2020- 1938&#xff09; 漏洞概述 Ghostcat(幽灵猫&#xff09;是由长亭科技安全研究员发现的存在于Tomcat 中的安全漏洞&#xff0c;由于Tomcat AJP 协议设计上存在缺陷,攻击者通过Tomcat AJP Connector可以读取或包含 Tomcat上所有…

Java岗面试题--Java并发(日积月累,每日三题)

目录面试题一&#xff1a;并行和并发有什么区别&#xff1f;面试题二&#xff1a;线程和进程的区别&#xff1f;追问&#xff1a;守护线程是什么&#xff1f;面试题三&#xff1a;创建线程的几种方式&#xff1f;1. 继承 Thread 类创建线程&#xff0c;重写 run() 方法2. 实现 …

详解垃圾回收算法,优缺点是什么?|金三银四系列

本文详细介绍了在 JVM 中如何判断哪些对象是需要回收的&#xff0c;以及不同的垃圾回收算法以及优缺点。点击上方“后端开发技术”&#xff0c;选择“设为星标” &#xff0c;优质资源及时送达上篇文章详细介绍了 JVM 的结构以及其内存结构&#xff0c;需要阅读请移步。本文主要…

Android 9.0系统源码_通知服务(二)应用发送状态栏通知的流程

前言 应用发送一个显示在状态栏上的通知&#xff0c;对于移动设备来说是很常见的一种功能需求&#xff0c;本篇文章我们将会结合Android9.0系统源码具体来分析一下&#xff0c;应用调用notificationManager触发通知栏通知功能的源码流程。 一、应用触发状态栏通知 应用可以通…

关于HDFS

目录 一、HDFS概述 二、HDFS架构与工作机制 三、HDFS的Shell操作 四、Hdfs的API操作 一、HDFS概述 HDFS&#xff1a;Hadoop Distributed File System&#xff1b;一种分布式文件管理系统&#xff0c;通过目录树定位文件。使用场景&#xff1a;一次写入&#xff0c;多次读出…