目录
一.C/C++内存分布
1. 选择题:
2. 填空题:
3.说明
4.堆栈深层理解的考察题目
(1)
(2)
C++内存管理方式——————————————————
二.new/delete操作内置类型
>:1 malloc和new区别
>:2 free和delete区别
举个[]不匹配的例子:
(1)例子1:开链表节点
(2)例子2:栈
(3)例子3:栈构造队列 MyQueue 形式
(4)malloc失败,返回空指针;new失败,抛异常
【2】捕获异常
【3】捕获异常通常写法
5.new的相关易错题目
(1)题目1:求构造函数和析构函数的执行次数
(2)题目2:参数存放在哪个区?new申请的空间存放在哪个区?
(3)题目3:
三.operator new与operator delete函数(重要点进行讲解)
1.operator new 底层是封装了malloc:
operator delete 底层是封装了free:
2.new的真正底层原理(针对自定义类型):
① call operator new :调用operator new函数申请空间 ② call Stack构造函数:在申请的空间上执行构造函数,完成对象的构造
delete的原理
new T[N]的原理
delete[]的原理
3.定位new表达式(placement-new) (了解)
4.malloc/free和new/delete的区别(总结)
C/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";
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 . 代码段
1. globalVar 在哪里?__C__ 2. staticGlobalVar在哪里?__C__ 3. staticVar 在哪里?__C__ 4. localVar 在哪里?__A__ 5. num1 在哪里?__A__ 6. char2 在哪里?__A__ 7. * char2 在哪里?__A_ 8. pChar3 在哪里?__A__ 9. * pChar3 在哪里? __D__ 10. ptr1 在哪里?__A__ 11. * ptr1 在哪里?__B__答:(从左往右,从上往下顺序)前3个选C:因为全局变量,静态变量或静态全局变量生命周期都是全局,都属于静态区。静态的全局的在静态区;不可修改的常量,编译好的指令在常量区。const都可以修饰局部,全局,静态变量,不改变其空间位置,只是使其不可修改。常变量也是在栈上的
4,5选A:局部变量和数组都是在栈上。
6,7选A:6是数组存在栈上没问题,7是数组指向的内容,他是把常量字符串的内容拷贝到数组中,数组指向的内容是在栈上
8选A, 9选D:局部指针变量 pChar3在栈上没问题, pChar3是常量字符串的地址,那么 * pChar3就是常量字符串,常量字符串存在于常量区,所以 * pChar3存在于常量区。 10选A,11选D:ptr1是指针变量,存在于栈上。 * ptr1是指针指向的内容,因为是malloc开辟的,所以 * ptr1存在于堆上。2. 填空题:
sizeof
(
num1
)
=
__40__
;
sizeof
(
char2
)
=
__5__
;
strlen
(
char2
)
=
__4__
;
sizeof
(
pChar3
)
=
__4/8__
;
strlen
(
pChar3
)
=
__4__
;
sizeof
(
ptr1
)
=
__4/8__
;
解释:
sizeof
(
num1
)
是整个数组大小,10个整形就是40字节。
sizeof
(
char2
)算数组大小算上 是5字节,strlen(char2)只算字符串长度不算 是4字节,
sizeof
(
pChar3
)
指针大小32位平台是4字节,64位平台是8字节。
strlen
(
pChar3
)
算字符串长度是4字节。
sizeof
(
ptr1
)是算指针大小和
sizeof
(
pChar3
) 一样。
3.说明
1.
栈
又叫堆栈,非静态局部变量
/
函数参数
/
返回值等等,栈是向下增长的。
2.
内存映射段
是高效的
I/O
映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共
享内存,做进程间通信。(
Linux
课程如果没学到这块,现在只需要了解一下)
3.
堆
用于程序运行时动态内存分配,堆是可以上增长的。
4.
数据段
--
存储全局数据和静态数据。
5.
代码段
--
可执行的代码
/
只读常量。
4.堆栈深层理解的考察题目
(1)
4.堆栈深层理解的考察题目
(1)
下面有关c++内存分配堆栈说法错误的是( )
A.对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制
B. 对于栈来讲,生长方向是向下的,也就是向着内存地址减小的方向;对于堆来讲,它的生长方向是向上的,是向着内存地址增加的方向增长
C.对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题
D.一般来讲在 32 位系统下,堆内存可以达到4G的空间,但是对于栈来讲,一般都是有一定的空间大小的
答:
A.栈区主要存在局部变量和函数参数,其空间的管理由编译器自动完成,无需手动控制,堆区是自己申请的空间,在不需 要时需要手动释放
B.栈区先定义的变量放到栈底,地址高,后定义的变量放到栈顶,地址低,因此是向下生长的,堆区则相反
C.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题
D错。
D.32位系统下,最大的访问内存空间为4G,所以不可能把所有的内存空间当做堆内存使用,故错误
(2)
C++中关于堆和栈的说法,哪个是错误的:( )
A.堆的大小仅受操作系统的限制,栈的大小一般较小
B.在堆上频繁的调用new/delete容易产生内存碎片,栈没有这个问题
C.堆和栈都可以静态分配
D.堆和栈都可以动态分配
答:
A.堆大小受限于操作系统,而栈空间一般有系统直接分配
B.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题
C.堆无法静态分配,只能动态分配
D.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放
C错。(第一次错选成了D)
C++内存管理方式——————————————————
C语言内存管理方式在
C++
中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此
C++
又提出了自己的内存管理方式:通过
new
和
delete
操作符进行动态内存管理
二.new/delete操作内置类型
申请和释放单个元素的空间,使用
new
和
delete
操作符,申请和释放连续的空间,使用
new[]
和
delete[]
new申请对象后用delete释放,并且要格式匹配:int* p1=new int 对应 delete p1;int* p2 = new int[10] 对应 delete[] p2; 对应不匹配,可能会报错,也可能不会报错。建议一定要匹配。
int main()
{
int* p1 = new int; // new 1个int对象
int* p2 = new int[10]; // new10个int对象
// new一个int对象,初始化成10
int* p3 = new int(10);
// new10个int对象,初始化成{}中的值
int* p4 = new int[10]{ 10, 1, 2, 3 };
// 不匹配,可能会报错,可能不会报错。建议一定要匹配
delete p1;
delete[] p2;
delete p3;
delete[] p4;
return 0;
}
>:1 malloc和new区别
对于内置类型而言,用malloc和new,除了用法不同,没有什么区别
①他们区别在于自定义类型, malloc只开空间,new是 开空间+调用构造函数初始化
②malloc失败,返回空指针;new失败,抛异常(当内存不足时会开辟失败)解释——>(4)
所以malloc必须assert检查,new不用检查。
③new既是操作符也是关键字,malloc是函数
>:2 free和delete区别
free是释放空间,delete是 调用析构函数清理资源+释放空间
举个[]不匹配的例子:
内置类型:
char* p = new char[100]; delete p;
内存泄漏?报错?崩溃?
虽然是对的,但是不建议这么写,应该写成 delete []p。
内存泄漏吗?:不会,对于内置类型,此时delete就相当于free,因此不会造成内存泄漏
报错吗?:不会,编译不会报错,这个写法虽然不规范但是是对的,建议针对数组释放使用delete[],如果是自定义类型,不使用方括号就会运行时错误
崩溃吗?不会,对于内置类型,程序不会崩溃,但不建议这样使用
自定义类型:
A* p = new A[100];
delete p; (自定义类型不用delete []p;会导致内存泄漏吗?)
不会内存泄漏:new []和new 底层调用operator new,operator new又调用malloc,delete和delete[] 同理,底层都是调用free。所以不会内存泄漏
不会报错,有可能崩溃:崩溃是源自于他底层实现的一些原因,底层实际上就是它为了知道调用多少次析构函数,这里的内存空间前面会给你多开几个字节去存那个对象的个数,像那种方框,delete的时候,他就不会以为你头上有四个字节,这个指针偏移不对,反正等等这些底层原因吧,这个地方可能就会崩溃
(1)例子1:开链表节点
struct ListNode
{
ListNode* _next;
int _val;
ListNode(int val = 0)
:_next(nullptr)
, _val(val)
{}
};
struct ListNode* BuyListNode(int x)
{
struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
assert(node);
node->_next = NULL;
node->_val = x;
return node;
}
int main()
{
//C语言完整开节点写法:
//ListNode* n1 = BuyListNode(1);
//直接使用malloc开节点:
ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
assert(n1);
//new写法开节点:
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(3);
ListNode* n4 = new ListNode(4);
return 0;
}
new使用起来会很方便!
(2)例子2:栈
class Stack
{
public:
Stack(int capacity = 0)
{
cout << "Stack(int capacity)" << endl;
_a = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_capacity = 0;
_top = 0;
}
void Push(int x)
{}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st;
Stack* ps1 = (Stack*)malloc(sizeof(Stack));
//ps1->Stack(); 这样是错的,直接malloc以后,就没法初始
//化了,构造函数不能显示调用,并且成员变量是私有,不能直接访问
assert(ps1);
Stack* ps2 = new Stack; // 开空间+调用构造函数初始化
free(ps1);
delete ps2; // 调用析构函数清理资源 + 释放空间
return 0;
}
(3)例子3:栈构造队列 MyQueue 形式
class Stack
{
public:
Stack(int capacity = 0)
{
cout << "Stack(int capacity)" << endl;
_a = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_capacity = 0;
_top = 0;
}
void Push(int x)
{}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue {
public:
//如果Stack没有写默认构造函数,而是普通带参构造函数就需要我们显示的写出来
//MyQueue()
// : _pushST(4)
// , _popST(4)
//{}
private:
Stack _pushST;
Stack _popST;
};
int main()
{
MyQueue* obj = new MyQueue;
delete obj;
return 0;
}
class Stack
{
public:
Stack(int capacity = 0)
{
cout << "Stack(int capacity)" << endl;
_a = new int[capacity];
_capacity = capacity;
_top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_capacity = 0;
_top = 0;
}
void Push(int x)
{}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue {
public:
//如果Stack没有写默认构造函数,而是普通带参构造函数就需要我们显示的写出来
//MyQueue()
// : _pushST(4)
// , _popST(4)
//{}
private:
Stack _pushST;
Stack _popST;
};
int main()
{
MyQueue* obj = new MyQueue;
delete obj;
return 0;
}
new一个 MyQueue 空间,调用 MyQueue类 的默认构造函数去初始化成员变量 _pushST 和_popST,因为成员变量是内置类型,MyQueue类 的默认构造函数再去调用对应类的默认构造函数去初始化 _pushST 和_popST 这些成员变量。
(4)malloc失败,返回空指针;new失败,抛异常
【1】(当内存不足时会开辟失败)为了展示malloc和new的报错信息,需要让malloc和new都失败,就先利用p0开一个GB的空间,目的是消耗很大空间,然后再malloc和new就会失败
int main()
{
void* p0 = malloc(1024 * 1024 * 1024);
cout << p0 << endl;
void* p1 = malloc(1024 * 1024 * 1024); //malloc失败,返回空指针
cout << p1 << endl;
void* p2 = new char[1024 * 1024 * 1024]; //new失败,抛异常
cout << p2 << endl;
return 0;
}
【2】捕获异常
为了防止new弹报错窗口,我们需要去捕获异常,这里简单了解一下。
把可能抛异常的部分放在 try 中,当捕捉到异常后,直接跳到catch中,这里就是在p2那一行捕获异常就直接跳到catch中,后面的 cout << p2 << endl; 根本没执行。
int main()
{
void* p0 = malloc(1024 * 1024 * 1024);
cout << p0 << endl;
void* p1 = malloc(1024 * 1024 * 1024); //malloc失败,返回空指针
cout << p1 << endl;
try
{
//new失败,抛异常
void* p2 = new char[1024 * 1024 * 1024];
cout << p2 << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
【3】捕获异常通常写法
函数封装程序后,把函数放进try中,当捕获到异常就会跳进catch,即:在程序外层捕获异常,不用抛异常后立即捕捉。
void func()
{
void* p0 = malloc(1024 * 1024 * 1024);
if (p0 == nullptr)
{
cout << p0 << endl;
}
// malloc失败,返回空指针
void* p1 = malloc(1024 * 1024 * 1024);
if (p1 == nullptr)
{
cout << p1 << endl;
}
// new失败,抛异常
void* p2 = new char[1024 * 1024 * 1024];
cout << p2 << endl;
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
5.new的相关易错题目
(1)题目1:求构造函数和析构函数的执行次数
【1】
ClassA *pclassa=new ClassA[5];
delete pclassa;
c++语言中,类ClassA的构造函数和析构函数的执行次数分别为( )
A.5,1
B.1,1
C.5,5
D.程序可能崩溃
答:申请对象数组,会调用构造函数5次,delete由于没有使用[],此时只会调用一次析构函数,但往往会引发程序崩溃
选D。
【2】
以下代码中,A 的构造函数和析构函数分别执行了几次: ( )
A*pa=new A[10];
delete []pa;
A.1、1
B.10、10
C.1、10
D.10、1
答:申请数组空间,构造函数调用的次数就是数组的大小,所以调用了10次构造函数;如果释放数组空间,delete使用了[],则会对应的调用数组大小次数的析构函数,调用了10次析构函数。
选B。
(2)题目2:参数存放在哪个区?new申请的空间存放在哪个区?
函数参数使用的空间是在()中申请的,malloc或new是在()中申请空间的?
答案:参数在栈空间存放,malloc或new申请的空间为堆区
栈区 堆区
(3)题目3:
下面有关malloc和new,说法错误的是? ( )
A.new 是创建一个对象(先分配空间,再调构造函数初始化), malloc分配的是一块内存
B.new 初始化对象,调用对象的构造函数,对应的delete调用相应的析构函数,malloc仅仅分配内存,free仅仅回收内存
C.new和malloc都是保留字,不需要头文件支持
D.new和malloc都可用于申请动态内存,new是一个操作符,malloc是是一个函数
答:
A.new会申请空间,同时调用构造函数初始化对象,malloc只做一件事就是申请空间
B.new/delete与malloc/free最大区别就在于是否会调用构造函数与析构函数
C.需要头文件malloc.h,只是平时这个头文件已经被其他头文件(
D.new是操作符,malloc是函数
选C。
三.operator new与operator delete函数(重要点进行讲解)
new
和
delete
是用户进行
动态内存申请和释放的操作符
,
operator new
和
operator delete
是系统提供的
全局函数
,
new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new与operator delete 和运算符重载没有任何关系,这也是CPP中的一个容易错误理解的误区
1.operator new 底层是封装了malloc:
operator new的源代码:
operator delete 底层是封装了free:
operator delete的源代码:
operator new 跟malloc功能一样, 唯一不同是失败后抛异常(CPP摒弃了C语言中开辟失败返回空指针值的做法,而是转为失败后抛异常)
int main()
{
// 跟malloc功能一样, 唯一不同是失败后抛异常
Stack* ps2 = (Stack*)operator new(sizeof(Stack));
operator delete(ps2);
Stack* ps1 = (Stack*)malloc(sizeof(Stack));
assert(ps1);
free(ps1);
Stack* ps3 = new Stack;
// call operator new
// call Stack构造函数
return 0;
}
2.new的真正底层原理(针对自定义类型):
① call operator new :调用operator new函数申请空间
② call Stack构造函数:在申请的空间上执行构造函数,完成对象的构造
② call Stack构造函数:在申请的空间上执行构造函数,完成对象的构造
new调用operator new,operator new又调用malloc
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来释放空间
3.定位new表达式(placement-new) (了解)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用(内存池相当于自己的钱包,堆相当于妈妈的钱包,使用内存池的空间时效率更高)。当你直接new的时候是在堆上开空间,如果你想在内存池上开空间,就需要用到定位new。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
int main()
{
//定位new写法:相当于先开空间,再调用构造函数初始化
Stack* obj = (Stack*)operator new(sizeof(Stack));
//针对一个空间,显示调用构造函数初始化
new(obj)Stack(4);
//等价于new的写法: Stack* obj = new Stack(4)
return 0;
}
4.malloc/free和new/delete的区别(总结)
malloc/free
和
new/delete
的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
用法上的区别 1~4
1. malloc
和
free
是函数,
new
和
delete
是操作符
2. malloc
申请的空间不会初始化,
new
可以初始化
3. malloc
申请空间时,需要手动计算空间大小并传递,
new
只需在其后跟上空间的类型即可
4. malloc
的返回值为
void*,
在使用时必须强转,
new
不需要,因为
new
后跟的是空间的类型
底层区别 5~6
5. malloc
申请空间失败时,返回的是
NULL
,因此使用时必须判空,
new
不需要,但是
new
需要捕获异常
6.
申请自定义类型对象时,
malloc/free
只会开辟空间,不会调用构造函数与析构函数,而
new
在申请空间
后会调用构造函数完成对象的初始化,
delete
在释放空间前会调用析构函数完成空间中资源的清理
delete[]的原理
1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
3.定位new表达式(placement-new) (了解)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用(内存池相当于自己的钱包,堆相当于妈妈的钱包,使用内存池的空间时效率更高)。当你直接new的时候是在堆上开空间,如果你想在内存池上开空间,就需要用到定位new。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
int main()
{
//定位new写法:相当于先开空间,再调用构造函数初始化
Stack* obj = (Stack*)operator new(sizeof(Stack));
//针对一个空间,显示调用构造函数初始化
new(obj)Stack(4);
//等价于new的写法: Stack* obj = new Stack(4)
return 0;
}
4.malloc/free和new/delete的区别(总结)
malloc/free
和
new/delete
的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
用法上的区别 1~4
1. malloc
和
free
是函数,
new
和
delete
是操作符
2. malloc
申请的空间不会初始化,
new
可以初始化
3. malloc
申请空间时,需要手动计算空间大小并传递,
new
只需在其后跟上空间的类型即可
4. malloc
的返回值为
void*,
在使用时必须强转,
new
不需要,因为
new
后跟的是空间的类型
底层区别 5~6
5. malloc
申请空间失败时,返回的是
NULL
,因此使用时必须判空,
new
不需要,但是
new
需要捕获异常
6.
申请自定义类型对象时,
malloc/free
只会开辟空间,不会调用构造函数与析构函数,而
new
在申请空间
后会调用构造函数完成对象的初始化,
delete
在释放空间前会调用析构函数完成空间中资源的清理
int main()
{
//定位new写法:相当于先开空间,再调用构造函数初始化
Stack* obj = (Stack*)operator new(sizeof(Stack));
//针对一个空间,显示调用构造函数初始化
new(obj)Stack(4);
//等价于new的写法: Stack* obj = new Stack(4)
return 0;
}



