百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

从缺陷中学习C/C++:聊聊 C++ 中常见的内存问题

xsobi 2024-11-24 00:34 2 浏览

在写C/C++程序时,一提到内存,大多数人会想到内存泄露。内存泄露是一个令人头疼的问题,尤其在开发大的软件系统时。一个经典的现象是,系统运行了10天、1个月都好好的,忽然有一天宕机了:

OOM(Out Of Memory,内存用光)!于是,痛苦地查找内存泄露根源的过程开始了…本拿主要讨论内存使用问题,包括内存泄露、悬挂指针、内存重复申请、变量的作用范围等,涉及指针、数组、引用等的使用。

6.1数组越界

代码示例

#define ARRAY_SIZE 1024
char strArray(ARRAY_SIZE];
strArray(ARRAY_SIZE]='\0';

现象&结果

数组访问越界,程序运行崩溃。

Bug分析

C或C++中,数组的下标地址范围是从[0-(size-1),下标size已经超出了数组范围。

正确代码

#define ARRAY_SIZE 1024
char strArray[ARRAY_SIZE];
strArray(ARRAY_SIZE-1] ='\0';

6.2数组定义和值初始化形式混淆

代码示例

int *ip = new int(12);
for (inti=0;i<12;i++ ){
ip[i]=i;
}
delete [] ip;

现象&结果

产生运行时错误,提示如下的错误信息:

glibc detected *** free() : invraid next size (fast)

Bug分析

int "ip=new int(12)表示new了一个整型变量,值是12, ip指向这个变量。

new返回的指针ip是in类型,不是一个数组指针,赋值的时候,采用数组的方式,造成越界访问内存,并且在结束的时候用deltel删除指针数组,造成程序崩渍。

解决方法是:把小括号改写成中括号。

正确代码

int* ip = new int(12];
for (int i= 0; i< 12; i++){
ip[i] =i;
}
delete [] ip;

6.3数组传参时的sizeof

代码示例

void copy(int a[], int b[]) {
memepy (b, a, sizeof(a));
}

现象&结果

copy函数执行后,内存中的内容与设想不符, 目标数组b中的内容不完整,没有把源数组a中的内容全部复制过来。

Bug分析

memepy函数的原型是void *memepy(void *dest, const void *src, size_tn);,它的功能是从源src所指的内存地址的起始位置开始复制n个字节到目标dest所指的内存地址的起始位置中。上述程序中, copy函数的两个形参悬数组a和数组b,函数体中调用了memcp函数,并且为memepy函数的第三个参数赋值sizeof(a)。程序的本意是期望sizeof(a)返回数组a所占的字节数,通过memcpy函数,把源数组a中的内容全部复制到目标数组b中。但是数组int a0作为copy函数的形参,在copy函数体内将退化成指针,所以, sizeof(a)返回的是指针的字节数,而不是数组a的字节数。因此,数组b中只是部分复制了数组a中的内容。解决办法是:在copy函数中增加一个参数,作为数组复制的字节数。

正确代码

void copr (int a[], int b[], int len) {
 
memspy(a, b, sizeof(int)*1en);
}
void del (int a[], int len){
 
memset(a, 0, sizeof(int)* en);
}

或者用数组的引用方式传参:

void copy(int a[], int (&b)[]) {
memcpy(a, b, sizeof(b));
}

编程建议

数组传递参数时,连同数组长度一起传入是一个好方法。或者用std::vector代替数组可以避免不必要的麻烦。使用数组的引用,作为函数的参数,也可以解决上面的问题。

6.4临时对象的生存期

代码示例

class MyString {
public:
MyString(){
s_= (char *)malloc(strlen(str) + 1);
strcpy (s_, str);
}
~MyString() {
printf("destory\ n");
}
friend MyString operator+(const MyString &lstr, const MrString
&rstr){
size_t llen = strlen(lstr);
size_t rlen = strlen(rstr);
char buf[llen + rlen +1];
strcpy (buf, Istr);
strcat(buf, rstr)
return MyString(buf);
}
operator const char *() const {//当string转换char时调用
return_s
}
private:
char* _s;
};
int main()
{
Mysting s1("hello"), s2( "wold!");
const char*p=s1+s2;
printi("%os\ n", p);
return 0;
}

现象&结果

程序运行时通常是正常的,但有时会出错,特别是在多线程时,会出现奇特的错误:例如,指针p指向的内容不是期望的内容。

Bug分析

错误出在main函数中的const char*p=s1+s2代码行处。程序会首先生成一个临时对象,用来存储s1+s2的值,然后再把临时对象的值赋给p,随后该临时对象析构。所以,指针p指向了一块非法内存。因为临时对象已经被析构,所以这块非法内存被系统识别为“未使用”的状态,可以被再分配使用。如果在程序中没有其他操作读写这块内存时,其内容还没有被改变,所以,可能输出符合程序预期的正确结果。但是,没有任何方法阻止那块内容的改变。所以,如果有其他操作对这块内存单元进行写操作后,可能输出的是随机值。

正确代码

在main函数中显式给出临时对象:

int main()
{
MyString s1("hello "), s2 ("world!");
String temp = t1+ t2;
const char*p = temp;
Printf("%s\n",p);
retutn 0;
}

6.5变量的作用域

代码示例

char *str=NULL:
if(!str){
char *str = (char*) malloc (100);
if(!str){
return -1;
}
st[0)='a';
}
print("%c\n", str[0]);

现象&结果

程序执行时出现coredumpo

Bug分析

程序出现coredump的原因是代码printf("%c \n",str[0])中使用的st是空指针。程序首行定义了指针str,并且试值为NULL,然后在if(!str){}语句块中,通过代码行char * str= (char*) malloc (100),重新定义了str指针,并且为str指针分配了内存空间。根据变量的作用域规则, char *str= (char*) malloc (100)这行代码定义的局部变量str,有效范围是在if(!str){}语句块中。代码最后部分printf("%c\n",str[0]) ,在if(!str){ }语句块范围之外,此处使用的str不是在if(!str){}语句块中经过malloc分配过内存的指针str,而是程序首行定义的char*str=NULL;此时st为NULL,所以访问st[0]出现coredump

正确代码

char *str =NULL
if(!str) {
str = (char*) malloc (100);
if(!str){
return -1;
}
str[0]='a';
}
printf("%c\n",st[0]);

6.6指针变量的传值和传址

代码示例

int fun (int* pRes)
{
if(pRes == NULL)
pRes =new int(12)://分配新的内存空间给指针pRes,并赋值
return 0;
}
int main ()
{
int "pInt =NULE;
int val = func(pInt);
Printi("%d\n", pInt);
return 0;
}

现象&结果

函数返回后,指针pRes所指内容不是12.

Bug分析

上述代码中func函数的形参是指针类型int *pRes,在函数体中new了一块内存并赋值12,将内存地址赋值给指针pRes.在main函数中,定义了指针pInt,调用func函数,把pInt作为参数传入func函数中。程序的本意是在func函数退出后,指针pInt所指的内容"pInt为12,但实际结果却不是。其原因是在func函数调用过程中,形参和实参的传递使用了值传递方式,这种情况下,形参变量在函数体内发生了变化,在函数结束之后,形参变量随之释放,不能把变化的结果返回给实参。要改变实参的值,必须使用指针传递或者引用传

递。在本程序中, func函数的形参是整形指针类型int*pRes,要在函数体内改变pRes的值,并把这个变化返回到main函数中,必须传递pRes的指针。因为pRes本身就是指针,所以应该传递指针的指针,或者指针的引用。

正确代码

int func(int *&pRes);

6.7指针赋值和指针赋址的混淆

代码示例

int main()
{
int a = 10;
int* num = &a;
function_b(num);
Printf ("%d", *num);
return 0;
}
void function_b(int* num)
{
int b = 20;
int* buf =&b;
if(*num<0)
num = num;
else
num = buf;
}

现象&结果

num指针指向的内容没有发生变化。

Bug分析

程序的本意是定义并初始化指针num,然后通过函数function_b,改变指针所指的值,即*num。但是实际上没有改变,问题出在function_b中,赋值的用法不正确。应该使用取内容运算符为指针所指的内容赋值,而不是直接为指针赋值。 *num=*buf的含义是把指针buf所指的value赋值给指针num所指的value,而num=buf的合义是把指针buf的地址赋给指针num

正确代码

int main()
{
int a = 10;
int* num = &a;
function_b(num)
printf(" %d", *num);
return 0;
}
void function_b(int* num)
{
int b = 20;
int*buf = &b;
if(*num <0)
*num =*num;
else
*num = *buf;
}

编程建议

函数中传递指针或引用参数,要注意修改是指针本身还是指针的内容,若不希望改变指针本身,建议加const声明,如: function_b(int * const num)。

6.8指针释放后再次使用

代码示例

class MyClass{
private:
int m_val:
public:
MrClass(int value){
m_val = value;
printf("new class A\n");
}
void getValue)() {
printf("%d\n", m_val);
}
};
void function_a()
{
MrClass*A = new MyClass (1);
function_b(A);
function_c(A);
}
void function_b(MrClass *& A)
{
MyClass *B = new MyClas(2);
A->getValue();
B->getValue();
delete A;
A=NULL
delete B;
B=NULL;}
void function_c(MrClass *&A){
A->getValue)();
}
int main()
{
function_a();
return 0
}

