文章目录
- 索引
- 内存分配和管理
- 1 malloc、calloc、realloc、alloca
- 2 malloc、free
- 3 new、delete
- 4 定位 new
- 5 如何定义一个只能在堆上(栈上)生成对象的类?
- 6 智能指针
- 7 强制类型转换运算符
- 8 运行时类型信息 (RTTI)
- 9 堆和栈的区别
- 10 你觉得堆快一点还是栈快一点?
- 11 变量声明和定义区别?
- 12 什么是内存泄漏,如何检测与避免
- 13 野指针和悬空指针
- 14 内存分配
索引
【复习整理归纳】| C++面经(基础概念、线程、进程)
【复习整理归纳】| C++面经(函数相关)
【复习整理归纳】| C++面经(内存管理)
【复习整理归纳】| C++面经(STL及项目)
内存分配和管理
====》C++【STL】| GNU空间配置器alloc刨析,如何管理内存池(附图解)…
====》C++ |【总结归纳三本书籍系列】一文透彻资源管理,动态内存分配【上】…
====》C++ |【总结归纳三本书籍系列】一文透彻资源管理,动态内存分配【下】…
====》C++【内存管理】| 【智能指针】动态内存管理
====》一文搞懂堆和栈的区别
====》Linux内存分配
1 malloc、calloc、realloc、alloca
- malloc:申请指定
字节数的内存
。申请到的内存中的初始值不确定
;- calloc:为指定
长度的对象
,分配能容纳其指定个数的内存
,申请到的内存的每一位(bit)都初始化为 0
;- realloc:更改以前分配的
内存长度
(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定;- alloca:在
栈上申请内存
,程序在出栈的时候,会自动释放内存
。但是需要注意的是,alloca不具可移植性
,而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
2 malloc、free
# 申请内存,确认是否申请成功
char *str = (char*) malloc(100);
assert(str != nullptr);
# 释放内存后指针置空
free(p);
p = nullptr;
3 new、delete
- new / new[]:自动计算,先底层
调用 malloc 分配了内存
,然后类型转换,在调用构造函数
(创建对象);- delete/delete[]:也完成两件事,先调用
析构
函数(清理资源),然后底层调用free 释放空间
;- new 在申请内存时会
自动计算所需字节数
,而 malloc 则需我们自己输入申请内存空间的字节数
;- malloc返回void类型,new返回具体类型的指针;
- malloc异常返回nullptr,new通过bad_alloc处理;
3.1 new的种类
- 抛出bad_alloc;
- nothrow;
- const new;
- 定位new;
3.2 有了malloc为什么还需要new
- 在使用非基本数据类型的对象时需要调用构造函数,销毁时需要调用析构函数;
- 由于malloc/free是库函数而不是运算符,是已编译的代码,不能够把执行构造函数和析构函数的任务强加于malloc/free;
3.3 被free回收的内存是立即返给操作系统吗?
- 不是,被回收到ptmalloc双链表中保存,当用户下次申请会尝试寻找合适的返回,避免频繁的系统调用;
- ptmalloc也会进行碎片合并,避免产生过多的内存碎片;
3.4 new和delete的实现原理, delete是如何知道释放内存的大小的?
- new在分配内存的时候会使用4字节的空间来记录下当前分配的大小;
3.5 malloc申请的存储空间能用delete释放吗?
- 一般不这样写,不保证有些编译器会出现问题;
3.6 malloc与free的实现原理?
- 主要使用brk、mmap、munmap;
- brk是将数据段的最高地址指针,_edata往高地址推;mmap是进程间虚拟地址空间,在堆栈中找一个块空闲的虚拟地址,该方式都没有分配物理地址空间,将会在第一次访问的时候发生页中断,系统建立虚拟地址和物理地址的映射;
- malloc小于128k使用brk内存分配;需要等高地址内存释放后才能释放
- 大于128k时使用mmap分配内存,内存可以单独释放
4 定位 new
允许我们向 new 传递额外的
地址参数
,从而在预先指定的内存区域创建对象
;
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
# place_address 是个指针
# initializers 提供一个(可能为空的)以逗号分隔的初始值列表
========》delete this 合法吗?《========
合法,但:
- 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的;
- 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
5 如何定义一个只能在堆上(栈上)生成对象的类?
5.1 只能在堆上
- 方法:将
析构函数
设置为私有
;- 原因:C++ 是
静态绑定语言
,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性
。若析构函数不可访问,则不能在栈上创建对象;
5.2 只能在栈上
- 方法:将
new 和 delete 重载为私有
;- 原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用
构造函数生成对象
。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象;
6 智能指针
========》C++【内存管理】| 【智能指针】动态内存管理《========
7 强制类型转换运算符
7.1 static_cast
- 用于
非多态类型
的转换,编译期,转换之间类型之间有联系的;不执行运行时类型检查
(转换安全性不如 dynamic_cast);- 通常用于转换
数值数据类型
(如 float -> int);- 可以在整个类层次结构中移动指针,
子类转化为父类安全
(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法);向上转换
是一种隐式转换;
7.2 dynamic_cast
- 主要用于
多态
类型的转换,运行期,支持RTTI类型识别的上下转换;- 执行行运行时
类型检查
;- 只适用于
指针或引用
;- 对不明确的指针的转换将失败(返回 nullptr),但
不引发异常
;- 可以在整个类层次结构中移动指针,包括向上转换、向下转换;
- 由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常;
7.3 const_cast
- 用于删除
const、volatile 和 __unaligned
特性(如将 const int 类型转换为 int 类型 );- 模板参数只能是指针或引用;
7.4 reinterpret_cast
- 类似C风格;
- 用于
位的简单
重新解释;- 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
- 允许将任何指针转换为任何其他指针类型(如 char* 到 int* 或 One_class* 到 Unrelated_class* 之类的转换,但其本身并不安全)
- 也允许将
任何整数类型转换为任何指针类型
以及反向转换
;- reinterpret_cast 运算符不能丢掉
const、volatile 或 __unaligned
特性;- reinterpret_cast 的一个实际用途是在
哈希函数
中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引;
8 运行时类型信息 (RTTI)
8.1 typeid
- typeid 运算符允许在
运行时确定对象的类型
;- type_id 返回一个
type_info
对象的引用;- 如果想通过基类的指针获得派生类的数据类型,基类必须带有
虚函数
;- 只能获取对象的
实际类型
;
8.2 type_info
- type_info 类描述编译器在程序中生成的
类型信息
; 此类的对象可以有效存储指向类型的名称的指针。- type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值。;
- 类型的编码规则和排列顺序是未指定的,并且可能因程序而异。
#include <iostream>
using namespace std;
class Flyable // 能飞的
{
public:
virtual void takeoff() = 0; // 起飞
virtual void land() = 0; // 降落
};
class Bird : public Flyable // 鸟
{
public:
void foraging() {...} // 觅食
virtual void takeoff() {...}
virtual void land() {...}
virtual ~Bird(){}
};
class Plane : public Flyable // 飞机
{
public:
void carry() {...} // 运输
virtual void takeoff() {...}
virtual void land() {...}
};
class type_info
{
public:
const char* name() const;
bool operator == (const type_info & rhs) const;
bool operator != (const type_info & rhs) const;
int before(const type_info & rhs) const;
virtual ~type_info();
private:
...
};
void doSomething(Flyable *obj) // 做些事情
{
obj->takeoff();
cout << typeid(*obj).name() << endl; // 输出传入对象类型("class Bird" or "class Plane")
if(typeid(*obj) == typeid(Bird)) // 判断对象类型
{
Bird *bird = dynamic_cast<Bird *>(obj); // 对象转化
bird->foraging();
}
obj->land();
}
int main(){
Bird *b = new Bird();
doSomething(b);
delete b;
b = nullptr;
return 0;
}
9 堆和栈的区别
- 管理方式:堆用户控制,栈有系统分配;
- 内存管理机制:
- 堆:系统会有记录空闲内存地址的链表,当程序申请时,遍历链表找到第一个空间大于申请空间的堆节点,删除空间节点,并将该节点分配给程序;会记录分配大小,为了保证delet释放内存,且会将多余的内存回收到空闲链表中;
- 栈:系统会自动分配,但当栈内存需求大于系统限制时,会出现栈溢出;
- 空间大小:堆是不连续的,空间较灵活,较大;栈是连续的内存空间;
- 碎片问题:堆中不断的new/delete会出现内存碎片;
- 生长方向:堆是向上增长,往高地址,栈是向下,向低地址增长;
- 分配方式:堆new只有动态分配,栈有静态分配和动态分配,可通过alloc分配但也由系统回收;
10 你觉得堆快一点还是栈快一点?
- 栈快,系统底层对栈由提供支持,由提供专门的寄存器存放栈的地址,专门指令对栈进行存取;
- 堆在分配内存时,需要通过一定的算法寻址合适的内存,在获取堆内容时需要进行两次的访问,先访问指针,在访问指针的的地址访问内存;
11 变量声明和定义区别?
11.1 变量
- 声明仅仅是把变量的声明位置及数据类型告知给编译器,不分配内存;
- 定义要在定义的分配存储空间;
11.2 函数
- 一般在头文件中声明;源文件中定义;
11.3 初始化和赋值的区别
- 对于简单类型,初始化和赋值没有什么区别;
- 但对于复杂类型,会涉及构造或拷贝构造;
12 什么是内存泄漏,如何检测与避免
- 分配内存后没有进行相应的释放;
- 被return;
避免:
- 计数法:使用new、malloc加1,free减1,若为0则不泄漏;
- 基类的析构声明为虚析构;
- 智能指针;
- 对象的数组需要使用[];
- 保证成对出现;
13 野指针和悬空指针
- 野指针,没有进行初始化;
- 悬空指针,内存被释放的指针;
14 内存分配
stl分配器有两级:
一级:当申请大小大于128byte时使用;
二级:当小于128byte时使用;
一级分配器:
使用malloc、realloc:设置new_handler
二级分配器:
一个链表内有16个节点,从8字节到128字节(内存对齐);
每个节点有一个嵌入式指针,用来挂载内存链表;
【申请】:
当申请一个内存时,会先经过
1、内存对齐
2、在相应的链表下查看是否有内存可以使用;
【则查看内存池是否有内存】,若有,则尝试是否满足20个区块的大小,若不行,则递归尝试数量减少;
若减少后也不满足,则将该内存池的碎片,切割挂到(碎片/8 - 1)节点上;
【若内存池位空】:则申请按20 * 2 * byte * (申请余量%16),将前面20个进行切割挂在该节点下供该节点,第一个区块分配
给客户使用,剩余交给内存池;并记录申请量;
【若申请不到内存】:则从离该节点最近的下一个节点上拿取内存,将该节点上的区块切割出来,余量给内存池;
【若有】:则直接分配给客户;