赞
踩
1.静态内存,栈内存,堆内存
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
栈内存用来保存定义在函数内的非static对象。
分配在静态或栈内存中的对象由编译器自动创建和销毁。
对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆。
程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。
c++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。
相对于智能指针,使用这两个运算符管理内存非常容易出错,随着我们逐步详细介绍这两个运算符,这一点会更为清楚。
而且,自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义。
因此,使用智能指针的程序更容易编写和调试。
在自由空间分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针:
int*pi = new int; // pi指向一个动态分配的、未初始化的无名对象
此new表达式在自由空间构造一个int型对象,并返回指向该对象的指针。
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
- string*ps = new string;// 初始化为空 string
- int*pi = new int; // pi指向一个未初始化的int
我们可以使用直接初始化方式来初始化一个动态分配的对象。
我们可以使用传统的构造方式(使用圆括号),在新标准下,也可以使用列表初始化(使用花括号):
- int *pi = new int(1024); // pi指向的对象的值为1024
- string *ps = new string(10,'9'); //*ps 为"9999999999"
- // vector有10个元素,值依次从0到9
- vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可;
-
- string *psl new string; //默认初始化为空string
- string *ps = new string();// 值初始化为空 string
- int *pil =new int;// 默认初始化;*pil的值未定义
- int *pi2 = new int(); // 值初始化为 0;*pi2为0
对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的;不管采用什么形式,对象都会过就认构造函数来初始化.
但对于内置类型,两种形式的差别就很大了;值初始化的内置类型对象有着良好定义的值而默认初始化的对象的值则是未定义的。
类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。
出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。
如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型。
但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:
- auto pi = new auto(obj); // p指向一个与obj类型相同的对象
- //该对象用obi进行初始化
- auto p2 = new auto{a, b, c}; //错误:括号中只能有单个初始化器
p1的类型是一个指针,指向从obj自动推断出的类型。
若obj是一个int,那么p1就是int*;若obj是一个string,那么pl是一个string*;依此类推。新分配的对象用obj的值进行初始化。
用new分配const对象是合法的
- // 分配并初始化一个 const int
- const int *pci new const int (1024);
- // 分配并默认初始化一个 const 的空 string
- const string *pcs = new const string;
类似其他任何const对象,一个动态分配的const对象必须进行初始化。
对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。
由于分配的对象是const的,new 返回的指针是一个指向const的指针。
虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。
一旦一个程序用光了它所有可用的内存,new表达式就会失败。
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。
我们可以改变使用new的方式来阻止它抛出异常:
- //如果分配失败,new返回一个空指针
- int *pl = new int;// 如果分配失败,new抛出std::bad_alloc
- int *p2 = new (nothrow) int; //如果分配失败,new返回一个空指针
我们称这种形式的new为定位new。定位new表达式允许我们向new传递额外的参数。
在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将 nothrow 传递给 new,我们的意图是告诉它不能抛出异常,如果这种形式的new 不能分配所需内存,它会返回一个空指t bad_alloc和nothrow都定义在头文件 new中.
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统,我们通过delete来将动态内存归还给系统,delete表达式接受一个指针,指表达式 向我们想要释放的对象:
delete p; // p必须指向一个动态分配的对象或是一个空指针
与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存
指针值和delete
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。
释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的:
- int i,*pil = &i,*pi2 = nullptr;
- double *pd = new double(33),*pd2 = pd;
- delete i; // 错误:i不是一个指针
- delete pil; // 未定义:pil指向一个局部变量
- delete pd; //正确
- delete pd2;//未定义:pd2 指向的内存已经被释放了
- delete pi2;// 正确:释放一个空指针总是没有错误的
对于delete i的请求,编译器会生成一个错误信息,因为它知道i不是一个指针。
执行delete pil和pd2所产生的错误则更具潜在危害:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放了。
对于这些delete表达式,大多数编译器会编译通过,尽管它们是错误的。
虽然一个const对象的值不能被改变,但它本身是可以被销毁的。如同任何其他动态对象一样,想要释放一个const动态对象,只要delete指向它的指针即可:
- const int *pci = new const int(1024);
- delete pci; // 正确:释放一个const对象
由shared_ptr管理的内存在最后一个shared_ptr销毁时会被自动释放。
但对于通过内置指针类型来管理的内存,就不是这样了。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:
- // factory返回一个指针,指向一个动态分配的对象
- Foo factory(T arg)
- {
- //视情况处理arg
- return new Foo(arg);//调用者负责释放此内存
- }
要是下面这种情况,就会造成内存泄漏
- #include<iostream>
- using namespace std;
- void A()
- {
- int*a=new int;
- }
- int main()
- {
- A();
- }
与类类型不同,内置类型的对象被销毁时什么也不会发生。特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指问的是动态内存,那么内存将不会被自动释放。
由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在
在本例中,a是指向动态内存分配的唯一指针。一旦A()执行完毕,我们就再也无法释放这片内存了,为此,我们提供了两种处理方法
- include<iostream>
- using namespace std;
- int* A()
- {
- int*a=new int;
- return a;
- }
- int main()
- {
- int* b=A();//由b来负责释放
- }
- include<iostream>
- using namespace std;
- void A()
- {
- int*a=new int;
- delete a;
- }
- int main()
- {
- A();
- }
小心:动态内存的管理非常容易出错
使用new和delete管理动态内存存在三个常见问题:
- 忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。
相对于查找和修正这些错误来说,制造出这些错误要简单得多。
坚持只使用智能指针,就可以避免所有这些问题。对于一块内存,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。
当我们delete一个指针后,指针值就变为无效了。
虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。
有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉之后,就没有机会继续使用指针了。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
动态内存的一个基本问题是可能有多个指针指向相同的内存。
在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。
例如:
- int *p(new int(42));// p指向动态内存
- auto q=p; //p和q指向相同的内存
- //p和q均变为无效
- delete p;
- P =nullptr; //指出p不再绑定到任何对象
本例中p和q指向相同的动态分配的对象。我们delete此内存,然后将p置为nullptr,指出它不再指向任何对象。
但是,重置p对q没有任何作用,在我们释放p所指向的(同时也是q所指向的!)内存时,q也变为无效了。在实际系统中,查找指向相同内存的所有指针是异常困难的。
为了让 new 分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。
在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针:
- //调用get_size确定分配多少个int
- int *pia = new int[get _size()];// pia指向第一个int
方括号中的大小必须是整型,但不必是常量。
也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式中就不需要方括号了:
- typedef int arrT[42]; // arrT表示42个int的数组类型
- int *p= new arrl; // 分配一个42个int的数组;p指向第一个int
在本例中,new分配一个int数组,并返回指向第一个int的指针。即使这段代码中没有方括号,编译器执行这个表达式时还是会用new[]。
即,编译器执行如下形式:
int *p = new int[42];
分配一个数组会得到一个元素类型的指针
虽然我们通常称new T[ ] 里分配的内存为“动态数组”,但这种叫法某种程度上有些误导。
当用new 分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。
在上例中,我们正在分配一个数组的事实甚至都是不可见的——连[num]都没有。new返回的是一个元素类型的指针。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度(回忆一下,维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。
要记住我们所说的动态数组并不是数组类型,这是很重要的。
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。
可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。
- int*pia new int[10]; //10个未初始化的int
- int*pia2 = new int[10](); //10个值初始化为0的int
- string*psa = new string[10]; //10个空string
- string*tpsa2=new string[10]();// 10个空string
在新标准中,我们还可以提供一个元素初始化器的花括号列表:
- // 10 个 int分别用列表中对应的初始化器初始化
- int *pia3 = new int [10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- // 10个string,前4个用给定的初始化器初始化,剩余的进行值初始化
- string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};
与内置数组对象的列表初始化一样,初始化器会用来初始化动态数组中开始部分的元素。
如果初始化器数目小于元素数目,剩余元素将进行值初始化如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。
在本例中new会抛出一个类型为bad_array_new_length的异常。类似bad al1oc,此类型定义在头文件new中。
虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。
可以用任意表达式来确定要分配的对象的数目:
- size_t n = get_size(); // get_size返回需要的元素的数目
- int*p = new int[n]; //分配数组保存元素
- for (int* q = p; q !=p + n; ++q)
- /*处理数组*/
这产生了一个有意思的问题:
如果get_size返回0,会发生什么?答案是代码仍能正常
虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new(n]工作。是合法的:
- char arr[0]; //错误:不能定义长度为0的数组
- char* cp =new char[0];// 正确:但cp不能解引用
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。
此指针保证new 返回的其他任何指针都不相同。
对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。
但此指针不能解引用——毕竟它不指向任何元素。
在我们假想的循环中,若get_size返回0,则n也是0,new会分配0个对象。for循环中的条件会失败(p等于q+n,因为n为0)。因此,循环体不会被执行。
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:
- delete p; // p必须指向一个动态分配的对象或为空
- delete [] pa; // pa必须指向一个动态分配的数组或为空
第二条语句销毁pa指向的数组中的元素,并释放对应的内存。
数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。
当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。
如果我们在delete一个指向数组的指针时忽略了方括号或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。
回忆一下,当我们使用一个类型别名来定义一个数组类型时,在 new 表达式中不使用。即使是这样,在释放一个数组指针时也必须使用方括号:
- typedef int arrT[42]; //arrT是42个int的数组的类型别名
- int *p = new arrT; // 分配一个42个int的数组;p指向第一个元素
- delete [] pi ;//方括号是必需的,因为我们当初分配的是一个数组
不管外表如何,p指向一个对象数组的首元素,而不是一个类型为arrT的单一对象。因此,在释放p时我们必须使用[]。
如果我们在delete一个数组指针时忘记了方括号,或者在delete一个单一对象的指针时使用了方括号,编译器很可能不会给出警告。我们的程序可能在执行过程中在没有任何警告的情况下行为异常。
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的 全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局 函数来释放空间。
- /*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);
- }
-
- /*
- 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);
- __FINALLY
- _munlock(_HEAP_LOCK); /* release other threads */
- __END_TRY_FINALLY
- return;
- }
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间 成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异 常。operator delete 最终是通过free来释放空间的。
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和 释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常, malloc会返回NULL。
new的原理
delete的原理
new T[N]的原理
delete[]的原理
6.1 malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
- 1. malloc和free是函数,new和delete是操作符
- 2. malloc申请的空间不会初始化,new可以初始化
- 3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可, 如果是多个 对象,[]中指定对象个数即可
- 4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- 5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间 后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
- #include<iostream>
- using namespace std;
-
- class A
- {
- public:
- A(int a = 0)
- : _a(a)
- {
- cout << "A():" << this << endl;
- }
- ~A()
- {
- cout << "~A():" << this << endl;
- }
-
- private:
- int _a;
- };
-
- int main()
- {
- // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数
-
- A* p1 = (A*)malloc(sizeof(A));
- A* p2 = new A(1);
-
- free(p1);
- delete p2;
-
- // 内置类型是几乎是一样的
- int* p3 = (int*)malloc(sizeof(int)); // C
- int* p4 = new int;
-
- free(p3);
- delete p4;
-
- A* p5 = (A*)malloc(sizeof(A) * 10);
- A* p6 = new A[10];
-
- free(p5);
- delete[] p6;
-
- return 0;
- }
注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。
内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而 造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会 导致响应越来越慢,最终卡死。
- void MemoryLeaks()
- {
- // 1.内存申请了忘记释放
- int* p1 = (int*)malloc(sizeof(int));
-
- int* p2 = new int; // 2.异常安全问题
-
- int* p3 = new int[10];
-
- delete[] p3; } Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存, 用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那 么以后这部分空间将无法再被使用,就会产生Heap Leak。
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。
在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks() 函数进行简单检测,该函数只报出 了大概泄漏了多少个字节,没有其他更准确的位置信息。
- int main()
- {
- int* p = new int[10];
-
- // 将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
- _CrtDumpMemoryLeaks();
- return 0;
- }
-
-
- // 程序退出后,在输出窗口中可以检测到泄漏了多少字节,但是没有具体的位置
- Detected memory leaks!
- Dumping objects ->
- {79} normal block at 0x00EC5FB8, 40 bytes long.
- Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
- Object dump complete.
因此写代码时一定要小心,尤其是动态内存操作时,一定要记着释放。
但有些情况下总是防不胜防,简单的 可以采用上述方式快速定位下。
如果工程比较大,内存泄漏位置比较多,不太好查时一般都是借助第三方内 存泄漏检测工具处理的。
在linux下内存泄漏检测:linux下几款内存泄漏检测工具http://t.csdnimg.cn/mUr1V
在windows下使用第三方工具:VLD工具说明http://t.csdnimg.cn/3E5yU
其他工具:内存泄漏工具比较内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)
ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下: 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工 具
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。