关于C++类的问题总结(一)

来源:互联网 发布:java enum 数字 编辑:程序博客网 时间:2024/04/30 03:58

关于C++类的问题总结(一)
作者:姜江
QQ:457283
E-mail:jznsmail@163.net

    自己在学习C++的过程中曾经碰到了许多问题,尤其是对类的学习。所以自己通过做了一些试验,将其总结了一下,算是自己的一个学习
总结,同样也是想给那些正在被这些问题迷惑的人起到抛砖引玉的作用。如果在文章中有一些不对的地方,或者你有你自己的看法,欢迎批评
指针。希望我们可以共同进步。

1.空类的大小
  例如:
  class A {};
  类A的大小是多少?等于0么?
  下面我们通过一个试验来说明问题,程序如下:
  #include <iostream>

  using namespace std;

  class A
  {
  };

  int main()
  {
 A a;
 cout<<"Size of A is = "<< sizeof(a) <<endl;
 cout<<"Address of A is = "<< &a <<endl;
 return 0;
 }

 程序在我的电脑上的输出结果是:
 Size of A is = 1
 Address of A is = 0012FF7C
 
    看了这个结果是不是傻眼了?等于1?没有定义任何成员变量怎会等于1?没错,你没有看错,确实是1。因为它暗自生成了1byte,是编译
器为我们做的,目的是为了标识这个类的两个不同的类在内存中有不同的地址。想想,如果没有这1byte,分配了两个类A的对想a,b 那么如何
标识?

2.含有成员变量的类的大小
  我们还是通过程序来说明问题,程序如下:
  #include <iostream>

  using namespace std;

  class A
  {
  public:
 int m_test1;
 int m_test2;
  };

  int main()
  {
 A a;
 cout<< "Size of A is = " << sizeof(a) << endl;
 cout<< "Address of A is = " << &a <<endl;
 return 0;
  }

  在我的电脑上程序输出的结果是:
  Size of A is = 8
  Address of A is = 0012FF78
 
  一看结果大家就马上会明白,因为在win32下一个int类型占用4byte,所以2个就理所当然的等于8咯,嗯没错,但是别高兴太早,我们看下面
这个程序:
  #include <iostream>

  using namespace std;

  class A
  {
  public:
 int m_test1;
 int m_test2;
 char m_trick[1];  //加入了这行代码
  };

  int main()
  {
 A a;
 cout<< "Size of A is = " << sizeof(a) << endl;
 cout<< "Address of A is = " << &a <<endl;
 return 0;
  }

  在我的电脑上程序输出结果是:
  Size of A is = 12
  Address of A is = 0012FF74

  为什么程序输出的结果是12而不是我们预期的9(int+int+char = 9byte)?这是因为有个内存对齐的原则,也就是将数值调整到某位数的整数
倍,在32位下这个数是4,这样可以使bus的传输最快。所以我们定义的含有一个元素的char数组被对齐为占用4个字节,所以一共是4+4+4=12。

3.类继承的情况
  继承一个类,那么派生类的成员变量是如何排列的呢?我们看看代码:
  #include <iostream>

  using namespace std;

  class A
  {
  public:
 int m_test1;
 int m_test2;
 char m_trick[1];  //加入了这行代码,为了产生内存对齐,这样好观察。嘿嘿
  };

  class B : public A
  {
  public:
 int m_test3;
 void printAddress()
 {
  cout << "Address of B::m_test1 is = " << &m_test1 << endl;
  cout << "Address of B::m_test2 is = " << &m_test2 << endl;
  cout << "Address of B::m_test3 is = " << &m_test3 << endl;
  cout << "Address of B::m_trick is = " << &m_trick << endl;
 }
  };

  int main()
  {
 A a;
 cout << "Size of A is = " << sizeof(a) <<endl;
 cout << "Address of A is = " << &a <<endl;

 B b;
 cout << "Size of B is = " << sizeof(b) <<endl;
 cout << "Address of B is = " << &b <<endl;

 cout <<"The member of B Address is: " << endl;
 b.printAddress();

 return 0;
  }

  在我的电脑上程序的输出结果是:
  Size of A is = 12
  Address of A is = 0012FF74
  Size of B is = 16
  Address of B is = 0012FF64
  The member of B Address is:
  Address of B::m_test1 is = 0012FF64
  Address of B::m_test2 is = 0012FF68
  Address of B::m_test3 is = 0012FF70
  Address of B::m_trick is = 0012FF6C
 
  注意这个结果,m_test1,m_test2,m_trick是从类A中继承来的,m_test3是派生类的成员变量,因为如果派生类中成员变量是紧接着基类,那
