我们来看内存区域划分
数据段就是我们所说的全局变量,代码段是我们所说的常量区,我们需要重点关注的是堆区,这部分是由我们自己控制的
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在哪里?__1__ staticGlobalVar在哪里?__2__
staticVar在哪里?__3__ localVar在哪里?__4__
num1 在哪里?__5__
char2在哪里?__6__ *char2在哪里?_7__
pChar3在哪里?__8__ *pChar3在哪里?__9__
ptr1在哪里?_10___ *ptr1在哪里?__11__
我们来依次讨论:
globalVar
是全局变量,不是静态的,所以它存储在数据段(静态区)staticGlobalVar
也是全局变量,但它是静态的,因此它同样存储在数据段(静态区)staticVar
是函数内的静态变量,所以它存储在数据段(静态区),因为它的生命周期贯穿程序的整个执行期localVar
是局部变量,存储在栈上num1
是局部变量,它是数组,存储在栈上char2
是局部变量,它是数组首元素的地址,存储在栈上*char2
(即char2数组的内容)存储在栈上,因为char2
本身就在栈上pChar3
是局部指针变量,存储在栈上*pChar3
指向的内容(即字符串"abcd")存储在代码段(常量区)ptr1
是局部指针变量,存储在栈上*ptr1
指向的内容(即通过malloc
分配的内存)存储在堆上*char2
(局部字符数组)
当你声明一个局部字符数组并用一个字符串字面量初始化它,如char char2[] = "abcd";
时,编译器在栈上为数组分配内存,然后将字符串字面量的内容(包括结尾的\0
字符)复制到这块内存中。因此,char2
和它的内容(*char2
指向的内容)都存储在栈上
*pChar3
(字符串字面量指针)
另一方面,当你使用指针指向一个字符串字面量,如const char* pChar3 = "abcd";
时,这个字符串字面量存储在程序的只读数据段(或称为代码段、常量区)中。pChar3
本身作为一个局部指针变量存储在栈上,但它指向的字符串(“abcd”)实际上存储在常量区。这是因为字符串字面量被视为常量数据,编译器会将它们放在程序的常量区域内,这个区域通常是只读的,以防止程序意外修改它的内容。因此,尽管pChar3
是一个指针,存储在栈上,但它指向的字符串内容存储在常量区
总结:
*char2
不在常量区,因为char2
是局部字符数组,其内容直接存储在栈上。*pChar3
在常量区,因为它指向的是一个字符串字面量,字符串字面量被存储在程序的常量区域,这部分内存是只读的。当我们讨论变量存储在哪里时,通常涉及到几个关键区域:栈(Stack)、堆(Heap)、数据段(Data Segment,又称静态区)、和代码段(Code Segment,又称常量区)。每种类型的变量根据其特性和声明周期被存储在这些区域中的相应位置
malloc
或new
),并在不再需要时释放这些内存(如使用free
或delete
)。
在C语言中,动态内存管理是通过一组标准库函数完成的,包括malloc
, calloc
, realloc
, 和 free
。这些函数允许程序在运行时动态地分配、调整和释放堆内存,这是对于管理变化的数据量和大小特别有用的能力。下面是这些函数的基本用法和它们之间的区别:
malloc
void* malloc(size_t size);
NULL
。int* ptr = (int*)malloc(sizeof(int) * 4);
这行代码为4个整数分配了内存calloc
void* calloc(size_t num, size_t size);
NULL
。int* ptr = (int*)calloc(4, sizeof(int));
这行代码为4个整数分配了内存,并将它们初始化为0。realloc
void* realloc(void* ptr, size_t size);
malloc
或calloc
分配的内存块的大小。如果新的大小大于原始大小,可能会移动内存块到新的位置以提供足够的连续空间。如果realloc
的第一个参数是NULL
,它的行为就像malloc
。ptr = (int*)realloc(ptr, sizeof(int) * 8);
这行代码将之前分配的内存大小调整为8个整数的大小。free
void free(void* ptr);
malloc
, calloc
, 或 realloc
分配的内存。一旦内存被释放,那块内存就不能再被访问了。注意
free
来避免内存泄露。realloc
时,如果分配失败,原始内存不会被释放。因此,建议先将realloc
的返回值赋给一个临时指针,以检查是否分配成功,再重新赋值给原始指针,以避免内存泄漏。malloc
, calloc
, 或 realloc
分配的指针使用free
,并且每个分配的内存块只被free
一次C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过
new
和delete
操作符进行动态内存管理
new
的基本用法
Type* variable = new Type(arguments);
示例:
int* ptr1 = new int;
在堆上分配了一个int
大小的内存
int* ptr2 = new int[10];
加上方括号[ ]
表示分配了十个int
大小的内存
释放:
对于ptr,我们直接delete
delete ptr1;
释放数组对象的内存ptr2,我们需要加上方括号:
delete [] ptr2;
我们也可以分配内存的同时直接初始化:
int* ptr5 = new int(5);
动态申请一个int
类型的空间并初始化为5
我们也可以同时开辟多个空间完成初始化:
int* ptr6 = new int[10] {1,2,3,4,5};
后面的空间默认初始化为零
new
和delete
提供了对象构造和析构的自动管理,但程序员仍然需要负责确保每个用new
分配的内存都被对应的delete
释放,以避免内存泄露malloc
和free
一样,试图delete
一个未经new
分配的指针,或者对同一个指针执行多次delete
,都是未定义行为,并且可能导致程序崩溃new[]
分配数组时,必须使用对应的delete[]
来释放内存。使用错误的delete
形式也是未定义行为来看下面的代码:
struct ListNode
{
ListNode* _next;
int _val;
ListNode(int val)
:_next(nullptr)
,_val(val)
{}
};
struct ListNode* CreateListNode(int val)
{
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
if (newnode == NULL)
{
perror("malloc fail");
return NULL;
}
newnode->_next = NULL;
newnode->_val = val;
return newnode;
}
这是c语言构造一个节点并完成初始化的过程,我们来看c++的实现:
int main()
{
ListNode* node1 = new ListNode(1);
return 0;
}
这行代码自动为ListNode
对象分配了内存,并调用了其构造函数进行初始化。这种方式更简洁,也更安全,因为它保证了对象在使用前被正确初始化,注意这里ListNode是自定义类型,除了开空间还会调用构造函数
只要我们写好构造函数,我们发现new的使用是十分方便的
我们来构建一个链表:
ListNode* CreateList(int n)
{
ListNode head(-1); // 哨兵位
ListNode* tail = &head;
int val;
printf("请依次输入%d个节点的值:>", n);
for (size_t i = 0; i < n; i++)
{
cin >> val;
tail->_next = new ListNode(val);
tail = tail->_next;
}
return head._next;
}
我们输入五个值,1 2 3 4 5
哨兵节点:
ListNode head(-1)
;这行代码创建了一个局部的哨兵节点,它的值被设为-1(这个值通常是任意的,因为哨兵节点本身不存储任何有意义的数据)。哨兵节点的主要目的是简化在链表头部的插入和删除操作,因为你总是有一个非空的节点作为链表的起始点,从而避免了处理空链表的特殊情况
最后,函数通过return head._next;
返回新构建链表的头节点。由于head是一个哨兵节点,它的_next成员实际上指向链表的第一个真实节点(如果有的话),或者是nullptr(如果n为0或用户没有输入任何有效数据)
我们不用手动检查new是否开辟成功,new失败了会抛出异常
void func()
{
int n = 1;
while (1)
{
int* p = new int[1024 * 1024*100];
cout <<n<<"->"<< p << endl;
++n;
}
}
我们一次申请400M的空间大小
再看c语言版本
void func()
{
int n = 1;
while (1)
{
//int* p = new int[1024 * 1024 * 100];
int* p = (int*)malloc(1024 * 1024 * 400);
cout << n << "->" << p << endl;
++n;
}
}
开辟失败,程序无限循环并返回空
c++中的抛异常:
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
这段代码是C++中的一个示例,展示了如何使用try-catch语句来处理异常。这里的重点是捕获并处理func()
函数中可能抛出的异常。如果func()
函数执行中出现了问题,它将抛出一个异常,这个异常会被catch块捕获。捕获到的异常类型为const std::exception&
,这是C++标准异常类型的一个基类。在catch块中,通过e.what()
调用来获取并打印出异常的具体信息
const std::exception&
的异常。这意味着它能够捕获任何是std::exception
实例或其派生类的异常。通过常量引用捕获异常是一种最佳实践,因为这样可以避免异常对象的切片问题,并且可以最小化性能开销
e
的引用,它引用了被捕获的异常。const
限定符表明在catch块中,e
是不会被修改的
std::exception
及其派生类有一个名为what()
的成员函数,它返回一个描述异常的空终止字符序列(C风格字符串)。cout << e.what() << endl;
语句将这个消息打印到标准输出中
后续我们还会遇到这个函数,再详细讲解
来看抛异常的结果:
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);
delete p1;
return 0;
}
new/delete
和 malloc/free
最大区别是 new/delete
对于【自定义类型】除了开空间,还会调用构造函数和析构函数
A* p1 = new A(1);
00007FF798AA260B mov ecx,4
00007FF798AA2610 call operator new (07FF798AA104Bh)
00007FF798AA2615 mov qword ptr [rbp+108h],rax
00007FF798AA261C cmp qword ptr [rbp+108h],0
00007FF798AA2624 je main+50h (07FF798AA2640h)
00007FF798AA2626 mov edx,1
00007FF798AA262B mov rcx,qword ptr [rbp+108h]
00007FF798AA2632 call A::A (07FF798AA1343h)
00007FF798AA2637 mov qword ptr [rbp+138h],rax
00007FF798AA263E jmp main+5Bh (07FF798AA264Bh)
00007FF798AA2640 mov qword ptr [rbp+138h],0
00007FF798AA264B mov rax,qword ptr [rbp+138h]
00007FF798AA2652 mov qword ptr [rbp+0E8h],rax
00007FF798AA2659 mov rax,qword ptr [rbp+0E8h]
00007FF798AA2660 mov qword ptr [p1],rax
new过程跳转到构造函数
delete调用析构函数
打印结果如下:
A():000001DB79796B50
~A():000001DB79796B50
我们发现,汇编代码中有这一步:
00007FF798AA2610 call operator new (07FF798AA104Bh)
operator new,接下来我们来讲解这一部分
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
operator new
:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
static const std::bad_alloc nomem;
申请失败则会抛异常
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_dbg(pUserData, pHead->nBlockUse);
operator delete: 该函数最终是通过free来释放空间的
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的
我们只需要简单了解一下,并不需要深入理解
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL
自定义类型:
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);
delete p1;
return 0;
}
class Stack
{
public:
Stack()
{
_a = (int*)malloc(sizeof(int) * 4);
_top = 0;
_capacity = 4;
}
~Stack()
{
free(_a);
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack* pst = new Stack;
delete pst;
return 0;
}
这里进行了双层嵌套:
我们就很清楚的能看到,现需要调用析构函数再进行释放
operator new[]
函数,在operator new[]
中实际调用operator new函数完成N个对象空间的申请delete[]
的原理
operator delete[]
释放空间,实际在operator delete[]
中调用operator delete来释放空间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;
A* p2 = new A[10];
delete p1;
delete[]p2;
return 0;
}
在这段代码中,p2
是指向由 new A[10]
分配的对象数组的指针。虽然你可能会认为 p2
只需要分配足够存储 10 个 A
类型对象的空间,即 10 * sizeof(A)
,实际上编译器通常会分配额外的空间来存储有关数组本身的信息,比如数组的大小。这是因为在执行 delete[] p2;
时,系统需要知道要调用多少次析构函数
让我们具体看一下为什么会这样:
new A[10]
,C++ 需要知道在稍后释放数组时应该调用多少次析构函数。为此,它可能在分配给数组的内存块中存储一些额外的元数据,通常是数组的长度
delete[] p2;
释放内存时,这个额外存储的信息就被用来确保为数组中的每个元素正确调用析构函数
p2
的内存实际上包含了更多比简单的 10 * sizeof(A)
字节。首先是数组长度的元数据(大小取决于系统和编译器),紧接着是 10 个 A
类型对象的存储空间
sizeof(A)
是 4(假设 int
类型是 4 字节,并且没有类对齐导致的额外空间),那么仅对象部分就占用了 40 字节。加上存储数组大小的额外空间,总大小就会超过 40 字节
我们再来看内置类型:
int* p1=new int[10];
00007FF7F031206B mov ecx,28h
刚好开辟了四十个字节的空间,因为它不需要调用析构函数
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
new (place_address) type
或者new (place_address) type(initializer-list)
place_address
必须是一个指针,initializer-list
是类型的初始化列表
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
int* p1 = new int[10];
return 0;
}
A* p1 = (A*)malloc(sizeof(A));
p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
new(p1)A;
显示调用构造函数对一块已经有的空间的初始化
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
[]
中指定对象个数即可void*
, 在使用时必须强转,new不需要,因为new后跟的是空间的类型分类: