http://blog.youkuaiyun.com/cxsjabcabc/article/details/7623630
想知道一个类内部是什么,没有比知道它存储的数据还更直接的了。
Q: 为什么一个不包含任何成员变量的类的大小不是0?
如下代码:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class Test
- {
- public:
- Test()
- {
- std::cout << "Test is called..." << std::endl;
- }
- ~Test()
- {
- std::cout << "~Test is called..." << std::endl;
- }
- };
- int main (int argc, const char * argv[])
- {
- COUT_ENDL(sizeof(Test))
- return 0;
- }
运行结果:
- sizeof(Test) is 1
A: 这在于,如果使用Test创建一个对象,那么这个对象的大小如果是0的话,可能造成不同对象具有相同的地址;这可能导致之后的访问出现不明确的状态。所以,一般,编译器会给大小为0的类中自动插入一个字节的大小,具体多大可能取决于编译器。我们能够肯定的是,它一定不是0.
Q: Test类的构造函数占用类对象的空间吗?
A: 根据上面sizeof得到的值,它应该是不占用的。从实际上说,从代码段的角度,它们和c函数中的全局函数没什么不同;只不过如何访问它们可能有限制而已。经过测试,发现如果想直接获取Test类构造函数的地址,赋值给c风格函数指针,编译器总是报错,必须采用指向类成员函数指针的方式才能使用,所以这里无法给出一个很好证明它类似全局函数的例子。
Q: 如果一个类中含有成员变量,可以通过获取此类对象的地址绕开访问权限函数来修改对象内部数据吗?
A: 通过地址来修改数据,c++无法阻止这样的操作,只要它不越界访问。如下代码:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class A
- {
- public:
- A(int value_one, int value_two):_value_one(value_one), _value_two(value_two) { }
- int value_one() const { return _value_one; }
- int value_two() const { return _value_two; }
- private:
- int _value_one;
- int _value_two;
- };
- int main (int argc, const char * argv[])
- {
- A a(10, 100);
- COUT_ENDL(a.value_one())
- COUT_ENDL(a.value_two())
- A *pa = &a;
- *(int *)pa = 11; // modify a's _value_one
- *((int *)pa + 1) = 111; // modify a's _value_two
- COUT_ENDL(a.value_one())
- COUT_ENDL(a.value_two())
- return 0;
- }
运行结果:
- a.value_one() is 10
- a.value_two() is 100
- a.value_one() is 11
- a.value_two() is 111
显然,此方法有效,而且根本不用担心所操作对应的成员是否是protected或者private的。而且可以确定的是,对象a中成员_value_one地址为a首地址;_value_two的地址紧挨着成员_value_one.
Q: 如果具备继承体系的类对象,成员如何摆放?
A: 如下示例:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class A
- {
- public:
- A(int value_one, int value_two):_value_one(value_one), _value_two(value_two) { }
- int value_one() const { return _value_one; }
- int value_two() const { return _value_two; }
- private:
- int _value_one;
- int _value_two;
- };
- class A_Ex : public A
- {
- public:
- A_Ex(int one, int two, int three):A(one, two), _value_three(three) { }
- int value_three() const { return _value_three; }
- private:
- int _value_three;
- };
- int main (int argc, const char * argv[])
- {
- A_Ex a(10, 100, 1000);
- A *pa = &a;
- COUT_ENDL(*(int *)pa)
- COUT_ENDL(*((int *)pa + 1))
- COUT_ENDL(*((int *)pa + 2))
- return 0;
- }
运行结果:
- *(int *)pa is 10
- *((int *)pa + 1) is 100
- *((int *)pa + 2) is 1000
可以看出,A_Ex类继承A类,它的对象首先存放基类成员信息,接着存放本类成员信息,很简单,很直接。
Q: 如果类中含有虚函数,类大小会发生怎样的变化?
A: 如下代码:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class A
- {
- public:
- A(int value_one, int value_two):_value_one(value_one), _value_two(value_two) { }
- int value_one() const { return _value_one; }
- int value_two() const { return _value_two; }
- virtual void show() const { }
- private:
- int _value_one;
- int _value_two;
- };
- class A_Ex : public A
- {
- public:
- A_Ex(int one, int two, int three):A(one, two), _value_three(three) { }
- int value_three() const { return _value_three; }
- private:
- int _value_three;
- };
- int main (int argc, const char * argv[])
- {
- A a(10, 100);
- A_Ex a_ex(97, 98, 99);
- COUT_ENDL(sizeof(A))
- COUT_ENDL(sizeof(A_Ex))
- return 0;
- }
运行结果(生成32位应用程序):
- sizeof(A) is 12
- sizeof(A_Ex) is 16
如果在函数结束前加上断点,并打印变量a和a_ex内部数据,得到如下:
很明显,可以看到,虚表指针占用了对象最初的部分,后面接着是成员变量。
Q: 虚表指针到底在哪里?
A: 根据上面的输出数据,虚表指针保存在对象首地址处。下面将来证明它的存在:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class A
- {
- public:
- A(int value_one, int value_two):_value_one(value_one), _value_two(value_two) { }
- int value_one() const { return _value_one; }
- int value_two() const { return _value_two; }
- virtual void show() const { std::cout << "show A..." << std::endl; }
- private:
- int _value_one;
- int _value_two;
- };
- class A_Ex : public A
- {
- public:
- A_Ex(int one, int two, int three):A(one, two), _value_three(three) { }
- int value_three() const { return _value_three; }
- virtual void show() const { std::cout << "show A_Ex..." << std::endl; }
- private:
- int _value_three;
- };
- typedef void (*c_style_show)();
- int main (int argc, const char * argv[])
- {
- A a(10, 100);
- A_Ex a_ex(97, 98, 99);
- int *p_vtable = (int *)&a; // get the address of a, also the vtable pointer's pointer
- COUT_ENDL(p_vtable)
- int *vtable = (int *)*p_vtable; // dereference to get the vtable pointer
- COUT_ENDL(vtable)
- // get the first virtual func addr
- int *firstVirtualFunc = (int *)vtable[0];
- COUT_ENDL(firstVirtualFunc)
- // convert it to the c_style func, and call it
- c_style_show show_func = (c_style_show)firstVirtualFunc;
- show_func();
- return 0;
- }
上面的代码,是要刻意取出虚函数表中第一个函数指针(理论上就会是show函数).
对象a虚表指针以及虚表内部函数指针的关系简图如下:
由上图,可知,show_func将被赋值为A类中的show函数。执行结果如下:
- p_vtable is 0xbffff9d8
- vtable is 0x2060
- firstVirtualFunc is 0x1b50
- show A...
同理,可以对a_ex对象进行剖析,找出它的虚表和虚表函数。
其实,从上面的分析,也能间接证明类中的成员函数也可以看成全局函数的特殊形式,只是访问方式有所限制。
Q: 当子类和父类的大小不一致的时候,出现的对象切割,该如何理解?
A: 如果非要将一个较大的对象赋值给较小的对象,而且没有按照特定的转换方式,终究可能会发生数据丢失的问题。如下例子:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class A
- {
- public:
- A(int value_one, int value_two):_value_one(value_one), _value_two(value_two) { }
- int value_one() const { return _value_one; }
- int value_two() const { return _value_two; }
- virtual void show() const { std::cout << "show A..." << std::endl; }
- private:
- int _value_one;
- int _value_two;
- };
- class A_Ex : public A
- {
- public:
- A_Ex(int one, int two, int three):A(one, two), _value_three(three) { }
- int value_three() const { return _value_three; }
- virtual void show() const { std::cout << "show A_Ex..." << std::endl; }
- private:
- int _value_three;
- };
- int main (int argc, const char * argv[])
- {
- A *pa = new A(10, 100);
- A_Ex *pa_ex = new A_Ex(97, 98, 99);
- pa->show();
- pa_ex->show();
- *pa = *pa_ex; // cause slice
- pa->show();
- pa_ex->show();
- delete pa_ex;
- delete pa;
- return 0;
- }
输出结果:
- show A...
- show A_Ex...
- show A...
- show A_Ex...
可以看出,*pa = *pa_ex; 虽然造成了对象切割,但是并没有影响pa对象调用show的结果。调试,具体查看下被切割后的结果:
在两个断点处打印*pa的数据:
可以看出*pa数据确实被对象a_ex的数据替换了,不过也可以看出对象a的虚表指针依然没变,导致了调用show函数没有改变。
Q: 可以改变对象a的虚表指针的值吗?
A: 当然可以。如下代码:
- #include <iostream>
- using namespace std;
- #define COUT_ENDL(str) std::cout << #str << " is " << (str) << std::endl;
- class A
- {
- public:
- A(int value_one, int value_two):_value_one(value_one), _value_two(value_two) { }
- int value_one() const { return _value_one; }
- int value_two() const { return _value_two; }
- virtual void show() const { std::cout << "show A..." << std::endl; }
- A & operator=(const A& a)
- {
- if(this != &a)
- {
- *(int *)this = *(int *)&a;
- _value_one = a._value_one;
- _value_two = a._value_two;
- }
- return *this;
- }
- private:
- int _value_one;
- int _value_two;
- };
- class A_Ex : public A
- {
- public:
- A_Ex(int one, int two, int three):A(one, two), _value_three(three) { }
- int value_three() const { return _value_three; }
- virtual void show() const { std::cout << "show A_Ex..." << std::endl; }
- private:
- int _value_three;
- };
- int main (int argc, const char * argv[])
- {
- A *pa = new A(10, 100);
- A_Ex *pa_ex = new A_Ex(97, 98, 99);
- pa->show();
- pa_ex->show();
- *pa = *pa_ex; // cause slice
- pa->show();
- pa_ex->show();
- delete pa_ex;
- delete pa;
- return 0;
- }
类A中增加了重载赋值运算符的函数,且内部将对象首地址的虚表指针更改。
运行结果:
- show A...
- show A_Ex...
- show A_Ex...
- show A_Ex...
可以看出,对象a的虚表指针已经被更改。
如此简单直白的对象模型, c++继承了c语言的特点。