C++初阶—C&C++内存管理

第一章:C/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);
}
//1. 选择题:
//选项 : A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
//globalVar在哪里?C 		staticGlobalVar在哪里?C
//staticVar在哪里?C  	localVar在哪里?A
//num1 在哪里?A
//
//char2在哪里?A			*char2在哪里?A   char2是数组,它把常量区的abcd\0拷贝到数组中
//pChar3在哪里?A		*pChar3在哪里?D  pChar3是指针,指向常量区的数据
//ptr1在哪里?A			*ptr1在哪里?B

//2. 填空题:
//sizeof(num1) = 40;
//sizeof(char2) = 5;      	strlen(char2) = 4;
//sizeof(pChar3) = 4/8;     strlen(pChar3) = 4;
//sizeof(ptr1) = 4/8;

【说明】

  1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段--存储全局数据和静态数据。
  5. 代码段--可执行的代码/只读常量

第二章:C语言中动态内存管理方式:malloc/calloc/realloc/free

cpp 复制代码
void Test() {
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);

	int* p2 = (int*)calloc(4, sizeof(int));
	int* p3 = (int*)realloc(p2, sizeof(int) * 10);
    // 这里需要free(p2)吗?不需要
    //如果是原地扩容,那么p3等于p2;如果是异地扩容,会把p2的内容拷贝过来并释放p2
	free(p3);
}

【面试题】

malloc/calloc/realloc的区别?

malloc 用于分配一块指定大小的内存,但 不会初始化 这块内存。

calloc 不仅分配内存,还会 初始化内存为 0。

realloc 用于重新调整先前分配的内存块的大小。它可以 增大或减小 已分配内存的大小,且会尝试保持原内存中的数据。

第三章:C++内存管理方式

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

3.1 new/delete操作内置类型

cpp 复制代码
int main() {
	// 单个对象的比较
	int* p1 = (int*)malloc(sizeof(int));

	//C++兼容C语言,内置类型的动态申请,用法简化了,功能保持一致
	//自动计算大小,不需要强制类型转换
	int* p2 = new int;


	//多个对象的比较
	int* p3 = (int*)malloc(sizeof(int) * 10);
	int* p4 = new int[10];

	//C语言释放方式
	free(p1);
	free(p3);
	//C++释放方式
	delete p2;
	delete[] p4;

	//C++额外支持开空间+初始化
	int* p5 = new int(1);
	int* p6 = new int[10]{ 1,2,3 };//只初始化了前三个元素,剩余是0
	int* p7 = new int[10]{};//所有元素默认是0
	return 0;
}

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用。

3.2 new和delete操作自定义类型

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

int main() {
	//malloc没有办法很好支持动态申请的自定义对象初始化
	//构造函数是实例化对象的时候自动调用,现阶段无法显示调用
	A* p1 = (A*)malloc(sizeof(A));

	//new主要针对自定义类型设计
	//对于自定义类型,开空间+调用构造函数初始化
	A* p2 = new A;
	A* p3 = new A(3);//支持传参

	//对于自定义类型,调用析构函数+释放空间
	delete p2;
	delete p3;

	
	//多个对象
	A* p4 = new A[10];
	delete[] p4;

	//new多个对象并初始化
	//1.有名对象
	A aa1(1);
	A aa2(2);
	A* p5 = new A[10]{ aa1, aa2 };
	delete[] p5;

	//2.匿名对象
	A* p6 = new A[10]{ A(1), A(2) };
	delete[] p6;

	//3.隐式类型转换
	A* p7 = new A[10]{ 1,2 };
	delete[] p7;

	return 0;
}

链表示例

cpp 复制代码
//C++的struct升级成类,所以struct名字也可以当作是类名
struct ListNode {
	ListNode* _next;
	int _val;

	ListNode(int val = 0)
		:_val(0)
		, _next(nullptr) {	
	}
};

int main() {
	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;
	n5->_next = nullptr;

	return 0;
}

栈示例

cpp 复制代码
//不能这么返回,因为这里的st出了函数作用域就销毁了。
Stack* func() {
	int n;
	cin >> n;
	Stack st(n);
	return &st;
}

//这种方式又要调用拷贝构造
Stack func() {
	int n;
	cin >> n;
	Stack st(n);
	return st;
}


Stack* func() {
	int n;
	cin >> n;
	Stack* pst = new Stack(n);
	return pst;
}

int main() {
	Stack* ptr = func();
	ptr->Push(1);
	ptr->Push(2);
	//....
	delete ptr;
	//上面例子中的new和delete不能换成malloc和free
	//因为malloc没有初始化(创建好栈对象就不能再初始化),而free没有调用析构函数,就没有释放资源
	return 0;
}

第四章:operator new与operator delete函数

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

operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;

申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。

operator delete: 该函数最终是通过free来释放空间的

