【C++修炼之路】内存管理

👑作者主页:@安 度 因
🏠学习社区:StackFrame
📖专栏链接:C++修炼之路

文章目录

  • 一、C/C++ 内存分布
  • 二、考题
  • 三、C语言动态内存管理方式
  • 四、C++内存管理方式
    • 1、对内置类型
    • 2、对自定义类型
  • 五、C++对动态管理的升级
  • 六、operator new/operator delete函数
  • 七、new/delete 的实现原理
    • 1、内置类型
    • 2、自定义类型
  • 八、定位new表达式(placement-new)
  • 九、内存泄漏
    • 1、内存泄漏分类
    • 2、如何检测内存泄漏
    • 3、如何避免内存泄漏
    • 4、补充

如果无聊的话,就来逛逛 我的博客栈 吧! 🌹

一、C/C++ 内存分布

划分是为了更加高效的管理

image-20230207173207635

说明

  1. 栈又叫堆栈,函数调用建立的栈帧,非静态局部变量、函数参数、返回值等,栈是向下增长的
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的;程序运行过程中按需求申请和释放空间,比如我们实现的数据结果都是在堆开空间
  4. 数据段(静态区)–存储全局数据和静态数据
  5. 代码段(常量区)–可执行的代码/只读常量(常量字符串);代码段是从操作系统/程序角度说的,常量区是从语法角度命名说的

补充

  1. 栈不大,Linux 32位下 8M;静态区和常量区不大,堆很大,32位下约 2G
  2. 函数编译完成后为指令,都在代码段
  3. 对于栈、数据、代码段,是自动控制的;堆是手动控制的
  4. 它们属于进程虚拟地址空间
  5. 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"; // char2 在栈区;*char2 ;*char2 是栈上的字符串
	const char* pChar3 = "abcd"; // pChar3 是栈上的指针;*pChar3 是常量区的常量字符串
	int* ptr1 = (int*)malloc(sizeof(int) * 4); // ptr1 是栈上的指针,*ptr1 就是指针指向的堆上的空间
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}

1)选择题

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

  1. globalVar在哪里?(C) 2. staticGlobalVar在哪里?(C)

  2. staticVar在哪里?(C) 4. localVar在哪里?(A)

  3. num1 在哪里?(A)

  4. char2在哪里?(A) 7. *char2在哪里?(A)

  5. pChar3在哪里?(A) 9. *pChar3在哪里?(D)

  6. ptr1在哪里?(A) 11. *ptr1在哪里?(B)

2)填空题

  1. sizeof(num1) = (40) 2. sizeof(char2) = (5)

  2. strlen(char2) = (4) 4. sizeof(pChar3) = (4)

  3. strlen(pChar3) = (4) 6. sizeof(ptr1) = (4)

三、C语言动态内存管理方式

malloc / calloc / realloc / free 都是库函数:

