C++的内存管理

前言

我们之前学习完了类和对象的相关内容,已经正式入门了。我们本节来学习一下C++的内存管理,看一看C++的语法和C语言的语法有什么区别和联系。那么废话不多说,我们正式进入今天的学习。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1. C语言和C++的内存分布

在开始学习之前,我们需要知道:C++的内存管理模式与C语言的内存管理形式保持一致。

通过回顾之前所学习过的知识,我们可以知道:C++和C语言程序内存区域是要进行划分的

内存管理存在的意义是:帮助计算机更加方便地处理各种各样的数据。那么不同的数据在内存之中又是如何划分的呢?我们知道:程序中会存在一些局部数据,需要建立栈帧,而且这一类数据都是用一会就要被销毁的;程序中还有一些长期存在的数据,例如全局数据、静态数据;程序中还有一些不能修改的数据,例如常量数据;程序中还有一些动态申请的数据。根据这些数据的使用环境和生命周期,C++和C语言将数据划分为以下几个段:

**************************************************************************************************************

内核空间

用户代码不能读写,内核空间使用场景较少,不做过多介绍

我们知道,函数的调用需要建立栈帧,栈帧就是存放在栈区域的。栈又叫堆栈,用于存放非静态局部变量、函数参数、返回值 等等。栈是向下增长的

内存映射段

内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。经常用于文件映射、动态库等,在Linux中应用广泛

堆用于程序运行时动态内存分配 ,内存中动态申请的数据就存放在堆中,堆是向上增长的

数据段(静态区)

全局数据和静态数据就定义在数据段中

代码段(常量区)

用于存放常量 以及编译好的指令

**************************************************************************************************************

下面我们来用一个题目深入理解C++中的内存管理:


cpp 复制代码
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.代码段(常量区)

(1)globalVar存放在哪里?____

(2)staticGlobalVar存放在哪里?____

(3)staticVar存放在哪里?____

(4)localVar存放在哪里?____

(5)num1存放在哪里?____

(6)char2存放在哪里?____

(7)*char2存放在哪里?___

(8)pChar3存放在哪里?____

(9)*pChar3存放在哪里?____

(10)ptr1存放在哪里?____

(11)*ptr1存放在哪里?____


(1)C

globalVar是一个全局的数据,存放于数据段中

(2)C

staticGlobalVar是一个全局的静态数据,故存放于数据段中

(3)C

staticVar虽然是函数中的一个变量,但它通过static的修饰变成了静态变量,故存放于数据段中

(4)A

localVar是Test函数中的一个临时变量,故存放于栈中

(5)A

num1是一个数组名,这里代表的是整个数组,故存放于栈中

(6)A

(注意:这里的 char2 开空间的时候会开5个字节,因为包含 \0)char2 是一个数组名,这里代表的是整个数组,表示在栈上开辟了一个五个字节的空间,并且将 abcd\0 拷贝过去

(7)A

做运算的时候数组名代表首元素的地址,所以 *char2 代表的是数组的首元素 a ,因为 a 是存放在栈上的,所以选A

(8)A

pchar3 虽然被const修饰,但 pchar3 是一个局部变量,故存在于栈上

(9)D

*pchar3 指向的是 "abcd" 这个常量字符串,所以 *pchar3 在代码段中

(10)A

ptr1 表示的是一个指针变量,所以仍然存放于栈中

(11)B

*ptr1 指向动态申请的空间,故在堆上

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2. C语言中的动态内存管理方式

我们在C语言中用 malloc / calloc / realloc /free 来实现动态内存的管理,我们来回顾一下:malloc / calloc / realloc 三者的区别是什么?

malloc 函数可以用来动态申请空间,但是动态申请的空间不会被初始化

calloc 函数也可以用来动态申请空间,而且可以将其初始化,其功能相当于 malloc + memset

realloc 函数是用来扩容的,如果原扩容的空间不足的时候将会重新开辟一块新的空间,并将原空间内的所有数据内容拷贝至新的空间中,返回新空间的地址

free 函数用来释放动态申请的空间


我们来看一个问题:

