class和struct的区别 类的内存结构
时间:2010-08-07 来源:苏霈儿咪咪
C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了,它已经获取了太多的功能。
struct能包含成员函数吗? 能!
struct能继承吗? 能!!
struct能实现多态吗? 能!!!
有很多人应该已经知道这样一个事实,但总有些不知道的人,看到这些会感到很惊讶。是的,当我第一次注意到这个事实的时候,我也同样很吃惊。
既然这些它都能实现,那它和class还能有什么区别?
最本质的一个区别就是默认的访问控制,体现在两个方面: 1)默认的继承访问权限。struct是public的,class是private的。 如果不知道什么是public继承,什么是private继承的,可以去查书,这里暂不讨论。 你可以写如下的代码: struct A { char a; }; struct B : A { char b; }; 这个时候B是public继承A的。如果都将上面的struct改成class,那么B是private继承A的。这就是默认的继承访问权限。所以我们在平时写类继承的时候,通常会这样写: struct B : public A 就是为了指明是public继承,而不是用默认的private继承。 当然,到底默认是public继承还是private继承,取决于子类而不是基类。我的意思是,struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。如下: struct A{}; class B : A{}; //private继承 struct C : B{}; //public继承 2)struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
注意我上面的用词,我依旧强调struct是一种数据结构的实现体,虽然它是可以像class一样的用。我依旧将struct里的变量叫数据,class内的变量叫成员,虽然它们并无区别。其实,到底是用struct还是class,完全看个人的喜好,你可以将你程序里所有的class全部替换成struct,它依旧可以很正常的运行。但我给出的最好建议,还是:当你觉得你要做的更像是一种数据结构的话,那么用struct,如果你要做的更像是一种对象的话,那么用class。
当然,我在这里还要强调一点的就是,对于访问控制,应该在程序里明确的指出,而不是依靠默认,这是一个良好的习惯,也让你的代码更具可读性。
说到这里,很多了解的人或许都认为这个话题可以结束了,因为他们知道struct和class的“唯一”区别就是访问控制。很多文献上也确实只提到这一个区别。
但我上面却没有用“唯一”,而是说的“最本质”,那是因为,它们确实还有另一个区别,虽然那个区别我们平时可能很少涉及。那就是:“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。这一点在Stanley B.Lippman写的Inside the C++ Object Model有过说明。
问题讨论到这里,基本上应该可以结束了。但有人曾说过,他还发现过其他的“区别”,那么,让我们来看看,这到底是不是又一个区别。 还是上面所说的,C++中的struct是对C中的struct的扩充,既然是扩充,那么它就要兼容过去C中struct应有的所有特性。例如你可以这样写:
struct A //定义一个struct
{
char c1;
int n2;
double db3;
};
A a={'p',7,3.1415926}; //定义时直接赋值
也就是说struct可以在定义的时候用{}赋初值。那么问题来了,class行不行呢?将上面的struct改成class,试试看。报错!噢~于是那人跳出来说,他又找到了一个区别。我们仔细看看,这真的又是一个区别吗?
你试着向上面的struct中加入一个构造函数(或虚函数),你会发现什么?对,struct也不能用{}赋初值了。的确,以{}的方式来赋初值,只是用一个初始化列表来对数据进行按顺序的初始化,如上面如果写成A a={'p',7};则c1,n2被初始化,而db3没有。这样简单的copy操作,只能发生在简单的数据结构上,而不应该放在对象上。加入一个构造函数或是一个虚函数会使struct更体现出一种对象的特性,而使此{}操作不再有效。事实上,是因为加入这样的函数,使得类的内部结构发生了变化。而加入一个普通的成员函数呢?你会发现{}依旧可用。其实你可以将普通的函数理解成对数据结构的一种算法,这并不打破它数据结构的特性。至于虚函数和普通成员函数有什么区别,我会具体写篇文章讨论。 那么,看到这里,我们发现即使是struct想用{}来赋初值,它也必须满足很多的约束条件,这些条件实际上就是让struct更体现出一种数据机构而不是类的特性。那为什么我们在上面仅仅将struct改成class,{}就不能用了呢?其实问题恰巧是我们之前所讲的——访问控制!你看看,我们忘记了什么?对,将struct改成class的时候,访问控制由public变为private了,那当然就不能用{}来赋初值了。加上一个public,你会发现,class也是能用{}的,和struct毫无区别!!! 做个总结,从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。所以我会提出什么时候用struct什么时候用class的建议。如果你有不同的看法,欢迎讨论。 //******************* 我的读后感: 1. 类的默认构造函数?(虚拟函数?析构函数?虚拟析构函数?)What is behind the scene? 等弄完这阵子有时间得看看《inside the C++ object model》 2. COM中在<objbase.h>中的#define interface struct定义给我留下了很深的印象! //******************再转一篇 也是跟类的内存结构有关的 http://blog.sina.com.cn/s/blog_54ce5b83010000z9.html 许多同学可能在学习C++的时候,都会感到一定的困惑,继承到底是怎样分配空间的,多态到底是如何完成的,许许多多的问题,必须挖掘到C++底层处理机制,才能搞明白。有许多C程序员也并不认同C++,他们认为C++庞大又迟缓,其更重要的原因是,他们认为“C++是在你的背后做事情”。的确,C++编译器背着程序员做了太多的事情,所以让很多不了解其底层机制的人感到困惑。想成为一个优秀的程序员,那么这样的困惑就不应该存在,只有了解了底层实现模型,才能写出效率较高的代码,自信心也比较高。
我们先从一个简单但有趣的例子谈起。有如下的4个类:
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
上面的4个类中,没有任何一个类里含明显的数据,之间只是表示了继承关系,那么如果我们用sizeof 来测量它们的大小,将会得到什么结果呢?
你可能会认为,既然没有任何数据和方法,大小当然为0,而结果肯定会出乎你的意料,即使是class X 的大小也不为0。
在不同的编译器上,将会得到不同的结果,而在我们现在最常用的VC++编译器上,将得到如下的结果:
sizeof X 的结果是 1 。
sizeof Y 的结果是 4 。
sizeof Z 的结果是 4 。
sizeof A 的结果是 8 。
惊讶吗?那么为什么会得到这样的结果?让我们一个一个来分析。
对于一个空的class,事实上并不是空的,它有一个隐晦的1 byte ,那是被编译器安插进去的一个char 。这使得这个class 的两个对象得以在内存中配置独一无二的地址,这就是为什么 sizeof X 的结果是 1。
那么Y和Z呢,怎么会占用 4 byte ?其实它们的大小受到三个因素的影响:
1. 语言本身所造成的额外负担:当语言支持多态时,就会导致一些额外负担。在派生类中,这个额外负担表现在一个指针上,它是用来指向一个被称作“虚函数列表”的表格。而在VC++编译器上,指针的大小正好是 4 byte 。
2. 编译器对于特殊情况所提供的优化处理:在class Y 和 class Z 中,也将带上它们因为继承class X 而带来的 1 byte ,传统上它被放在派生类的固定部分的尾端。而某些编译器(正如我们现在所讨论的VC++编译器)会对空的基类提供特殊的处理,在这个策略下,一个空的基类被视为派生类对象最开头的一部分,也就是说它并没有花费任何额外空间(因为既然有了成员,就不需要原本为了空类而安插一个char了),这样也就节省了这 1 byte 的空间。事实上,如果某个编译器没有提供这种优化处理,你将发现class Y 和 class Z 的大小将是8 byte ,而不仅仅是5 byte 了,原因正如下面第3点所讲。
3. “对齐”(Alignment)机制:在大多数机器上,群聚的结构体大小都会受到alignment的限制,使它们能够更有效率地在内存中被存取。Alignment 就是将数值调整到某数的整数倍。在32位计算机上,通常alignment 为 4 byte(32位),以使总线达到最大的“吞吐量”,在这种情况下,如上面所说,如果 class Y 和class Z 的大小为 5 byte ,那么它们必须填补 3 byte ,最终得到的结果将是 8 byte 。是不是开始感谢VC++编译器幸好有这样的优化机制,使得我们节约了不少内存空间。
最后,我们再来看看 class A ,它的大小为什么是 8 byte ?显而易见,它继承了class Y 和class Z ,那么它的大小直接就把 class Y 和class Z 的大小加起来就够了。真有这么简单吗?实际上这只是一个巧合而已,这是因为之前编译器的优化机制掩盖这里的一些事实。对于没有优化的 class Y 和class Z 来说,他们的大小都是8 byte ,那么继承了它们两个的 class A 将是多大呢?16 byte?如果你有这样的编译器试一下的话,你会发现答案是12 byte 。怎么会是12 byte 呢?记住,一个虚拟继承的基类只会在派生类中存在一份实体,不管它在 class 继承体系中出现了多少次!class A的大小由下面几部分决定:
这个例子看懂了吗?是不是对C++的底层机制开始感兴趣了?那么我们再来举一个同样有趣的例子。
有这样一个类:
class A {
private:
int a;
char b;
char c;
char d;
};
它的大小是多少呢?
如果你有记得我之前提到的alignment机制的话,你应该会猜到它的大小是8 byte 。的确如此,int a占用4 byte ,char b , char c 和char d各占1 byte ,加起来是7 byte ,再加上alignment额外带来的1 byte ,总共是8 byte 。
瞧,就是这么简单,那么现在我们把里面的成员变量换换位置,如下:
class A {
private:
char d;
int a;
char b;
char c;
};
我们将char d拿到第一个位子,放在int a之前。那么现在你能告诉我class A的大小是多少呢?你肯定不会再猜8 byte了,因为你会觉得这与上面似乎有些不同,但你不能肯定到底是多大。不敢确定的时候就去试试吧,原来是12 byte ,这又是怎么回事呢?同样的类,只是改变了成员变量的位子,怎么就会多出4 byte的存储空间?其实这一切又是由变量的存储规则造成的。对于一个类来说,它里面的成员变量(这里单指非静态的成员变量.静态的成员变量是在全局存储区,不在类的size中)是按声明的顺序存储在内存空间中的。在第一种的情况中,它们紧紧的排列在一起,除了由于alignment所浪费的1 byte空间外,它们几乎用了最小的存储空间;而在第二种情况中,它们则不是排列得那么紧密了,错误就在于char d ,它一个人就占用了4 byte 。为什么它会占用4 byte呢,其实责任也不全在它,后面的int a也有不可推卸的责任。Int 型数据在VC++编译器中正好是占用4 byte的,等于一个alignment量,而这4 byte一定是密不可分的。当char d占用了1 byte后,其后空出了3 byte(对于一个alignment量来说),而一个int型数据不能被拆成3 byte +1byte来存储,那样编译器将无法识别,因此int a只有向后推到下一个alignment的开始,这样char d就独占了4 byte ,中间有3 byte浪费掉了。而后面的char b和char c依旧紧密排列,最后又由于alignment调整2 byte ,整个类的大小就变为了12 byte 。
看了这个例子,是不是该反省以前随意定义成员变量了?如果你要定义一个含3个int型数据和4个char型数据的类,本来最优化的方法只需要16 byte ,而你却随意的定义成如下的样子:
class F{
private:
char c1;
int i1;
char c2;
int i2;
char c3;
int i3;
char c4;
};
看看结果是什么,这个类竟然要占据28 byte的空间,比刚才整整大了12 byte!
再来看看继承的时候,成员变量是怎样存放的。我们将第2个例子中的class A 改成三层的继承模式,或许我们在做项目中,真的会遇到这样的情况。
class A1{
private:
int a;
char b;
};
class A2: public A1{
private:
char c;
};
class A3:public A2{
private:
char d;
};
现在我们来预测一下class A3 的大小,是8 byte吗?不,结果竟是16 byte ,竟然整整多了1倍。这是为什么呢?按照成员变量的排列顺序,int a,char b,char c,char d应该紧密的排列在一起,8 byte没错。但事实并非如此,这些都是因为继承而造成的。知道“在继承关系中,基类子对象在派生类中会保持原样性”吗?或许这样专业的一句话,你并不能明白是什么意思,那么听我下面的分析。在为派生类分配内存空间的时候,都是先为基类分配一块内存空间,而所谓的“原样性”是指基类原本在内存空间中是什么样子,那么它在派生类里分配的时候就是什么样子。拿这个例子来说,class A1占据了8 byte的空间,其中int a占4 byte ,char b占1 byte ,因alignment而填补3 byte 。对于class A1来说,占据8 byte空间没什么好抱怨的,但是class A2呢?轻率的程序员会认为,class A2只在class A1的基础上增加了唯一一个char c ,那么它应该会和char b绑在一起,占用原本用来填补空间的1 byte ,于是class A2的大小是8 byte,其中2 byte用于填补空间。然而事实上,char c是被放在填补空间所用的3 byte之后,因为在class A2中分配的class A1应该完全保持原样,于是class A2的大小变成12 byte ,而不是8 byte了,其中有6 byte浪费在填补空间上。相同的道理使得class A3 的大小是16 byte ,其中9 byte用于填补空间。
那么也许你会问,既然“原样性”会造成这样多的空间浪费,那么编译器为什么还要这样做呢?其实这样做是有它的必要的。我们考虑下面这种情况:
A1* pA1=new A1();
A1* pA2=new A2();
*pA1=*pA2;
我们定义了两个A1型指针,一个指向A1对象,一个指向A2对象。现在我们执行一个默认的复制操作(复制一个个的成员变量),那么这样一个操作应该是把pA2所指的对象的A1那部分完全复制到pA1所指的对象里。假设编译器不遵循“原样性”,而是将派生类的成员和基类的成员捆绑在一起存放,去填补空间,那么这样的操作变会产生问题了。A1和A2都占8 byte ,pA2会将其所指的8 byte空间里的内容全部复制给pA1所指的对象,那么pA1所指的对象本来只有2个数据,3 byte的填补空间,而复制后却变成了3个数据,2 byte的填补空间了,对于char c ,我们并不想把它复制过来的。这样完全破坏了原语意,而这样引起的bug几乎是无法察觉的。 ////////////////////////////////// 附注 五大内存分区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多
struct能包含成员函数吗? 能!
struct能继承吗? 能!!
struct能实现多态吗? 能!!!
有很多人应该已经知道这样一个事实,但总有些不知道的人,看到这些会感到很惊讶。是的,当我第一次注意到这个事实的时候,我也同样很吃惊。
既然这些它都能实现,那它和class还能有什么区别?
最本质的一个区别就是默认的访问控制,体现在两个方面: 1)默认的继承访问权限。struct是public的,class是private的。 如果不知道什么是public继承,什么是private继承的,可以去查书,这里暂不讨论。 你可以写如下的代码: struct A { char a; }; struct B : A { char b; }; 这个时候B是public继承A的。如果都将上面的struct改成class,那么B是private继承A的。这就是默认的继承访问权限。所以我们在平时写类继承的时候,通常会这样写: struct B : public A 就是为了指明是public继承,而不是用默认的private继承。 当然,到底默认是public继承还是private继承,取决于子类而不是基类。我的意思是,struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。如下: struct A{}; class B : A{}; //private继承 struct C : B{}; //public继承 2)struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。
注意我上面的用词,我依旧强调struct是一种数据结构的实现体,虽然它是可以像class一样的用。我依旧将struct里的变量叫数据,class内的变量叫成员,虽然它们并无区别。其实,到底是用struct还是class,完全看个人的喜好,你可以将你程序里所有的class全部替换成struct,它依旧可以很正常的运行。但我给出的最好建议,还是:当你觉得你要做的更像是一种数据结构的话,那么用struct,如果你要做的更像是一种对象的话,那么用class。
当然,我在这里还要强调一点的就是,对于访问控制,应该在程序里明确的指出,而不是依靠默认,这是一个良好的习惯,也让你的代码更具可读性。
说到这里,很多了解的人或许都认为这个话题可以结束了,因为他们知道struct和class的“唯一”区别就是访问控制。很多文献上也确实只提到这一个区别。
但我上面却没有用“唯一”,而是说的“最本质”,那是因为,它们确实还有另一个区别,虽然那个区别我们平时可能很少涉及。那就是:“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。这一点在Stanley B.Lippman写的Inside the C++ Object Model有过说明。
问题讨论到这里,基本上应该可以结束了。但有人曾说过,他还发现过其他的“区别”,那么,让我们来看看,这到底是不是又一个区别。 还是上面所说的,C++中的struct是对C中的struct的扩充,既然是扩充,那么它就要兼容过去C中struct应有的所有特性。例如你可以这样写:
struct A //定义一个struct
{
char c1;
int n2;
double db3;
};
A a={'p',7,3.1415926}; //定义时直接赋值
也就是说struct可以在定义的时候用{}赋初值。那么问题来了,class行不行呢?将上面的struct改成class,试试看。报错!噢~于是那人跳出来说,他又找到了一个区别。我们仔细看看,这真的又是一个区别吗?
你试着向上面的struct中加入一个构造函数(或虚函数),你会发现什么?对,struct也不能用{}赋初值了。的确,以{}的方式来赋初值,只是用一个初始化列表来对数据进行按顺序的初始化,如上面如果写成A a={'p',7};则c1,n2被初始化,而db3没有。这样简单的copy操作,只能发生在简单的数据结构上,而不应该放在对象上。加入一个构造函数或是一个虚函数会使struct更体现出一种对象的特性,而使此{}操作不再有效。事实上,是因为加入这样的函数,使得类的内部结构发生了变化。而加入一个普通的成员函数呢?你会发现{}依旧可用。其实你可以将普通的函数理解成对数据结构的一种算法,这并不打破它数据结构的特性。至于虚函数和普通成员函数有什么区别,我会具体写篇文章讨论。 那么,看到这里,我们发现即使是struct想用{}来赋初值,它也必须满足很多的约束条件,这些条件实际上就是让struct更体现出一种数据机构而不是类的特性。那为什么我们在上面仅仅将struct改成class,{}就不能用了呢?其实问题恰巧是我们之前所讲的——访问控制!你看看,我们忘记了什么?对,将struct改成class的时候,访问控制由public变为private了,那当然就不能用{}来赋初值了。加上一个public,你会发现,class也是能用{}的,和struct毫无区别!!! 做个总结,从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。所以我会提出什么时候用struct什么时候用class的建议。如果你有不同的看法,欢迎讨论。 //******************* 我的读后感: 1. 类的默认构造函数?(虚拟函数?析构函数?虚拟析构函数?)What is behind the scene? 等弄完这阵子有时间得看看《inside the C++ object model》 2. COM中在<objbase.h>中的#define interface struct定义给我留下了很深的印象! //******************再转一篇 也是跟类的内存结构有关的 http://blog.sina.com.cn/s/blog_54ce5b83010000z9.html 许多同学可能在学习C++的时候,都会感到一定的困惑,继承到底是怎样分配空间的,多态到底是如何完成的,许许多多的问题,必须挖掘到C++底层处理机制,才能搞明白。有许多C程序员也并不认同C++,他们认为C++庞大又迟缓,其更重要的原因是,他们认为“C++是在你的背后做事情”。的确,C++编译器背着程序员做了太多的事情,所以让很多不了解其底层机制的人感到困惑。想成为一个优秀的程序员,那么这样的困惑就不应该存在,只有了解了底层实现模型,才能写出效率较高的代码,自信心也比较高。
我们先从一个简单但有趣的例子谈起。有如下的4个类:
class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
上面的4个类中,没有任何一个类里含明显的数据,之间只是表示了继承关系,那么如果我们用sizeof 来测量它们的大小,将会得到什么结果呢?
你可能会认为,既然没有任何数据和方法,大小当然为0,而结果肯定会出乎你的意料,即使是class X 的大小也不为0。
在不同的编译器上,将会得到不同的结果,而在我们现在最常用的VC++编译器上,将得到如下的结果:
sizeof X 的结果是 1 。
sizeof Y 的结果是 4 。
sizeof Z 的结果是 4 。
sizeof A 的结果是 8 。
惊讶吗?那么为什么会得到这样的结果?让我们一个一个来分析。
对于一个空的class,事实上并不是空的,它有一个隐晦的1 byte ,那是被编译器安插进去的一个char 。这使得这个class 的两个对象得以在内存中配置独一无二的地址,这就是为什么 sizeof X 的结果是 1。
那么Y和Z呢,怎么会占用 4 byte ?其实它们的大小受到三个因素的影响:
1. 语言本身所造成的额外负担:当语言支持多态时,就会导致一些额外负担。在派生类中,这个额外负担表现在一个指针上,它是用来指向一个被称作“虚函数列表”的表格。而在VC++编译器上,指针的大小正好是 4 byte 。
2. 编译器对于特殊情况所提供的优化处理:在class Y 和 class Z 中,也将带上它们因为继承class X 而带来的 1 byte ,传统上它被放在派生类的固定部分的尾端。而某些编译器(正如我们现在所讨论的VC++编译器)会对空的基类提供特殊的处理,在这个策略下,一个空的基类被视为派生类对象最开头的一部分,也就是说它并没有花费任何额外空间(因为既然有了成员,就不需要原本为了空类而安插一个char了),这样也就节省了这 1 byte 的空间。事实上,如果某个编译器没有提供这种优化处理,你将发现class Y 和 class Z 的大小将是8 byte ,而不仅仅是5 byte 了,原因正如下面第3点所讲。
3. “对齐”(Alignment)机制:在大多数机器上,群聚的结构体大小都会受到alignment的限制,使它们能够更有效率地在内存中被存取。Alignment 就是将数值调整到某数的整数倍。在32位计算机上,通常alignment 为 4 byte(32位),以使总线达到最大的“吞吐量”,在这种情况下,如上面所说,如果 class Y 和class Z 的大小为 5 byte ,那么它们必须填补 3 byte ,最终得到的结果将是 8 byte 。是不是开始感谢VC++编译器幸好有这样的优化机制,使得我们节约了不少内存空间。
最后,我们再来看看 class A ,它的大小为什么是 8 byte ?显而易见,它继承了class Y 和class Z ,那么它的大小直接就把 class Y 和class Z 的大小加起来就够了。真有这么简单吗?实际上这只是一个巧合而已,这是因为之前编译器的优化机制掩盖这里的一些事实。对于没有优化的 class Y 和class Z 来说,他们的大小都是8 byte ,那么继承了它们两个的 class A 将是多大呢?16 byte?如果你有这样的编译器试一下的话,你会发现答案是12 byte 。怎么会是12 byte 呢?记住,一个虚拟继承的基类只会在派生类中存在一份实体,不管它在 class 继承体系中出现了多少次!class A的大小由下面几部分决定:
l 被大家共享的唯一一个 class X的实体,大小为1 byte。
l 基类class Y 的大小,减去因虚拟继承的基类class X而配置的大小,也就是4 byte 。基类class Z的算法相同,它们加起来就是8 byte 。
l class A自己的大小,0 byte 。
l class A 的alignment的大小(如果有的话)。前述三项的总和是9 byte ,那么调整到4 byte的整数倍,也就是12 byte 。
我们前面讨论的VC++编译器得出的结果之所以是8 byte ,是因为 class X 实体的那1 byte被拿掉了,于是额外的3 byte也同样不必了,因此就直接把class Y 和class Z的大小加起来,得到8 byte 。这个例子看懂了吗?是不是对C++的底层机制开始感兴趣了?那么我们再来举一个同样有趣的例子。
有这样一个类:
class A {
private:
int a;
char b;
char c;
char d;
};
它的大小是多少呢?
如果你有记得我之前提到的alignment机制的话,你应该会猜到它的大小是8 byte 。的确如此,int a占用4 byte ,char b , char c 和char d各占1 byte ,加起来是7 byte ,再加上alignment额外带来的1 byte ,总共是8 byte 。
瞧,就是这么简单,那么现在我们把里面的成员变量换换位置,如下:
class A {
private:
char d;
int a;
char b;
char c;
};
我们将char d拿到第一个位子,放在int a之前。那么现在你能告诉我class A的大小是多少呢?你肯定不会再猜8 byte了,因为你会觉得这与上面似乎有些不同,但你不能肯定到底是多大。不敢确定的时候就去试试吧,原来是12 byte ,这又是怎么回事呢?同样的类,只是改变了成员变量的位子,怎么就会多出4 byte的存储空间?其实这一切又是由变量的存储规则造成的。对于一个类来说,它里面的成员变量(这里单指非静态的成员变量.静态的成员变量是在全局存储区,不在类的size中)是按声明的顺序存储在内存空间中的。在第一种的情况中,它们紧紧的排列在一起,除了由于alignment所浪费的1 byte空间外,它们几乎用了最小的存储空间;而在第二种情况中,它们则不是排列得那么紧密了,错误就在于char d ,它一个人就占用了4 byte 。为什么它会占用4 byte呢,其实责任也不全在它,后面的int a也有不可推卸的责任。Int 型数据在VC++编译器中正好是占用4 byte的,等于一个alignment量,而这4 byte一定是密不可分的。当char d占用了1 byte后,其后空出了3 byte(对于一个alignment量来说),而一个int型数据不能被拆成3 byte +1byte来存储,那样编译器将无法识别,因此int a只有向后推到下一个alignment的开始,这样char d就独占了4 byte ,中间有3 byte浪费掉了。而后面的char b和char c依旧紧密排列,最后又由于alignment调整2 byte ,整个类的大小就变为了12 byte 。
看了这个例子,是不是该反省以前随意定义成员变量了?如果你要定义一个含3个int型数据和4个char型数据的类,本来最优化的方法只需要16 byte ,而你却随意的定义成如下的样子:
class F{
private:
char c1;
int i1;
char c2;
int i2;
char c3;
int i3;
char c4;
};
看看结果是什么,这个类竟然要占据28 byte的空间,比刚才整整大了12 byte!
再来看看继承的时候,成员变量是怎样存放的。我们将第2个例子中的class A 改成三层的继承模式,或许我们在做项目中,真的会遇到这样的情况。
class A1{
private:
int a;
char b;
};
class A2: public A1{
private:
char c;
};
class A3:public A2{
private:
char d;
};
现在我们来预测一下class A3 的大小,是8 byte吗?不,结果竟是16 byte ,竟然整整多了1倍。这是为什么呢?按照成员变量的排列顺序,int a,char b,char c,char d应该紧密的排列在一起,8 byte没错。但事实并非如此,这些都是因为继承而造成的。知道“在继承关系中,基类子对象在派生类中会保持原样性”吗?或许这样专业的一句话,你并不能明白是什么意思,那么听我下面的分析。在为派生类分配内存空间的时候,都是先为基类分配一块内存空间,而所谓的“原样性”是指基类原本在内存空间中是什么样子,那么它在派生类里分配的时候就是什么样子。拿这个例子来说,class A1占据了8 byte的空间,其中int a占4 byte ,char b占1 byte ,因alignment而填补3 byte 。对于class A1来说,占据8 byte空间没什么好抱怨的,但是class A2呢?轻率的程序员会认为,class A2只在class A1的基础上增加了唯一一个char c ,那么它应该会和char b绑在一起,占用原本用来填补空间的1 byte ,于是class A2的大小是8 byte,其中2 byte用于填补空间。然而事实上,char c是被放在填补空间所用的3 byte之后,因为在class A2中分配的class A1应该完全保持原样,于是class A2的大小变成12 byte ,而不是8 byte了,其中有6 byte浪费在填补空间上。相同的道理使得class A3 的大小是16 byte ,其中9 byte用于填补空间。
那么也许你会问,既然“原样性”会造成这样多的空间浪费,那么编译器为什么还要这样做呢?其实这样做是有它的必要的。我们考虑下面这种情况:
A1* pA1=new A1();
A1* pA2=new A2();
*pA1=*pA2;
我们定义了两个A1型指针,一个指向A1对象,一个指向A2对象。现在我们执行一个默认的复制操作(复制一个个的成员变量),那么这样一个操作应该是把pA2所指的对象的A1那部分完全复制到pA1所指的对象里。假设编译器不遵循“原样性”,而是将派生类的成员和基类的成员捆绑在一起存放,去填补空间,那么这样的操作变会产生问题了。A1和A2都占8 byte ,pA2会将其所指的8 byte空间里的内容全部复制给pA1所指的对象,那么pA1所指的对象本来只有2个数据,3 byte的填补空间,而复制后却变成了3个数据,2 byte的填补空间了,对于char c ,我们并不想把它复制过来的。这样完全破坏了原语意,而这样引起的bug几乎是无法察觉的。 ////////////////////////////////// 附注 五大内存分区
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多
相关阅读 更多 +