void Test ()
{
    int* p1 = (int*) malloc(sizeof(int));
    free(p1);
    // 1.malloc/calloc/realloc的区别是什么?
    int* p2 = (int*)calloc(4, sizeof (int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);
    // 这里需要free(p2)吗?
    free(p3);
}

calloc 和 malloc 区别:

calloc 会按字节初始化,空间每个字节初始化为 0,相当于 malloc + memset 。calloc 开辟的空间也需要释放。

这里不需要 free(p2):

因为 realloc 对 p2 指向的空间进行了扩容,此刻无论是空间充足还是不足并扩容成功的情况下,之后对于申请内存的释放只要针对 p3 即可。

如果你能正确回答以上两个问题,请阅读:深度剖析动态内存管理

四、C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

new 和 delete 是操作符,不是函数。

1、对内置类型

int main()
{
	int* p1 = new int; // 动态申请 1 个 int 类型的空间
	int* p2 = new int[5]; // 申请 5 个 int 类型的空间

	delete p1; // 释放 p1
	delete[] p2; // 释放 p2
    p1 = nullptr;
    p2 = nullptr;

	return 0;
}

显而易见比C语言的更加简洁,而 new 申请的空间,和 malloc 一样的:

image-20230207201734559

总结:malloc/free 和 new/delete 对于内置类型没有本质区别,只有用法上的区别;而 delete 完的指针最好置空,更加安全,不置也没关系,但是要注意不能使用,因为此刻为野指针。

区分:

int* p1 = new int(5); // 申请一个 int 的空间,空间初始化为 5
int* p2 = new int[5]; // 申请 5 个 int 类型的空间

C++98不支持初始化 new 的数组;C++11 可以通过如下方式进行初始化:

int main()
{
	int* p3 = new int[5] {1, 2};
}

申请五个 int ,按照 {} 中顺序依次对空间进行初始化,其他的初始化为 0 :

image-20230207202538139

2、对自定义类型

对于自定义类型来说,malloc 不会调用构造函数,free 不会调用析构函数初始化:

image-20230207220454886

而 new 会调用构造函数,对于数组来说,则会调用数组元素个数的次;同理对于 delete 会调用析构函数,也会析构数组元素个数的次:

image-20230207221843320

(delete[] ,这个 [] 是为了告诉编译器它是一个数组,数组有多少个元素,就要调用对应次数的析构函数,之前 [] 会把对象个数存起来,方便之后调用)

总结

  • new 在堆上申请对象空间(指针指向的) + 调用构造函数初始化对象

  • delete 先调用指针类型的析构函数(清理资源) + delete 释放空间申请的空间给堆

若没有默认构造会报错:

image-20230207222409587

但是可以解决报错:

image-20230207222534271

一定要 malloc / free 和 new / delete 和 new[] / delete[] 匹配使用,否则可能会造成死循环或程序奔溃等情况,总之后果自负

例如 new 和 delete[] 造成死循环 :

image-20230207223016742

new[] 和 delete 报错:

之前说过 new 和 delete 开辟多个空间的方式是把对象个数存起来,以此知道大小。对于 vs ,这块空间会在头部有一块空间,存储个数,这时指针指向的位置是个数的下一个位置:

image-20230221205340876

delete[] 就会往前减去四个字节,取到空间里的值,然后根据值来决定调用多少次析构函数,然后从存放数值的空间的地址开始释放空间。

new[] 和 delete 崩溃的原因:释放空间的指针位置不对。

但是如果把析构函数屏蔽,就不会崩溃:

因为对于自己的析构函数,调用与否也无所谓,这时,不会在头部开这一块空间。

对于初始化,也会进行隐式类型转换:

A* p = new A[4]{1, 2, 3, 4}; // 将 1 进行隐式类型转换为 A 对象
A* p = new A[4]{ A(1), A(2), A(3), A(4) }; // 匿名对象进行调用也可以
// 多参构造函数
A* p = new A[4]{ A(1, 2), A(2, 2), A(3, 2), A(4, 2) }; // 会优化,优化为一次构造
A* p = new A[4]{ {1, 2}, {1, 2}, {1, 2}, {1, 2} }; // 隐式类型转换能成功

五、C++对动态管理的升级

两个方面:

第一个升级的地方自定义类型对象自动申请的时候,初始化和销毁清理的问题,new/delete会调用构造函数和析构函数。

第二个升级则是new失败了以后要求抛异常,这样才符合面向对象语言的出错处理机制。

了解面向过程和对象语言处理错误的方式:

  • 面向对象的语言,处理错误的方式一般是抛异常,C++中也要求错误抛异常 – try catch
  • 面向过程的语言,处理错误的方式是返回错误码 – perror

比如 C 语言在动态内存开辟时,堆空间是有限的,很有可能申请失败:

int main()
{
	char* p1 = (char*)malloc(1024u*1024u*1024u*2u); // u 表示为无符号正数
	if (p1 == nullptr)
	{
		printf("%d\n", errno); // 错误码
		perror("malloc fail"); // perror 报错
		exit(-1);
	}
	else
	{
		printf("%p\n", p1);
	}
}

image-20230207225132698

对于 C++ 来说。则是抛出异常,这时再使用上次的检查就无效了,甚至还会奔溃:

int main()
{
	char* p1 = new char[1024u * 1024u * 1024u * 2u - 1]; // new 的大小不能超过 7fff ffff,所以要 -1 
	if (p1 == nullptr)
	{
		printf("%d\n", errno);
		perror("malloc fail");
		exit(-1);
	}
	else
	{
		printf("%p\n", p1);
	}
}

image-20230207210911478

用 C++ 的方法就是 抛异常 ,但是异常其实很难,所以简单了解一下(之后会讲):

image-20230207211216811

bad allocation 就是坏的申请,就是申请失败。

正常申请:

image-20230207211605049

若不抛异常,则走完 try ,不走 catch ;否则会直接走 catch ;对于异常的捕获只会捕获在 try 内的。

抛异常可以跳过函数:

image-20230207230721841

当在函数中捕获到异常后,函数之后的语句不再执行,直接跳转到 catch 处执行。

这样就不必在可能出错的下面检查,只要在 main 函数中捕获即可。不捕获异常就会弹出未经处理的异常的错误。

抛异常解决的是抛出来的错误,对于一些严重错误:内存错误、断言错误等,会终止程序,不会走异常。

ps:delete/free一般不会失败,如果失败了,都是释放空间存在越界或者释放指针的位置不对

六、operator new/operator delete函数

new 是要先申请空间,再调用构造函数;当一个对象被 new 时,是怎么做到开辟空间的?难道是舍弃了之前的 malloc 开辟空间的方式,另辟蹊径来开空间?我们试着探究。

当调试起来后,看到反汇编,一共调了两个函数 operator new 和 它的构造函数,可 operator new 到底是什么?

image-20230208091704697

new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数 ,new在底层调用operator new全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。

使用方法,例如开一个栈的空间:

image-20230208092258391

operator new 是不会调用构造函数的(如果会调用,就没有 new 什么事了),里面的仍然是随机值。

而 operator new 又是对 malloc 的封装:

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0) // 申请不成功
		if (_callnewh(size) == 0) // 下面在抛异常
		{
			// report no memory
			// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
			static const std::bad_alloc nomem;
			_RAISE(nomem);
		}
	return (p);
}

