【C++内存管理】—— 策略、陷阱及应对之道
欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡
本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创📩!欢迎评论区留言交流🌟
个人主页 👉 ZyyOvO
本文专栏➡️C++ 进阶之路
本文目录
- ==各位于晏,亦菲请阅==
- 引言
- new和delete操作符
- new做了什么事
- delete做了什么事
- new和delete的细节
- new[ ]做了什么事
- delete[ ]又做了什么事
- new 和delete 的使用陷阱
- 对于自定义类型
- 对于内置类型
- C和C++内存管理对比
- new和malloc的对比
- delete和free的对比
- 本文小结
- 写在最后
各位于晏,亦菲请阅
引言
在前面的学习中,我们已经掌握了C语言的动态内存管理,包括 malloc
,realloc
,calloc
,free
等用于动态开辟和释放内存的函数,忘记了?没关系,点击一键复习 C语言动态内存管理
那么C++和C语言在内存管理方面有哪些不同之处呢?本文带大家一探究竟!
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
看一段代码,思考以下问题:
选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar
在哪里?____staticGlobalVar
在哪里?____staticVar
在哪里?____localVar
在哪里?____num1
在哪里?____char2
在哪里?____*char2
在哪里?___pChar3
在哪里?____*pChar3
在哪里?____ptr1
在哪里?____*ptr1
在哪里?____
根据下图不难得出答案,这里我就不在赘述
值得注意的是:
char2是栈上的字符数组,而*char2则指向代码段的只读常量字符串
pChar3是栈上的字符指针变量,而*pChar3同样指向代码段的只读常量字符串
ptr是栈上的整形指针变量,而*ptr1则指向开辟的堆空间
- 栈: 又叫堆栈 —— 非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
- 堆用于程序运行时动态内存分配,堆是向上增长的。
- 数据段 —— 存储全局数据和静态数据。
- 代码段 —— 可执行的代码/只读常量
new和delete操作符
new是一个C++中的一个关键字,也叫做操作符,用于在堆(heap)上动态分配内存,并调用类的构造函数(如果适用)来初始化新分配的对象。new 操作符返回指向分配的内存的指针。使用new开辟内存失败后抛出 std::bad_alloc
异常
delete 同样也是一个操作符,用于释放先前由 new 分配的内存,以防止内存泄漏。它删除之前分配的内存,并将指针置为空,以防止访问已释放的内存。
对于内置类型:
new
操作符允许我们在堆上为变量申请内存空间,并且可以设定初始化的值,如果没有显式给出,则由编译器初始化为随机值。使用完毕后,需要手动的使用delete
操作符释放空间,防止内存泄漏。
// 动态申请一个int类型的空间
int* ptr1 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr2 = new int(10);// 编译器警告使用未初始化的内存*ptr1
std::cout << *ptr1 << std::endl;
std::cout << *ptr2 << std::endl;
delete ptr1;
delete ptr2;
输出:
-842150451
10
对于数组类型:
- 此时申请内存需要使用
new[]
而释放内存需要使用delete[]
//动态申请3个int类型的空间
int* ptr3 = new int[3];
for (int i = 0; i < 3; ++i)
{// 同样编译器警告使用未初始化的内存ptr3[]std::cout << ptr3[i] << std::endl;
}
delete[] ptr3;
输出:
-842150451
-842150451
-842150451
对于自定义类型:
class A
{
public:A(int a = 0): _a(a){std::cout << "A():" << this << std::endl;}~A(){std::cout << "~A():" << this << std::endl;}
private:int _a;
};
- new对于 自定义类型 除了开空间还会调用 构造函数,完成对自定义类型的初始化工作
A* p1 = new A(1);
delete p1;
- 使用delete手动释放内存时,delete会先调用 析构函数 完成对自定义类型的资源清理,最后完成对内存的释放。
new做了什么事
有很多同学会有疑惑,new是一个关键字或者叫做操作符,new为什么可以完成对内存的开辟和初始化,甚至它是如何调用自定义类型的构造函数的?
A *p1=new A(3);
在Visual Studio 中,光标定位到new关键字上,按一下F12键,会跳转到一个系统文件的某位置。在该位置处可以发现operator new字样如下(不同版本的内容看上去可能不同):
#endif_VCRT_EXPORT_STD _NODISCARD _Ret_notnull_ _Post_writable_byte_size_(_Size) _VCRT_ALLOCATOR
void* __CRTDECL operator new(size_t _Size);
不难看出,operator new
是一个函数,其返回值为void*
,参数为size_t _Size
由此我们推测,new
的底层实现很可能调用了operator new
!
可以在“A *p1=new A(3);”代码行处设置一个断点,并按F5键(或选择“调试”→“开始调试”命令)进行调试,当断点停留在该代码行时,通过选择“调试”→“窗口”→“反汇编”命令打开反汇编窗口,这样就可以看到这行代码对应的汇编语言代码是什么,如图所示。
不难发现,new关键字主要做了两件事:
- ①一个是调用
operator new
; - ②一个是调用类A的构造函数。
继续调试中可以使用F11键(或选择“调试”→“逐语句”命令)跳转进operator new
,发现operator new
调用了malloc
,如图所示。
这里就会有同学发出疑问了,operator new
是什么?为什么new
要 调用 operator new
呢?
实际上operator new是 C++标准库中的库函数,函数原型为:
//throwing (1)
void* operator new (std::size_t size);
//nothrow (2)
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
//placement (3)
void* operator new (std::size_t size, void* ptr) noexcept;
那么既然它是一个函数,就一定可以被调用,我们在main函数中添加如下一行代码尝试调用operator new
int *p=(int*)operator new(3);
成功编译,如果计算机上安装了 Visual Studio ,在其安装目录下的某个子目录中会存在一个叫作new_scalar.cpp的文件,operator new
的实现源码就在该文件中。
我的文件在如下路径:
Visual Studio 2022\VC\Tools\MSVC\14.41.34120\crt\src\vcruntime
源码类似如下:
void* __CRTDECL operator new(size_t const size)
{for (;;){if (void* const block = malloc(size)){return block;}if (_callnewh(size) == 0){if (size == SIZE_MAX){__scrt_throw_std_bad_array_new_length();}else{__scrt_throw_std_bad_alloc();}}// The new handler was successful; try to allocate again...}
}
- 不难发现,
operator new
在分配内存失败时,会抛出std::bad_alloc
异常,这也就是为什么new
在分配内存失败时,会抛出std::bad_alloc
异常的原因 - 因为new需要在底层调用
operator new
来完成内存分配,而operator new
最终调用malloc
完成内存分配!
根据上面这些线索,我们进行如下小结:
A *pa = new A(); //操作符operator new(); //函数malloc(); //C函数A::A(); //有构造函数就调用构造函数
进而可以写出new关键字分配内存时的大概调用关系。new关键字的调用关系如下表示:
delete做了什么事
- 有内存分配,就必定要有内存释放,我们通过
delete
释放刚才通过new
动态开辟的内存
A *p1=new A(3);
delete p1;
设置断点到delete p1
;行并进行跟踪调试,通过反汇编窗口,进入反汇编我们可以看到delete具体干了什么,如下图:
根据反汇编我们可以看出,delete 首先调用了类A的析构函数来释放资源,然后继续逐语句调试,我们跳转进入类A的析构函数内部查看反汇编代码如下:
- 我们发现类A的析构函数在内部除了清理资源,还调用了
operator delete
函数
继续调试中可以使用F11键(或选择“调试”→“逐语句”命令)跳转进operator delete
,发现operator delete
调用了free函数
,如图所示。
- 看到这里,想必大家已经明白了,
operator delete
实际上也是C++标准库当中的库函数,而它在底层调用了free
函数完成了对内存的释放。 函数原型为:
//ordinary (1)
void operator delete (void* ptr) noexcept;
//nothrow (2)
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
//placement (3)
void operator delete (void* ptr, void* voidptr2) noexcept;
如果计算机上安装了Visual Studio ,在其安装目录下的某个子目录中会存在一个叫作delete_scalar.cpp 的文件,operator delete
的实现源码就在该文件中。 我的路径为:
Visual Studio 2022\VC\Tools\MSVC\14.41.34120\crt\src\vcruntime
- 源码类似如下:
_CRT_SECURITYCRITICAL_ATTRIBUTE
void __CRTDECL operator delete(void* const block) noexcept
{#ifdef _DEBUG_free_dbg(block, _UNKNOWN_BLOCK);#elsefree(block);#endif
}
最终可以写出delete关键字释放内存时的大概调用关系(注意调用顺序)如下:
delete pa;A::~A(); // 如果有析构函数,则先调用析构函数operator delete(); //函数free(); //C函数释放内存
new和delete的细节
读到这里,我们已经知道了new和delete在底层的调用顺序,那么还有一些关于new和delete的细节问题需要我们知道.
new delete
和 new[] delete[]
的配对使用
在 《Effective C++》中有这么一段话:如果你在 new 表达式中使用 [ ],必须在相应的 delete 表达式中也使用 [
]。如果你在 new 表达式中不使用 [ ],一定不要在相应的 delete 表达式中使用 [ ]。
为什么new new[]
、delete delete[]
必须要配对使用呢?
关于这个问题,我们先来看看 new[]
和delete[]的底层实现
new[ ]做了什么事
实际上,当我们使用new[]操作符时,我们会根据数组的大小多次调用new操作符来完成对内存的申请和初始化(对内置类型直接赋值或者调用自定义类型的构造函数)
这里我们在如下代码上打上断点
A *p = new A[3];
并按F5键(或选择“调试”→“开始调试”命令)进行调试,当断点停留在该代码行时,通过选择“调试”→“窗口”→“反汇编”命令打开反汇编窗口,可以看到如下界面:
operator new[]
同样是C++标准库中的库函数,原型如下:
//throwing (1)
void* operator new[] (std::size_t size);
//nothrow (2)
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
//placement (3)
void* operator new[] (std::size_t size, void* ptr) noexcept;
我们继续调试中可以使用F11键(或选择“调试”→“逐语句”命令)跳转进operator new[]
,发现operator new[]
调用了operator new
,如图所示。
我们看到实际上operator new[]
在内部通过多次调用operator new
来完成开辟空间和初始化的任务,那么究竟要调用多少次?换句话说,operator new[]怎么知道要申请多少空间呢?
如下图所示:
- 实际上,我们在使用
new[]
操作符时,new[]
除了为其分配指定空间外,还会额外多开辟 4 个字节用来存储实际分配的对象个数,这个信息就存储在 块头 中。 - 但是使用
new
操作符则不会保留存储信息 - 根据块头内存储对象个数的信息,就知道需要调用多少次构造函数来初始化,同时也是后面
delete[]
调用析构函数的次数。 - 但是
new[]
为用户返回的地址并不是从块头开始,而是忽略了块头内的存储信息,返回为对象分配的起始地址。
delete[ ]又做了什么事
聊完这个,我们再来看delete[],使用delete[]对我们刚才申请的内存释放:
delete[] p;
同样转到反汇编,可以看到如下界面:
发现delete[]
先调用了类A的析构函数,接着我们跳转到析构函数内部:
我们发现,调用了析构函数之后,delete[]
实际上调用operator delete[]
完成对对象资源的清理。
同理,operator delete[]
也是C++标准库中的库函数,原型如下:
ordinary (1)
void operator delete[] (void* ptr) noexcept;
nothrow (2)
void operator delete[] (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
placement (3)
void operator delete[] (void* ptr, void* voidptr2) noexcept;
- 最后跳转到
operator delete[]
内部,发现operator delete[]
最终调用了operator delete
那么,delete[]
调用析构函数的次数就显得尤为重要了,还记得我们前面提到的new[]
开辟内存时会多开辟4个字节作为块头,存储数组内的对象个数吗。此时当delete[]需要释放空间时,将传入的 起始地址向前偏移4个字节 就可以拿到块头中的存储信息,也就可以确实析构函数的调用次数,合理的释放内存。
看到这里,想必你已经明白为什么前面提到的为什么使用new
开辟的内存必须要用delete
释放,而使用new[]
开辟的内存必须要用delete[]
释放。
new 和delete 的使用陷阱
对于自定义类型
- 如果是
new
开辟的空间,用delete[]
释放会发生什么问题?
A* p = new A(1);
delete[] p;
在C++标准中,这样做会导致未定义的行为,不同的编译器具体处理方式不同,可能会造成以下错误:
- 运行时错误: 可能导致程序崩溃,因为
delete[]
的实现逻辑是按照数组的方式去释放内存,它会试图按照数组的规则去调用多次析构函数等操作,而实际指向的只是一个对象,这可能会破坏内存结构等。 - 内存泄漏:由于释放方式不正确,对象所占用的内存可能没有被正确释放回操作系统,导致内存泄漏。
- 未定义的输出:在程序运行过程中,可能会输出一些不符合预期的结果,比如析构函数调用次数异常等。
我们说过delete[]
会将需要释放对象的地址向前偏移 4 个字节来找到块头内的存储信息,而new
在开辟空间时不会额外开辟 4 个字节来存储对象个数,这样一来就会发生未定义的错误!
在Linux平台下的运行错误:
A():0x1e73c20
~A():0x1e73ca0
~A():0x1e73c9c
~A():0x1e73c98
~A():0x1e73c94
~A():0x1e73c90
~A():0x1e73c8c
~A():0x1e73c88
~A():0x1e73c84
~A():0x1e73c80
~A():0x1e73c7c
~A():0x1e73c78
~A():0x1e73c74
~A():0x1e73c70
~A():0x1e73c6c
~A():0x1e73c68
~A():0x1e73c64
~A():0x1e73c60
~A():0x1e73c5c
~A():0x1e73c58
~A():0x1e73c54
~A():0x1e73c50
~A():0x1e73c4c
~A():0x1e73c48
~A():0x1e73c44
~A():0x1e73c40
~A():0x1e73c3c
~A():0x1e73c38
~A():0x1e73c34
~A():0x1e73c30
~A():0x1e73c2c
~A():0x1e73c28
~A():0x1e73c24
~A():0x1e73c20
*** Error in `./code': free(): invalid pointer: 0x0000000001e73c18 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x81329)[0x7fc444535329]
./code[0x400a5c]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7fc4444d6555]
./code[0x400929]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:01 1056886 /home/zwy/code/Lesson/cplusplus/code
00601000-00602000 r--p 00001000 fd:01 1056886 /home/zwy/code/Lesson/cplusplus/code
00602000-00603000 rw-p 00002000 fd:01 1056886 /home/zwy/code/Lesson/cplusplus/code
01e62000-01e94000 rw-p 00000000 00:00 0 [heap]
7fc440000000-7fc440021000 rw-p 00000000 00:00 0
7fc440021000-7fc444000000 ---p 00000000 00:00 0
7fc4444b4000-7fc444678000 r-xp 00000000 fd:01 132472 /usr/lib64/libc-2.17.so
7fc444678000-7fc444877000 ---p 001c4000 fd:01 132472 /usr/lib64/libc-2.17.so
7fc444877000-7fc44487b000 r--p 001c3000 fd:01 132472 /usr/lib64/libc-2.17.so
7fc44487b000-7fc44487d000 rw-p 001c7000 fd:01 132472 /usr/lib64/libc-2.17.so
7fc44487d000-7fc444882000 rw-p 00000000 00:00 0
7fc444882000-7fc444897000 r-xp 00000000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fc444897000-7fc444a96000 ---p 00015000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fc444a96000-7fc444a97000 r--p 00014000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fc444a97000-7fc444a98000 rw-p 00015000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7fc444a98000-7fc444b99000 r-xp 00000000 fd:01 132480 /usr/lib64/libm-2.17.so
7fc444b99000-7fc444d98000 ---p 00101000 fd:01 132480 /usr/lib64/libm-2.17.so
7fc444d98000-7fc444d99000 r--p 00100000 fd:01 132480 /usr/lib64/libm-2.17.so
7fc444d99000-7fc444d9a000 rw-p 00101000 fd:01 132480 /usr/lib64/libm-2.17.so
7fc444d9a000-7fc444f0b000 r-xp 00000000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7fc444f0b000-7fc44510b000 ---p 00171000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7fc44510b000-7fc445115000 r--p 00171000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7fc445115000-7fc445117000 rw-p 0017b000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7fc445117000-7fc44511b000 rw-p 00000000 00:00 0
7fc44511b000-7fc44513d000 r-xp 00000000 fd:01 138861 /usr/lib64/ld-2.17.so
7fc445330000-7fc445335000 rw-p 00000000 00:00 0
7fc445339000-7fc44533c000 rw-p 00000000 00:00 0
7fc44533c000-7fc44533d000 r--p 00021000 fd:01 138861 /usr/lib64/ld-2.17.so
7fc44533d000-7fc44533e000 rw-p 00022000 fd:01 138861 /usr/lib64/ld-2.17.so
7fc44533e000-7fc44533f000 rw-p 00000000 00:00 0
7ffcdbb05000-7ffcdbb26000 rw-p 00000000 00:00 0 [stack]
7ffcdbba8000-7ffcdbbaa000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Aborted
错误提示:
*** Error in ./code': free(): invalid pointer: 0x0000000001e73c18 ***
表明程序在运行时遇到了错误,free 函数(delete[]最终会调用到底层的free等内存释放函数)检测到传入了一个无效的指针。这是因为delete []` 以处理数组的方式去释放单个对象内存,破坏了内存管理的内部结构,导致后续的内存释放操作失败。
回溯信息(Backtrace):
======= Backtrace: ========
后面的内容是程序崩溃时的函数调用栈回溯信息,显示了程序在崩溃时调用了哪些函数以及这些函数在内存中的地址等信息。通过这些信息可以辅助定位错误发生的位置和调用逻辑,但这里错误的根源是 new 和 delete[] 不匹配的内存释放操作。
内存映射(Memory map):
======= Memory map: ========
后面的内容是程序运行时的内存映射情况,显示了各个动态链接库、程序代码段、数据段、堆、栈等在内存中的地址范围和权限等信息。
- 如果是
new[]
开辟的空间,用delete
释放会发生什么问题?
A* p = new A[3];
delete p;
在C++标准中,这样做会同样导致未定义的行为,不同的编译器具体处理方式不同。
对于自定义类型A,delete
只会调用一次析构函数,而不是为数组中的每个对象调用,这样会导致数组中其他对象的资源无法正常释放,造成内存泄漏。
在Linux平台下的运行错误:
A():0x1ce3c28
A():0x1ce3c2c
A():0x1ce3c30
~A():0x1ce3c28
*** Error in `./code': munmap_chunk(): invalid pointer: 0x0000000001ce3c28 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x7f474)[0x7f5861fd0474]
./code[0x400a5b]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f5861f73555]
./code[0x400929]
======= Memory map: ========
00400000-00401000 r-xp 00000000 fd:01 1056886 /home/zwy/code/Lesson/cplusplus/code
00601000-00602000 r--p 00001000 fd:01 1056886 /home/zwy/code/Lesson/cplusplus/code
00602000-00603000 rw-p 00002000 fd:01 1056886 /home/zwy/code/Lesson/cplusplus/code
01cd2000-01d04000 rw-p 00000000 00:00 0 [heap]
7f5861f51000-7f5862115000 r-xp 00000000 fd:01 132472 /usr/lib64/libc-2.17.so
7f5862115000-7f5862314000 ---p 001c4000 fd:01 132472 /usr/lib64/libc-2.17.so
7f5862314000-7f5862318000 r--p 001c3000 fd:01 132472 /usr/lib64/libc-2.17.so
7f5862318000-7f586231a000 rw-p 001c7000 fd:01 132472 /usr/lib64/libc-2.17.so
7f586231a000-7f586231f000 rw-p 00000000 00:00 0
7f586231f000-7f5862334000 r-xp 00000000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f5862334000-7f5862533000 ---p 00015000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f5862533000-7f5862534000 r--p 00014000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f5862534000-7f5862535000 rw-p 00015000 fd:01 131093 /usr/lib64/libgcc_s-4.8.5-20150702.so.1
7f5862535000-7f5862636000 r-xp 00000000 fd:01 132480 /usr/lib64/libm-2.17.so
7f5862636000-7f5862835000 ---p 00101000 fd:01 132480 /usr/lib64/libm-2.17.so
7f5862835000-7f5862836000 r--p 00100000 fd:01 132480 /usr/lib64/libm-2.17.so
7f5862836000-7f5862837000 rw-p 00101000 fd:01 132480 /usr/lib64/libm-2.17.so
7f5862837000-7f58629a8000 r-xp 00000000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7f58629a8000-7f5862ba8000 ---p 00171000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7f5862ba8000-7f5862bb2000 r--p 00171000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7f5862bb2000-7f5862bb4000 rw-p 0017b000 fd:01 926012 /home/zwy/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6
7f5862bb4000-7f5862bb8000 rw-p 00000000 00:00 0
7f5862bb8000-7f5862bda000 r-xp 00000000 fd:01 138861 /usr/lib64/ld-2.17.so
7f5862dcd000-7f5862dd2000 rw-p 00000000 00:00 0
7f5862dd6000-7f5862dd9000 rw-p 00000000 00:00 0
7f5862dd9000-7f5862dda000 r--p 00021000 fd:01 138861 /usr/lib64/ld-2.17.so
7f5862dda000-7f5862ddb000 rw-p 00022000 fd:01 138861 /usr/lib64/ld-2.17.so
7f5862ddb000-7f5862ddc000 rw-p 00000000 00:00 0
7ffc8a3db000-7ffc8a3fc000 rw-p 00000000 00:00 0 [stack]
7ffc8a3fc000-7ffc8a3fe000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Aborted
Visual Studio 2022平台下程序崩溃:
根据终端的错误信息,我们可以看到:
-
构造函数执行3次,即构造了3个对象
-
析构函数执行了1次,只释放了1个对象的资源
这样会导致剩下两个对象的资源无法正确释放,从而导致内存泄漏!
程序崩溃的可能原因 :
delete 操作在处理内存时,其逻辑与 delete[] 不同,它并不知道之前 new[]分配内存时存储在头部的额外存储信息。delete可能会错误地解释内存结构,破坏堆内存管理器维护的内部数据结构(比如内存块的链表、元数据等)。当后续程序尝试再次进行内存分配或释放等操作时,堆内存管理器可能会因为这些被破坏的数据结构,无法正确管理内存,进而引发运行时错误,最终导致程序崩溃。
对于内置类型
new
和delete[]
混用:
int* p = new int;
delete[] p;
以及new[]
和delete
混用:
int* q = new int[3];
delete q;
这两种情况在Linux平台 和 Visual Studio 2022平台下程序都可以正常运行结束,似乎对于内置类型来说这样写是正确的,事实确并非如此:
- 简单类型的析构需求:对于内置的简单数据类型,如
int
、char
、double
等,它们没有复杂的构造函数和析构函数逻。当使用new分配单个内置类型对象的内存,然后使用delete[]来释放,或者相反,在基本的内存释放操作上可能看起来没有问题,因为没有需要特别处理的析构过程来清理资源或执行其他复杂操作,系统只是简单地将分配的内存回收。 - 内存布局的简单性:内置类型的内存布局相对简单,不像自定义的类对象可能包含 成员变量 、虚函数表指针 等复杂的结构。所以在混用
new/delete
和new[]/delete[]
时,从内存地址的角度看,似乎能够正确地找到并释放相应的内存空间,不会因为内存结构的复杂性而导致立即出现明显的错误,比如内存访问冲突或程序崩溃等。
实际上这种做法仍然是错误的,存在着极大的风险,因此无论是内置类型还是自定义类型,我们在编程中都应该始终严格遵循 “new 搭配 delete,new[] 搭配 delete[]” 的规则。
C和C++内存管理对比
new和malloc的对比
类型不同:
- new是C++中的 关键字/操作符 ,而malloc是C语言中的 库函数。
功能不同:
- new一个对象的时候,不但分配内存,而且还会调用类的构造函数(当然如果类没有构造函数,系统也没有给类生成默认构造函数,那没法调用构造函数了)完成初始化。
- malloc只负责分配指定大小的内存块,不会调用对象的构造函数,也不会完成对内存的初始化工作。
返回值不同:
- 使用new分配内存时,无需关注返回类型,编译器会自动转化。
- 而使用malloc时,malloc函数默认返回值为
void*
,必须要手动进行强制类型转换才可以使用
内存分配失败的处理不同:
- 当 new 无法分配所需的内存时,默认情况下会抛出
std::bad_alloc
异常。可以使用std::nothrow
来让new在分配失败时返回nullptr
而不抛出异常。 - malloc 在内存分配失败时会返回
nullptr
,因此在使用 malloc 分配内存后,需要手动检查返回值是否为nullptr
来判断内存分配是否成功。
内存释放方式不同:
- 用 new 分配的内存必须使用
delete
(对于单个对象)或delete[]
(对于数组对象)来释放,delete会调用对象的析构函数,确保对象的资源被正确释放。 - 使用 malloc 分配的内存需要使用
free
函数来释放,free
不会调用对象的析构函数。
分配内存的大小指定不同:
- new 会根据对象的类型自动计算所需的内存大小,不需要显式指定。
- malloc 需要显式指定要分配的内存字节数,通常使用
sizeof
操作符来计算所需的字节数。
比较项 | new | malloc |
---|---|---|
类型 | C++中的关键字/操作符 | C语言中的函数 |
功能 | 分配内存并调用类的构造函数完成初始化(若有构造函数) | 只负责分配指定大小的内存块,不调用构造函数,不初始化内存 |
返回值 | 无需关注返回类型,编译器自动转化 | 默认返回值为void* ,需手动强制类型转换 |
内存分配失败处理 | 默认抛出std::bad_alloc 异常,可使用std::nothrow 使分配失败时返回nullptr | 分配失败时返回nullptr ,需手动检查返回值判断是否成功 |
内存释放方式 | 用delete (单个对象)或delete[] (数组对象)释放,会调用对象析构函数 | 使用free 函数释放,不会调用对象析构函数 |
分配内存大小指定 | 根据对象类型自动计算所需内存大小,无需显式指定 | 需显式指定要分配的内存字节数,常使用sizeof 操作符计算 |
delete和free的对比
类型不同:
-
free 是 C 语言标准库中的函数,在 C++ 中也可以使用,其原型在
<stdlib.h>
(C++ 中为<cstdlib>
)头文件中声明。 -
delete 是 C++ 中的 关键字 / 操作符,是 C++ 语言专门用于内存管理的一部分。
功能不同:
- free 仅用于释放由
malloc
、calloc
或realloc
函数分配的内存块。它只是将该内存标记为可被后续分配使用,不会调用对象的析构函数。这意味着如果内存块中存储的是对象,对象内部可能持有的资源(如动态分配的成员变量、打开的文件句柄等)不会被自动清理。 - delete 用于释放由 new 操作符分配的内存。对于单个对象使用 delete,对于数组对象使用delete[]。在释放内存之前,delete 会调用对象的析构函数,确保对象的资源(如关闭文件、释放成员变量占用的内存等)被正确释放。
使用对象不同:
- free 只能用于释放通过 malloc、calloc 或 realloc 分配的内存。如果尝试使用 free 释放由 new分配的内存,会导致未定义行为,因为 new 可能涉及对象的构造和内存布局的特殊处理,free 无法正确处理这些情况。
- delete 专门用于释放由 new 分配的内存。如果尝试使用 delete 释放由 malloc
分配的内存,虽然在某些简单情况下可能不会立即出现问题,但由于 delete 会尝试调用析构函数,而 malloc分配的内存没有经过构造函数初始化,调用析构函数可能会导致未定义行为。
参数不同:
- free 函数接受一个
void*
类型的指针作为参数。这个指针必须是之前通过 malloc、calloc 或 realloc 函数返回的指针。由于 void* 类型的指针可以接收任意类型的指针,所以在传递给 free 时不需要进行显式的类型转换。 - delete 操作符接受一个指向对象的指针作为参数,并且该指针的类型必须与使用 new 操作符分配内存时的类型严格匹配。对于单个对象,直接使用 delete 后跟对象指针;对于数组对象,要使用 delete[] 后跟数组指针。
比较项 | free | delete |
---|---|---|
类型 | C语言标准库函数,在C++中也可使用 | C++中的操作符 |
功能 | 仅释放由malloc 、calloc 、realloc 分配的内存块,不调用对象的析构函数 | 释放由new 分配的内存,对于单个对象使用delete ,对于数组对象使用delete[] ,会调用对象的析构函数,确保对象的资源被正确释放 |
使用对象 | 只能用于释放malloc 、calloc 、realloc 返回的指针所指向的内存 | 用于释放new 操作符分配的内存 |
安全性 | 不会自动处理对象资源的释放,如果释放的内存中包含对象且有需要释放的资源(如动态分配的成员变量),可能会导致资源泄漏 | 会调用对象的析构函数,能正确处理对象资源的释放,减少资源泄漏的风险 |
参数 | 接受一个void* 类型的指针作为参数,该指针必须是之前由malloc 、calloc 、realloc 返回的指针 | 接受一个指向对象的指针作为参数,指针类型必须与new 操作时的类型匹配 |
本文小结
new 和 delete 操作符:
- new 作为 C++中用于动态内存分配的核心操作符,它承担着双重职责。一方面,它在堆上为对象分配所需的内存空间;另一方面,会自动调用对象的构造函数,完成对象的初始化过程,确保对象处于可用状态。delete则是 new 的对应操作,用于释放 new 分配的内存。在释放内存之前,delete会调用对象的析构函数,这一机制保证了对象内部资源(如动态分配的成员变量、打开的文件句柄等)能够被正确清理,避免资源泄漏。
操作符细节:
- new[] 用于处理数组对象的内存分配。它不仅会为数组元素分配连续的内存空间,还会额外记录数组元素的数量,以便后续 delete[]操作能够正确执行。delete[] 会依据 new[]记录的元素数量,逐个调用数组元素的析构函数,然后释放整个数组占用的内存。这种机制确保了数组对象的内存管理和资源清理的正确性。
C 与 C++ 内存管理对比:
- C 语言使用 malloc 和 free 进行内存管理,这两个函数仅关注内存的分配和释放,不涉及对象的构造和析构过程。而 C++ 引入的 new 和 delete操作符,将对象的生命周期管理纳入内存管理体系,通过构造和析构函数的自动调用,实现了对象资源的自动化管理。这种差异使得 new 与malloc、delete 与 free 在功能、返回值类型、内存分配失败处理机制、内存释放方式以及内存大小指定方式等方面存在显著区别。
写在最后
本文到这里就结束了,有关C++内存管理更深入的讲解,如malloc
和free
的源码和底层原理、定位new表达式 placement-new
等高级话题,后面会发布专门的文章为大家讲解。感谢您的观看!
如果你觉得这篇文章对你有所帮助,请为我的博客 点赞👍收藏⭐️ 评论💬或 分享🔗 支持一下!你的每一个支持都是我继续创作的动力✨!🙏
如果你有任何问题或想法,也欢迎 留言💬 交流,一起进步📚!❤️ 感谢你的阅读和支持🌟!🎉
祝各位大佬吃得饱🍖,睡得好🛌,日有所得📈,逐梦扬帆⛵!
相关文章:
【C++内存管理】—— 策略、陷阱及应对之道
欢迎来到ZyyOvO的博客✨,一个关于探索技术的角落,记录学习的点滴📖,分享实用的技巧🛠️,偶尔还有一些奇思妙想💡 本文由ZyyOvO原创✍️,感谢支持❤️!请尊重原创…...
分布式版本控制系统---git
Git:从基础到进阶的全面指南 Git 是一个分布式版本控制系统,广泛应用于软件开发中,用于跟踪文件的更改、支持团队协作以及管理项目代码。通过 Git,开发者可以在本地拥有完整的项目历史记录,进行离线开发,并…...
pg_sql关于时间的函数
1、时间戳和日期之间的相互转换 时间戳转日期(时间戳为数值类型,若为字符型需进行转换) # 保留到秒:2025-10-02 04:46:40 (字符型转换数值型) select to_timestamp(1759351600::bigint)# 保留到日&#x…...
【Kafka】Windows下安装Kafka(全面)
目录 1.前提条件 2.下载 3.安装 4.环境变量配置 5.验证 1.前提条件 参考版本:zookeeper为3.6.4 kafka版本为3.5.1 1.先安装zookeeper: 【Zookeeper】Windows下安装Zookeeper(全面)-CSDN博客https://blog.csdn.net/…...
【Qt】:概述(下载安装、认识 QT Creator)
🌈 个人主页:Zfox_ 🔥 系列专栏:Qt 目录 一:🔥 介绍 🦋 什么是 QT🦋 QT 发展史🦋 Qt版本🦋 QT 优点 一:🔥 搭建Qt开发环境 ǹ…...
Netty源码解析之异步处理(二):盛赞Promise中的集合设计
前言 在阅读Netty源码的过程中,我越来越相信一句话:“Netty的源码非常好,质量极高,是Java中质量最高的开源项目之一”。如果认真研究,会有一种遍地黄金的感觉。 本篇文件我将记录一下鄙人在Promise的实现类DefaultPr…...
Spring Boot 的约定优于配置,你的理解是什么?
“约定优于配置” 是 Spring Boot 极为重要的设计理念,它极大地简化了 Spring 应用的开发流程,下面从多个方面详细解释这一理念: 减少配置复杂性 传统开发的痛点 在传统的 Spring 开发里,配置工作相当繁琐。以配置 Spring MVC …...
图形渲染(一)——Skia、OpenGL、Mesa 和 Vulkan简介
1.Skia —— 2D 图形库 Skia 是一个 2D 图形库,它的作用是为开发者提供一个高层次的绘图接口,方便他们进行 2D 图形渲染(比如绘制文本、形状、图像等)。Skia 本身不直接管理 GPU 或进行底层的渲染工作,而是通过 底层图…...
git使用,注意空格
第一节 安装完成后,找个目录用于存储,打开目录右击选择git bash here 命令1 姓名 回车 git config --global user.name "li" 命令2 邮箱 回车 git config --global user.email "888163.com" 命令3 初始化新仓库,下载克隆 回…...
以用户为中心,汽车 HMI 界面设计的创新之道
在汽车智能化飞速发展的当下,汽车 HMI(人机交互界面)成为连接人与车的关键桥梁。如何打造出优秀的 HMI 界面?答案是以用户为中心,探索创新之道。 用户需求是汽车 HMI 界面设计的指南针。在设计前期,深入调…...
CentOS安装Docker,Ubuntu安装Docker,Docker解决方案
文章目录 CentOS7安装DockerUbuntu修改Docker镜像源docker设置容器自动启动启动时加--restartalways如果已经过运行的项目docker compose设置容器自启动 docker file修改时区docker在容器执行命令简单粗暴的办法安装curl docker compose命令安装docker compose Docker WEB 图形…...
c#中“事件-event”的经典示例与理解
在C#编程语言中,事件(Event)是一个非常重要的概念,它提供了一种松耦合的方式,让对象间能够通知彼此,而无需直接联系。事件的使用可以让我们的代码更加灵活、可扩展且易于维护。 事件可以视作委托的实例&…...
git bash在github的库中上传或更新本地文件
一、将本地文件上传到 GitHub 仓库 1. 创建 GitHub 仓库 如果你还没有在 GitHub 上创建仓库,首先需要创建一个新的仓库: 登录到 GitHub。点击右上角的 按钮,选择 New repository。给你的仓库起个名字,并选择 Public 或 Privat…...
【编程实践】vscode+pyside6环境部署
1 PySide6简介 PySide6是Qt for Python的官方版本,支持Qt6,提供Python访问Qt框架的接口。优点包括官方支持、LGPL许可,便于商业应用,与Qt6同步更新,支持最新特性。缺点是相比PyQt5,社区资源较少。未来发展…...
vue 文件下载(导出)excel的方法
目前有一个到处功能的需求,这是我用过DeepSeek生成的导出(下载)excel的一个方法。 1.excel的文件名是后端生成的,放在了响应头那里。 2.这里也可以自己制定文件名。 3.axios用的是原生的axios,不要用处理过的ÿ…...
服务器延迟给视频网站造成的影响
在数字化时代中,网络视频已经成为人们日常娱乐和获取信息的重要平台,网络视频的流畅性会影响着用户的体验度,那么,当服务器出现延迟会对视频网站造成哪些影响呢?本文就来共同了解一下吧! 当所使用的服务器由…...
django上传文件
1、settings.py配置 # 静态文件配置 STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR /static, ]上传文件 # 定义一个视图函数,该函数接收一个 request 参数 from django.shortcuts import render # 必备引入 import json from django.views.decorators.http i…...
Mysql数据库
一.数据定义语言DDL 一.概述 DDL用于定义和管理数据库的结构 DDL关键字:1.CREATE; 2.ALTER; 3.DROP 二.SQL命名规定和规范 1.标识符命名规则 2.标识符命名规范 三.库管理 1. CREATE DATABASE 数据库名; 2. CREATE DATABASE IF NOT EXISTS 数据库名; 3. CREATE…...
机器学习 - 大数定律、可能近似正确学习理论
一、大数定律: 大数定律是概率论中的一个基本定理,其核心思想是:当独立重复的随机试验次数足够大时,样本的平均值会趋近于该随机变量的期望值。下面从直观和数学两个角度来说明这一概念: 1. 直观理解 重复试验的稳定…...
Kotlin 2.1.0 入门教程(十七)接口
接口 接口可以包含抽象方法的声明,也可以包含方法的实现。 接口与抽象类的不同之处在于,接口无法存储状态。接口可以拥有属性,但这些属性要么必须是抽象的,要么就得提供访问器的实现。 接口使用 interface 关键字来定义&#x…...
USB Flash闪存驱动器安全分析(第一部分)
翻译原文链接:Hacking Some More Secure USB Flash Drives (Part I) | SySS Tech Blog 文章翻译总结:文章对一些具有AES硬件加密的USB闪存驱动器的网络安全分析研究。研究由SySS的IT安全专家Matthias Deeg进行,他在2022年初发现了几个安全漏…...
报名丨Computer useVoice Agent :使用 TEN 搭建你的 Mac Assistant
与 TEN 相聚在「LET’S VISION 2025」大会,欢迎来展位上跟我们交流。这次我们还准备了一场聚焦「computer use」的工作坊,功能新鲜上线,线下首波体验! 📅 TEN 展位:2025年3月1日-2日 TEN workshop&#x…...
常用架构图:业务架构、产品架构、系统架构、数据架构、技术架构、应用架构、功能架构及信息架构
文章目录 引言常见的架构图I 业务架构图-案例模块功能说明1. 用户界面层 (UI)2. 应用服务层3. 数据管理层4. 基础设施层业务流程图示例技术实现II 功能架构图 -案例功能模块说明1. 船舶监控模块2. 报警管理模块3. 应急响应模块4. 通信管理模块5. 数据分析模块数据管理层基础设施…...
微信小程序的制作
制作微信小程序的过程大致可以分为几个步骤:从环境搭建、项目创建,到开发、调试和发布。下面我会为你简要介绍每个步骤。 1. 准备工作 在开始开发微信小程序之前,你需要确保你已经完成了以下几个步骤: 注册微信小程序账号&…...
C# 特性 学习记录
在C#中,特性(Attribute)是一种用于向代码元素(如类、方法、属性等)添加元数据的机制。特性本身不会直接影响代码的执行,但它们可以提供额外的信息,这些信息可以在运行时通过反射(Ref…...
DBeaver clickhouse 时区不对 时间少了8小时
选择DataBase选择Driver Manager选择clickhouse数据库点中之后,选择编辑添加两个全局属性 use_server_time_zone use_time_zone 鼠标移动到User Properties上,右键即可添加一列空白 然后断开重连...
如何测试和验证CVE-2024-1430:Netgear R7000 路由器信息泄露漏洞分析
CVE-2024-1430 是一个影响 Netgear R7000 路由器的安全漏洞,漏洞来源于该路由器 Web 管理界面的信息泄露问题。攻击者通过访问 /currentsetting.htm 文件,可能泄露敏感信息,如 Wi-Fi 密码等。 在测试和验证 CVE-2024-1430 时,您需…...
Express 中间件是什么
在 Express 里,中间件是具备访问请求对象(req)、响应对象(res)以及应用程序请求-响应循环中的下一个中间件函数(一般用next表示)的函数。 一、中间件的定义与调用顺序 在 Express 应用中&…...
【做一个微信小程序】校园地图页面实现
前言 上一个教程我们实现了小程序的一些的功能,有背景渐变色,发布功能有的呢,已支持图片上传功能,表情和投票功能开发中(请期待)。下面是一个更高级的微信小程序实现,包含以下功能:…...
(萌新入门)如何从起步阶段开始学习STM32 —— 0.碎碎念
目录 前言与导论 碎碎念 所以,我到底需要知道哪些东西呢 从一些基础的概念入手 常见的工具和说法 ST公司 MDK5 (Keil5) CubeMX 如何使用MDK5的一些常用功能 MDK5的一些常见的设置 前言与导论 非常感谢2301_77816627-CSDN博客的提问,他非常好奇…...
java nio 原理 非阻塞IO Netty
一、为什么必须去了解NIO 首先你需要之后Netty的主要实现手段就是Nio,很多人一直学不明白Netty,根本原因是 除了日常开发中很难能够实践,很大一部分原因是不熟悉NIO,事实上真正熟悉了NIO和它背后的原理之后,去查看Netty的源码就有…...
【ClickHouse】Ubuntu下离线安装ClickHouse数据库并使用DBeaver连接
目录 0. 安装前准备1 安装ClickHouse1.1 下载安装包1.2 离线安装1.3 配置密码1.4 启动ClickHouse服务 2 DBeaver连接配置2.1 下载ClickHouse驱动2.2 DBeaver配置2.2.1 配置主要参数2.2.2 配置驱动 2.3 常见问题处理2.3.1 修改远程登录配置2.3.2 更新驱动配置 0. 安装前准备 有…...
vue2打包带路径的项目,刷新404问题解决
问题描述 Vue 2 项目打包时设置了 publicPath: /web/,并通过 Nginx 配置访问 http://ip/web 时可以正常加载首页,但刷新页面时出现 404 错误 原nginx的配置 location /web {alias /www/dist; # 静态文件地址try_files $uri $uri/ /index.html;index i…...
【计算机视觉】文本识别
计算机视觉,广义的文本识别是指对输入的图像进行分析处理,识别出图像中的文字信息,这里的图像可以使传统的文档图像,也可以是现实世界中的场景图像。 简介 无论是传统方法还是基于深度深度学习的方法,完整的文本识别…...
Vue和React的区别
组件开发方式: Vue 使用单文件组件(SFC), HTML, JS 和 CSS 在一个文件内实现 <template><div class"my-component"><!-- HTML模板 --></div> </template><script> export default {// JavaScr…...
STM32 是什么?同类产品有哪些
STM32 是什么? STM32 是由意法半导体(STMicroelectronics)推出的基于 ARM Cortex-M 内核 的 32 位微控制器(MCU)系列。它专为高性能、低功耗的嵌入式应用设计,广泛应用于以下领域: 工业控制&am…...
Git学习使用笔记
目录 一、基本介绍 1.1 版本控制 1.2 版本控制软件的基础功能 1.3 多人协作开发/集中式版本控制 1.4 分布式版本控制 二、Git安装 2.1下载git 2.2 使用Github Desktop软件 2.2.1 创建本地仓库 2.2.2 删除本地仓库 2.2.3 仓库文件操作 2.2.4 多人协作 2.2.4.1 分…...
Bash 中的运算方式
目录 概述: 1. (()) 运算符 2. let 命令 3. expr 命令 4. $[] 直接运算 5. bc(计算器,支持浮点数) 6. awk(强大的文本处理工具,也可计算) 概述: Bash 本身只支持整数运算&am…...
NLP Word Embeddings
Word representation One-hot形式 在上一周介绍RNN类模型时,使用了One-hot向量来表示单词的方式。它的缺点是将每个单词视为独立的,算法很难学习到单词之间的关系。 比如下面的例子,即使语言模型已经知道orange juice是常用组合词…...
Unity UI个人总结
个人总结,太简单的直接跳过。 一、缩放模式 1.固定像素大小 就是设置一个100x100的方框,在1920x1080像素下在屏幕中长度占比1/19,在3840x2160,方框在屏幕中长度占比1/38。也就是像素长款不变,在屏幕中占比发生变化 2.…...
开发基础(8):鸿蒙图表开发
mpchart mpchart是一个包含各种类型图表的图表库,主要用于业务数据汇总,例如销售数据走势图,股价走势图等场景中使用,方便开发者快速实现图表UI,mpchart主要包括线形图、柱状图、饼状图、蜡烛图、气泡图、雷达图、瀑布图等自定义图表库。 柱状图 导入import {BarChart, …...
Vue的简单入门 一
声明:本版块根据B站学习,创建的是vue3项目,用的是vue2语法风格,仅供初学者学习。 目录 一、Vue项目的创建 1.已安装15.0或更高版本的Node.js 2.创建项目 二、 简单认识目录结构 三、模块语法中的指令 1.v-html 1.文本插值…...
vs2022支持.netframework4.0
下载nuget包 .netframework4.0 解压nuget 复制到C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework 参考 https://www.cnblogs.com/bdqczhl/p/18670152 https://blog.csdn.net/xiaomeng1998_/article/details/135979884...
[前端] axios网络请求二次封装
一、场景描述 为什么要对axios网络请求进行二次封装? 解决代码的复用,提高可维护性。 —这个有两个方案:一个是二次封装一个是实例化。(设置一些公共的参数,然后进行请求) 为什么可以解决代码的复用: 这是…...
前端包管理器的发展以及Npm、Yarn和Pnpm对比
在现代前端开发中,包管理器是不可或缺的核心工具。随着 JavaScript 生态的快速发展,开发者经历了从 npm 一统天下到 Yarn 挑战格局,再到 pnpm 创新突破的技术演进。这里将对三种主流包管理器(npm/Yarn/pnpm)进行全方位…...
城电科技| 光伏太阳花:让绿色能源随处绽放
在追求绿色可持续发展的今天,清洁能源设备不断涌现,城电科技的光伏太阳花便是其中的佼佼者。它不仅造型独特美观,更凭借出色的性能,在多个场景中都能发挥重要作用。那么,这款神奇的光伏太阳花究竟适合安装在哪里呢&…...
LVS集群(DR/NAT)
特性NAT 模式DR 模式工作原理Director 修改请求/响应的 IP 地址和端口,流量双向经过 DirectorDirector 仅修改请求的 MAC 地址,响应由 Real Server 直接返回客户端性能较低(需处理双向流量,易成瓶颈)高(仅处…...
保姆级GitHub大文件(100mb-2gb)上传教程
GLF(Git Large File Storage)安装使用 使用GitHub desktop上传大于100mb的文件时报错 The following files are over 100MB. lf you commit these files, you will no longer beable to push this repository to GitHub.com.term.rarWe recommend you a…...
【Jenkins流水线搭建】
Jenkins流水线搭建 01、SpringBoot项目 - Jenkins基于Jar持续集成搭建文档基于手动方式发布项目基于dockerfile基于jenkins + dockerfile + jenkinsfile +pieline基于jenkins + jar方式的发布01、环境说明01、准备项目02、准备服务器03、安装git04、安装jdk1.805、安装maven依赖…...
linux 安装ftp
1、安装vsftpd sudo yum install -y vsftpd 2、运行以下命令,启动FTP服务,并设置开机自启动。 sudo systemctl start vsftpdsudo systemctl enable vsftpd 3、运行以下命令,查看FTP服务监听的端口。 sudo netstat -antup | grep ftp 出现…...