现象&结果

程序运行时出现coredump

Bug分析

上述代码中,在函数function_a中实例化了MyClass对象指针A,然后调用函数function_b,在funcion_b中,对指针A操作之后,执行delete操作,将A所指对象释放掉。随后, function_a调用函数functionc,在

function_c中又再次操作指针A.

此时,指针A为空,因此,当调用A-getVvalue时发生coredump.这里存在的一个问题是编码风格不好,对象的分配使用释放混乱。

一个函数分配,一个函数释放,一个函数再次使用。导致function_c不知道前面A已经被释放。避免这种问题的一个办法是:编码时遵循“谁分配,谁释放”的原则。即对象在哪里分配,就在哪里释放。

正确代码

void function_a()
{
MyClass *A = new MyClas (1);
function_b(A)
function_c(A);
delete A;
A= NULL;
}
void function_b(MrClass*& A)
{
MyClass *B = new MyClass (2);
A->getValue();
B->getValue();
delete B;
B=NULL;
}
void function_c(MyClass *&A){
A-> geValue();
}
int main()
{
function_a();
return 0;
}

6.9重复申请内存未释放

代码示例

unsigned char* Func(void)
{
unsigned char *stra;
stra = (unsigned char *)malloc(10);
return stra;
}
int maino()
{
unsigned char *strb:
strb = Func()
strb = (unsigned char*)mallo(10);
free(strtb);
return 0;
}