在升级中,谈到了抛异常,这里申请失败,就是使用抛异常的方式。

同理,operator delete 底层也使用了 free :

/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
	_CrtMemBlockHeader* pHead;
	RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
	if (pUserData == NULL)
		return;
	_mlock(_HEAP_LOCK); /* block other threads */
	__TRY
		/* get a pointer to memory block header */
		pHead = pHdr(pUserData);
	/* verify block type */
	_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
	_free_dbg(pUserData, pHead->nBlockUse);
	__FINALLY
		_munlock(_HEAP_LOCK); /* release other threads */
	__END_TRY_FINALLY
		return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

总结:

  • operator new 和 operator delete 就是对 malloc 和 free 的封装
  • operator new 和 operator delete 调用 malloc 申请内存,失败后,改为抛异常处理错误,这样才符合 C++ 面向对象语言处理错误的方式

如果没有 operator new ,那么 new Stack 之后,就会 call malloc + call Stack构造,而 malloc 不符合 C++ 处理错误方式(失败返回 0 ,而 new 失败也是返回 0)。

operator new 是给 new 的,我们直接使用 new 即可。

补充

operator new 会抛异常,并且调用 malloc 函数,如果 malloc 失败,则会抛异常;对于 delete 底层,也进行过了封装,先调用析构函数,再用 operator delete 进行释放。

例:

image-20230517185702944

image-20230517185613864

七、new/delete 的实现原理

1、内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

