一、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";
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);
}
【说明】
- 栈 又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段 是高效的i/o映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。(linux具体讲解)
- 堆 用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段 --存储全局数据和静态数据。
- 代码段 --可执行的代码/只读常量。
c语言中动态内存管理方式:malloc/calloc/realloc/free
【面试题】: malloc/calloc/realloc
的区别? 参考 一文。
二、c++内存管理方式
c语言内存管理方式在c++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此c++又提出了自己的内存管理方式:通过new
和delete
操作符 进行动态内存管理。
2.1 new/delete操作内置类型
- 用法上,变简洁了
int* p0 = (int*)malloc(sizeof(int));
int* p1 = new int;
int* p2 = new int[10]; // new 10 个int 对象
delete p1;
delete[] p2;
- 可以控制初始化
int* p3 = new int(10); //动态申请一个int类型的空间
int* p4 = new int[10] {1, 2, 3, 4, 5}; //动态申请十个int类型的空间并初始化为{...}, 其余为0
注意:申请和释放单个元素的空间,使用new
和delete
操作符,申请和释放连续的空间,使用new[]
和delete[]
, 注意:匹配起来使用。
2.2 new和delete操作自定义类型
- new/delete对于自定义类型除了开空间还会调用构造函数和析构函数,内置类型是几乎是一样的
class a
{
public:
a(int a = 0)
: _a(a)
{
std::cout << "a():" << this << std::endl;
}
~a()
{
std::cout << "~a():" << this << std::endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是
// new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
a* p1 = (a*)malloc(sizeof(a));
a* p2 = new a(1);
free(p1);
delete p2;
return 0;
}
调用new
动态开辟内存,编译器会自动帮我们计算要开辟的空间,并调用operator new
全局函数(其是对malloc
的封装,失败抛异常也是在这一层,为了实现new
),然后再调用自定义类型的构造函数。从汇编角度,如下:
new [n]
是会调用operator new[]
函数(其是对operator new
的封装) 和 n 次构造函数。
delete
释放空间也相似,只不过先调用析构函数,再释放空间。 至于为什么,参考如下情况:
class mystack
{
public:
mystack()
: _a((int*)malloc(sizeof(int) * 4))
,_capacity(4)
, _top(0)
{}
~mystack()
{
free(_a);
_top = _capacity = 0;
}
private:
int* _a;
int _capacity;
int _top;
};
int main()
{
mystack* st = new mystack;
delete st;
return 0;
}
若先调用operator delete
,_a
指针变量所在的地址空间将被释放,无法找到malloc
开辟的堆上空间!
再来观察如下现象,new a
开辟的是4字节空间,但是new a[10]
开辟的却是44字节空间,这是为什么呢?
new a
调用一次operator new
和一次构造函数;同理new a[10]
调用十次operator new
和十次构造函数,因为[]
中传有开辟对象个数。那么delete
调用一次析构和一次operator delete
,但是delete[]
可就不一样了,因为[]
中没有传析构次数,所以编译器就不知道。那么为了让编译器知道次数,就在开辟的空间顶上多开辟4个字节来存放对象个数(x86环境,实测x64环境下多开辟8字节),只有这样delete[]
才知道调用多少次析构函数。
当然也有很多情况不会在顶上多开辟空间:1. new
内置类型,不需要析构;2. 没有显示写析构函数的自定义类型。(基于编译器的优化)
new
和delete
不匹配问题:
一个非常典型的问题(基于编译器的优化)就是:当new
多个自定类型时(a* p = new a[10]
),且直接使用delete a
,如果a
类显示实现析构函数就会报错,如果不写析构函数就不会报错! 这与上面那个问题密切相关,即是否多开辟空间存对象个数。
如果显示实现了析构函数,p3
并没有指向动态开辟内存的起始位置,且delete
又不知道要向前偏移,所以直接释放了动态开辟的内存的中间位置,导致报错! 而不实现析构函数,就不会多开辟空间,也就避免了这样的问题。当然两者情况都可能会导致内存泄漏的问题!:
所以new
和delete
一定要匹配使用,因为导致的结果可能是不确定的!
- new失败了以后抛异常,不需要手动检查,捕获异常方式:
try
{
func(); // 其中调用new
}
catch(const std::exception& e)
{
std::cout << e.what() << std::endl;
}
运用如上这些定理我们自己实现单链表也变得方便的多了!首先我们可以先创建一个类来描述单链表,然后单独实现创建链表的函数。
可以先创建一个哨兵位(mylist head(-1);
,栈上开辟,此节点为了方便后续链表节点的链接,且在创建单链表函数结束时自动销毁);然后通过cin
输入链表节点值(val
),并在堆上开辟链表节点(new mylist(val);
,此时还会调用mylist
类的构造函数);最后再链接各节点,并返回哨兵位后一个节点(head._next
),即链表初始节点(哨兵位节点,栈上空间,出作用域自动销毁)。
//c++中list单链表的创建
struct mylist
{
mylist(int val = 0)
:_next(nullptr)
,_val(val)
{}
mylist* _next;
int _val;
};
mylist* creatlist(int n)
{
mylist head(-1);//哨兵位 --- 出栈销毁
mylist* tail = &head;
int val;
std::cout << "请以此输入" << n << "个节点的值:> " << std::endl;
for (size_t i = 0; i < n; i++)
{
std::cin >> val;
tail->_next = new mylist(val); // 堆上开辟,链表实体; 且自动调用构造函数
tail = tail->_next;
}
//返回哨兵位后面一个节点
return head._next;
}
int main()
{
mylistnode* head = creatlistnode(1);
return 0;
}
注意:在申请自定义类型的空间时,new
会调用构造函数,delete
会调用析构函数,而malloc
与free
不会。
三、operator new与operator delete函数
new
和delete
是用户进行动态内存申请和释放的操作符,operator new
和operator delete
是系统提供的全局函数(不是重载!),new
在底层调用operator new
全局函数来申请空间(对malloc
的封装),delete
在底层通过operator delete
全局函数来释放空间(对free
的封装)。
/*
operator new:该函数实际通过malloc来申请空间,当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来释放空间的
*/
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
实际也是通过malloc
来申请空间,如果malloc
申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。 operator delete
最终是通过free
来释放空间的。
四、new和delete的实现原理
4.1 内置类型
如果申请的是内置类型的空间,new
和malloc
,delete
和free
基本类似,不同的地方是:new/delete
申请和释放的是单个元素的空间,new[]
和delete[]
申请的是连续空间,而且new
在申请空间失败时会抛异常,malloc
会返回null
。
4.2 自定义类型
new的原理
- 调用
operator new
函数申请空间 - 在申请的空间上执行构造函数,完成对象的构造
delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用
operator delete
函数释放对象的空间
new t[n]的原理
- 调用
operator new[]
函数,在operator new[]
中实际调用operator new
函数完成n个对象空间的申请 - 在申请的空间上执行n次构造函数
delete[]的原理
- 在释放的对象空间上执行n次析构函数,完成n个对象中资源的清理
- 调用
operator delete[]
释放空间,实际在operator delete[]
中调用operator delete
来释放空间
发表评论