目录
C/C++内存分布
常见区域介绍
经典习题(读代码回答问题)
选择题
填空题
C语言内存管理方式
malloc/free
calloc
realloc
C++内存管理方式
new和delete操作内置类型
new和delete操作自定义类型
operator new和operator delete函数
new和delete的实现原理
定位new(placement new)
malloc/free和new/delete的区别
内存泄露
C/C++内存分布
函数调用建立栈帧,去堆上申请一块空间,这个变量存在静态区等等,上述的描述在我们学习语言的过程中应该经常会听到。今天我们站在语言层面上去探索一下C/C++中程序内存的分布(注意:我们所谈论的是在语言层面上关注的几个区域)。
常见区域介绍
1.栈:存非静态局部变量,函数参数,返回值等。栈是向下增长的(高地址到低地址)。
如下图所示的局部变量,返回值等,它们都是一些临时数据,一般存在栈区。可以把它们想象成一次性的物品,只是短暂的使用它完成某项任务。
普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
2.堆:程序运行时动态内存分配。堆是向上增长的(低地址到高地址)。
比如实现一个动态增长的顺序表,当顺序表满了以后,要进行扩容。每次扩容就要动态的去堆区申请一些空间。常见的操作如malloc、calloc、realloc、C++中更习惯用new来进行空间的动态申请。
3.静态区:存储全局变量和静态数据。如全局变量,static修饰的局部变量等。
被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。
4.常量区:可执行的代码、只读常量等。
经典习题(读代码回答问题)
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在哪里?C(全局变量) staticVar在哪里?C(静态局部变量)
num1 在哪里?A(静态数组) staticGlobalVar在哪里?C(静态全局变量)
localVar在哪里?A(局部变量)
第二组:
char2在哪里?A(数组名) pChar3在哪里?A(指针变量) ptr1在哪里?A(指针变量)
*char2在哪里?A(栈区) *pChar3在哪里?D *ptr1在哪里?B
填空题
sizeof(num1) = _40_; sizeof(ptr1) = _4/8(指针大小)_;
sizeof(char2) = _5(含‘\0’)_; sizeof(pChar3) = _4/8(指针大小)_;
strlen(char2) = _4(只求\0前的字符长度)_; strlen(pChar3) = _4(pChar3指向的字符串长度);
C语言内存管理方式
malloc/free
malloc:向内存申请一块连续可用size个sizeof(数据类型)大小的空间。
free:释放指针指向的空间。
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* dt = (Date*)malloc(sizeof(Date)*10);
if (dt == nullptr)
{
perror("malloc 失败");
exit(-1);
}
free(dt);
dt = nullptr;
return 0;
}
如上图所示,调用malloc函数在堆区动态开辟了3个Date类型成员大小的空间。
calloc
calloc:这个函数也是用来动态开辟内存的,和malloc不同的是,使用calloc函数会在返回地址之前把申请的空间的每个字节初始化为0。
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* dt = (Date*)calloc(3,sizeof(Date));
if (dt == nullptr)
{
perror("calloc 失败");
exit(-1);
}
free(dt);
dt = nullptr;
return 0;
}
calloc在申请空间的同时,还对数据进行了初始化。
realloc
realloc:调整ptr指向的空间大小。
class Date
{
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* dt = (Date*)malloc(sizeof(Date)*1);
Date* ptr = (Date*)realloc(dt,sizeof(Date)*5);
free(ptr);
dt = ptr = nullptr;
return 0;
}
原地扩容:如果原空间后的空间足够扩容,则直接在“原地”进行扩容。
异地扩容:在扩容期间可能存在,原空间后的可用空间不够扩容。这时realloc就会采用异地扩容的方式,先找一块连续的空间(扩容后大小),在将原空间中的内容拷贝过来,释放原空间。
了解了realloc的扩容机制,就可以理解在上述代码中,最后只free了ptr。因为如果是原地扩容,dt和ptr指向的是同一块空间,如果是异地扩容,在扩容期间原空间就已经被释放了。
C++内存管理方式
C++中引入了新的内存管理方式,通过new和delete操作符进行动态内存管理。
int main()
{
//动态申请一个int类型大小的空间
int* ptr1 = new int;
//动态申请一个int类型大小的空间,初始化为10
int* ptr2 = new int(10);
//动态申请5个int类型的空间
int* ptr3 = new int[5];
//动态申请10个int类型的空间
int* ptr4 = new int[3]{ 0,1,2 };
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
return 0;
}
需要注意的是,new和delete、new[]/delete不要混合使用,否则可能会出现一些错误。
new和delete操作内置类型
对于内置类型而言,new和malloc只是在语法使用上有些区别。
int main()
{
int* ptr1 = (int*)malloc(sizeof(int)*5);
int* ptr2 = new int[5];
return 0;
}
new和delete操作自定义类型
对于自定义类型而言,除了开空间外,new会调用自定义类型对象的构造函数。delete会调用自定义类型对象的析构函数。malloc与free不会这样做。
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A* a = new A();
delete a;
return 0;
}
operator new和operator delete函数
operator new和operator delete是系统提供的全局函数,new在底层调用operator new申请空间,delete在底层通过operator delete释放空间。
●operator new函数实际上是通过malloc来申请空间的。
●operator delete函数实际上是通过free来释放空间的。
new和delete的实现原理
1.对于内置类型来说,new和malloc。delete和free基本类似。
2.new/delete申请和释放的是单个元素的空间,new[]和delete[]申请和释放的是连续的空间。new失败时会抛异常,malloc失败会返回nullptr。
3.对于自定义类型来说,new要做两件事情:
●调用operator new申请空间。
●在申请的空间上执行构造函数,完成对象的构造。(调用自定义类型的构造函数)
4.delete的原理
●调用析构函数,对对象中的资源进行清理。
●调用operator delete函数释放对象的空间。
5.new T[N]的原理
●调用N次operator new完成N个对象空间的申请。
●在每次申请的空间上执行构造函数
6.delete[]
●执行N次析构函数,对N个对象完成资源的清理。
●调用N次operator delete释放空间。
定位new(placement new)
定位new的作用是对已经分配内存的对象调用构造函数初始化一个对象。
语法:new(指针)对象类型(构造传参)
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* dt = (Date*)malloc(sizeof(Date)*3);
new(dt)Date(2001, 10, 3);
new(dt+1)Date(2002,9,9);
return 0;
}
定位new使用场景:如内存池,内存池中的分配的内存没有初始化。如果是自定义类型对象,需要使用定位new表达式来显示的调用构造函数进行初始化。
malloc/free和new/delete的区别
1.它们的共同点都是在堆上申请空间,并且需要用户显示的去释放。
2.malloc和free是函数,new和delete是操作符。
3.malloc只是完成空间的申请,对于自定义类型而言new还会调用器自身的构造函数,完成对对象的初始化工作。
4.free只是释放空间,对于自定义类型而言delete会代用其自身的析构函数,对对象的资源进行清理。
5.malloc申请空间时,需要手动计算空间大小并传递,new只需要在其后面跟上空间的类型即可,如果是多个对象【】中指定数量。
6.malloc的返回值是void*,在使用对象时必须强转,new不需要,因为new的后面跟的是空间的类型。
7.malloc申请空间失败后,返回的是nullptr,因此使用时必须判空。而new在申请空间后会抛异常,捕获异常即可。
8.对于自定义类型对象的空间申请,malloc和free只会开辟空间。不会调用构造函数和析构函数。
而new在空间申请后,会调用构造函数完成对对象的初始化;
而delete在释放空间前会调用析构函数完成对象空间资源的清理。
内存泄露
内存泄露实际上可以理解为一块空间不在使用了没有释放,且丢掉了能够找到这块空间的指针。内存还在指针丢了。内存泄露并不是物理上的消失,而是失去了对这块空间的控制!!!
1.内存泄露的危害
对于长期运行的程序而言,内存泄露的影响很大,如操作系统,后台服务等。出现内存泄露的情况会导致响应越来越慢,最终卡死。
2.内存泄露的分类
堆内存泄露,通过函数调用或者new在堆空间申请空间后,没有被释放。导致这部分空间无法再被使用。
系统资源泄露,文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,可能导致系统不稳定出现一些问题。
3.如何避免内存泄露
预防:编码规范,采用RAII思想,用智能指针来管理资源等。
检测:内存泄露的检测工具。