通过上述两个全局函数的实现知道operator new 实际也是通过malloc来申请空间,

如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。

operator delete 最终是通过free来释放空间的。

演示抛异常

cpp 复制代码
int main() {
	//char* p1 = new char[1000000000];//10亿字节约等于1GB,依然能开辟

	//cout << p1 << endl;
	其他指针都是按指针去打印,char*可以是字符串
	字符串要遇到/0才停止,由于没有初始化,所以死循环打印
	要打印char*就需要强转
	//cout << (void*)p1 << endl;

	try {
		char* p2 = new char[0x7fffffff];
		cout << "hello world" << endl;//上方出异常后,不会执行这行代码,直接跳转catch
		//如果try里面没有异常,执行完try就不再走catch
	}
	catch (const exception& e) {
		cout << e.what() << endl;
	}
	//通过上方捕获异常的方式获取错误信息:bad allocation

	return 0;
}

第五章:new和delete的实现原理

5.1 内置类型

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

5.2 自定义类型

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来释放空间

cpp 复制代码
//operator new和malloc的功能是一样的
int main() {
	Stack* ptr = (Stack*)operator new(sizeof(Stack));//只开辟了空间,没有调用构造函数
	//如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
	//new / delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,
	//而且new在申请空间失败时会抛异常,malloc会返回NULL。


	//为什么 operator new 封装 malloc 并在分配失败时抛出异常?
    //operator new封装malloc为new准备的,如果new直接调用malloc,申请失败会返回0不符合要求。

	//C++ 鼓励使用异常机制来处理错误,而不是依赖返回值来指示错误。在 C++ 中,
	//异常被认为是一种更合适的错误处理方式,它让程序员能够集中处理错误,保持代码的清晰性和简洁性。

	//malloc 的做法:当 malloc 失败时,返回 nullptr,这要求程序员在每次调用 malloc 后都要显式检查返回值。
	//否则,如果程序未能检查返回值而继续使用 nullptr,就可能发生内存访问错误(例如崩溃或不稳定)。

	//operator new 的做法:operator new 在分配内存失败时抛出异常(std::bad_alloc)。
	//这种做法不需要显式检查返回值,程序员只需要在适当的位置捕获异常来处理内存分配失败的情况。
	//异常的方式更符合 C++ 风格,因为它可以将错误处理和正常逻辑分开,使代码更具可读性。
	return 0;
}

操作符匹配问题

示例1

cpp 复制代码
int main() {
	int* p1 = new int[10];
	//free(p1);
	delete p1;
	//上面2种方法都可以,不使用delete[]不会造成内存泄露,
	//因为内置类型不需要调用构造函数,所以也不需要调用析构函数

	return 0;
}

示例2

cpp 复制代码
class A {
public:
	A(int a = 0)
		:_a1(0)
	{}

private:
	int _a1;
};

int main() {
	A* p2 = new A[10];
	//free(p2);//无报错
	//delete p2;//无报错
	//编译器优化了,因为自动生成的析构函数发现没有需要释放的资源,所以没有调用析构函数

	return 0;
}

示例3

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

private:
	int _a1;
};

int main() {
	A* p2 = new A[10];
	//free(p2);//报错
	//delete p2;//报错
	delete[] p2;//结论:匹配使用
	//new A[10] 开辟了10个整型(每个4字节),并且在这段空间前面还额外申请4字节存了数字10。
	//因为delete[]要调用析构函函数,但不知道需要调用几次,所以需要上面的数字10。
	//delete[]不仅可以释放10个整型,还需要释放前面的4字节。
	//但p2指针指向的是10个整型的起始位置,如果不使用delete[]那么释放的位置就不对,所以报错。
	return 0;
}

第六章:定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式:
new (place_address) type 或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:

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

cpp 复制代码
int main() {
	//下方功能等价于new
	Stack* pst1 = (Stack*)operator new(sizeof(Stack));
	//pst1->Stack(4);//不支持
	new(pst1)Stack(4);//显示调用构造函数

	//下方功能等价于delete
	pst1->~Stack();
	operator delete(pst1);
	//但实践中一般不会这么使用,主要应用场景是为内存池准备
	//即提前开辟一块内存作为备用,而不是需要才去申请
	return 0;
}

第七章:常见面试题

7.1 malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

作业

1. 以下代码中,A 的构造函数和析构函数分别执行了几次:( )

cpp 复制代码
A*pa=new A[10];
delete []pa;

A.1、1

B.10、10

C.1、10

D.10、1

答案:B

A.申请数组空间,构造函数调用的次数就是数组的大小

B.正确

C.申请数组空间,构造函数调用的次数就是数组的大小

D.如果释放数组空间,delete使用了[],则会对应的调用数组大小次数的析构函数

