感谢,Thanks!
四 堆栈与函数调用
一 C++程序内存分配
1) 在栈上创建。在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,一般使用寄存器来存取,效率很高,但是分配的内存容量有限。
2) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete来释放内存。动态内存的生存期由程序员自己决定,使用非常灵活。
3) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
4) 文字常量分配在文字常量区,程序结束后由系统释放。
5)程序代码区。
经典实例:(代码来自网络高手,没有找到原作者)
Code#includeinta=0;//全局初始化区char *p1;//全局未初始化区 voidmain()
{intb;//栈chars[]="abc";//栈char *p2;//栈char *p3="123456";//123456"0在常量区,p3在栈上。static intc=0;//全局(静态)初始化区p1=(char*)malloc(10);
p2=(char*)malloc(20);//分配得来得10和20字节的区域就在堆区。strcpy(p1,"123456");//123456"0放在常量区,编译器可能会将它与p3所向"123456"0"优化成一个地方。}
二 三种内存对象的比较
栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般
较堆对象快,因为分配堆对象时,会调用operator new操作,operator
new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈
空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,
所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。
堆对象创建和销毁都要由程序员负责,所以,如果
处理不好,就会发生内存问题。如果分配了堆对象,却忘记了释放,就会产生内存泄漏;而如
果已释放了对象,却没有将相应的指针置为NULL,该指针就是所谓的“悬挂指针”,再度使用此指针时,就会出现非法访问,严重时就导致程序崩溃。但是高效
的使用堆对象也可以大大的提高代码质量。比如,我们需要创建一个大对象,且需要被多个函数所访问,那么这个时候创建一个堆对象无疑是良好的选择,因为我们
通过在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享,相比整个对象的传递,大大的降低了对象的拷贝时间。另外,相比于栈空间,堆的容量要
大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。
静态存储区。所有的静态对象、全局对象都于静态存储区分配。关于全局对象,是在main()函数执行前就分配好了的。其实,在main()函数中的显示代
码执行之前,会调用一个由编译器生成的_main()函数,而_main()函数会进行所有全局对象的的构造及初始化工作。而在main()函数结束之
前,会调用由编译器生成的exit函数,来释放所有的全局对象。比如下面的代码:
void main(void)
{
… …// 显式代码
}
实际上,被转化成这样:
void main(void)
{
_main(); //隐式代码,由编译器产生,用以构造所有全局对象
… … // 显式代码
… …
exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
}
除了全局静态对象,还有局部静态对象通和class的静态成员,局部静态对象是在函数中定义的,就像栈对象一样,只不过,其前面多了个
static关键字。局部静态对象的生命期是从其所在函数第一次被调用,更确切地说,是当第一次执行到该静态对象的声明代码时,产生该静态局部对象,直到
整个程序结束时,才销毁该对象。class的静态成员的生命周期是该class的第一次调用到程序的结束。
三 函数调用与堆栈
1)编译器一般使用栈来存放函数的参数,局部变量等来实现函数调用。有时候函数有嵌套调用,这个时候栈中会有多个函数的信息,每个函数占用一个连续的区域。一个函数占用的区域被称作帧(frame)。同时栈是线程独立的,每个线程都有自己的栈。例如下面简单的函数调用:
另外函数堆栈的清理方式决定了当函数调用结束时由调用函数或被调用函数来清理函数帧,在VC中对函数栈的清理方式由两种:
参数传递顺序
谁负责清理参数占用的堆栈
__stdcall
从右到左
被调函数
__cdecl
从右到左
调用者
2) 有了上面的知识为铺垫,我们下面细看一个函数的调用时堆栈的变化:
代码如下:
CodeintAdd(intx,inty)
{returnx+y;
}voidmain()
{int *pi= new int(10);int *pj= new int(20);intresult= 0;
result=Add(*pi,*pj);
delete pi;
delete pj;
}
对上面的代码,我们分为四步,当然我们只画出了我们的代码对堆栈的影响,其他的我们假设它们不存在,哈哈!
第一,int *pi= new int(10);int *pj= new int(20);intresult= 0; 堆栈变化如下:
第二,Add(*pi,*pj);堆栈如下:
第三,将Add的结果给result,堆栈如下:
第四,delete pi; delete pj; 堆栈如下:
第五,当main()退出后,堆栈如下,等同于main执行前,哈哈!
四 完!
感谢,Thanks!
五 sizeof与内存布局
有了前面几节的铺垫,本节开始摸索C++的对象的内存布局,平台为windows32位+VS2008。
一 内置类型的size
内置类型,直接上代码,帮助大家加深记忆:
CodevoidTestBasicSizeOf()
{
cout<<__function__>
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
}
运行结果如下:
二 struct/class的大小
在C++中我们知道struct和class的唯一区别就是默认的访问级别不同,struct默认为public,而class的默认为
private。所以考虑对象的大小,我们均以struct为例。对于struct的大小对于初学者来说还确实是个难回答的问题,我们就通过下面的一个
struct定义加逐步的变化来引出相关的知识。
代码如下:
Codestructst1
{shortnumber;floatmath_grade;floatChinese_grade;floatsum_grade;charlevel;
}; //20structst2
{charlevel;shortnumber;floatmath_grade;floatChinese_grade;floatsum_grade;
};//16#pragmapack(1)structst3
{charlevel;shortnumber;floatmath_grade;floatChinese_grade;floatsum_grade;
}; //15#pragmapack()voidTestStructSizeOf()
{
cout<<__function__>
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
cout<
}
运行结果如下;
基于上面的对struct的测试,我们是不是有些惊呆哦,对于C++的初学者更是情不自禁的说:“我靠!原来顺序不同所占空间都不同啊,还有那个
pack是啥东东啊?”,其实这里蕴含了一个内存对齐的问题,在计算机的底层进行内存的读写的时候,如果内存对齐的话可以提高读写效率,下面是VC的默认
规则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍, 如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
当然VC提供了工程选项/Zp[1|2|4|8|16]可以修改对齐方式,当然我们也可以在代码中对部分类型实行特殊的内存对齐方式,修改方式为#pragma pack( n ),n为字节对齐
数,其取值为1、2、4、8、16,默认是8,取消修改用#pragma pack(),如果结构体某成员的sizeof大于你设置的,则按你的设置来对齐。
三 struct的嵌套
1)实例:
CodestructA
{inti;charc;doubled;shorts;
};//24structB
{charcc;
A a;intii;
};//40
布局:(使用VS的未发布的编译选项/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)
2)实例:
Code#pragmapack(4)structA2
{inti;charc;doubled;shorts;
};//20#pragmapack()structB2
{charcc;
A2 a;intii;
};//28
布局:(使用VS的未发布的编译选项/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)
总结:
由于结构体的成员可以是复合类型,比如另外一个结构体,所以在寻找最宽基本类型成员时,应当包括复合类型成员的子成员,而不是把复合成员看成是一个整体。但在确定复合类型成员的偏移位置时则是将复合类型作为整体看待。
四 空struct/class和const,static成员
实例:
Codestructempty{};//1structconstAndStatic
{const inti;static charc;const doubled;static voidTestStatic(){}voidTestNoStatic(){}
};//16
布局:(使用VS的未发布的编译选项/d1 reportAllClassLayout 或 /d1 reportSingleClassLayout)
上面的实例中empty的大小为1,而constAndStatic的大小为16。
总结:
因为static成员和函数其实是类层次的,不在对象中分配空间,而成员函数其实是被编译为全局函数了,所以也不在对象中。
五 本节完,下次探讨虚函数对内存布局的影响!
感谢,Thanks!
六 单继承与虚函数表
一 单继承
1) 代码:
Code#includeusing namespacestd;classA
{public:voidf1(){cout<
};classB :publicA
{public:voidf2(){cout<
};classC :publicB
{public:voidf3(){cout<
};
2)类图:
3)VS2008的编译选项查看布局:
4)可视化表示:
5)代码验证:
Codetypedefvoid(*Fun)();voidPrintVTable(A*pA)
{int *pVT=(int*)*(int*)(pA);
Fun*pF=(Fun*)(pVT+ 0);intiLength= 0;while(*pF!=NULL)
{
(*pF)();++iLength;
pF=(Fun*)(pVT+iLength);
}
}voidPrintMembers(A*pA)
{int *p=(int*)(pA);inti= 1;while(i<= 3)
{
cout<
i++;
}
}voidTestVT()
{
A*pA= newC();
C*pC=dynamic_cast(pA);
pC->x= 10;
pC->y= 20;
pC->z= 30;
PrintVTable(pA);
PrintMembers(pA);
delete pA;
}
6)验证代码运行结果:
7)总结:
单继承的对象的布局,第一个为虚函数表指针vtbl,其后为成员且先基类后子类,虚函数表里包含了所有的虚函数的地址,以NULL结束。虚函数如果子类有重写,就由子类的重新的代替。
二 单继承运行时类型转化
1)代码验证:
CodevoidTestDynamicCast()
{
A*pA= newC();
cout<
B*pB=dynamic_cast(pA);
cout<
C*pC=dynamic_cast(pA);
cout<
}
2)验证代码运行结果:
3)总结:
我们上面看了单继承的内存布局,而这样的内存布局也就决定了当dynamic_cast的时候,都还是同一地址,不需要做指针的移动。只是类型的改变即所能访问的范围的改变。
三 完!
感谢,Thanks!
七 多继承与虚函数表
一 多重继承
1) 代码:
Code#includeusing namespacestd;classB1
{public:intx;virtual voidv1(){ cout<
};classB2
{public:inty;virtual voidv2(){ cout<
};classB3
{public:intz;virtual voidv3(){ cout<
};classD :publicB1,publicB2,publicB3
{public:inta;voidv3(){ cout<
};
2)类图:
3)VS2008的编译选项查看布局:
4)可视化表示:
5)代码验证:
Codetypedefvoid(*Fun)();voidPrintMember(int *pI)
{
cout<
}voidPrintVT(int *pVT)
{while(*pVT!=NULL)
{
(*(Fun*)(pVT))();
pVT++;
}
}voidPrintVTAndMember(B1*pD)
{int *pRoot=(int*)pD;int *pVTB1=(int*)*(pRoot+ 0);PrintVT(pVTB1);int *pMB1=pRoot+1; PrintMember(pMB1);int *pVTB2=(int*)*(pRoot+ 2);PrintVT(pVTB2);int *pMB2=pRoot+3; PrintMember(pMB2);int *pVTB3=(int*)*(pRoot+ 4);PrintVT(pVTB3);int *pMB3=pRoot+5; PrintMember(pMB3);
}voidTestVT()
{
B1*pB1= newD();
D*pD=dynamic_cast(pB1);
pD->x= 10;
pD->y= 20;
pD->z= 30;
pD->a= 40;
PrintVTAndMember(pD);
delete pD;
}
6) 验证代码运行结果:
7)总结:
与单继承相同的是所有的虚函数都包含在虚函数表中,所不同的多重继承有多个虚函数表,当子类对父类的虚函数有重写时,子类的函数覆盖父类的函数在对应的虚函数位置,当子类有新的虚函数时,这些虚函数被加在第一个虚函数表的后面。
二 多重继承运行时类型转化
1)代码验证:
CodevoidTestDynamicCast()
{
B1*pB1= newD();
cout<
D*pD=dynamic_cast(pB1);
cout<
B2*pB2=dynamic_cast(pB1);
cout<
B3*pB3=dynamic_cast(pB1);
cout<
delete pD;
}
2)验证代码的运行结果:
3)总结:
从多重继承的内存布局,我们可以看到子类新加入的虚函数被加到了第一个基类的虚函数表,所以当dynamic_cast的时候,子类和第一个基类的地址相同,不需要移动指针,但是当dynamic_cast到其他的父类的时候,需要做相应的指针的移动。
三 完!
感谢,Thanks!
八 虚继承与虚函数表
一 虚继承
1) 代码:
Code#includeusing namespacestd;classB
{public:inti;virtual voidvB(){ cout<
};classD1 :virtual publicB
{public:intx;virtual voidvD1(){ cout<
};classD2 :virtual publicB
{public:inty;voidvB(){ cout<
};classGD :publicD1,publicD2
{public:inta;voidvB(){ cout<
};
2)类图:
3)VS2008的编译选项查看布局:
4)可视化表示:
5)代码验证:(此时的虚函数表不是以NULL结尾,为什么?)
Codetypedefvoid(*Fun)();voidPrintMember(int *pI)
{
cout<
}voidPrintVT(int *pVT)
{while(*pVT!=NULL)
{
(*(Fun*)(pVT))();
pVT++;
}
}voidPrintMemberAndVT(GD*pGD)
{int *pRoot=(int*)pGD;int *pD1VT=(int*)*(pRoot+ 0);
(*(Fun*)(pD1VT))(); (*(Fun*)(pD1VT+1))();int *pVB=(int*)*(pRoot+1); cout<
(*(Fun*)(pD2VT))();int *pVB2=(int*)*(pRoot+4); cout<
(*(Fun*)(pBVT))();int *pI=(pRoot+ 8); PrintMember(pI);
}voidTestVT()
{
B*pB= newGD();
GD*pGD=dynamic_cast(pB);
pGD->i= 10;
pGD->x= 20;
pGD->y= 30;
pGD->a= 40;
PrintMemberAndVT(pGD);
delete pGD;
}
6)验证代码结果:
7)总结:
虚继承,使公共的基类在子类中只有一份,我们看到虚继承在多重继承的基础上多了vbtable来存储到公共基类的偏移。
二 虚继承运行时类型转化
1)代码验证:
CodevoidTestDynamicCast()
{
B*pB= newGD();
GD*pGD=dynamic_cast(pB);
cout<
D1*pD1=dynamic_cast(pB);
cout<
D2*pD2=dynamic_cast(pB);
cout<
cout<
}
2)验证代码结果:
3)总结:
还是从内存布局来看dynamic_cast时地址的变化,第一个基类的地址与子类相同,其他的基类和虚基类需要做偏移。
三 完!
感谢,Thanks!
九 类型转换
一 typeid与dynamic_cast
1)RTTI, Runtime Type Identification (RTTI) or Run-time type
information (RTTI),表示在运行时动态决定变量的类型,来调用正确的虚函数。
RTTI在VS2008中默认为关闭,可以通过修改编译选项Enable Run-Time Type Info 为
Yes,来启用RTTI,只有当启动RTTI时,用来RTTI功能的typeid和dynamic_cast才能正常工作。
2)type_info,用来描述类型信息。type_info存储了它所描述的类型的名字。RTTI就是使用type_info来实现的。type_info的定义如下:
Codeclasstype_info {public:virtual ~type_info();bool operator==(consttype_info&rhs)const;bool operator!=(consttype_info&rhs)const;boolbefore (consttype_info&rhs)const;const char*name()const;private:
type_info (consttype_info&rhs);
type_info& operator=(consttype_info&rhs);
};
问题:RTTI怎么实现那?对象,type_info,虚函数怎么关联那?《深入C++对象模型》中说在虚函数表的开始存储了类型信息,但是实际的VS2008中好像并没有此信息,请高人指点哦!
3)typeid,在运行时获得对象的类型,typeid()返回的是const type_info&,而
type_info包含了对象真实类型的名字。typeid能被用来获取一个引用对象或指针指向的对象的运行时的真实类型。当然如果对象为null或编译
时没有使用/GR的话,typeid的会抛出异常bad_typeid exception或__non_rtti_object。实例代码:
CodeclassBase
{public:virtual voidf(){ }
};classDerived :publicBase
{public:voidf2() {}
};voidmain ()
{
Base*pB= newDerived();consttype_info&t=typeid(*pB);cout<
delete pB;
Derived d;
Base&b=d;
cout<
}
运行结果:
4)dynamic_cast,用来运行时的类型转化,需要/GR来正确运行。
适用: 第一,用于所有的父子和兄弟间指针和引用的转化,有类型安全检查; 第二,对指针类型,如果不成功,返回NULL,对引用类型,如果不成功,则抛出异常; 第三,类型必须要有虚函数,且打开/GR编译选项,否则不能使用dynamic_cast。实例代码:
CodeclassAA
{public:virtual voiddo_sth(){ std::cout<
};classBB
{public:virtual voiddo_sth(){ std::cout<
};classCC :publicAA,publicBB
{public:virtual voiddo_sth(){ std::cout<
};voidDynamicCastTest()
{
AA*pA= newCC;
BB*pB=dynamic_cast(pA);if(pB!=NULL)
cout<
CC*pC=dynamic_cast(pA);if(pC!=NULL)
cout<
}
二 其他cast
1)隐式转化,不需要任何操作符,转化被自动执行,当一个值被赋值到它所兼容的类型时。
适用:
第一,内置基本类型的兼容转化;
第二, 子类指针,引用向父类的转化;
实例:
CodeclassA
{public:virtual ~A(){}
};classB :publicA
{
};voidImplicitCast()
{shorta= 2000;intb;
b=a;doubled= 10.05;inti;
i=d;intj= 75;charc;
c=j;
A*pA= newB();
}
2)强制类型转化,即我们常说的C风格的类型转化,基本上可以用于所有的转化,但是没有意义的转化除外,但是父子类,兄弟间的转化没有类型检查可能导致运行是错误。
适用:
第一,基本类型转化;
第二,void*到其他指针的转化;
第三,去除const;
第五,函数指针的转化;
第六,父子类转化,但是多重继承和兄弟转化,可能有运行时错误,没有类型检查;
第七,任何两个类,但是没有实际意义,运行可能出错;
第八,不能用于没有意义的转化,严厉禁止,例如,你不能用static_cast象用C风格的类型转换一样把struct转换成int类型,或者把double类型转换成指针类型;
第九,在C++一般更推荐新加的static_cast,const_cast,dynamic_cast和reinterpret_cast转化方式;
实例:
CodeclassCDummy
{public:
CDummy(floatx,floaty)
{
i=x;
j=y;
}private:floati,j;
};classCAddition
{public:
CAddition (inta,intb) { x=a; y=b; }intresult() {returnx+y;}private:intx,y;
};intTesting()
{
std::cout<
}voidExplicitCast()
{doubler=(double)1 / 3;int *pi= new int(10);void *pV;
pV=pi;int *pj=(int*)pV;//或 int *pj = int*(pV);const int*pa= new int(20);int *pb;
pb=(int*)pa;*pb= 30;
std::cout<
typedefvoid(*Fun)();
Fun f=(Fun)Testing;
f();//多重继承或将兄弟间的转化可能会出错//虽然可以正确的编译,但是运行有问题,所以我们不做没有意义的转化//CDummy d(10,30);//CAddition * padd;//padd = (CAddition*) &d;//std::cout <result();//不做没有意义的转化 error//struct st{int i; double d;};//st s;//int x = (int)s;//c2440//double y = 10.0;//int *p = (int*)y;//c2440}
3)static_cast在功能上基本上与C风格的类型转换一样强大,含义也一样。
它也有功能上限制:
第一,不能兄弟间转化,父子间转化没有类型安全检查,有可能会导致运行时错误,父子兄弟的动态转化应该适用dynamic_cast;
第二,不能去除const,适用专用的const_cast;
第三,不能用于两个没有继承关系的类,当然实际上这样的转化也是没有意义的;
第四,当然也不支持没有意义的转化,例如,你不能用static_cast象用C风格的类型转换一样把struct转换成int类型,或者把double类型转换成指针类型;
4)const_cast,用来修改类型的const或volatile属性。
适用:
第一,常量指针被转化成非常量指针,并且仍然指向原来的对象;
第二,常量引用被转换成非常量引用,并且仍然指向原来的对象;
第三,常量对象被转换成非常量对象;
实例:
CodevoidConstCastTest()
{const int*pa= new int(20);int *pb;
pb=const_cast(pa);*pb= 30;
std::cout<
}
5)reinterpret_cast,此转型操作符的结果取决于编译器,用于修改操作数类型,非类型安全的转换符。
适用:
一般不推荐使用,但是一般用来对函数指针的转化。实例:
Code//不可以移植,不推荐使用intReinterpretTest()
{structdat {shorta;shortb;};longvalue= 0x00100020;
dat*pd=reinterpret_cast(&value);
std::cout<a<<:endl>b<<:endl>
}
typedefvoid(*Fun)();intTesting()
{
std::cout<
}voidReinterpretTest2()
{//Fun f = (Fun)Testing;//f();Fun f=reinterpret_cast(Testing);
f();
}
三 总结
在C++一般更推荐新加的static_cast,const_cast,dynamic_cast和reinterpret_cast转化方式;
感谢,Thanks!