么应该m_test3的地址是0012FF6D而不应该是0012FF70,所以可以知道派生类的成员变量并不是紧接着基类成员变量后面定义的,而为什么m_test3
的地址是0012FF70是因为在m_trick后面有3个字节的内存对齐。

4.含有虚函数的类继承
  如果类中含有虚成员函数,那么内存的结构会是怎样的呢?我们通过分析程序来看看:
  #include <iostream>

  using namespace std;

  class A
  {
  public:
 virtual void fun() { cout << " A::fun " <<endl; }
  };

  int main()
  {
 A a;
 cout << "Size of A is = " << sizeof(a) <<endl;
 cout << "Address of A is = " << &a <<endl;
 return 0;
  }
 
  这个类A的大小是多少呢?也许你会说是0,因为没有定义任何成员变量。嗯,不对,我们看看结果。
  在我的电脑上输出的结果是:
  Size of A is = 4
  Address of A is = 0012FF7C
 
  为什么会是4而不是0呢,原来是因为虚函数是通过一个vptr(虚函数指针)的指针指向一个vtbl(虚函数表)来完成函数调用的。而在生成
类对象的时候编译器给我们做了一些工作,那就是建立了一个vptr和一个vtbl,把函数地址存放倒vtbl里,然后用类中编译器建立的vptr指针
指向vtbl。
  也许你还会怀疑这个结果,觉得是不是保存的涵数指针而不是虚函数指针?嗯那么让我们看看多虚函数的情况。
  pace std;

  class A
  {
  public:
 virtual void fun1() { cout << "A::fun1" << endl; }
 virtual void fun2() { cout << "A::fun2" << endl; }
 virtual void fun3() { cout << "A::fun3" << endl; }
  };

  int main()
  {
 A a;
 cout << "Size of A is = " << sizeof(a) << endl;
 cout << "Address of A is = " << &a <<endl;
 return 0;
  }

  在我的电脑上输出的结果是:
  Size of A is = 4
  Address of A is = 0012FF7C

  嗯,没错结果还是4,正如我上面说道的,编译器自动为我们加入了一个vptr指针,它指向一个vtbl,由这个vtbl来维护虚函数。
  你可能还会问,如果有虚函数表(vptr),那它安放在那里?在内存中vptr和成员变量是如何排列的?我的代码如下:
  #include <iostream>

  using namespace std;

  class A
  {
  public:
 int m_test1;
 int m_test2;
 A(const int x = 0, const int y = 0):m_test1(x),m_test2(y){}
 int GetTest1() const { return m_test1; }
 int GetTest2() const { return m_test2; }
 virtual void fun() {}
  };

  int main()
  {
 A a(10,20);

 int* pInt = (int*)&a;
 *(pInt + 0) = 100;
 *(pInt + 1) = 200;
 
 cout << "test1 = " << a.GetTest1() <<endl;
 cout << "test2 = " << a.GetTest2() <<endl;
 return 0;
  }
 
  如果没有看到结果你会不会利马答到,输出应该是100,200。嗯好像有点问题,不信?我们来看看我的输出结果:

  test1 = 200
  test2 = 20

  哦,确实不是开始猜测的那样,为什么?到底出了什么问题?还记得我上面提到的么?对于虚函数编译器自动生成了一个vptr么?没错成员
函数的第一个位置存放的就是这个vptr了。所以才会造成这样的结果。如果我们把程序做一点点修改,如下:

  #include <iostream>

  using namespace std;

  class A
  {
  public:
 int m_test1;
 int m_test2;
 A(const int x = 0, const int y = 0):m_test1(x),m_test2(y){}
 int GetTest1() const { return m_test1; }
 int GetTest2() const { return m_test2; }
  };

  int main()
  {
 A a(10,20);

 int* pInt = (int*)&a;
 *(pInt + 0) = 100; //注意这一行!
 *(pInt + 1) = 200; //注意这一行
 
 cout << "test1 = " << a.GetTest1() <<endl;
 cout << "test2 = " << a.GetTest2() <<endl;
 return 0;
  }

  在我电脑上输出的结果是:
  test1 = 100
  test2 = 200

  当我修改了一下偏移量,这样输出结果就正确了,确实在成员函数的第一个位置上放置了一个vptr。

