本文的组织思路:从C++的内存分区谈起,简单比较堆和栈的区别;然后是内存分配和释放相关的new/delete,malloc/free的比较;再然后是内存的不合理分配或释放可能造成的问题(野指针,悬浮指针,内存泄漏等),以及为了避免这些问题的常用解决方案,也就是内存管理方面的手段(智能指针);最后是一些重要但是与计算机体系结构相关的问题,如内存对齐、乱序执行等。
1.C++内存分区
C++程序对应的虚拟内存从低地址到高地址分为:代码区—>常量区—>全局/静态区—>堆—>栈。
2.堆和栈
栈和堆都是存储程序数据的内存区域。
栈:内存空间比较小,存储局部变量、函数调⽤信息,栈上的变量⽣命周期与其所在函数的执⾏周期相同;
堆:动态分配的内存区域,存储程序运⾏时动态分配的数据,堆上的变量⽣命周期由程序员显式控制,手动分配(使⽤ new
或 malloc)和释放(使⽤ delete 或 free)。
3.new与malloc的区别
从内存块大小、内存分配失败时的行为、返回类型和类型安全性等角度阐述。
new:C++的运算符,为对象分配内存并调⽤相应的构造函数,返回的是具体类型的指针,⽽且不需要进⾏类型转换;
malloc:C语⾔库函数,分配指定⼤⼩的内存块,不会调⽤构造函数,内存分配成功返回的是void*,需要进⾏类型转换,在内存分配失败时返回 NULL。
4.delete和free的区别
delete:调⽤对象的析构函数,确保资源被正确释放,释放的内存块的指针值会被设置为 nullptr(避免野指针),通过delete[]释放通过 new[] 分配的数组;
free:不了解对象的构造和析构,只是简单地释放内存块,且不会修改指针的值(可能导致野指针问题),不了解数组的⼤⼩,不适⽤于释放数组。
5.野指针和悬浮指针
野指针:指向已被释放的或⽆效的内存地址的指针,野指针可能导致程序崩溃、数据损坏或其他不可预测的⾏为。
产生野指针的情况:
- 指针变量释放后没有置空
int main(){
int* ptr = new int;
delete ptr;
cout<<ptr<<endl; //0xf54a60, ptr被delete了,但是还能访问到它指向的内存
cout<<*ptr<<endl; //15930352
ptr = nullptr;
system("pause");
return 0;
}
- 返回局部变量的指针
int* createInt(){
int x = 10;
return &x; //x 是局部变量,函数结束后 x 被销毁,返回的指针成为野指针;warning: address of local variable 'x' returned [-Wreturn-local-addr]
}
int main(){
int* ptr = createInt();
//cout<<*ptr<<endl; //ptr指向的内存已经被释放了,不应该被允许访问
system("pause");
return 0;
}
- 函数参数指针被释放
void foo(int* ptr) {
delete ptr;
}
int main() {
int* ptr = new int;
*ptr = 10;
foo(ptr);
cout<<*ptr<<endl; //16847856
// 在 foo 函数中 ptr 被释放,但在 main 函数中仍然可⽤,成为野指针
// 避免:在 foo 函数中不要释放调⽤⽅传递的指针
system("pause");
}
可以通过使用智能指针来避免野指针。
悬浮指针:指向已经被销毁的对象的引⽤,访问悬浮引⽤会导致未定义⾏为,因为引⽤指向的对象已经被销毁,数据不再有效。
区别:
4. 野指针涉及指针类型;悬浮指针涉及引⽤类型;
5. 野指针可能导致访问已释放或⽆效内存,引发崩溃或数据损坏;悬浮指针可能导致访问已销毁的对象,引发未定义⾏为;
6. 野指针通常由于不正确管理指针⽣命周期引起;悬浮指针通常由于在函数中返回局部变量的引⽤引起;
6.内存泄漏
内存泄漏(memory leak):程序未能释放掉不再使⽤的内存的情况。内存泄漏并⾮指内存在物理上的消失,⽽是应⽤程序分配某段内存后,失去了对该段内存的控制,因⽽造成了内存的浪费。
注:可以使⽤Valgrind, mtrace进⾏内存泄漏检查。
7.智能指针
智能指针⽤于管理动态内存的对象,其主要⽬的是在避免内存泄漏和⽅便资源管理。更详细的内容。
- shared_ptr(共享智能指针):允许多个智能指针共享同⼀块内存资源。内部使⽤引⽤计数来跟踪对象被共享的次数,当计数为零时,资源被释放。提供更灵活的内存共享,但可能存在循环引⽤的问题。
int main() {
shared_ptr<int> ptr1 = std::make_shared<int>(42);
shared_ptr<int> ptr2 = ptr1;
cout<<*ptr1<<','<<*ptr2<<endl; //42, 42
system("pause");
}
一种shared_ptr的简单实现:
//shared_ptr源码
template<typename T>
class SharedPtr
{
public:
SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1))
{}
SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount){
(*_pcount)++;
}
//=左边的智能指针的引用计数需要-1,如果为0,则删除其指向的对象
SharedPtr<T>& operator=(const SharedPtr& s){
if (this != &s)
{
if (--(*(this->_pcount)) == 0)
{
delete this->_ptr;
delete this->_pcount;
}
_ptr = s._ptr;
_pcount = s._pcount;
*(_pcount)++;
}
return *this;
}
T& operator*()
{
return *(this->_ptr);
}
T* operator->()
{
return this->_ptr;
}
~SharedPtr()
{
--(*(this->_pcount));
if (*(this->_pcount) == 0)
{
delete _ptr;
_ptr = NULL;
delete _pcount;
_pcount = NULL;
}
}
private:
T* _ptr;
int* _pcount;//指向引用计数的指针
};
- unique_ptr(独占智能指针):提供对动态分配的单⼀对象所有权的独占管理。通过独占所有权,确保只有⼀个std::unique_ptr 可以拥有指定的内存资源。移动语义和右值引⽤允许 std::unique_ptr 在所有权转移时⾼效地进⾏转移。
int main() {
unique_ptr<int> ptr = std::make_unique<int>(42);
cout<<*ptr<<endl; //42
system("pause");
}
- weak_ptr(弱引⽤智能指针):⽤于解决 shared_ptr 可能导致的循环引⽤问题,weak_ptr 可以从 shared_ptr 创建,但不会增加引⽤计数,不会影响资源的释放,通过lock函数转换成shared_ptr后再访问数据。
int main() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
cout<<*weakPtr.lock()<<','<<*sharedPtr<<endl; //42,42
system("pause");
}
8.内存对齐
内存对⻬:是指数据在内存中的存储起始地址是某个值的倍数。在C语⾔中,结构体是⼀种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是⼀些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其⾃然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第⼀个成员的地址和整个结构体的地址相同。为了使CPU能够对变量进⾏快速的访问,变量的起始地址应该具有某些特性,即所谓的“对⻬”,⽐如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对⻬”跟数据在内存中的位置有关。如果⼀个变量的内存地址正好位于它⻓度的整数倍,他就被称做⾃然对⻬。⽐如在32位cpu下,假设⼀个整型变量的地址为0x00000004(为4的倍数),那它就是⾃然对⻬的,⽽如果其地址为0x00000002(⾮4的倍数)则是⾮对⻬的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照⼀定的规则在空间上排列,⽽不是顺序的⼀个接⼀个的排放,这就是对⻬。字节对⻬的根本原因在于提高CPU访问数据的效率。内存对齐规则。