看看C++中class里这些奇怪的东西吧

来源:互联网 发布:亲戚是明星知乎 编辑:程序博客网 时间:2024/04/29 12:33

1 空的class并非不占空间

它占有一个字节的空间,据说这是为了区别对象,编译器为空类默默的安插进了一个char到空对象内。既然这样,那么我门就来看一下,这个对像能否被继承到子对象中呢?

Code:
  1. #include <iostream>   
  2. using namespace std;   
  3. class base   
  4. {   
  5. };   
  6. class derived:public base   
  7. {   
  8. };   
  9. int main()   
  10. {   
  11.     cout << sizeof(derived) << endl;   
  12.     return 0;   
  13. }  

输出结果 1 根据输出结果,还是输出了一个空类的大小,但是若基类中被安插的哪个char能被继承到派生类中的话,那么派生类的大小应改为2才对啊。由此可见这个char并不能被继承或是使用,仅仅是作为一个区别标志而已。那么我们再看一下下面这个程序:

Code:
  1. #include <iostream>   
  2. using namespace std;   
  3. class base   
  4. {   
  5. };   
  6. class derived:public base   
  7. {   
  8. private:   
  9. int x;   
  10. base t;   
  11. };   
  12. int main()   
  13. {   
  14.     cout << sizeof(derived) << endl;   
  15.     return 0;   
  16. }  

大家可以猜测一下输出结果。输出结果为8.也许很出乎你的意料吧。其实这里编译器还是吧空对象的大小当成1了。然后在派生类中根据字节对齐,结果就为8了。

2 类中不占空间的成员

类中并不是所有的成员都占空间的。哪那些不占用空间呢?其实当我们知道那些占用空间,剩下的就是不占用空间的了。占用空间的有:非静态数据成员,虚函数。基本上我就知道这两个。也就是说静态数据成员,静态函数,枚举,非虚函数,typedef这些东西都是不占用空间的。我们带着这些知识来看一个程序:

Code:
  1. #include<iostream>   
  2. using namespace std;   
  3. class DS   
  4. {   
  5.     int i;   
  6.    short int j;   
  7.    union u1{double p; };   
  8. private:   
  9.     enum e1{i1}a;    
  10. };   
  11.   
  12. int main()   
  13. {   
  14.     cout<<sizeof(DS)<<endl;   
  15.     return 0;   
  16. }  