5.深入了解虚函数
  上面我们提到了vptr和vtbl,那么让我们通过程序来看看这两个的分配情况:
  4#include <iostream>

  using namespace std;

  class A
  {
 virtual fun() { cout << "A::fun" <<endl; }
  };

  int main()
  {
 A a;
 cout << "Address of virtual pointer is = " << (int*)(&a+0) << endl;
 cout << "Value at virtual pointer is = " << (int*)*(int*)(&a+0) <<endl;
 return 0;
  }

  在我的电脑上输出结果是:
  Address of virtual pointer is = 0012FF7C
  Value at virtual pointer is = 0046C06C

  上面我用了两个小技巧来得到虚函数指针的地址,和虚函数表的地址。
  (int*)(&a+0)首先取类的地址,然后加上0的偏移量,为什么加上0,是因为在4点中已经知道vptr在成员函数的第一个位置。
  (int*)*(int*)(&a+0) 先取得虚函数表的指针的内容,然后再求其地址,这样就得到了vtbl的地址了。

  既然这样,那么让我们通过这个技巧来调用一下虚函数:)
  #include <iostream>

  using namespace std;

  typedef void (*Fun)(void);

  class A
  {
 virtual void fun() { cout << "A::fun" <<endl; }
  };

  int main()
  {
 A a;
 cout << "Address of virtual pointer is = " << (int*)(&a+0) << endl;
 cout << "Value at vritual pointer is = " << (int*)*(int*)(&a+0) <<endl;
 cout << "Value at first entry of virtual table is = " << (int*)*(int*)*(int*)(&a+0) <<endl;
 cout << endl << "Executing vritual funciton" << endl << endl;
   
 Fun pFun = (Fun)*(int*)*(int*)(&a+0);
 pFun();

 return 0;
  }
  
  在我的电脑里输出结果是:

  Address of virtual pointer is = 0012FF7C
  Value at vritual pointer is = 0046C0C0
  Value at first entry of virtual table is = 0040128F

  Executing vritual funciton

  A::fun

  嗯,调用了虚函数,果真是通过vptr指向vtbl,vtbl保存了虚函数的地址。
  那问题又来了,虚函数是如何在内存中排列的?是如何来判断虚函数表的长度的?答案是NULL,我们通过程序来看看:
 
  #include <iostream>

  using namespace std;

  typedef void (*Fun)(void);

  class A
  {
 virtual void fun() { cout << "A::fun" <<endl; }
 virtual void fun1() { cout << "A::fun1" <<endl; }
  };

  int main()
  {
 A a;
 cout << "Address of virtual pointer is = " << (int*)(&a+0) << endl;
 cout << "Value at vritual pointer is = " << (int*)*(int*)(&a+0) <<endl;
 cout << "Value at first entry of virtual table is = "
  << (int*)*(int*)*(int*)(&a+0) <<endl;
 cout << "Value at 2nd entry of virtual table is = "
  << (int*)*((int*)*(int*)(&a+0)+1) <<endl;
 cout << "Value at 3rd entry of virtual table is = "
  << (int*)*((int*)*(int*)(&a+0)+2) <<endl;
 cout << "Value at 4th entry of virtual table is = "
  << (int*)*((int*)*(int*)(&a+0)+3) <<endl;
 cout << endl << "Executing vritual funciton" << endl << endl;
   
 Fun pFun = (Fun)*(int*)*(int*)(&a+0);
 pFun();

 return 0;
  }

  在我的电脑上的输出结果是:

  Address of virtual pointer is = 0012FF7C
  Value at vritual pointer is = 0046C15C
  Value at first entry of virtual table is = 00401294
  Value at 2nd entry of virtual table is = 004010DC
  Value at 3rd entry of virtual table is = 00000000
  Value at 4th entry of virtual table is = 663A3A41

  因为我们定义了两个虚函数,所以输出到第三个虚函数的地址时,输出的是0,这样我们可以知道,确实是我刚才说的,须函数表通过NULL来
判断其长度。
 
  关于多层继承的情况我将在后续的文章中给出。
  (待续。。。)

 

 

 

 


 

原创粉丝点击