标签:
01 - malloc内存分配,new和malloc区别
02 - 哈希表
03 - 归并排序的原理及时间复杂度
04 - 进程通信有哪几种方式?每种方式的特点是什么?读写者问题的进程通信方式是怎样的?
05 - 类对象内存分布
——————————————————————————————————————
?
考点 - 语言 |
? sizeof/union。sizeof在计算变量所占空间大小时采取的机制。
? const用法
? 在32位操作系统中的字对齐方式
? 继承和派生类
? 内联函数
? 引用和多态的区别?
? 面向对象的三个特征,分别有什么作用?
? 虚函数的实现机制
? 结构体struct和联合体union的区别。
? 重载和覆盖的区别是什么?
? C++和Java的区别,JVM是什么,具体用来做什么?
? 简单介绍一下Java中的集合框架(有哪些类构成和包括哪些接口)
? 如果是自己为一个类写一个sizeof函数,应该考虑哪些问题
? 虚函数和虚继承对于一个类求sizeof的影响有什么差别
? JAVA:collection有几种类型。set有什么特点。
? C语言:内存泄露是怎么导致的。有什么工具可以检测是否有内存泄露。
? 什么是弱引用?
? Activity怎么解释给一个不知道的人听。
C++内存分配原理。
C++中所有的强制转换运算符(const_cast static_cast等等)
C++ I/O流操作。
Java中classpath的概念
C++与Java区别(IT面试) |
http://blog.csdn.net/hustcqb/article/details/11771679
这是Java与C++区别的一个比较完整的答案,大家可以学习一下。
JAVA和C++都是面向对象语言。也就是说,它们都能够实现面向对象思想(封装,继乘,多态)。而由于c++为了照顾大量的C语言使用者,而兼容了C,使得自身仅仅成为了带类的C语言,多多少少影响了其面向对象的彻底性!JAVA则是完全的面向对象语言,它句法更清晰,规模更小,更易学。它是在对多种程序设计语言进行了深入细致研究的基础上,据弃了其他语言的不足之处,从根本上解决了c++的固有缺陷。
Java和c++的相似之处多于不同之处,但两种语言问几处主要的不同使得Java更容易学习,并且编程环境更为简单。我在这里不能完全列出不同之处,仅列出比较显著的区别:
1.指针JAVA语言让编程者无法找到指针来直接访问内存无指针,并且增添了自动的内存管理功能,从而有效地防止了c/c++语言中指针操作失误,如野指针所造成的系统崩溃。但也不是说JAVA没有指针,虚拟机内部还是使用了指针,只是外人不得使用而已。这有利于Java程序的安全。
2.多重继承c++支持多重继承,这是c++的一个特征,它允许多父类派生一个类。尽管多重继承功能很强,但使用复杂,而且会引起许多麻烦,编译程序实现它也很不容易。Java不支持多重继承,但允许一个类继承多个接口(extends+implement),实现了c++多重继承的功能,又避免了c++中的多重继承实现方式带来的诸多不便。
3.数据类型及类Java是完全面向对象的语言,所有函数和变量部必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。而c++允许将函数和变量定义为全局的。此外,Java中取消了c/c++中的结构和联合,消除了不必要的麻烦。
4.自动内存管理Java程序中所有的对象都是用new操作符建立在内存堆栈上,这个操作符类似于c++的new操作符。下面的语句由一个建立了一个类Read的对象,然后调用该对象的work方法:
Read r=new Read();
r.work();
语句Read r=new Read();在堆栈结构上建立了一个Read的实例。Java自动进行无用内存回收操作,不需要程序员进行删除。而c++中必须由程序员释放内存资源,增加了程序设计者的负担。Java中当一个对象不被再用到时,无用内存回收器将给它加上标签以示删除。JAVA里无用内存回收程序是以线程方式在后台运行的,利用空闲时间工作。
5.操作符重载Java不支持操作符重载。操作符重载被认为是c++的突出特征,在Java中虽然类大体上可以实现这样的功能,但操作符重载的方便性仍然丢失了不少。Java语言不支持操作符重载是为了保持Java语言尽可能简单。
6.预处理功能Java不支持预处理功能。c/c++在编译过程中都有一个预编泽阶段,即众所周知的预处理器。预处理器为开发人员提供了方便,但增加了编译的复杂性。JAVA虚拟机没有预处理器,但它提供的引入语句(import)与c++预处理器的功能类似。
7. Java不支持缺省函数参数,而c++支持在c中,代码组织在函数中,函数可以访问程序的全局变量。c++增加了类,提供了类算法,该算法是与类相连的函数,c++类方法与Java类方法十分相似,然而,由于c++仍然支持c,所以不能阻止c++开发人员使用函数,结果函数和方法混合使用使得程序比较混乱。
Java没有函数,作为一个比c++更纯的面向对象的语言,Java强迫开发人员把所有例行程序包括在类中,事实上,用方法实现例行程序可激励开发人员更好地组织编码。
8 字符串c和c++不支持字符串变量,在c和c++程序中使用Null终止符代表字符串的结束,在Java中字符串是用类对象(string和stringBuffer)来实现的,这些类对象是Java语言的核心,用类对象实现字符串有以下几个优点:
(1)在整个系统中建立字符串和访问字符串元素的方法是一致的;
(2)Java字符串类是作为Java语言的一部分定义的,而不是作为外加的延伸部分;
(3)Java字符串执行运行时检查,可帮助排除一些运行时发生的错误;
(4)可对字符串用“+”进行连接操作。
9 goto语句“可怕”的goto语句是c和c++的“遗物”,它是该语言技术上的合法部分,引用goto语句引起了程序结构的混乱,不易理解,goto语句子要用于无条件转移子程序和多结构分支技术。鉴于以广理由,Java不提供goto语句,它虽然指定goto作为关键字,但不支持它的使用,使程序简洁易读。
l0.类型转换在c和c++中有时出现数据类型的隐含转换,这就涉及了自动强制类型转换问题。例如,在c++中可将一浮点值赋予整型变量,并去掉其尾数。Java不支持c++中的自动强制类型转换,如果需要,必须由程序显式进行强制类型转换。
11.异常JAVA中的异常机制用于捕获例外事件,增强系统容错能力
try{
//可能产生意外的代码
}catch(exceptionType name){
//处理
}
其中exceptionType表示异常类型。而C++则没有如此方便的机制。
C++ - 不可以被继承的类 |
#include<iostream>
using namespace std;
template <typename T> class MakeFinal
{
friend T;
private:
MakeFinal(){cout<<"MakeFinal的构造函数"<<endl;}
~MakeFinal(){}
};
class FinalClass2 : virtual publicMakeFinal<FinalClass2> //重点是这里的虚继承
{
public:
FinalClass2(){cout<<"FinalClass2的构造函数"<<endl;}
~FinalClass2(){}
};
class Try : public FinalClass2
{
public:
Try() {}
~Try() {}
};
int main()
{
Try temp;
system("pause");
return 0;
}
/*
由于类FinalClass2是从类MakeFinal<FinalClass2>虚继承过来的,
在调用Try的构造函数的时候,会直接跳过FinalClass2而
直接调用MakeFinal<FinalClass2>的构造函数。
非常遗憾的是,Try不是MakeFinal<FinalClass2>的友元,因此不能调用其私有的构造函数。
基于上面的分析,试图从FinalClass2继承的类,一旦实例化,都会导致编译错误,
因此是FinalClass2不能被继承。这就满足了我们设计要求。
*/
用C++设计一个不能被继承的类:
类的构造函数和析构函数都是私有函数
定义静态来创建和释放类的实例
classFinalClass1
{
public :
staticFinalClass1* GetInstance()
{
Return new FinalClass1;
}
Static void DeleteInstance(FinalClass1* pInstance)
{
deletepInstance;
pInstance = 0;
}
private :
FinalClass1() {}
~FinalClass1() {}
};
这个类是不能被继承,但在总觉得它和一般的类有些不一样,使用起来也有点不方便。比如,我们只能得到位于堆上的实例,而得不到位于栈上实例。
能不能实现一个和一般类除了不能被继承之外其他用法都一样的类呢?办法总是有的,不过需要一些技巧。请看如下代码:
///////////////////////////////////////////////////////////////////////
// Define a class which can‘t be derived from
///////////////////////////////////////////////////////////////////////
template<typename T> class MakeFinal
{
Friend T;
private :
MakeFinal() {}
~MakeFinal() {}
};
Class FinalClass2 : virtual public MakeFinal<FinalClass2>
{
public :
FinalClass2() {}
~FinalClass2() {}
};
这个类使用起来和一般的类没有区别,可以在栈上、也可以在堆上创建实例。
尽管类 MakeFinal<FinalClass2> 的构造函数和析构函数都是私有的,但由于类 FinalClass2 是它的友元函数,因此在FinalClass2 中调用 MakeFinal <FinalClass2> 的构造函数和析构函数都不会造成编译错误。
但当我们试图从 FinalClass2 继承一个类并创建它的实例时,却不同通过编译。
classTry : public FinalClass2
{
public :
Try() {}
~Try() {}
};
Trytemp;
由于类 FinalClass2 是从类 MakeFinal <FinalClass2> 虚继承过来的,在调用 Try 的构造函数的时候,会直接跳过 FinalClass2 而直接调用 MakeFinal<FinalClass2> 的构造函数。非常遗憾的是, Try 不是 MakeFinal<FinalClass2> 的友元,因此不能调用其私有的构造函数。
基于上面的分析,试图从 FinalClass2 继承的类,一旦实例化,都会导致编译错误,因此是 FinalClass2不能被继承。这就满足了我们设计要求
======================================================================
C++中的虚继承:或者叫做虚基类。
防止多重继承产生的二义性问题。
举例来说:假如类A和类B是由类X继承而来(非虚继承且假设类X包含一些成员),且类C同时继承了类A和B,那么C就会拥有两套和X相关的成员(可分别独立访问,一般要用适当的消歧义修饰符)。但是如果类A虚继承自类X,那么C将会只包含一组类X的成员数据。
在多重继承的时候,如果父类中有同名的成员变量(类似这篇文章中谈及的例子),为了防止二义性,一般要采用虚继承的方式,并且最右边的基类中的那个成员变量会出现在派生类对象中。
======================================================================
一个类不能被继承,也就是说它的子类不能构造父类,这样子类就没有办法实例化整个子类从而实现子类无法继承父类。我们可以将一个类的构造函数声明为私有,使得这个类的构造函数对子类不可见,那么这个类也就不能继承了。但是,这引出一个问题,客户程序岂不是也无法实例化这个类了?OK,让我们参考一下Singleton模式,用一个static函数来帮助创建这个类的实例,问题就解决了!
[cpp] view plaincopy
class CParent
{
private:
CParent(int v){m_v = v;}
~CParent(){}
int m_v;
static CParent * m_instance;
public:
void fun(){cout << "The value is: "<< m_v << endl;}
static CParent * getInstance(int v);
};
CParent * CParent::m_instance = NULL;
CParent * CParent::getInstance(int v)
{
m_instance = new CParent(v);
return m_instance;
}
这是一个有效的方法,但是static函数创建出来的实例必然是static的。而且,这个类不能像普通的类那样构建对象,比如:
[cpp] view plaincopy
CParent c; // impossible
换个思路考虑一下,友元不也是不能被继承的么?我们可以把类的构造函数定义为private的同时,定义友元函数来帮助构造类的实例。[cpp] view plaincopy
class CParent
{
private:
CParent(int v){m_v = v;}
~CParent();
int m_v;
public:
void fun(){cout << "The value is: "<< m_v << endl;}
friend CParent* getInstance(int v);
};
CParent * getInstance(int v)
{
CParent * pinstance = new CParent(v);
return pinstance;
}
这个类也是不能被继承的,但是出现的问题和前面一样:我们还是不能像对普通类那样对待这个类。
现在设想一下,有一个CParent类,我们不希望他能够被继承。在友元不能被继承的思路指引下,我们要考虑让CParent的友元属性不能被继承。假设有一个辅助类CNoHeritance,CParent是CNoHeritance类的友元。还要假设一个CChild类,它试图去继承CParent类(如果它有这个能耐的话)。
先把CNoHeritance类的构造函数定义成private,然后将CParent声明为CNoHeritance的友元类。同时CParent继承了CNoHeritance类。到目前为止,CNoHeritance除了CParent类以外,谁也无法对它进行访问和实例化。CChild因为无法继承CParent的友元特性,所以CChild无法对CNoHeritance直接进行实例化(但是可以通过CParent)。
到目前为止,CParent还是可以被继承的。这是一个trick.让我们整理一下思路,下面的图说明了CNoHeritance, CParent和CChild三个类之间的关系。
如果我们让CParent类虚继承CNoHeritance类,根据虚继承的特性,虚基类的构造函数由最终的子类负责构造。因此CChild如果要想继承CParent,它必须能够构造CNoHeritance,这是不可能的!因此,我们的CParent也就终于成为了一个无法继承的类。
[cpp] view plaincopy
class CNoHeritance
{
private:
CNoHeritance(){}
~CNoHeritance(){}
friend class CParent;
};
class CParent : virtual public CNoHeritance
{
public:
CParent(int v){m_v = v;}
~CParent(){};
private:
int m_v;
public:
void fun(){cout << "The value is: "<< m_v << endl;}
};
class CChild : public CParent
{
public:
CChild():CParent(10){}
~CChild(){}
};
需要注意的是,我们这里引入的CNoHeritance类对整个程序而言,只引入了Private的构造函数和析构函数,所以不会因为可能的菱形继承带来二义性.
内存管理之分段分页机制 |
分页概念:逻辑空间分页,物理空间分块(页框),页与块同样大,页连续块离散,用页号查页表,由硬件做转换,页面和内存块大小一般选为2的若干次幂(便于管理)。页表作用:实现从页号到物理地址的映射。操作系统需要为每个进程维护一个页表,页表给出了该进程的每一页对应的页框的位置。
简单分页类似于固定分区,只是采用分页技术的分区相当小,一个程序可以占据多个分区,而且这些分区不需要是连续的。而固定分区不一样,一个程序装入一个分区中。请求分页的基本思想1.请求分页=分页+请求2.请求分页提供虚拟存储器3.页表项中的状态位指示该页面是否在内存,若不在,则产生一个缺页中断页面置换:把一个页面从内存调换到磁盘的对换区中抖动:在具有虚存的计算机中,由于频繁的调页活动使访问磁盘的次数过多而引起的系统效率降低的一种现象常用的页面置换算法:先进先出法:(置换次数比较多)最佳置换法(OPT):选择将来不再使用或在最远的将来才被访问的页调换出去(不便于实现)最近最少使用置换法(LRU):当需要置换一页时,选择在最近一段时间里最久没有使用过的页面予以淘汰最近未使用置换法(NUR):是LRU算法的近似方法,选择在最近一段时间里未被访问过的页面予以淘汰
地址转换:若使用16位地址,页大小为1KB,即1024字节,相对地址为1502的二进制形式为000001 0111011110.由于页大小为1KB,偏移量为10位,剩下的6位为页号,因此一个程序最多有2^6=64页组成,每页大小1KB。
考虑一个n+m位的地址,最左边的n位是页号,最右边的m位是偏移量,设n=6,m=10,地址转换需要经过以下步骤
1,提取页号,即逻辑地址最左边的n位
2,以这个页号为索引,查找该进程页表中相应的页框号k
3,该页框的起始物理地址为k*2^m,该地址加上偏移量就是物理地址
逻辑地址为 000001 0111 0111 10,页号为1,偏移量为478,假设该页在内存页框6(000110)中,则物理地址页框号为6,偏移量为478,物理地址为
000110 0111 011110相对地址=1502 000001 0111 0111 10
用户进程2700字节
a分区
逻辑地址=页号1,偏移量=478 000001 0111 0111 10
478
页0 |
页1 |
页2 |
内部碎片 |
b分页页大小1kb
逻辑地址=段号1,偏移量752 00010010 1111 0000
段0 750字节 |
段1 1950字节 |
段式管理的基本思想是:把程序按内容或过程(函数)关系分成段,每个段有自己的名字(编号)。一个作业或进程的虚拟存储空间都对应于一个由段号(段号:段内偏移)构成的二维地址,编译程序在编译链接过程中就直接形成这样的二维地址形式。段式管理以段为单位分配内存,然后通过地址变换将段式虚拟地址转换成实际的内存物理地址。和页式管理一样,段式管理也采用只把那些经常访问的段驻留内存,而把那些将来一段时间不被访问的段放入外存,待需要时自动调入的方法实现虚拟存储器。段式管理把一个进程的虚拟地址空间设计成二维结构,即段号(段号:段内偏移)的形式。前面己经谈到,与页式管理编译程序产生一维连续地址不同,段式管理系统中的编译程序编译形成多个段及段的名字或编号,各个段号之间无顺序关系。与页式管理页长度相同不一样,段的长度是不同的,每个段定义一组逻辑上完整的程序或数据。例如,在DOS操作系统中,一个程序内部被分为了正文段、数据段、堆栈段等。每个段是一个首地址为O并连续的一维线性空间。
由于使用大小不等的段,分段类似于动态分区。在没有采用覆盖技术和使用虚拟内存的情况下,为执行一个程序,需要把它所有的段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,它会产生外部碎片。
进行地址转换:考虑一个n+m位的地址,左边的n位是段号,右边的m位是偏移量。n=4;m=12;因此最大段长度为2^12=4096
1,提取段号,即逻辑地址最左边的n位
2,以这个段号为索引,查找该进程段表中该段的起始物理地址
3,最右边m位代表偏移量,偏移量和段长度进行比较,如果偏移量大于该段的长度,则该地址无效
4,物理地址为该段的起始物理地址与偏移量的和。
假设逻辑地址为 0001 0010 1111 0000 段号为1,偏移量为752,假设该段的起始武力地址为 0010 0000 0010 0000 则相应的物理地址为:
+ 0000 0010 1111 0000
= 0000 0011 0001 0000
总之,采用简单分段技术,进程被划分为许多段,段的大小不需要相等,当一个进程需要调入内存时,,它的所有段装入内存的可用区域,并建立一张段表。
虚函数 sizeof内存占用 |
C++中虚函数工作原理和(虚)继承类的内存占用大小计算
非虚函数,不占类的内存。
没有任何函数和成员的类,为空,但是占有1个字节。
enum类型声明不占内存。 enum变量占int的字节数。
class C
{
//char ch1;
//char ch2;
virtual void func() { }
virtual void func1() { }
virtual void func4() { }
virtual void func6() { }
};
Sizeof(C)=4.
首先,存在虚函数的类都有一个一维的虚函数表叫虚表,虚表里存放的就是虚函数的地址了,因此,虚表是属于类的。这样的类对象的前四个字节是一个指向虚表的指针。
二、(虚)继承类的内存占用大小
首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。
计算一个类对象的大小时的规律:
1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
3、因此一个对象的大小≥所有非静态成员大小的总和;
4、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;
5、虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);
6、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;
7、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。
示例一:含有普通继承
class A { }; //1 空类
class B { char ch; virtualvoid func0() { } //8 因为有虚函数,有一个指针指向虚表:4. 对齐到8.};
class C { char ch1; char ch2; virtual voidfunc() { } virtual void func1() { } //8 因为有虚函数,有一个指针指向虚表:4. 对齐到8.};
class D: public A, public C{ intd; virtual void func() { } virtual voidfunc1() { } //4 (vptr C的虚函数地址) + 4(ch1 ch2) + 4 (int d)};
class E: public B, public C{ inte; virtual void func0() { } virtual voidfunc1() { }
4(B的虚函数地址),4(B中的数据成员),4(C的虚函数地址),4(C中的数据成员),4(E中的数据成员e),同样注意内存对齐,这样4+4+4+4+4=20。
就算有自己其他的虚函数,也不会增加指针了。};
int main(void){cout<<"A="<<sizeof(A)<<endl; //result=1 cout<<"B="<<sizeof(B)<<endl; //result=8 cout<<"C="<<sizeof(C)<<endl; //result=8cout<<"D="<<sizeof(D)<<endl; //result=12 cout<<"E="<<sizeof(E)<<endl; //result=20
//注意:sizeof后是实例,对象,所以前4个字节都是指向虚表的。
return 0;}
前面三个A、B、C类的内存占用空间大小就不需要解释了,注意一下内存对齐就可以理解了。
求sizeof(D)的时候,需要明白,首先VPTR指向的虚函数表中保存的是类D中的两个虚函数的地址(指向虚表,4字节),然后存放基类C中的两个数据成员ch1、ch2,注意内存对齐,然后存放数据成员d,这样4+4+4=12。
求sizeof(E)的时候,首先是类B的虚函数地址,然后类B中的数据成员,再然后是类C的虚函数地址,然后类C中的数据成员,最后是类E中的数据成员e,同样注意内存对齐,这样4+4+4+4+4=20。
示例二:含有虚继承
class CommonBase class Base1: virtual public CommonBase class Base2: virtual public CommonBase class Derived: public Base1, public Base2 |
sizeof(Derived)=32,其在内存中分布的情况如下:
class Derived size(32): |
示例3:
class A class B: virtual public A int main(void) |
执行结果:A‘s size is 8
B‘s size is 16
说明:对于虚继承,类B因为有自己的虚函数,所以它本身有一个虚指针,指向自己的虚表。另外,类B虚继承类A时,首先要通过加入一个虚指针来指向父类A,然后还要包含父类A的所有内容。因此是4+4+8=16。
const的用法:如何解除const限制 |
const B b1;
B* b2 = const_cast<B*> (&b1);
常量指针被转化为非常量指针,并且仍然指向原来的对象。
宏定义 |
#define exchange(a,b) {a=a^b;b=a^b;a=a^b;}
注意点:加括号(不能使用大于,小于,if语句)
一个int整形变量,最高位是正负位,只要知道两者差值最高位是正还是负,差是零还是非零就能知道两个数的大小。
1. #define MAX(a,b) ( fabs(((a)-(b))) == ((a)-(b)) ) ? (a) : (b) )
2. #define MAX(a,b) ( ((a)-(b))&0x80000000 ? (b):(a))
3. #define max(a,b)((((a)-(b))&(1<<31))?(b):(a))
《new与malloc的区别》 |
free和delete如何知道应该释放多少内存
malloc的时候记录了分配的内存大小(作为辅助信息)
辅助信息存放在分配的内存块的前面。
对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。
——《堆与栈的区别》
内部有保存一个类型信息和长度信息,delete只检查类型信息,delete[]会检查类型信息和长度,这样的话内存长度是当delete时sizeof (type);delete[] 时sizeof (type) *len,你可以用_msize来看new出来内存的大小,包括new[]也行。
相同点:都可用于申请动态内存和释放内存
不同点:
(1)操作对象有所不同。
malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。
(2)用法上也有所不同。
函数malloc 的原型如下:
void * malloc(size_t size);
用malloc 申请一块长度为length 的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
1、malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
2、 malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
函数free 的原型如下:
void free( void * memblock );
为什么free 函数不象malloc 函数那样复杂呢?这是因为指针p 的类型以及它所指的内存的容量事先都是知道的(why? ),语句free(p)能正确地释放内存。如果p 是NULL 指针,那么free对p 无论操作多少次都不会出问题。如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。
new/delete 的使用要点:
运算符new 使用起来要比函数malloc 简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如
Obj *objects = newObj[100]; // 创建100 个动态对象
不能写成
Obj *objects = newObj[100](1); // 创建100 个动态对象的同时赋初值1
在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如
delete []objects; // 正确的用法
delete objects; // 错误的用法
后者相当于delete objects[0],漏掉了另外99 个对象。
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1、new自动计算需要分配的空间,而malloc需要手工计算字节数
2、new是类型安全的,而malloc不是,比如:
int* p = new float[2]; // 编译时指出错误
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
new operator 由两步构成,分别是 operator new 和 construct
3、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
4、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
5、malloc/free要库文件支持,new/delete则不要。
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1、本质区别
malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符。
对于用户自定义的对象而言,用maloc/free无法满足动态管理对象的要求。
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
[cpp] view plaincopy
class Obj
{
public:
Obj( )
{ cout << "Initialization" << endl; }
~ Obj( )
{ cout << "Destroy" << endl; }
void Initialize( )
{ cout << "Initialization" << endl; }
void Destroy( )
{ cout << "Destroy" << endl; }
}obj;
void UseMallocFree( )
{
Obj * a = (Obj *) malloc( sizeof ( obj ) ); // allocate memory
a ->Initialize(); // initialization
// …
a ->Destroy(); // deconstruction
free(a); // release memory
}
void UseNewDelete( void )
{
Obj * a = new Obj;
// …
delete a;
}
类Obj的函数Initialize实现了构造函数的功能,函数Destroy实现了析构函数的功能。函数UseMallocFree中,由于malloc/free不能执行构造函数与析构函数,必须调用成员函数Initialize和Destroy来完成“构造”与“析构”。所以我们不要用malloc/free来完成动态对象的内存管理,应该用new/delete。
由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
2、联系
既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete、malloc/free必须配对使用。
new是C++里的运算符,而malloc是c里面的函数。c++之所以要引入new关键字是因为malloc是封装好的库函数,无法修改内部结构。但是,在C++里,如果不是内部数据类型,在绝大多数情况下分配内存的时候是要调用构造函数,释放内存的时候要调用析构函数的(注意并非所有的类都会调用,一些极其简单的类是没有构造函数和析构函数的,分配方式和C完全一致)。由于malloc无法实现,因此C++里增加了new运算符。可以这么理解:new=malloc+构造函数。delete=free+析构函数。而且new和delete还可以申请数组和释放数组,如new int[10],delete[]等。
free和delete如何知道应该释放多少内存
答:malloc和new在分配内存的时候会在内存块前添加一个头部,通常是四字节(4G)或八字节(64位的,多少G就自己算吧),然后在free p或deletep的时候找到p前面四字节或八字节大小就知道应该释放多少内存了。
C++内存分配原理 - 内存泄露 |
堆与栈的区别
具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因。
和栈不同,堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free 函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:
1. 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。
2. 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。
3. 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。
堆和栈的对比
从以上知识可知,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活;而栈是函数库提供的功能,特点是灵活方便,数据适应面广泛,但是效率有一定降低。栈是系统数据结构,对于进程/线程是唯一的;堆是函数库内部数据结构,不一定唯一。不同堆分配的内存无法互相操作。栈空间分静态分配和动态分配两种。静态分配是编译器完成的,比如自动变量(auto)的分配。动态分配由alloca函数完成。栈的动态分配无需释放(是自动的),也就没有释放函数。为可移植的程序起见,栈的动态分配操作是不被鼓励的!堆空间的分配总是动态的,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/ 释放内存匹配是良好程序的基本要素。
1.碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以>参考数据结构,这里我们就不再一一讨论了。
2.生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
3.分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
4.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
明确区分堆与栈:
在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
首先,我们举一个例子:
void f()
{
int* p=new int[5];
}
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operatornew (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
打开工程,依次操作菜单如下:Project->Setting->Link,在Category 中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
另外对存取效率的比较:
代码:
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
void main()
{
char a = 1;
char c[] = "1234567890";
char *p ="1234567890";
a = c[1];
a = p[1];
return;
}
对应的汇编代码
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据edx读取字符,显然慢了.
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,编写稳定安全的代码才是最重要的
http://www.cnblogs.com/chenleiustc/archive/2011/04/08/2009994.html
内存分配原理
内存管理向来是C/C++程序设计的一块雷区,大家都不怎么愿意去碰她,但是有时不得不碰它。
虽然利用C++中的smartpointer已经可以完全避免使用指针,但是对于指针的进一步了解,有助于我们编写出更有效率的代码,也有助于我们读懂以前编写的程序。
五大内存分区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,在《const的思考》一文中,我给出了6种方法)
明确区分堆与栈
在bbs上,堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
首先,我们举一个例子:
void f() { int* p=new int[5]; }
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator
new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operatornew (00401060)
0040102F add esp,4
00401032 mov dwordptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dwordptr [ebp-4],eax
这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete
[]p,这是为了告诉编译器:我删除的是一个数组,VC6就会根据相应的Cookie信息去进行释放内存的工作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下几点:
1、管理方式不同;
2、空间大小不同;
3、能否产生碎片不同;
4、生长方向不同;
5、分配方式不同;
6、分配效率不同;
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
打开工程,依次操作菜单如下:Project->Setting->Link,在Category
中选中Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug可是相当困难的:)
对了,还有一件事,如果有人把堆栈合起来说,那它的意思是栈,可不是堆,呵呵,清楚了?
new/delete究竟做了些什么?
在理解这个问题之前,我们先看一下下面的这段程序,有这么一个程序段:
class A
{
public:
A() { cout<<"A is here!"<<endl; }
~A(){ cout<<"A is dead!"<<endl; }
private:
int i;
};
A* pA=new A;
delete pA;
在这个简单的程序段里面,new/delete究竟做了些什么?
实际上,这段程序里面隐含调用了一些我们没有看到的东西,那就是:
static void* operator new(size_t sz);
static void operator delete(void* p);
值得注意的是,这两个函数都是static的,所以如果我们重载了这2个函数(我们要么不重载,要重载就要2个一起行动),也应该声明为static的,如果我们没有声明,系统也会为我们自动加上。另外,这是两个内存分配原语,要么成功,要么没有分配任何内存。
size_t是什么东西呢?我在第一次看到这个动动的时候也是十分的困惑,毕竟以前没有见过。size_t在<cstddef>中定义,是一种无符号整数类型(不一定是int),用来保存对象的大小,这一用法是从C语言中借用过来的,现在你应该明白了吧(我学习的时候可是郁闷了好几天,没有人可以问,因为不知道有个csdn:)
new A;实际上做了2件事:调用opeator
new,在自由存储区分配一个sizeof(A)大小的内存空间;然后调用构造函数A(),在这块内存空间上类砖砌瓦,建造起我们的对象。同样对于delete,则做了相反的两件事:调用析构函数~A(),销毁对象,调用operator
delete,释放内存。不过需要注意的是,new分配一块内存的时候,并没有对这块内存空间做清零等任何动作,只是拿了过来,这块内存上放的仍然是原来的数据(垃圾数据),delete的时候,也只是释放这块内存,归还给操作系统,上面的数据还在上面,所以delete
pA之后,pA的值没变,他指向的那块内存的值也没有变,不过似乎有什么问题,我们看一下下面的这个程序段:
int *p=new int(50000);
cout<<*p<<" "<<p<<endl;
delete p;
cout<<*p<<" "<<p<<endl;
我们可以清楚地看到,指针p存放的数据仍然是原来的地址,但是*p的内容却发生了变化,在我的机器上(win2000,
VC6)始终是-572662307,不清楚这是为什么,难道系统做了什么手脚?还望高手指教。
在这里我们可以看到,new的工作实际上就是保证相互分离的存储分配和初始化工作能够很好的在一起工作,不过这里可能让初学者迷惑的是,我们定义了一个带有参数的new,但是我们用的时候却没有显式的去调用,而是让系统“神秘”的去提供这个参数。是的,这样做毫无疑问增加了复杂性,但是让基类获取了为一集派生类提供分配和释放服务的能力。
new/delete有什么好处和坏处?
从C程序员转换过来的C++程序员总是有个困惑:new/delete到底究竟和C语言里面的malloc/free比起来有什么优势?或者是一样的?
其实,就算我不说,你也应该很清楚了,new/delete当然比malloc/free要好,要不然,为什么还引进这个东东呢?其实通过上面的分析,我们看到了new/delete实际上做了很多malloc/free没有做的事情:malloc/free只是对内存进行分配和释放;new/delete还负责完成了创建和销毁对象的任务。
另外,new的安全性要高一些,因为他返回的就是一个所创建的对象的指针,对于malloc来说返回的则是void*,还要进行强制类型转换,显然这是一个危险的漏洞。
最后,我们可以对new/delete重载,使内存分配按照我们的意愿进行,这样更具有灵活性,malloc则不行。
不过,new/delete也并不是十分完美,大概最大的缺点就是效率低(针对的是缺省的分配器),原因不只是因为在自由存储区上分配(和栈上对比),具体的原因目前不是很清楚,不过在MCD上说了2个可能的原因:
1、new只是对于堆分配器(malloc/realloc/free)的一个浅层包装,没有针对小型的内存分配做优化。
2、缺省分配器具有通用性,它管理的是一块内存池,这样的管理往往需要消耗一些额外空间。
各种各样的new
一般来说,new有很多种形式,不过真的归纳起来,也就是2种:
1、最常用的形式:
void *operator new(std::size_t sz) throw(std::bad_alloc);
(普通的)
void *operator new[](std::size_t sz) throw(std::bad_alloc);
(数组的)
void *operator new(std::size_t sz);
void *operator new[](std::size_t sz)
这一种大家用得最为频繁,我就不举例子了。
2、放置new形式:
void *operator new(std::size_t count, void *ptr) throw();
(普通的)
void *operator new[](std::size_t count, void *ptr)throw();
(数组的)
要使用这种方式,必须包含头文件<new>。这个机制引入的初始目的是为了解决2个相关的问题:
1、把一个对象放在某个特定位置;
2、在某个特定分配区里面分配对象;
但是引入之后,发现这种机制远超出了简单的存储分配机制,我们可以给特定的存储位置关联任意的逻辑性值,这样一来,new就有了一种通用资源管理器的作用。同时第二个参数,也被扩展成了任意的可以识别的类型,并且配备了相应的nothrow版本:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
new能够返回NULL么?
我们经常看到有很多初学者喜欢写如下代码:
A*p=new A();
if(p==NUL) ....
写下这段代码的可能是受到了一些书上错误的影响,因为new
A()从来就不可能返回NULL,如果在这个过程中用完了内存,那么他就会抛出bad_alloc异常,绝对不会返回NULL,如果你想让他返回null,应该用new(nothrow)
A(),而不是new A()。不过从异常的观点来看,这实际上是一种倒退,我们应该尽量回避。
存储器的结构层次
我想大家都很清楚,在计算机的存储中,有各种各样的存储器,对他们的访问频率和访问方式直接影响到我们的程序效率,一般来说,可以分为5个等级:寄存器、一级缓存、二级缓存、主存、磁盘存储器。下面我们就把他们的特性大体的说一下:
1、寄存器,是所有存储器里面延迟时间最短、带宽最大、开销最少的,毫无疑问,这是目前速度最快的存储器,但是代价比较昂贵,所以寄存器的个数有限,我们的程序应该尽量充分利用寄存器,C/C++里面不是有一个关键字:register么?对于那些频繁使用,有可能成为性能瓶颈的变量放入寄存器,可能会在效率上有些提高(毕竟程序的瓶颈在一个变量的情况很少出现,一般都在算法上进行改进),但是对于个别的函数,性能可能会有数量级上的提升。不过,和inline一样的是,register也只是一个提示符,提示编译器不需要为变量分配内存,但是决定权在编译器,有的编译器(当然时说那些比较差的了)会完全忽略register指示符。
2、一级缓存,又称为芯片内缓存,速度也是非常的快,其延迟也与寄存器处在同一个数量级,但是容量也很有限。目前,很多CPU上面都有2级芯片内缓存,我们可以把他们看作是一个芯片内缓存,不影响我们的讨论。
根据1999年的数据,我们知道寄存器延迟2ns,一级缓存也是2ns,,看起来似乎2者具有相同的性能,实则不然。通常情况下,一个寄存器组可以在一个时钟周期内读2个操作数并且写一个操作数,一共处理3个操作数,对于一级缓存来说2个时钟周期才能够处理一个操作数,相比之下,寄存器组的带宽大约是一级缓存的6倍。当然,对于某些特殊的硬件可能不符合这个规律,但是无论如何,寄存器的带宽都要比一级缓存的大。
3、二级缓存,又称为芯片外缓存,速度比一级缓存要慢上1-2倍。
4、主存,也就是我们平常所说的内存,是一种半导体动态随机访问存储器,常见的有DRAM、SDRAM、RAMBUS等,就目前来说,内存的容量已经变得很大了,常见的是256M、512M。
计算机的主存访问有一个非常有规律的特性,就是人们所说的“6-1-1-1-2-1-1-1特性”,什么意思呢?就是说,最初访问内存需要6个总线周期,随后的3次访问,每次只需一个总线周期,接下来的1次访问需要2个总线周期,随后的3次访问,又是只需要一个总线周期。这就是所谓的“6-1-1-1-2-1-1-1特性”
5、磁盘存储器,最常用的就是我们所说的硬盘,速度和上面的相比,差了好几个数量级,访问硬盘的动作属于I/O访问,对于性能影响很大,所以尽量避免。当然,有时候非要访问不可,那就要采用一些有效的策略,例如:开个缓冲池。对于大型的数据访问,其性能会和系统的虚拟内存机制紧密联系,也与文件结构紧密相关。
虚拟内存的秘密
虚拟内存机制是一种很不错的机制,表面上看来,他把有限的内存转化为无限的内存空间,特别是现代的操作系统,往往具有把永久数据映射到系统的虚拟存储来访问这些永久数据的能力,系统的增强,也加剧了我们编写的程序大量依赖于虚拟内存机制。
虽然,在我们访问硬盘文件时,数据在内存中的驻留有系统控制,系统在硬盘上开辟一段空间作为虚拟内存,用这块虚拟内存来动态的管理运行时的文件。但是,不要忘了,由于硬盘的访问,使它不得不使用内存管理代码,结果他的开销变得和系统的虚拟换页系统一样的昂贵。
根据硬盘厂商提供的数据,磁盘访问平均需要12--20ms,典型的磁盘访问至少包括2次上下文转换以及低级设备接口的执行,这就需要数千条指令,数百万个时钟周期的延迟。所以如果我们的程序对于性能要求比较高的话,在使用虚拟内存的时候要考虑一下。
内存分配策略
关于内存分配,通常有2种比较常用的分配策略:可变大小分配策略、固定尺寸分配策略。
可变大小分配策略,关键就在用他们的通用性上,通过他们,用户可以向系统申请任意大小的内存空间,显然,这样的分配方式很灵活,应用也很广泛,但是他们也有自己致命的缺陷,不过,对于我们来说,影响最大的大约在2个方面:
1、为了满足用户要求和系统的要求,不得不做一些额外的工作,效率自然就会有所下降;
2、在程序运行期间,可能会有频繁的内存分配和释放动作,利用我们已有的数据结构和操作系统的知识,这样就会在内存中形成大量的、不连续的、不能够直接使用的内存碎片,在很多情况下,这对于我们的程序都是致命的。如果我们能够每隔一段时间就重新启动系统自然就没有问题,但是有的程序不能够中断,就算是能够中断,让用户每隔一段时间去重起系统也是不现实的(谁还敢用你做的东东?)
固定尺寸分配策略,这个策略的关键就在于固定,也就是说,当我们申请内存时,系统总是为我们返回一个固定大小(通常是2的指数倍)的内存空间,而不管我们实际需要内存的大小。和上面我们所说的通用分配策略相比,显得比较呆板,但是速度更快,不会产生太多的细小碎片。
一般情况下,可变大小分配策略和固定尺寸分配策略经常共同合作,例如,分配器会有一个分界线M,当申请的内存大小小于M的时候,就采用固定尺寸分配,当申请的大小大于M的时候就采用可变大小的分配。其实,在SGI
STL里面就是采用的这种混合策略,它采用的分界线是128B,如果申请的内存大小超过了128B,就移交第一级配置器处理,如果小于128B,则采用内存池策略,维护一个16个free-lists的小额区块。
内存服务层次
内存分配有很多种策略,那么我们怎么知道是谁负责内存分配的呢?
内存的分配服务和存储结构一样,也是分层次的:
第一层,操作系统内核提供最基本服务,这是内存分配策略的基础,也是和硬件系统联系最紧密的,所以说不同的平台这些服务的特点也是不一样的。
第二层,编译器的缺省运行库提供自己的分配服务,new/malloc提供的就是基于本地的分配服务,具体的服务方式要依赖于不同的编译器,编译器的服务可以只对本地分配服务做一层简单的包装,没有考虑任何效率上的强化,例如,new就是对malloc的一层浅包装。编译器的服务也可以对本地服务进行重载,使用去合理的方式去分配内存。
第三层,标准容器提供的内存分配服务,和缺省的运行库提供的服务一样,他也可以简单的利用编译器的服务,例如,SGI中的标准配置器allocator,虽然为了符合STL标准,SGI定义了这个配置器,但是在实际应用中,SGI却把它抛弃了。也可以对器进行重载,实现自己的策略和优化措施。,例如,SGI中使用的具有此配置能力的SGI空剑配置器。
最外面的一层,用户自定义的容器和分配器提供的服务,我们可以对容器的分配器实施自己喜欢的方案,也可以对new/delete重载,让他做我们喜欢的事情。
内存分配的开销
&nbs
p; 内存的开销主要来自两部分:维护开销、对齐开销。
1、维护开销
在可变大小的分配策略下,在分配的时候,会采用一定的策略去维护分配和释放内存空间的大小,例如,在VC6下面,就会在分配的内存块其实位置放一个Cookie,,当进行delete的时候,指针前移4个字节,读出内存大小size,然后释放size+4的空间,我们可以用下面的小程序进行简单的测试:
#include <iostream>
using namespace std;
class A
{
public:
A(){cout<<"A"<<endl;}
int i;
~A(){cout<<"~A"<<endl;}
};
int main()
{
A* pA=new A[5];
int* p=(int*)pA;
*(p-1)=1;
delete []pA;
return 0;
}
对于固定大小分配策略,因为已经知道内存块的确定大小,自然就不需要这方面的开销。
2、对齐开销
很多的平台都要求数据的对齐,在数据的间隙或尾端进行填充,我们可以利用sizeof进行测试:
struct A
{
char c1;
int i;
char c2;
};
在我们进行如下运算的时候,我们可能会发现sizeof(A)=12,但是char只是占用1个字节,int占用4个字节,加起来也不过6个字节,怎么会多了一倍呢?
这就是对齐现象在起作用,实际占用的空间是这3个变量都占用4个字节,在每一个char型的末尾都会填充3个字节的0。那你把char
c2和 int i交换位置,看看结果是多少?怎么解释呢?
初看起来,只是一种浪费,为什么会有这个特点呢?目的很简单,就是要使bus运输量达到最大。
一提到内存管理 ,我们头脑中闪出的两个概念,就是虚拟内存,与物理内存。这两个概念主要来自于linux内核的支持。
Linux在内存管理上份为两级,一级是线性区,类似于00c73000-00c88000,对应于虚拟内存,它实际上不占用实际物理内存;一级是具体的物理页面,它对应我们机器上的物理内存。
这里要提到一个很重要的概念,内存的延迟分配。Linux内核在用户 申请内存的时候,只是给它分配了一个线性区(也就是虚存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页 面,将其全部释放的过程。
char *p=malloc(2048) //这里只是分配了虚拟内存2048,并不占用实际内存。
strcpy(p,”123”) //分配了物理页面,虽然只是使用了3个字节,但内存还是为它分配了2048字节的物理内存。
free(p) //通过虚拟地址,找到其所对应的物理页面,释放物理页面,释放线性区。
我们知道用户的进程和内核是运行在不同的级别,进程与内核之间的通讯是通过系统 调用来完成的。进程在申请和释放内存,主要通过brk,sbrk,mmap,unmmap这几个系统调用,传递的参数主要是对应的虚拟内存。
注意一点,在进程只能访问虚拟内存,它实际上是看不到内核物理内存的使用,这对于进程是完全透明的。
glibc内存管理器
那么我们每次调用malloc来分配一块内存,都进行相应的系统调用呢?
答案是否定的,这里我要引入一个新的概念,glibc的内存管理器。
我们知道malloc和free等函数都是包含在glibc库里面的库函数,我们试想一下,每做一次内存操作,都要调用系统调用的话,那么程序将多么的低效。
实际上glibc采用了一种批发和零售的方式来管理内存。glibc每次通过系统调用的方式申请一大块内存(虚拟内存),当进程申请内存时,glibc就从自己获得的内存中取出一块给进程。
内存管理器面临的困难
我们在写程序的时候,每次申请的内存块大小不规律,而且存在频繁的申请和释放,这样不可避免的就会产生内存碎块。而内存碎块,直接会导致大块内存申请无法满足,从而更多的占用系统资源;如果进行碎块整理的话,又会增加cpu的负荷,很多都是互相矛盾的指标,这里我就不细说了。
我们在写程序时,涉及内存时,有两个概念heap和stack。传统的说法stack的内存地址是向下增长的,heap的内存地址是向上增长的。
函数malloc和free,主要是针对heap进行操作,由程序员自主控制内存的访问。
在这里heap的内存地址向上增长,这句话不完全正确。
glibc对于heap内存申请大于128k的内存申请,glibc采用mmap的方式向内核申请内存,这不能保证内存地址向上增长;小于128k的则采用brk,对于它来讲是正确的。128k的阀值,可以通过glibc的库函数进行设置。
这里我先讲大块内存的申请,也即对应于mmap系统调用。
对于大块内存申请,glibc直接使用mmap系统调用为其划分出另一块虚拟地址,供进程单独使用;在该块内存释放时,使用unmmap系统调用将这块内存释放,这个过程中间不会产生内存碎块等问题。
针对小块内存的申请,在程序启动之后,进程会获得一个heap底端的地址,进程每次进行内存申请时,glibc会将堆顶向上增长来扩展内存空间 ,也就是我们 所说的堆地址向上增长。
在对这些小块内存进行操作时,便会产生内存碎块的问题。实际上brk和sbrk系统调用,就是调整heap顶地址指针。
那么heap堆的内存是什么时候释放呢?
当glibc发现堆顶有连续的128k的空间是空闲的时候,它就会通过brk或sbrk系统调用,来调整heap顶的位置,将占用的内存返回给系统。这时,内核会通过删除相应的线性区,来释放占用的物理内存。
下面我要讲一个内存空洞的问题:
一个场景,堆顶有一块正在使用的内存,而下面有很大的连续内存已经被释放掉了,那么这块内存是否能够被释放?其对应的物理内存是否能够被释放?
很遗憾,不能。
这也就是说,只要堆顶的部分申请内存还在占用,我在下面释放的内存再多,都不会被返回到系统中,仍然占用着物理内存。为什么会这样呢?
这主要是与内核在处理堆的时候,过于简单,它只能通过调整堆顶指针的方式来调整调整程序占用的线性区;而又只能通过调整线性区的方式,来释放内存。所以只要堆顶不减小,占用的内存就不会释放。
提一个问题:
char *p=malloc(2);
free(p)
为什么申请内存的时候,需要两个参数,一个是内存大小,一个是返回的指针;而释放内存的时候,却只要内存的指针呢?
这主要是和glibc的内存管理机制有关。glibc中,为每一块内存维护了一个chunk的结构。glibc在分配内存时,glibc先填写chunk结构中内存块的大小,然后是分配给进程的内存。
chunk ------size
p------------ content
在进程释放内存时,只要指针-4 便可以找到该块内存的大小,从而释放掉。
注:glibc在做内存申请时,最少分配16个字节,以便能够维护chunk结构。
glibc提供的调试工具:
为了方便调试,glibc 为用户提供了 malloc 等等函数的钩子(hook),如 __malloc_hook
对应的是一个函数指针,void *function (size_t size,const void *caller)
其中 caller 是调用 malloc 返回值的接受者(一个指针的地址)。另外有 __malloc_initialize_hook函数指针,仅仅会调用一次(第一次分配动态内存时)。(malloc.h)
一些使用 malloc 的统计量(SVID 扩展)可以用 struct mallinfo 储存,可调用获得。
struct mallinfo mallinfo (void)
如 何检测 memory leakage(内存泄漏)?glibc 提供了一个函数void mtrace (void)及其反作用void muntrace (void)这时会依赖于一个环境变量 MALLOC_TRACE 所指的文件, 把一些信息记录在该文件中用于侦测 memory leakage,其本质是安装了前面提到的 hook。一般将这些函数用#ifdef DEBUGGING 包裹以便在非调试态下减少开销。产生的文件据说不建议自己去读,而使用 mtrace 程序(perl 脚本来进行分析)。下面用一个简单的例子说明这个过程,这是源程序:
#include
#include
#include
intmain( int argc, char *argv[] )
{
int *p, *q ;
#ifdef DEBUGGING
mtrace( ) ;
#endif
p = malloc( sizeof( int ) ) ;
q = malloc( sizeof( int ) ) ;
printf( "p = %p/nq = %p/n", p, q ) ;
*p = 1 ;
*q = 2 ;
free( p ) ;
return 0 ;
}
很简单的程序,其中 q 没有被释放。我们设置了环境变量后并且 touch 出该文件执行结果如下:
p = 0x98c0378q = 0x98c0388
该文件内容如下
= Star
t@./test30:[0x8048446] + 0x98c0378 0x4
@ ./test30:[0x8048455] + 0x98c0388 0x4
@ ./test30:[0x804848f] - 0x98c0378
到这里我基本上讲完了,我们写程序时,数据部分内存使用的问题。
代码占用的内存
数据部分占用内存,那么我们写的程序是不是也占用内存呢?
在linux中,程序的加载,涉及到两个工具,linker 和loader。Linker主要涉及动态链接库的使用,loader主要涉及软件的加载。
1、 exec执行一个程序
2、 elf为现在非常流行的可执行文件的格式,它为程序运行划分了两个段,一个段是可以执行的代码段,它是只读,可执行;另一个段是数据段,它是可读写,不能执行。
3、 loader会启动,通过mmap系统调用,将代码端和数据段映射到内存中,其实也就是为其分配了虚拟内存,注意这时候,还不占用物理内存;只有程序执行到了相应的地方,内核才会为其分配物理内存。
4、 loader会去查找该程序依赖的链接库,首先看该链接库是否被映射进内存中,如果没有使用mmap,将代码段与数据段映射到内存中,否则只是将其加入进程的地址空间。这样比如glibc等库的内存地址空间是完全一样。
因此一个2M的程序,执行时,并不意味着为其分配了2M的物理内存,这与其运行了的代码量,与其所依赖的动态链接库有关。
运行过程中链接动态链接库与编译过程中链接动态库的区别。
我们调用动态链接库有两种方法:一种是编译的时候,指明所依赖的动态链接库,这样loader可以在程序启动的时候,来所有的动态链接映射到内存中;一种是在运行过程中,通过dlopen和dlfree的方式加载动态链接库,动态将动态链接库加载到内存中。
这两种方式,从编程角度来讲,第一种是最方便的,效率上影响也不大,在内存使用上有些差别。
第一种方式,一个库的代码,只要运行过一次,便会占用物理内存,之后即使再也不使用,也会占用物理内存,直到进程的终止。
第二中方式,库代码占用的内存,可以通过dlfree的方式,释放掉,返回给物理内存。
这个差别主要对于那些寿命很长,但又会偶尔调用各种库的进程有关。如果是这类进程,建议采用第二种方式调用动态链接库。
占用内存的测量
测量一个进程占用了多少内存,linux为我们提供了一个很方便的方法,/proc目录为我们提供了所有的信息,实际上top等工具也通过这里来获取相应的信息。
/proc/meminfo 机器的内存使用信息
/proc/pid/maps pid为进程号,显示当前进程所占用的虚拟地址。
/proc/pid/statm 进程所占用的内存
[root@localhost ~]# cat /proc/self/statm
654 57 44 0 0 334 0
输出解释
CPU 以及CPU0。。。的每行的每个参数意思(以第一行为例)为:
参数 解释 /proc//status
Size (pages) 任务虚拟地址空间的大小 VmSize/4
Resident(pages) 应用 程序正在使用的物理内存的大小 VmRSS/4
Shared(pages) 共享页数 0
Trs(pages) 程序所拥有的可执行虚拟内存的大小 VmExe/4
Lrs(pages) 被映像到任务的虚拟内存空间的库的大小 VmLib/4
Drs(pages) 程序数据段和用户态的栈的大小(VmData+ VmStk )4
dt(pages) 04
查看机器可用内存
/proc/28248/>free
total used free shared buffers cached
Mem: 1023788 926400 97388 0 134668 503688
-/+ buffers/cache: 288044 735744
Swap: 1959920 896081870312
我们通过free命令查看机器空闲内存时,会发现free的值很小。这主要是因为,在linux中有这么一种思想,内存不用白不用,因此它尽可能的cache和buffer一些数据,以方便下次使用。但实际上这些内存也是可以立刻拿来使用的。
所以 空闲内存=free+buffers+cached=total-used
查看进程使用的内存
查看一个进程使用的内存,是一个很令人困惑的事情。因为我们写的程序,必然要用到动态链接库,将其加入到自己的地址空间中,但是/proc/pid/statm统计出来的数据,会将这些动态链接库所占用的内存也简单的算进来。
这样带来的问题,动态链接库占用的内存有些是其他程序使用时占用的,却算在了你这里。你的程序中包含了子进程,那么有些动态链接库重用的内存会被重复计算。
因此要想准确的评估一个程序所占用的内存是十分困难的,通过写一个module的方式,来准确计算某一段虚拟地址所占用的内存,可能对我们有用。
C++ - 内存泄露机制
内存泄漏的定义
一般我们常说的内存泄漏是指堆内存的泄漏。
堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显式释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
标签:
原文地址:http://www.cnblogs.com/lsx1993/p/4841833.html