现象&后果

用一个指针指向两次动态分配的内存,但只free一次,造成内存泄露。使用cppcheck工具检测,可以得到类似下面的信息: (erro) Memory leaks strb。

Bug分析

Func函数中申请了内存赋值给strb,然后在main函数中又动态分配了内存赋值给strb.在free(strb)时, 实际只是释放了最后一次动态申请的内存, Func函数中申请的内存被漏掉了。第一次申请的内存没有被释放,造成内存泄露。

正确代码

unsigned char* Func(void)
{
unsigned char *stra;
stra = (unsigned char*malloc(10);
return stra;
}
int main()
{
unsigned char *strb;
strb = Func();fee(strb);
strb = (unsigned char*)malloc(10);
free(strb);
return 0;
}

编程建议

这是一个小问题。两次动态分配的内存,但只free一次,造成内存泄露。记得申请释放内存时要注意malloc和free记对,申请几次释放几次。

6.10 delete与delete[]的区别

代码示例

char *buff= new char[reslen];
delete buff;

现象&结果

申请的数组空间没有全部释放,造成内存泄露。用cppcheck工具执行静态代码扫描可以看到如下信息:(error) Mismatching allocation and deallocation: buf。

Bug分析

对于数组类型,如string*str= new string[10],用delete str和delete[]str的区别是, delete str只对st[0]调用了析构函数,而delete[]str则对str数组里的每个元素都调用了析构函数。对于单个元素,如int *p=new int(10), delete和delete[]都可以释放内存。

正确代码

char *buff = new char[reslen];
delete [] buff;

编程建议

操作内存的时候, new[]一定要和delete[]对应。

6.11函数中途退出忘记释放内存

代码示例

int func(char* in, int inlen)
{
char *p =new char[20];
if (inlen < 20) {
return 0;
}
stncpy (p, in, 20);
delete []p:
rêturn 1;

现象&结果

若inlen<20,则函数中途退出,而未释放内存,导致内存泄露。

Bug分析

上述代码中, func函数内new了一块内存,在条件分文中,没有释放内存,就直接retun 0,致使函数在条件分支中退出,导致了内存泄露。

正确代码

在程序退出前添加释放内存语句delete []p。
if (inlen <20) {
delete []p;
return 0;
}

6.12 二维数组的内存泄露

代码示例

int main()
{
int **pVal = new int* [2];
for(inti =0;i<2i++){
pVal[i]= new int[3];
}
delete [] pVal;
return 0;
}

现象&结果

二维数组的释放,没有将每个元素逐一释放,造成内存泄露。使用valgrind检测工具检测,可以得到类似的信息, LEAK SUMMARY: definitely lost: 24 bytes in 2 blocks.

Bug分析

泄露点在delete []pVa, pval是2*3的二维数组指针, delete[] pVal只释放了pVal所指向的行空间,没有释放每个pVal[i]所指向的列空间。

正确代码

int main()
{
int **pVal = new int*[2];
for(int i = 0;i < 2;i++)
pVal[i] = newin[3];
for(int j =0;j < 2; j++)
delete []pVal[i];
delete [] pVal;
return 0
}

6.13 临时变量内存不能返回

代码示例

char *initialize()
{
char str[300];
char* ptr = str;
return ptr;
}
void useMyStr(char *str){
char tmp[300]="123";
printf("%s\n", tmp);
print("%s\n", str);
}
int main()
{
char *myStr = initialize();
useMyStr (myStr);
return 0;
}

y现象&后果

返回的指针将指向一个不确定内容的地址。

Bug分析

在函数initialize中的本地变量char数组分配的内存在栈上,随着函数的返回会被收回。正确的做法是:返回指针对应的内存块需要用函数malloc动态分配。

正确代码

chat *initialize()
{
char *myStr= (char*) malloc(300);
return ptr;
}
void useMrSt(char *str){
char tmp[300] ="123";
printf("%s\n", tmp);
printf("%s\n",str);
}
int main()
{
char*myStr = initialize()
if(myStr != NULL)
useMyStr(myStr);
delete myStr;
return 0;
}

6.14 正确使用引用参数和引用返回值

return n;

代码示例

int &add(int n, int m)
{
n=n+m;
}
int main()
{
inti = 10;
int b = add(i, 1);
cout <<b<< endl:
return 0;
}

现象&后果

预期的输出结果是11,但实际输出的结果不确定。

Bug分析

add函数是一个函数引用,而add函数的返回值是形参n,形参是函数内的局部变量,函数执行结束之后,函数内的局部变量就被销毁,内存空间就被收回。因此, add函数返回的内存单元中的值就不确定, b的内容就不确定。正确方法是:确保返回的内存空间不会随着函数的调用结束、被回收。

正确代码

int &add (int &n, int m)
{
n=n+m;
return n;
}
int main()
{
int i = 10;
int b = add(i, 1);
cout<<b<<end1;
return 0;
}

6.15 试图产生的指针很可能不存在

代码示例0

void func(const int* pInt, size_t size){
size =*pInt;
cout<<"size:" << size << endl:
}
int main(){
vector<int>veclnt;
func (&veclnt[0), 4);
return 0;
}

现象&后果

程序运行时,产生core dump。

Bug分析

在程序中, vector<int>veclnt定义了一个没有初始化的vector变量veclnt,因此, veclnt只是一个没有内存空间的空vector对象。调用函数func时, &vecInt作为参数,此时就产生了一个不存在的指针。因此,在函数func内对其进行取值操作时,导致程序core dump。

正确代码

int main(){
int func(const int *pInt, size_t size)
{
if (pInt ==NULL) return-1;
}

编程建议

使用指针前需要判断指针是否为NULL,避免空指针导致的程序异常。

6.16 结构体成员内存对齐问题

代码示例

struct{
char flag;
int i;
} foo;
int main()
{
foo.flag ="T";
int *pi = (int*) (&foo.flag + 1);
*pi =0x01020304;
printf("fag=%c, i=%x\n",foo.flag, foo.i);
return 0;
}

现象&后果

代码中定义了一个结构体,包括一个字符成员flag和整型成员i。在main函数中想通过指针方式将结构体整型成员赋值为0x01020304,但打印输出显示的实际值为0x01,赋值错误。

Bug分析

上面程序的问题出在指针赋值处,即int*pi = (int *(&foo.flag+1)。程序员误以为结构体字符成员flag地址加1就是整型成员i的地址,然后给该地址赋值,期望变量i会得到相应的赋值。但赋值结果并非所期望的。导致这个问题的根源是内存字节对齐。

内存字节对齐是指,为了保证CPU对内存的访问效率,各种类型数据需要按照一定的规则在内存存放,而不是完全字节挨宇节的顺序存放。每种数据类型的默认对齐长度依赖于编译器具体实现,不同编译器可能

有所不同。大多数情况下,基本数据类型的对齐长度就是自已数据类型所占空间大小(sizeof值) 。例如,char型占一个字节,那么对齐长度就是一个字节; int型占4个字节,对齐长度就是4个字节, double型占8个字节,对齐长度就是8个字节。

对于结构体数据类型,默认的宇节对齐一般需满足3个准则。

(1)结构体变量的首地址能够被其最宽数据类型成员的大小整除。

(2)结构体每个成员相对结构体首地址的偏移量都是该成员本身大小的整数倍,如有需要会在成员之间充字节。

(3)结构体变量所占总空间的大小必定是最宽数据类型大小的整数倍。如有需要会在最后一个成员末尾填充若干字节,使得结构体所占空间大小是最宽数据类型大小的整数倍。

在结构体foo里,整型成员i占用4个字节,是占用空间最多的成页,所以foo必须驻留在4的整数倍内存地址。字符成员lag的起始地址即为foo的起始地址, flag占用1个字节。整型成员的起始地址因为必须是4的整数倍,所以不能直接存放于flag+1的位置(flag已占用1个字节, flag+1地址不再是4的整数倍),而是存放于flag+4的位置。因此, flag后面的有3个字节浪费掉了。这样foo一共需要占用8个字节的内存空间,而不是5个字节(char型和int型的sizeof和) 。

程序中,给flag+1地址处赋值为一个4字节整数0x01020304,因为有3个字节并未影响到变量i,所以赋值结果为0x01

正确代码

不使用flag地址加1给变量i减值,直接使用的地址赋值。

struct{
chat flag
int i;
} foo;
int main()
{
foo.flag = "T";
int *pi =&foo.i;
*pi = 0x01020304;
printf("flag=%c, i=%x\n", foo.flag, foo.i);
return 0;
}

编程建议

字节对齐的细节与具体编译器实现有关,不同的平台可能有所不同。一些编译器允许程序员在代码中通过预处理指令#pragma pack()或类型属性_attribute_((packed))来改变默认的内存对齐条件。

6.17 String对象何时需delete

代码示例

int main()
{
string str1 ("stack-allocated str1");
string str2 = "stack-allocated str2";
string str3 = new string("heap-allocated str3");
return 0;
}
现象&后果

程序在运行时发生内存泄露。

Bug分析

程序中使用了string对象的不同初始化或生成方式,容易让人迷惑,先解释一下。

str1是string对象的显式初始化,调用string类的构造函数string(constchar*s)初始化。

str2是复制初始化,会首先生成一个临时string对象,该临时对象以所赋值字符串为输入,调用string (const char*s)构造函数生成。然后以该临时对象的引用为参数调用string类的复制构造函数初始化str2。因为str1和str2对象都是在main函数体内声明的,所以都是分配在栈上

str3是一个string对象指针,指向一个由new操作符生成的string对象。由于是由new生成的,所以该对象分配于堆上。

上述代码混淆了string对象的用法,不知道该什么时候调用delete释放对象。C++中没有垃圾回收机制, 申请动态内存空间后,使用完后必须释放掉,否则会引起内存泄露。

那什么时候必须自己显式调用delete语句呢?答案是,如果对象是在栈上分配的,不需要人工处理,当超出对象作用范围时,该对象的析构函数会自动被调用以释放该对象;如果对象是使用new操作符在堆上分配的,则必须使用delete操作符释放该对象。

上面代码中str1和str2都是在栈上分配的局部变量,所以,会在程序退出main函数前被自动析构。而st3是由new操作符在堆上分配的,必须使用delete操作符来释放。

正确代码

int main()
{
string str1 ("stack-allocated strl");
string str2 ="stack-allocated str2"
string* str3 = new string("heap-allocated str3");
delete str3;
return 0;
}

6.18 小结

C/C++内存使用是一个深入的话题。内存使用是对内存申请、读写、释放过程的安排与统筹,从而实现内存的正确、高效使用。本掌主要从正确性上讲解了内存使用的全过程,包含了常见的错误案例。希望通过本章节的内容,使读者对C/C++内存使用有正确的了解和认识。

本文节选自《从缺陷中学习C/C++》

“从缺陷中学习C/C++”这本书收集并整理了102个实例,这些实例都来自于工程一线实践,虽然大多数看起来像是初学者犯的低级错误,但实质上有一定的代表性,有的错误根源是对C++机制的不理解或者编译过程中的副作用,或者C++标准库的实现依赖。通过阅读这些实例,你可以对C++有更细致的理解。


相关推荐

好用的云函数!后端低代码接口开发,零基础编写API接口

前言在开发项目过程中,经常需要用到API接口,实现对数据库的CURD等操作。不管你是专业的PHP开发工程师,还是客户端开发工程师,或者是不懂编程但懂得数据库SQL查询,又或者是完全不太懂技术的人,通过...

快速上手:Windows 平台上 cURL 命令的使用方法

在工作流程中,为了快速验证API接口有效性,团队成员经常转向直接执行cURL命令的方法。这种做法不仅节省时间,而且促进了团队效率的提升。对于使用Windows系统的用户来说,这里有一套详细...

使用 Golang net/http 包:基础入门与实战

简介Go的net/http包是构建HTTP服务的核心库,功能强大且易于使用。它提供了基本的HTTP客户端和服务端支持,可以快速构建RESTAPI、Web应用等服务。本文将介绍ne...

#小白接口# 使用云函数,人人都能编写和发布自己的API接口

你只需编写简单的云函数,就可以实现自己的业务逻辑,发布后就可以生成自己的接口给客户端调用。果创云支持对云函数进行在线接口编程,进入开放平台我的接口-在线接口编程,设计一个新接口,设计和配置好接口参...

极度精神分裂:我家没有墙面开关,但我虚拟出来了一系列开关

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:iN在之前和大家说过,在iN的家里是没有墙面开关的。...

window使用curl命令的注意事项 curl命令用法

cmd-使用curl命令的注意点前言最近在cmd中使用curl命令来测试restapi,发现有不少问题,这里记录一下。在cmd中使用curl命令的注意事项json不能由单引号包括起来json...

Linux 系统curl命令使用详解 linuxctrl

curl是一个强大的命令行工具,用于在Linux系统中进行数据传输。它支持多种协议,包括HTTP、HTTPS、FTP等,用于下载或上传数据,执行Web请求等。curl命令的常见用法和解...

Tornado 入门:初学者指南 tornados

Tornado是一个功能强大的PythonWeb框架和异步网络库。它最初是为了处理实时Web服务中的数千个同时连接而开发的。它独特的Web服务器和框架功能组合使其成为开发高性能Web...

PHP Curl的简单使用 php curl formdata

本文写给刚入PHP坑不久的新手们,作为工具文档,方便用时查阅。CURL是一个非常强大的开源库,它支持很多种协议,例如,HTTP、HTTPS、FTP、TELENT等。日常开发中,我们经常会需要用到cur...

Rust 服务器、服务和应用程序:7 Rust 中的服务器端 Web 应用简介

本章涵盖使用Actix提供静态网页...

我给 Apache 顶级项目提了个 Bug apache顶级项目有哪些

这篇文章记录了给Apache顶级项目-分库分表中间件ShardingSphere提交Bug的历程。说实话,这是一次比较曲折的Bug跟踪之旅。10月28日,我们在GitHub上提...

linux文件下载、服务器交互(curl)

基础环境curl命令描述...

curl简单使用 curl sh

1.curl--help#查看关键字2.curl-A“(添加user-agent<name>SendUser-Agent<name>toserver)”...

常用linux命令:curl 常用linux命令大全

//获取网页内容//不加任何选项使用curl时,默认会发送GET请求来获取内容到标准输出$curlhttp://www.baidu.com//输出<!DOCTYPEh...

三十七,Web渗透提高班之hack the box在线靶场注册及入门知识

一.注册hacktheboxHackTheBox是一个在线平台,允许测试您的渗透技能和代码,并与其他类似兴趣的成员交流想法和方法。它包含一些不断更新的挑战,并且模拟真实场景,其风格更倾向于CT...