cpp 复制代码
void Test ()
 {

    int* p2 = (int*)calloc(4, sizeof (int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);

    free(p3 );
 }

我们 free 完了 p3 还需要 free 掉 p2 吗?

答案是:不需要,因为 p3 存放的是 p2 扩容后的地址,扩容分为原地扩容和异地扩容,原地扩容的地址不变,而异地扩容会返回扩容后的地址,原地址就会被自动销毁,所以不需要释放 p2


我们可以通过下面的链接来扩展了解一下 malloc 的实现原理,不作硬性的要求:https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0&vd_source=416ea8b68b7adbc90fbd084c6cf37138

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

3. C++的内存管理方式

C++中可以继续使用C语言的内存管理方式,但是有些地方在使用C语言的内存管理方式的时候就会遇到一些问题,而且在使用的时候会比较麻烦,因此C++提出了自己的内存管理方式:通过new和delete操作符来实现动态内存的管理,下面我们就来介绍new和delete的使用方法

假设我们要申请一个类型为 int 的动态空间

如果使用 malloc 就需要传很多的数据,还需要强制类型转换,我们使用 new 就会方便很多,new 简化了 malloc 的操作步骤:

cpp 复制代码
int* p1 = new int;

假设动态申请一个int类型的空间并初始化为10

cpp 复制代码
int* p2 = new int(10);

假设动态申请10个int类型的空间

cpp 复制代码
int* p3 = new int[10];

**************************************************************************************************************

如果我们需要释放动态申请的空间也很方便,但是我们需要注意,释放单个的空间和多个的空间的方法有些不同:

释放单个动态申请的空间只需要在 delete 后直接加上指向该空间的指针变量即可;而释放多个动态申请的空间需要在 delete 后面加上一个 [] 再写指针:

cpp 复制代码
delete p1;
delete p2;
delete[] p3;

**************************************************************************************************************

我们来具体讲解一下 new 的初始化:

刚才我们讲了单个对象的初始化,假设要进行多个对象的初始化,就需要采取下面的语法:

cpp 复制代码
	int* p4 = new int[10] {0};
	int* p5 = new int[10] {1, 2, 3, 4, 5};

如果像 p5 一样采取的是不完全的初始化,那么未初始化的部分会默认初始化为0

(还有部分更复杂的初始化在下面会提及)

**************************************************************************************************************

学习到这里,我们对于 new 和 delete 的用法就有了基本的了解,那么C++设计 new 和 delete 仅仅是为了更加方便吗?还有没有其他的用处?

答案是肯定的,我们先来创建一个类A

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

我们再 new 一个类型为 A 的动态空间:

cpp 复制代码
int main(void)
{
	A* p1 = new A;
	A* p2 = new A(1);

	delete p1;
	delete p2;
	return 0;
}

若此时我们运行程序就会发现,我们在动态申请一个自定义类型的空间的时候,自动调用了它的构造函数;而当我们用 delete 释放动态申请的空间的时候会调用它的析构函数:

**************************************************************************************************************

.................................................................................................................................

当有了 new 和 delete 的时候,我们再去写链表等数据结构的时候就会非常的轻松:

cpp 复制代码
struct ListNode
{
	int val;
	ListNode* next;

	ListNode(int x)
		:val(x)
		,next(nullptr)
	{}
};
cpp 复制代码
int main(void)
{
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	ListNode* n4 = new ListNode(4);
	ListNode* n5 = new ListNode(5);
	n1->next = n2;
	n2->next = n3;
	n3->next = n4;
	n4->next = n5;
	return 0;
}

.................................................................................................................................

我们现在接着上面的内容继续讲 new 的初始化:

刚才我们所讲的类中只含有一个参数,如果类A中含有两个参数,且含有默认构造函数:

cpp 复制代码
class A
{
public:
	A(int a1 = 0, int a2 = 0)
		: _a1(a1)
		, _a2(a2)
	{
		cout << "A(int a1 = 0, int a2 = 0):" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};
cpp 复制代码
int main(void)
{
	A* p1 = new A;
	A* p2 = new A(2, 2);
	A* p3 = new A[3];
	return 0;
}

如上述代码,无论我们是给参数还是不给参数都能成功的完成初始化,而且 new n次,就会调用n次构造函数

但是假设没有默认构造函数呢?

我们来修改一下A类,将其改为没有默认构造函数的类:

cpp 复制代码
class A
{
public:
	A(int a1, int a2 = 0)
		: _a1(a1)
		, _a2(a2)
	{
		cout << "A(int a1 = 0, int a2 = 0):" << this << endl;
	}
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a1 = 1;
	int _a2 = 1;
};

那么我们该修改下面的代码,让它能够成功定义并且初始化呢?

cpp 复制代码
	A* p3 = new A[3];

答案是:我们需要先定义三个对象,再把这三个对象作为参数传递给动态申请的空间:

cpp 复制代码
int main(void)
{
	A aa1(1, 1);
	A aa2(2, 2);
	A aa3(3, 3);
	A* p3 = new A[3]{ aa1,aa2,aa3 };
	return 0;
}

但是如果像这样定义对象来初始化就会比较麻烦,而且严格意义上来讲这里调用的就不是构造函数了,而是拷贝构造函数,因为 aa1、aa2、aa3 是已经存在的对象。所以此时我们就可以换一种写法,用匿名对象来初始化:

cpp 复制代码
int main(void)
{
	A* p4 = new A[3]{ A(1,1),A(2,2),A(3,3) };
	return 0;
}

这种写法还会被编译器优化,就只需要调用构造函数就好了,而不用再调用拷贝构造函数了(详情见上一节的类和对象收尾)

除了这两种写法还有第三种写法:

cpp 复制代码
int main(void)
{
	A* p5 = new A[3]{ {1,1},{2,2},{3,3} };
	return 0;
}

我们之前学习过了:单参数构造函数支持隐式类型转换,而多参数构造函数也支持隐式类型转换,第三种写法在本质上和第二种写法是一样的,这种写法也会被编译器优化,会转变为直接构造,而不去调用拷贝构造

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4.C++的抛异常(精简)

之前在学习C语言的时候,我们在 malloc 结束以后通常都会用 perror 检查申请空间是否成功。而我们在 new 一个动态空间的时候通常不用 perror 去检查,那么C++为什么不去检查呢?此时我们就要提前引入一个抛异常的概念(这里只作简单的说明,具体的细节后面会详细讲解)

在C语言中如果 malloc 失败了就会返回空指针,而在C++中 new 失败了并不会返回空指针,new 失败了会抛异常

抛异常由三个关键字组成:throw、try、catch

throw:发生异常了以后用 throw 抛出一个对象

try / catch:对异常进行处理

(面向对象的语言通常都会有抛异常的步骤)

我们先来看一下申请空间失败了是怎么抛异常的

因为在正常的情况下,我们动态申请的空间都很小很小,很难开辟空间失败抛异常,所以这里我们一次申请1GB的内存空间(32位下):

cpp 复制代码
int main(void)
{
	void* p1 = new char[1024 * 1024 * 1024];
	cout << p1 << endl;

	void* p2 = new char[1024 * 1024 * 1024];
	cout << p2 << endl;

	void* p3 = new char[1024 * 1024 * 1024];
	cout << p3 << endl;
	return 0;
}

此时我们就需要用到 try、catch 就可以大致知道发生了什么错误(此部分仅作大致了解即可):

cpp 复制代码
int main(void)
{
	try
	{
		void* p1 = new char[1024 * 1024 * 1024];
		cout << p1 << endl;

		void* p2 = new char[1024 * 1024 * 1024];
		cout << p2 << endl;

		void* p3 = new char[1024 * 1024 * 1024];
		cout << p3 << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

bad allocation 的意思是内存申请失败了,没有足够的内存

若是像下面这种情况,当在函数中申请内存失败了以后就不会继续往函数下面走了,而是直接跳到 catch 的地方去:

cpp 复制代码
void func()
{
	void* p1 = new char[1024 * 1024 * 1024];
	cout << p1 << endl;

	void* p2 = new char[1024 * 1024 * 1024];
	cout << p2 << endl;

	void* p3 = new char[1024 * 1024 * 1024];
	cout << p3 << endl;
}

int main(void)
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

严格意义来讲,写C++的程序的时候都需要 try 和 catch 来对异常进行处理

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

5. operator new 与 operator delete 函数

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

operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间 失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常

我们先来看一下 operator new 的底层代码:

cpp 复制代码
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);
}

由上述的代码可以知道,当申请内存空间失败的时候,就会抛出 bad_alloc 类型的异常。bad _alloc 是 exception 的子类

这里不做详细的讲解,我们需要学习过继承与多态以后才能知道,只需要了解即可

上述代码中的 RAISE 在底层是一个宏,RAISE 就是 throw 即当 malloc == 0 的时候就会抛异常。通过这些我们就可以知道,其实 new 的底层还是 malloc

下面我们来看一下 delete 的底层实现:

cpp 复制代码
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;
}

delete 的这一大段代码我们暂时不需要去关注,我们只需要关注最重要的一行代码:

cpp 复制代码
_free_dbg(pUserData, pHead->nBlockUse);

那么这里的 _free_dbg 和 free 的关系是什么呢?我们来看一下 free 的实现:

cpp 复制代码
#define   free(p)               _free_dbg(p, _NORMAL_BLOCK)

由此我们知道,free 是一个宏函数,free 的底层也是调用的 _free_dbg ,所以我们也可以知道 delete 的底层是 free

学到这里我们可能会有疑问,new 不是一个函数调用,那又是如何转换成 operator new 的呢?其实是编译器在编译的时候直接把它生成对应的指令

我们先来看看 new 的反汇编

其中下面的代码表示的是申请空间,如果申请失败就抛异常:

而这里的代码则表示调用构造函数:

我们再来看看 delete 的反汇编:

我们在看 delete 函数的反汇编时,并没有直接的看到 _free_dbg ,而是调用了析构函数:

来调用完了析构函数再调用 operator delete

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

6. new和delete的实现原理

内置类型:

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

自定义类型:

new的原理:

  1. 调用operator new函数申请空间

  2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作

  2. 调用operator delete函数释放对象的空间

new T[N]的原理:

  1. 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成N个对象空间的申请

  2. 在申请的空间上执行N次构造函数

delete[] 的原理

  1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

  2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

在使用 delete[] 的时候我们不需要自己去传参数

cpp 复制代码
int main(void)
{
	A* p1 = new A(1);
	delete p1;

	A* p2 = new A[5];
	delete[] p2;

	return 0;
}

此时我们可能会有疑问:假设我们使用语法不匹配会发生什么?

假设我们用 free 来释放 new 出来的空间

cpp 复制代码
int main(void)
{
	int* p1 = new int;
	free(p1);

	return 0;
}

我们可以看到,这里并没有出现什么问题,也不存在内存的泄露,因为这里的代码是内置类型的数据,不涉及调用析构函数。那如果这里的数据类型使用一个自定义类型呢?

cpp 复制代码
	A* p2 = new A;
	free(p2);

此时使用 free 相比较于 delete 少调用了析构函数,如果自定义类型的析构函数涉及释放动态开辟的空间的时候,就会出现内存泄漏

所以使用语法不匹配可能会出现内存泄漏的风险

那么如果我们是 delete 和 delete[] 使用不当呢?

此时我们再来创建一个类B

cpp 复制代码
class B
{
private:
	int _b1 = 1;
	int _b2 = 2;
};
int main(void)
{
	int* p1 = new int[10];
	delete p1;
	return 0;
}

对于上述代码中的内置类型 而言,这里仍然不会出现问题。因为 new 在底层就是调用了 malloc 函数,因为内置类型没有涉及调用构造和析构函数,所以这里不会出现问题。

但是如果是自定义类型

cpp 复制代码
int main(void)
{
	B* p2 = new B[10];
	delete p2;
	return 0;
}

当自定义类型为B的时候,我们发现这里还是没有出现问题,我们把类型改成A:

cpp 复制代码
int main(void)
{
	A* p3 = new A[10];
	delete p3;
	return 0;
}

此时我们就发现程序崩溃了。A和B都是自定义类型,但是为什么B不会崩溃而A就崩溃了呢?

这里我们就需要从底层进行分析:我们先看到 A 类型的对象和 B 类型对象都是8个字节

我们转到反汇编,这里 new 在底层调用的是 operator new ,因为一个B类型的对象是8个字节,所以这里需要申请80个字节

但是我们申请了10个A对象的空间发现此时的空间大小不是80个字节了,而是84个字节,这是为什么呢?

当编译器开辟多个自定义类型的对象的数组的时候,会在开辟的空间前面多开辟4个字节,这开辟出来的4个字节用于存储对象的个数

而且我们 new A 的时候返回来的地址不是 这个存放对象个数的4个字节的地址,而是向后偏移4个字节的地址。此时直接释放就会导致不完全释放,而B中指针指向的就是它的起始位置,它没有额外多申请那4个字节的空间,所以不会存在问题

那为什么A开辟了4个字节存储对象个数,而B没有多开辟4个字节去存储个数呢?严格意义上来说A和B都需要开空间去存储对象的个数,B没有开空间是因为编译器发现B没有写析构函数所以将其优化了

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

7. 定位new表达式(placement-new)

通过刚才的学习我们知道,我们写 new 的时候,编译器会自动帮我们调用 operator new 并且调用构造函数,其实我们也可以自己手动去调用 operator new 但是它不会自动帮我们调用构造函数:

cpp 复制代码
int main(void)
{
	A* p1 = new A(1);
	cout << "**********************************" << endl;
	A* p2 = (A*)operator new(sizeof(A));
	return 0;
}

可以看到:p2 只开了空间,而没有调用构造函数。假设我们现在需要对一块已经存在了的空间显示的调用构造函数,此时定位 new 就可以帮助我们完成这个功能。接下来我们来讲解一下定位 new 的使用方法:

假设 p2 是一个已经存在了的空间,我们要对 p2 调用构造函数只需要采取以下的语法:

cpp 复制代码
int main(void)
{
	A* p1 = new A(1);
	cout << "**********************************" << endl;
	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(1);
	return 0;
}

假设我们要调用析构函数呢?

这里我们需要注意,调用构造函数是只能通过定位 new 来调用的,但是调用析构函数,是可以直接显示调用的:

cpp 复制代码
int main(void)
{
	A* p1 = new A(1);
	cout << "**********************************" << endl;
	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(1);
	delete p1;
	p2->~A();
	return 0;
}

或者也可以写 operator delete(p2)

有人可能会觉得这个动作多此一举,直接用 new 和 delete 更加方便,为什么还要专门设计一个定位 new 呢?感觉这个语法有点多此一举。

其实这个定位 new 在我们日常生活中写代码的时候基本上不需要使用,但是某些特定的场景下使用定位 new 会非常好。后面我们会学习到STL中有一个内存池的概念

内存池:我们首先需要知道一个技术叫做池化技术,池化技术的意思是我们可以建造一个"池子",将某些资源存入这个池子中,使用的时候就会更加方便、更快。池化技术有利于提升性能,常见的池化技术有:内存池、线程池、连接池......

我们以内存池为例:假设我们需要高频的申请和释放内存块,此时我们就可以专门建造一个内存池,内存池里面的空间仍然是在堆中来的,但是内存池里面的空间是专供使用的,别的地方是使用不了的,此时效率就会更加高。但是此时就会有一个问题,内存池只能给空间而不能调用构造函数进行初始化,此时定位 new 就有它存在的意义了

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

结尾

C++的内存管理和C语言的内存管理模式基本一样,所以上手比较快,那么关于C++的内存管理的所有内容就到此结束了,下一节我们将学习C++模板,模板是真正让C++和C语言拉开差距的语法。希望本节的内存管理内容可以给你带来帮助,谢谢您的浏览!!!

相关推荐
李元豪4 小时前
【智鹿空间】c++实现了一个简单的链表数据结构 MyList,其中包含基本的 Get 和 Modify 操作,
数据结构·c++·链表
UestcXiye4 小时前
《TCP/IP网络编程》学习笔记 | Chapter 9:套接字的多种可选项
c++·计算机网络·ip·tcp
一丝晨光5 小时前
编译器、IDE对C/C++新标准的支持
c语言·开发语言·c++·ide·msvc·visual studio·gcc
丶Darling.5 小时前
Day40 | 动态规划 :完全背包应用 组合总和IV(类比爬楼梯)
c++·算法·动态规划·记忆化搜索·回溯
奶味少女酱~6 小时前
常用的c++特性-->day02
开发语言·c++·算法
我是哈哈hh6 小时前
专题十八_动态规划_斐波那契数列模型_路径问题_算法专题详细总结
c++·算法·动态规划
_小柏_7 小时前
C/C++基础知识复习(15)
c语言·c++
_oP_i7 小时前
cmake could not find a package configuration file provided by “Microsoft.GSL“
c++
mingshili8 小时前
[python] 如何debug python脚本中C++后端的core dump
c++·python·debug
PaLu-LI8 小时前
ORB-SLAM2源码学习:Frame.cc: Frame::isInFrustum 判断地图点是否在当前帧的视野范围内
c++·人工智能·opencv·学习·算法·ubuntu·计算机视觉