2、自定义类型

  • new的原理
    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的构造
  • delete的原理
    1. 在空间上执行析构函数,完成对象中资源的清理工作
    2. 调用operator delete函数释放对象的空间
  • new T[N]的原理
    1. 调用operator new[]函数(实际上是 operator new 的封装),在operator new[]中实际调用operator new函数完成N个对象空间的申请,类似于 Stack p1 = new Stack[10] ---> Stack* pst1 = (Stack*)operator new[](sizeof(Stack) * 10)
    2. 在申请的空间上执行N次构造函数
  • delete[]的原理
    1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

八、定位new表达式(placement-new)

能否调用构造函数?

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = (A*)malloc(sizeof(A));

	return 0;
}

不能。

在之前构造函数不支持显示调用;而现在则可以使用 定位 new 表达式 来调用构造函数。

定位new表达式是在 已分配的原始内存空间中调用构造函数初始化一个对象 ,例如 malloc 开辟空间后初始化对象。

使用格式:

  • new (place_address) type 或者 new (place_address) type(initializer-list)

  • place_address 必须是一个指针,initializer-list 是类型的初始化列表

class A
{
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = (A*)malloc(sizeof(A));
	new(p)A; // 不带参初始化,显示调用构造函数
	new(p)A(1); // 带参初始化

	return 0;
}

在这里插入图片描述

模拟 new 行为:

int main()
{
	A* p2 = new A(2); // operator new + 构造函数
	// 等价于 
	A* p3 = (A*)operator new(sizeof(A)); // operator new
	new(p3)A(3); // 调用构造函数

	return 0;
}

析构函数可以显示调用:

int main()
{
	A* p3 = (A*)operator new(sizeof(A)); // operator new
	new(p3)A(2); // 调用构造函数

	p3->~A(); // 调用析构函数
	operator delete(p3); // 释放内存

	return 0;
}

使用场景

定义 new 表达式在平常作用不大,它的真正应用场景是对内存池(池化技术–内存池、线程池、连接池):

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化 ,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

内存池解读:

image-20230221214439878

平常都是在堆上拿,现在在内存池拿,如果内存池有,则直接拿(比较快),没有内存池就去堆上拿,拿一大块,慢慢用。

定位 new 的应用:链表中创建节点时,使用内存池来进行,下面两步为显示调用构造函数和显示调用析构函数

image-20230517164150108

九、内存泄漏

动态申请的内存,不使用了,没有释放,就可以说成是内存泄漏。

好玩的:

image-20230208105835021

内存泄漏不是一定有危害,就比如:

int main()
{
	char* p = new char[1024u * 1024u * 1024u];
	printf("%p\n", p);

	return 0;
}

每次耗费一个 g 。虽然没有释放,但是当程序执行结束后,内存会被释放,还给OS.

而内存泄漏无非几种现象:

  1. 出现内存泄漏的进程正常结束,进程结束时内存会还给OS,不会有什么损害
  2. 出现内存泄漏的进程非正常结束,比如僵尸进程
  3. 需要长期运行的程序,出现内存泄漏,危害很大,OS会越来越慢,甚至卡死宕机,例如服务器程序:王者匹配机制、美团骑手匹配

2、3 两点会造成很大的危害。尤其是第三点,每天一点点,服务器崩坏一点点,不容易发现。

内存泄漏有两个典型现象:

void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

若 Func 有异常,则直接跳转到 catch ,这时 delete[] p3 没有执行,就内存泄漏了。

java 也可能会有内存泄漏,虽然有回收机制,但是 java 后台虚拟机会有一定代价,但是比 C++ 更优。

1、内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak)

    • 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak 。
  • 系统资源泄漏

    • 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2、如何检测内存泄漏

在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。

int main()
{
    int* p = new int[10];
    // 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
    _CrtDumpMemoryLeaks();
    return 0;
}

// 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。但有些情况下总是防不胜防,简单的可以采用上述方式快速定位下。如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内存泄漏检测工具处理的。