输出结果为12 。为什么为12呢?这里也许很多同学想不通了。。我们一起来看一下:int 4个字节,short int 2 个字节,字节对齐补充为4个字节,union不占空间。为什么不占空间呢?union其实也属于我们上面所说的不占空间的类成员中的一种。enum不占空间。那现在才8个字节啊,为什么结果是12呢?其实大家仔细看一下就知道了。enum这里定义了一个对象a。所以它这里就占用空间了。若是我们这里仅仅是enum{//这里写多少为所谓};它不占用空间。所以问题就出在这个a上。a占用几个字节呢?我们都知道enum定义出来的对象本质上就是一个int型。故它占4个字节,这不12了字节了吗?

对于上面这个程序,大家可以自己试试,无论你往里面加多少个非虚函数,多少个静态成员,多少个类似这样的enum{//这里写多少为所谓};枚举和类似 union u1{double p; };这样的联合体。类对象的大小都不会增加。那么他们占用的空间在那里呢?输出的类对象的大小是属于内存中的那一部分空间呢?这个问题留给大家补充吧,我也不清楚。对于上述说明我们可以看下面程序证明下:

Code:
  1. #include<iostream>   
  2. using namespace std;   
  3. class DS   
  4. {   
  5. public:   
  6.    union u1{double p; };   
  7.     //enum e1{i1}a;    
  8.     static a;   
  9.     static b;   
  10.     void f(){}   
  11.     void g(){}   
  12. };   
  13.   
  14. int main()   
  15. {   
  16.     DS t;   
  17.     cout<<sizeof(t)<<endl;   
  18.     return 0;   
  19. }   

输出结果为1.说明此刻编译器仍把类当做一个空类来处理。噢,我的天啊。不过这样的类还真是有用处。我们可以试想一下。我们若是想在一个类继承层次里面。若是一个基类仅仅是为派生类提供一个接口,而并提供数据。不就是用的这样的类吗?当然我们这里有静态成员的数据。我们可以定义一个仅提供接口的类。STL里好像就使用了好多这样的所谓的空类。

3 虚继承

我们来看一下下面这个程序:

Code:
  1. #include<iostream>   
  2. using namespace std;   
  3. class base   
  4. {   
  5. public:   
  6.  virtual void f()=0;   
  7. };   
  8. class derived1:public base   
  9. {   
  10. };   
  11. class derived2:virtual public base   
  12. {   
  13. };   
  14. int main()   
  15. {   
  16.     cout<<sizeof(derived1)<<endl;   
  17.     cout<<sizeof(derived2)<<endl;   
  18.     return 0;   
  19. }   

输出结果为4,8.大吃一惊吧。呵呵。第一个结果应该是正常的,我想大家对这个不会有异议。因为derived1继承了基类的虚函数。虚函数是占4个字节的。故结果为4.那第二个为什么是8呢。也不应该是4吗?其实这时编译器好像是往虚函数表里又增加了一个指针。具体也不是很清楚,求知中。。

4 我们可以为纯虚函数定义

这点我以前确实不知道。今天才在书上看到。原来我们也可以为纯虚函数定义,我们看下面这个程序:

Code:
  1. #include<iostream>   
  2. using namespace std;   
  3. class base   
  4. {   
  5. public:   
  6.  virtual void f()=0;   
  7. };   
  8. void base::f()   
  9. {   
  10.     cout << "hello world!" << endl;   
  11. }   
  12. class derived1:public base   
  13. {   
  14.     void f()   
  15.     {   
  16.         base::f();   
  17.         cout << "derived1::f()" << endl;   
  18.     }   
  19. };   
  20. class derived2:virtual public base   
  21. {   
  22.     void f()   
  23.     {   
  24.         base::f();   
  25.         cout << "derived2::f()" << endl;   
  26.     }   
  27. };   
  28. int main()   
  29. {   
  30.     base *t = new derived1();   
  31.     t->f();   
  32.     base *d = new derived2();   
  33.     d->f();   
  34.     return 0;   
  35. }  

我们为基类的纯虚函数做了定义。然后我们又在两个派生类进行了重定义,并在定义体里调用了基类的虚函数。呵呵。。具体结果是什么自己试试吧。但是若我们不在派生类中进行重定义的话,派生类也会被编译器默认为虚类的。即无法进行实例化。对于这样的用法又有什么用呢?我们可以设想这样一种情况,即我们不想让客户对我们的基类进行实例化,而我们又像我们的客户在派生类中重定义自己的函数时引用我们基类中的函数部分。这样若是我们将我们的基类中的函数定义为虚函数的话,可以试现我们的后一部分,但可以还是可以实例化,这样我们就可以用我们这个方法,用户不可以进行实例化了,又可以通过调用基类的哪个我们定义过的纯虚函数来引用基类部分。

5 析构函数也可以被继承?

我们都知道我们若是若是准备让一个类作为基类来用的话,一般都会吧它的析构函数定义为虚函数的。为的是多态性,即让我们用一个基类的指针指向一个派生类对象时。我们用delete删除这个基类指针时,可以动态的调用派生类的析构函数来析构派生类部分。否则就会产生内存泄露。我们来看下面这个程序:

Code:
  1. #include<iostream>   
  2. using namespace std;   
  3. class base   
  4. {   
  5. public:   
  6. virtual ~base()   
  7.  {   
  8.     cout << "base::~base()" << endl;   
  9.  }   
  10. };   
  11. class derived:public base   
  12. {   
  13. };   
  14. int main()   
  15. {   
  16.     base *t = new derived();   
  17.     delete t;   
  18.     return 0;   
  19. }  

结果仅仅调用了基类的析构函数,具体派生类的析构函数是否调用了。我们无法看出来。若我们重定义一下,那肯定就调用了。。呵呵。但是在这种情况下到底调用了吗?一个谜底。只有写编译器,定义C++标准的人才能告诉我们正确的答案,因为规则使他们定的。但我们根据我们对C++的学习也是可以猜出答案的。

可是我们仔细分析一下你估计就会迷惑了!现在derived对象的大小为多少。1对吧。也就是编译器为我们安插进去的那个char。这个char是属于base还是derived的呢?不得而知。。暂且不管他吧。。我们看一下析构函数,析构函数是用来干什么的?释放空间。对吧。现在我们的base指针t目前仅仅有一个字节的空间,而我们这里却调用了两次析构函数,这又怎么解释。一个字节的空间需要两次释放?不不。我们换个方式思考,我们看下面这个程序:

Code:
  1. #include<iostream>   
  2. using namespace std;   
  3. class base   
  4. {   
  5. public:   
  6.  ~base()   
  7.  {   
  8.     cout << "base::~base()" << endl;   
  9.  }   
  10. };   
  11. class derived:public base   
  12. {   
  13. public:   
  14.     ~derived()   
  15.     {   
  16.         cout << "derived::~derived()" << endl;   
  17.     }   
  18.        
  19. };   
  20. int main()   
  21. {   
  22.     cout << sizeof(derived) << endl;   
  23.     derived t;   
  24.     return 0;   
  25. }  

这个程序中明确输出了派生类有一个字节的空间。然而当我们的程序结束时却调用了两次析构函数。我们都知道当我们一个派生类对象的生命期结束时会调用派生类的析构函数析构派生类部分,然后调用基类的构造函数调用基类部分。然而我们这个派生类中仅有一个字节的空间啊。到底它是怎么析构的呢。迷。。迷。。。。

我们再看一下我们这个题目,析构函数也可以被继承?为什么这么问呢?它确实会令人产生联想。设想多态行为。是有一个基类对象的指针或引用指向派生类对象,然后调用虚函数产生的。被称为执行期行为,即只有到执行期才能确定掉用的具体对象。然而我们的析构函数的多态行为,和这个很是类似。这又是怎么回事?我们的析构函数该是不能被继承啊。。毕竟他在每个类中的名字都不一样。然而这样的多态行为是怎么产生的?仅仅是一个规则,还是有章可循的?。。