文章目录
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); }
- 选择题: 选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里? ——是全局变量 存在于数据段(静态区)
staticGlobalVar在哪里? ——是静态全局变量 static只是让该全局变量只能在该cpp文件使用,仍然存在于数据段(静态区)
staticVar在哪里? ——静态局部变量! 存在数据段(静态区)
localVar在哪里? ——局部变量 存在于栈上!
num1 在哪里?——数组 存在于栈上!
char2在哪里? ——是数组 存在于栈上
*char2在哪里?——存在于栈上!
pChar3在哪里?——是一个指针 存在于栈上
*pChar3在哪里? ——是一个常量 存在于代码段(常量区)
ptr1在哪里?——是一个指针存在栈上!
*ptr1在哪里?——由malloc开辟的空间都是在堆上!
sizeof(num1) =40 ;
sizeof(char2) = 5;
strlen(char2) = 4;
sizeof(pChar3) = 4/8;
strlen(pChar3) =4 ;
sizeof(ptr1) = 4/8;
sizeof 和 strlen 区别?
sizeof计算的是占用内存的大小
strlen遇到/0会停下来
C语言中动态内存管理方式:malloc/calloc/realloc/free
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(p3);
}
1.malloc/calloc/realloc的区别是什么?
malloc是直接在堆中申请内存
.calloc是相当于malloc+memset 申请内存+初始化
realloc是用来扩容的,如果一开始没有空间就相当于malloc!
2.这里需要free(p2)吗?
不需要因为realloc扩容的时候要么是在原地址扩容,要么是空间够,异地扩容,同时释放原来的空间!
C++内存管理方式!
new和delete!
new和delete都是关键字!malloc/calloc/realloc都是函数!
对于内置类型
int test()
{
//申请一个int的空间!
int* p1 = new int;
//释放是delete!
delete p1;
//和c语言一样这样都是没有初始化的!
int* p2 = new int(0);
//这样就可以完成初始化!
delete p2;
int* p3 = new int[10];
//一次开辟多个空间!
delete[] p3;
//释放的时候也要加上[]!
//这样也不会初始化!
int* p4 =
}
int main()
{
test();
return 0;
}
对于内置类型,new和delete 和malloc与free除了用法上,其实没有任何区别!
对于自定义类型!
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = new A;
delete p1;
A* p2 = (A*)malloc(sizeof(A));
free(p2);
return 0;
}
我们可以看到new和delete申请和释放自定义类型的空间的时候会调用构造函数和析构函数!
但是使用C的malloc和free却不会调用!
所以c++引入new和delete就是为了配合类来使用的!
相比C语言上的new和delete的优势
//使用c语言创建链表!
struct ListNode
{
int _val;
ListNode* _next;
};
void InitList(ListNode* Ls)
{
Ls->_next = NULL;
Ls->_val = 0;
}
int main()
{
ListNode* n1 = (ListNode*)malloc(sizeof(ListNode));
ListNode* n2 = (ListNode*)malloc(sizeof(ListNode));
ListNode* n3 = (ListNode*)malloc(sizeof(ListNode));
ListNode* n4 = (ListNode*)malloc(sizeof(ListNode));
InitList(n1);
InitList(n2);
InitList(n3);
InitList(n4);
n1->_val = 1;
n2->_val = 2;
n3->_val = 3;
n4->_val = 4;
n1->_next = n2;
n2->_next = n3;
n3->_next = n4;
return 0;
}
//使用c++创建链表
struct ListNode
{
ListNode(int val = 0)
:_val(val),
_next(nullptr)
{
}
int _val;
ListNode* _next;
};
int main()
{
ListNode* n1 = new ListNode(1);
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(3);
ListNode* n4 = new ListNode(4);
n1->_next = n2;
n2->_next = n3;
n3->_next = n4;
return 0;
}
使用new和delete注意
new就要和delete去匹配!
new数组就要和delete[]匹配!
malloc就要和free去匹配!绝不可以混着使用!
可能报错也可能不报错!可能会出问题,也可能不会出问题!
与new与delete在不同编译器下的底层实现机制有关!
会引发各种情况!甚至是内存泄漏这种严重的情况!
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { //情况1一 A* p1 = new A[10]; delete p1; return 0; }
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } /*~A() { cout << "~A():" << this << endl; }*/ private: int _a; }; int main() { //情况1一 A* p1 = new A[10]; delete p1; return 0; }
现在所有情况都是在vs2022版本下面的
为什么会导致这样的情况呢?
我们先思考一下正常的时候delete是如何判断要调用多少次析构函数?
A* p1 = new A[10]; delete[] p1; //我们没有写,它是如何知道的? A* p1 = new A[10]; delete p1;
所以为什么delete的时候为什么会报错呢?
因为我们delete匹配的是一个对象!
不会像delete[]从前面的4个字节开始释放!(红色p1的位置!)
而是从4字节后的位置开始释放!(黑色p1的位置!)
所以这才导致了报错!
那为什么我们把析构函数屏蔽了反而不会报错了不是会产生默认析构函数吗?
因为屏蔽掉后编译器反倒会智能起来
因为编译器发现A类里面只有内置类型,不需要进行析构,所以可以选择生成但是不调用!
所以就不用产生前面4字节,因为前面4字节本质就是为了存储要调用多少次的析构!
A* p1 = new A; delete[] p1; //这个情况一定会出问题!
这个出问题的原因是因为使用delete[] 编译器以为前面存了数字
取到了那个随机值后,开始疯狂的调用析构函数!
总归一句话一定要匹配的去使用!
在各种编译器下new和delete的实现方式的不同,如果不匹配的使用可能会导致各种未知的情况发生!
delete和new的底层!
我们知道malloc申请失败后返回NULL
那么new失败呢?
class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; int main() { while (1) { int* p1 = new int[1024*10000]; if (p1) { cout << p1 << endl; } else { cout << "申请失败!" << endl; break; } } return 0; }
我们发现它并没有打印失败!
因为new失败后不是会返回nullptr,而是会抛异常!
void test() { while (1) { int* p1 = new int[1024 * 10000]; if (p1) { cout << p1 << endl; } else { cout << "申请失败!" << endl; break; } } } int main() { try { test(); } catch (exception& e) { cout <<e.what() << endl; } return 0; }
最后出现了bad allocation这就是异常!
所以以后一般只要在main函数去捕获异常就可以了!
不需要像malloc一样去检查返回值了!
operator new与operator delete函数
**new ** delete
- operator new 1. operator delete
- 调用构造函数 2. 调用析构函数!
c++有很多让人混淆的点比如这个operator new,new是运算符没有错 !但是 operator new并不是new的重载!
/*
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);
}
void* operator new(size_t size);
//我们简化一下!
//这个是库里面实现的全局函数,不是运算符重载!(参数没有自定义类型!)
//这个其实是malloc的封装!因为malloc失败是返回nullptr
//封装后失败就是抛异常!
从汇编上我们也可以看出来确实调用了operator new
之所以封装malloc是为了符合面向对象处理错误的方式!
int main()
{
A* p1 = (A*)operator new(sizeof(A));
return 0;
} //我们甚至可以手动的去调用!
/*
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);//也调用了和free一样的函数!
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
/*~A()
{
cout << "~A():" << this << endl;
}*/
private:
int _a;
};
int main()
{
A* p1 = new A(1);
free(p1);
//所以我们可以看到其实这个代码是可以的,因为delete最后也会调用了和free一样的函数!
//记住还是要匹配使用!
//之所以举证例子是为了让读者更好的理解operator new与operator delete函数
}
//
new和delete的实现原理
内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL。
自定义类型
- 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来释 放空间
定位new表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
定位new的用法
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
//这个和malloc + 定位 约等于new 差别在于 报错返回nullptr new是抛异常!
A* p2 = (A*)operator new(sizeof(A));
if (p2 == nullptr)
{
perror("malloc fail");
exit(-1);
}
//假设这里有一块空间!但是没有调构造函数!
new(p2)A(1);
//定位new --对p2指向的空间 显示的调用构造函数初始化!
//如何释放这个空间!
//构造函数不支持显示调用!必须用定位new
//但是析构支持!
p2->~A();
free(p2);
//也可以使用delete p2;
return 0;
}
但是我们一般不会怎么用!定位new的真正的应用场景是在内存池中!
什么叫内存池?
我们先看一个例子有一个村在在山上,有一条小河在山下,山上的人要水就得到山下去,而且如果取水的人多了可能要排队!那么有没有什么办法能让我们方便快捷的取水呢?答案是有的,直接用水管连河里在家里搞个蓄水池就很方便快捷了!
内存池也是同样的原理!我们使用new或者malloc去堆中要内存,其实就像是在河中取水,那么我们为了提高效率我们可以去制造一个内存池出来,用于提高效率!
那么我们如何从内存池中取内存呢?答案就是使用new的重定位
常见面试题
malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地 方是:
malloc和free是函数,new和delete是操作符
malloc申请的空间不会初始化,new可以初始化
malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个对象,[]中指定对象个数即可
malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需 要捕获异常
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成 空间中资源的清理
不必要强行硬记!我们只要从new和malloc的用法和底层实现原理出发!
内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。
总结来说是指针的丢失而不是内存的丢失!
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。