3、如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

4、补充

对于 32 位,申请 2g 的空间是失败的,因为对于 32 位的堆空间一共就 2g ,一下子申请显而不太可能,但是将程序改为 64 位就可以,因为此刻的堆空间就变大了很多:

image-20230208113037831

image-20230208111959538

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

查找和二叉树(基础知识和基本操作)

查找&#xff1a; 1.二分查找&#xff1a;先定一个大范围&#xff0c;想一个数&#xff0c;看是在起始范围到中间范围还是中间范围到结束范围&#xff0c;依次循环直到确定值&#xff08;相当于一直把范围折半&#xff0c;直到找到&#xff09; while(low<high) {int mid(…

分布式光伏电站运维平台在石化行业的应用光伏发电数据实时监控

摘要&#xff1a;为实现绿色发展和“净零排放”的目标&#xff0c;近些年来国内外不少能源化工企业进入光伏发电领域。如何做好光伏电站的运行维护&#xff0c;成为石化企业不得不思考的重要课题。本文从分布式光伏电站消防安全、作业安全、环保管理等方面进行思考&#xff0c;…

为什么学习SpringSpring框架核心与设计思想(IOC与DI)?

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE进阶 目录 文章目录 一、Spring是什么&#xff1f; 二、为什么要学习框架&#xff1f; 三、Spring核心概念 3.1 什么是容器&#xff1f; 3.2 什么是IOC&#xff1f; 四、再谈Spring中的 IOC 五…

mac如何提取视频中的音频?

mac如何提取视频中的音频&#xff1f;我们经常在平时工作的时候&#xff0c;需要将一个视频里面的音频单独提取出来另做他用&#xff0c;例如很多视频自媒体博主就经常使用这种方法来储备音频素材&#xff0c;这个操作在Windows上面比较容易实现&#xff0c;毕竟有相当多的软件…

计算机网络微课堂学习笔记(详细图解讲解)-长期更新

前言&#xff1a; 计算机网络在信息时代的作用 计算机网络已由一种通信基础设施发展成为一种重要的信息服务基础设施&#xff0c;计算机网络已经像水、电、煤气这些基础设施一样&#xff0c;成为我们生活中不可或缺的一部分 一、因特网概述 &#xff08;1&#xff09;网络、…

黑马 pink h5+css3+移动端前端

网页概念 网页是网站的一页,网页有很多元素组成,包括视频图片文字视频链接等等,以.htm和.html后缀结尾,俗称html文件 HTML 超文本标记语言,描述网页语言,不是编程语言,是标记语言,有标签组成 超文本指的是不光文本,还有图片视频等等标签 常用浏览器 firefox google safari…

Git标签管理(对版本打标签,起别名)

tag 理解标签创建标签git tag [name]git show [tagname] 操作标签删除标签git tag -d < tagname > 推送某个标签到远程git push origin < tagname > 理解标签 标签 tag &#xff0c;可以简单的理解为是对某次 commit 的⼀个标识&#xff0c;相当于起了⼀个别名。 …

实际上手体验maven面对冲突Jar包的加载规则 | 京东云技术团队

一、问题背景 相信大家在日常的开发过程中都遇到过Jar包冲突的问题&#xff0c;emm&#xff0c;在最近处理业务需求时我也遇到了不同版本jar包冲突导致项目加载出错的问题。主要是一个完整的项目会不可避免的使用第三方的Jar包来实现功能开发&#xff0c;各种第三方包之间可能…

【Linux】自动化构建工具-make/Makefile详解

前言 大家好吖&#xff0c;欢迎来到 YY 滴 Linux系列 &#xff0c;热烈欢迎&#xff01;本章主要内容面向接触过Linux的老铁&#xff0c;主要内容含 欢迎订阅 YY 滴Linux专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 订阅专栏阅读&#xff1a;YY的《…

跨网段耦合器的作用

