引言
现在是5月30日的正午,图书馆里空空的,也许是大家都在午休,也许是现在37摄氏度的气温。穿着球衣的我已经汗流浃背,今天热火战胜了凯尔特人,闯入决赛。以下克上的勇气也激励着我,在省内垫底的大学中,我不觉得气馁,我要更加努力学习,让自己能够越来越好,以后肯定也会”晋级决赛”。
1.C/C++程序的内存分布
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享共享内存,做进程间通信。 - 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段–存储全局数据和静态数据。
- 代码段–可执行的代码/只读常量
2.变量在内存中存储的位置
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);
}
下面我按照从左往右从上到下的顺序依次分析上面的题目。globalVar和staticGlobalVar都是定义在全局域中,所以是存储在静态区中的。staticVar虽然是在局部域内定义的,但是它是static修饰的变量所以依旧是存储在静态区中的。localVar和num1都是在局部域内定义的局部变量,都是存储在栈区空间的。上面第一部分比较简单,下面我以画图加分析的来看下面部分。
为什么char2和*char2都在局部域呢?
pChar3是一个定义在栈区的const char类型指针,它保存的是常量字符串"abcd"的首元素地址。*pChar3是对常量字符串的首元素地址进行解引用操作,访问的是常量区的空间。ptr1是一个在栈区上创建的指针变量,存放的是动态开辟空间首字节地址。解引用访问ptr1访问的是堆区空间。
数组名单独放在sizeof内部表示整个数组,所以是4*10字节。"abcd"其实是隐含了’\0’字符,所以是4+1=5字节。无论什么指针都是4 or 8字节,指针的大小取决于平台,64位平台8字节,32位平台4字节。
3.C语言的动态内存管理
常见的C语言动态内存管理如下:malloc/calloc/realloc/free。具体细节可以移步c语言动态内存管理,查看这里不做赘述。
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
1.malloc就是单纯地开辟堆区空间。calloc是可以指定初始化内容开辟堆区空间。realloc就是动态调整堆区申请的空间。当调整后的空间无法在原空间后扩容,则会将原空间的内容拷贝到新的空间上再申请到连续空间。
4.C++的动态内存管理
因为C++是兼容C语言的,所以C语言的动态内存管理依旧是可以在C++中使用的。由于C语言的动态管理方式存在缺陷,所以C++也提供了两个操作符来进行动态内存管理。分别是new 和 delete。
4.1.new和delete管理内置类型
int main()
{
//c
int* p1 = (int*)malloc(sizeof(int));
free(p1);
//cpp
//new后面跟的是类型
int* p2 = new int;
delete p2;
//malloc单纯开辟空间
//new支持初始化
int* p3 = new int(10);
delete p3;
int* p4 = (int*)malloc(sizeof(int) * 10);
free(p4);
int* p5 = new int[10];
delete[] p5;//一定要带[]
//开辟连续空间也是支持初始化的
int* p6 = new int[10]{1,2,3,4};
delete[] p6;
}
需要注意的是使用malloc申请的内存就用free来释放,使用new申请的内存,就用delete来清理。不可以free来释放new的空间,或者用delete来释放malloc的空间。虽然在内置类型中,可能不会出问题,但是,这样使用是不规范也不正确的。这就好比吃面要用筷子,吃炸鸡要用手套,吃炒饭用勺子。你用free去释放new的内存,就好比用勺子去吃炸鸡,这样是不合适的。在某些场景下,你用free去释放new的内存就会好比用手套去吃火锅,那肯定是不行的。
4.1.new和delete操作自定义类型
struct ListNode
{
//c++写法
ListNode(int x)
:_next(nullptr)
, _val(x)
{}
int _val;
struct ListNode* _next;
};
//c语言写法
struct ListNode* BuyNode(int x)
{
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->_next = NULL;
newnode->_val = x;
return newnode;
}
int main()
{
struct ListNode* n1 = BuyNode(1);
struct ListNode* n2 = BuyNode(2);
struct ListNode* n3 = BuyNode(3);
free(n1);
free(n2);
free(n3);
//struct升级成了类
ListNode* nn1 = new ListNode(1);
ListNode* nn2 = new ListNode(2);
ListNode* nn3 = new ListNode(3);
delete nn1;
delete nn2;
delete nn3;
return 0;
}
new可以去调用自定义类型的构造函数来初始化对象,这样写比c语言的写法香多了。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A) * 4);
A* p2 = new A[4]{1,2,3,4};
free(p1);
delete[] p2;
return 0;
}
从上面样例可以看到,new自定义类型对象的时候,会去调用它的构造函数。当然这里是以整型值初始化自定义类型,会产生隐式类型转换,因为是在同一行编译器自动进行了优化,所以没有调用拷贝构造函数。而malloc只是单纯地开空间。delete自定义类型对象时,会去调用他的析构函数,而free只是单纯释放空间。
4.1.c++和c语言在开辟动态内存时失败处理细节
在C语言中,当使用malloc()函数开辟动态内存时,如果内存不足或者没有足够的连续空间,函数将返回NULL指针,表示内存分配失败。而在C++中,使用new操作符开辟动态内存时,如果内存不足或者没有足够的连续空间,将抛出一个std::bad_alloc异常,表示内存分配失败。
//C语言
int main()
{
int* p1 = (int*)malloc(sizeof(int) * 1024);
if (p1 == NULL)
{
perror("malloc fail");
exit(-1);
}
return 0;
}
//C++
int main()
{
int* p1 = nullptr;
try
{
do
{
p1 = new int[1024];
cout << p1 << endl;
} while (p1);
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
在面向对象的编程语言的编程中,try-catch是一种异常处理机制。在try块中,我们编写可能引发异常的代码。如果在try块中的代码引发了异常,程序会立即跳转到与其对应的catch块。catch块定义了异常处理程序,它会处理try块中引发的异常。这里稍微了解即可,具体细节得等到后面学习后再和大家介绍了。
5.new和delete的原理
5.1.operator new与operator delete函数的介绍
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
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 new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
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 delete:通过调用free来释放空间,而且operator delete和宏函数free的底层都是调用_free_dbg来进行释放空间的
5.2.operator new、new、operator delete和delete函数的底层实现
首先,通过下面代码的汇编代码来看看operator new、new、operator delete和delete函数究竟是怎么样的吧。
class A
{
public:
A(int a = 0,int b = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)operator new(sizeof(A));
A* p2 = new A(1,1);
operator delete(p1);
delete p2;
return 0;
}
通过查看汇编代码我们可以看到,new的会先调用operator new开辟空间,然后在调用构造函数初始化。delete会先调用析构函数进行清理,然后调用operator delete来释放动态内存。当然用new 自定义类型[N] 个对象,会先调用operator new开辟空间,然后在调用N次构造函数初始化。delete[] 会先调用N次析构函数,然后释放动态内存。
下面通过一个比较复杂的场景来看一下new和delete对于自定义类的的处理
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
cout << "Stack(size_t capacity = 3)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack* p1 = new Stack;
delete p1;
return 0;
}
6.定位new
定位new是new关键字的另一种用法,用于给已经分配好的堆区空间进行调用构造函数来初始化对象。
6.1.代码样例演示
class A
{
public:
A(int a = 0,int b = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
p1->~A();//显示调用析构函数
free(p1);
return 0;
}
6.2.定位new的应用场景
在一些需要频繁申请堆区内存的程序中,通常需要提前开辟一个内存池,用于提高获取堆区内存的效率。而定位new就能将申请的内存通过调用构造函数来进行初始化。在后面学习的STL的链表中就能遇到这一场景。
7. malloc/free和new/delete的区别
1、malloc/free是库函数。new和delete是操作符。
2、对于内置类型来说malloc/free和new/delete的区别不是很大,对于自定义类型new/delete分别会去调用构造函数/析构函数。来进行初始化和清理工作。
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
4. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。
8.内存泄漏的概念
8.1.什么是内存泄漏
内存泄漏指的是在程序运行过程中,程序分配了一段内存空间,但在使用完这段内存空间后,没有及时释放掉这段内存,导致这段内存不能被再次使用,从而造成了内存空间的浪费。如果程序中存在内存泄漏问题,并且这种泄漏的情况不断累积,最终可能会导致程序所能使用的内存空间越来越小,甚至导致程序崩溃。内存泄漏是一种常见的编程错误,需要开发人员注意及时释放不再使用的内存空间,避免内存泄漏问题的出现。下面我通过一个样例来看看内存。
int main()
{
char* p1 = (char*)malloc(1024 * 1024 * 1024);
cout << p1 << endl;
return 0;
}
8.2.内存泄漏的危害
对于客户端,内存泄漏的危害比较的小。对于服务端,内存泄漏的危害极大。因为想游戏服务、电商服务等服务端中,内存泄漏会导致服务程序宕机,进而导致软件业务的事故。所以我们程序员在操作动态内存时,一定要注意在哪里申请了就在对应的位置释放。内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。