2. 使用 char* p = new char[100]申请一段内存,然后使用delete p释放,有什么问题?( )

A.会有内存泄露

B.不会有内存泄露,但不建议用

C.编译就会报错,必须使用delete []p

D.编译没问题,运行会直接崩溃

答案:B

A.对于内置类型,此时delete就相当于free,因此不会造成内存泄漏

B.正确

C.编译不会报错,建议针对数组释放使用delete[],如果是自定义类型,不使用方括号就会运行时错误

D.对于内置类型,程序不会崩溃,但不建议这样使用

3. 设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为( )

cpp 复制代码
C c;
void main()

{
  A*pa=new A();
  B b;
  static D d;
  delete pa;
}

A.A B C D

B.A B D C

C.A C D B

D.A C B D

答案:B

分析:首先手动释放pa, 所以会先调用A的析构函数,其次会跟定义相反的顺序释放局部对象,这里只有b,就释放b,再释放静态局部对象d,再释放全局对象c

4. 下面有关malloc和new,说法错误的是? ( )

A.new 是创建一个对象(先分配空间,再调构造函数初始化), malloc分配的是一块内存

B.new 初始化对象,调用对象的构造函数,对应的delete调用相应的析构函数,malloc仅仅分配内存,free仅仅回收内存

C.new和malloc都是保留字,不需要头文件支持

D.new和malloc都可用于申请动态内存,new是一个操作符,malloc是是一个函数

答案:C

A.new会申请空间,同时调用构造函数初始化对象,malloc只做一件事就是申请空间

B.new/delete与malloc/free最大区别就在于是否会调用构造函数与析构函数

C.需要头文件malloc.h,只是平时这个头文件已经被其他头文件所包含了,用的时候很少单独引入,故错误

D.new是操作符,malloc是函数

5. 函数参数使用的空间是在()中申请的,malloc或new是在()中申请空间的?()

A.堆,栈

B.栈,堆

C.栈, 栈

D.堆,堆

答案:B

A.参数在栈空间存放,malloc或new申请的空间为堆区

B.正确

C.参数在栈空间存放,malloc或new申请的空间为堆区

D.参数在栈空间存放,malloc或new申请的空间为堆区

6. c++语言中,类ClassA的构造函数和析构函数的执行次数分别为( )

cpp 复制代码
ClassA *pclassa=new ClassA[5];
delete pclassa;

A.5,1

B.1,1

C.5,5

D.程序可能崩溃

答案:D

A.申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃

B.构造函数会调用5次

C.析构函数此时只会调用1次,要想完整释放数组空间,需要使用[]

D.正确

7. C++中关于堆和栈的说法,哪个是错误的:( )

A.堆的大小仅受操作系统的限制,栈的大小一般较小

B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题

C.堆和栈都可以静态分配

D.堆和栈都可以动态分配

答案:C

A.堆大小受限于操作系统,而栈空间一般有系统直接分配

B.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

C.堆无法静态分配,只能动态分配

D.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放

8. 下面有关c++内存分配堆栈说法错误的是( )

A.对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制

B. 对于栈来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆来讲,它的生长方向是向上的,是向着内存地址增加的方向增长

C.对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题

D.一般来讲在 32 位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的

答案:D

A.栈区主要存在局部变量和函数参数,其空间的管理由编译器自动完成,无需手动控制,堆区是自己申请的空间,在不需 要时需要手动释放

B.栈区先定义的变量放到栈底,地址高,后定义的变量放到栈顶,地址低,因此是向下生长的,堆区则相反

C.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

D.32位系统下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用,故错误

相关推荐
夕阳_醉了7 分钟前
如何在JS里进行深拷贝
开发语言·javascript·ecmascript
Ring__Rain39 分钟前
野指针bug
c++·bug
武昌库里写JAVA2 小时前
React方向:react中5种Dom的操作方式
java·开发语言·spring boot·学习·课程设计
xqhoj3 小时前
C++学习指南(七)——stack/queue/priority_queue
开发语言·c++
数据小小爬虫3 小时前
利用Java爬虫获取义乌购店铺所有商品列表:技术探索与实践
java·开发语言·爬虫
埃菲尔铁塔_CV算法3 小时前
双线性插值算法:原理、实现、优化及在图像处理和多领域中的广泛应用与发展趋势(二)
c++·人工智能·算法·机器学习·计算机视觉
叫我龙翔3 小时前
【算法日记】从零开始认识动态规划(一)
c++·算法·动态规划·代理模式
NoneCoder3 小时前
JavaScript系列(24)--内存管理机制详解
开发语言·javascript·ecmascript
Pafey3 小时前
c++ 中的容器 vector、deque 和 list 的区别
开发语言·c++
大草原的小灰灰3 小时前
C++ STL之容器介绍(vector、list、set、map)
数据结构·c++·算法