你是否曾经遇到过需要跨网段访问设备的问题&#xff1f;比如在工业自动化领域&#xff0c;PLC和数控设备的连接。这时候&#xff0c;远创智控YC8000-NAT就能帮你轻松解决。 1, 远创智控YC8000-NAT是一款功能强大的设备&#xff0c;它可以将LAN1口所连接PLC的IP地址和端口号&a…

使用wxPython和pillow开发拼图小游戏(四)

上一篇介绍了使用本地图片来初始化游戏的方法&#xff0c;通过前边三篇&#xff0c;该小游戏的主要内容差不多介绍完了&#xff0c;最后这一篇来介绍下游戏用时的计算、重置游戏和关闭窗口事件处理 游戏用时的计算 对于游戏用时的记录&#xff0c;看过前几篇的小伙伴可能也发现…

结构型设计模式之代理模式【设计模式系列】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 设计模式系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everythi…

springBoot使用webSocket的几种方式以及在高并发出现的问题及解决

一、第一种方式-原生注解&#xff08;tomcat内嵌&#xff09; 1.1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>1.2、配置文件 package …

如何动态修改 spring aop 切面信息?让自动日志输出框架更好用

业务背景 很久以前开源了一款 auto-log 自动日志打印框架。 其中对于 spring 项目&#xff0c;默认实现了基于 aop 切面的日志输出。 但是发现一个问题&#xff0c;如果切面定义为全切范围过大&#xff0c;于是 v0.2 版本就是基于注解 AutoLog 实现的。 只有指定注解的类或…

数据结构之BinaryTree(二叉树)的实现

BinaryTree要实现的方法 总结 remove不在BinNode里&#xff0c;而是BinTree里 递归的两种写法 从上往下&#xff1a;同一对象的递归&#xff08;参数多一个&#xff0c;判空用一句话&#xff09;&#xff0c;子对象的递归&#xff08;参数void&#xff0c;判空用两句话&#…

对于awd

最近我们老师直接说要我准备awd&#xff0c;大概率要我上场我就顺便整理一下awd的资料&#xff08;准备写很多所以建议大家收藏一下&#xff09; 攻防指北 先来一个思维导图 Awd竞赛 AWD(Attack With Defense&#xff0c;攻防兼备)是一个非常有意思的模式&#xff0c;你需要…

Nginx配置整合:基本概念、命令、反向代理、负载均衡、动静分离、高可用

一、基本概念 1.什么是Nginx Nginx是一个高性能的HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP代理server。其特点是占有内存少。并发能力强&#xff0c;其并发能力确实在同类型的网页server中表现较好。 http服务器 Web服务器是指驻留于因特网上某种类型计算机的程…

IT技术岗的面试技巧分享

我们在找工作时,需要结合自己的现状,针对意向企业做好充分准备。作为程序员,你有哪些面试IT技术岗的技巧?你可以从一下几个方向谈谈你的想法和观点。 方向一:分享你面试IT公司的小技巧 1、事先和邀约人了解公司的基本情况,比如公司的行业,规模,研发人员占比等 2、事先和…

cmder 使用简介

文章目录 1. cmder 简介2. 下载地址3. 安装4. 配置环境变量5. 添加 cmder 到右键菜单6. 解决中文乱码问题 1. cmder 简介 cmder 是一个增强型命令行工具&#xff0c;不仅可以使用 windows 下的所有命令&#xff0c;更爽的是可以使用 linux的命令, shell 命令。 2. 下载地址 …

【C#】MVC页面常见的重定向方式和场景

本篇文章主要简单讲讲&#xff0c;C# MVC 页面常见跳转或者重定向的方式和场景。 在实际项目开发中&#xff0c;在一些特定场景肯定会用到重定向&#xff0c;比如&#xff1a;不同角色跳转到不同视图地址 目录 一、种常见重定向方式1.1、RedirectToAction1.2、RedirectToRoute1…