- 一、C++中继承的基本理解
- 二、多重继承问题
- 三、父类指针指向子类对象
- 四、private/public到底在干嘛
- 五、从继承角度看看多态
一、C++中继承的基本理解
#includeclass A { public: int x=3; int y=4; A(int x, int y) { this->x = x; this->y = y; } A(){} }; class B :public A { public: int x; int y; B(int x, int y) { this->x = x; this->y = y; } }; int main() { B b(1, 2); printf("%dn", b.A::x);//输出3 printf("%dn", sizeof(b));//输出16 return 0; }
上面举了一个最简单的例子,对象b是在main函数中定义的,其存储在main函数的栈空间中,共占用16个字节,下面这个就是b对象在栈空间的情况,其实编译器本身是不知道什么x,y的,它只知道ebp-xx等这种表述,所以即使是变量重名,对它没有影响。
可以看出,这里继承就相当于是复制,A中的成员变量复制了一份给了B中对象,而且位置还在低地址处。
其次从A中继承的变量是什么时候执行了x=3,y=4呢,是在B的构造函数调用之前会调用A的空构造函数,在A的空构造函数中给x和y赋值了(这也就是为什么这里必须要显示的写一个A的空构造函数)
首先,我们知道C++中是允许继承多个父类的,不像JAVA中只能继承一个父类,但是微软并不建议这么做。
#includeclass A { public: int x=1; int y=2; A(int x, int y) { this->x = x; this->y = y; } A(){} }; class B { public: int x = 3; int y = 4; B(int x, int y) { this->x = x; this->y = y; } B() {} }; class C :public A ,public B{ public: int x; int y; C(int x, int y) { this->x = x; this->y = y; } }; int main() { C c(5, 6); printf("%dn", c.A::x);//输出1 printf("%dn", sizeof(c));//输出24 return 0; }
上面c继承了A和B,通过反汇编调试,我们在栈空间中看到下面,即前两个字节是从父类A中复制过来的,后面两个是从B中复制过来的,最后两个是自己的。这里0x00cFFD90就是this指针的值。
三、父类指针指向子类对象#includeclass Persion { public: int age=23; }; class Student:public Persion { public: int grade=97; }; int main() { Student s; Persion* p=&s; printf("%dn", p->age); printf("%dn", *(p+1));//这里直接访问grade无法访问,要采用指针+1方式 return 0; }
上面创建了一个Persion*数据类型的变量p,但是在赋值的时候却给了一个Student类型的变量的地址,此时在编译器的眼里,这个p指向的就是一个Persion类型对象【即使实际上是一个Student】,而Persion对象应该就只有一个成员变量age,因此直接用p->grade是无法访问的,但我们也知道grade成员变量就在下一个4字节处,所以我们直接给p+1就可以访问了。
同样的,如果这里如果是成员函数的话,只能访问到父类中的成员函数。
#includeclass Persion { public: int age=23; private: int sex = 1; }; class Student:public Persion { }; int main() { Student s; printf("%dn", sizeof(s)); printf("%dn", s.age); printf("%pn", &s ); printf("%dn", *((int*)(&s)+1));//得到sex=1,如果不清楚为什么这么写的可以看看我写的关于指针的文章 return 0; }
上面Student实际上是继承了age和sex的,只不过是编译器限制了我们去直接访问sex,但是通过运用指针可以访问到。
五、从继承角度看看多态说多态之前,要说一下虚函数,因为虚函数是实现多态的方式。
#includeclass Persion { public: int age = 25; int sex = 1; virtual int getAge() { return this->age; } virtual int getSex() { return this->sex; } }; int main() { Persion p; Persion* s = &p; printf("%dn", sizeof(p));//输出12 p.getAge(); s->getAge(); return 0; }
上面类中定义了一个虚函数,在返回对象大小的时候返回了12字节,说明虚函数占用了4字节,这4个字节位于对象栈空间的低4字节处,值是指向了一个虚函数表,而虚函数表中是对应函数的首地址。
我们从反汇编角度看一下。
Persion p;
000619BF 8D 4D EC lea ecx,[p] //p表示对象p在main函数栈中的首地址
000619C2 E8 EB F8 FF FF call Persion::Persion (0612B2h) //调用默认构造函数
Persion* s = &p;
000619C7 8D 45 EC lea eax,[p] //p的值给eax
000619CA 89 45 E0 mov dword ptr [s],eax //s地址处的值是p
printf("%dn", sizeof(p));
000619CD 6A 0C push 0Ch
000619CF 68 3C 7B 06 00 push offset string "%dn" (067B3Ch)
000619D4 E8 FE F6 FF FF call _printf (0610D7h)
000619D9 83 C4 08 add esp,8
p.getAge();
000619DC 8D 4D EC lea ecx,[p] //ecx为this指针参数,即对象栈中首地址
000619DF E8 F7 F7 FF FF call Persion::getAge (0611DBh) //E8形式的call
s->getAge();
000619E4 8B 45 E0 mov eax,dword ptr [s] //eax存放对象首地址
000619E7 8B 10 mov edx,dword ptr [eax] //edx存放首地址处的值,即虚函数表地址
000619E9 8B F4 mov esi,esp
000619EB 8B 4D E0 mov ecx,dword ptr [s]
000619EE 8B 02 mov eax,dword ptr [edx]//eax存放getAge第一行代码地址
000619F0 FF D0 call eax //FF形式call
000619F2 3B F4 cmp esi,esp
从反汇编中可以看出对于s->getAge这种通过指针对象来访问虚函数的形式,其采用的是FF这种call,即间接调用。
上面说了最简单的虚函数的形式,如果有继承关系时,这里直接给结论了
(1)父类中有3个虚函数,子类中有3个虚函数,则子类虚函数表中先存放父类虚函数、再放子类虚函数
(2)父类中有3个虚函数,子类中重写了对应的3个虚函数,则子类虚函数表中仅有子类的虚函数。
(3)一个子类继承了两个父类,每个父类均有虚函数,则子类有两个单独的虚函数,至于子类的虚函数放到哪一个里面,我调试的时候是放在前面继承的类中虚函数表中。我用下面的图说明一下情况3.
上面对于虚函数基本上介绍完了,下面说多态,多态也叫动态绑定,因为我们前面分析,通过指针去调用虚函数的时候,反汇编看出是call 某个寄存器(这个寄存器的值我们知道是某个虚函数的地址值),而不是直接call 某个地址 ,即在编译阶段没有把call的地址写死,而是等到运行的时候根据实际虚函数表中的地址值确定调用哪个虚函数。
#includeclass A { public: int a = 1; virtual void get() { printf("A called"); } }; class B :public A { public: int b = 2; virtual void get() { printf("B calledn"); } }; int main() { B b; A* a = &b; a->get();//输出B called,,动态绑定 printf("%dn", (*((int*)a + 2)));//正常无法访问,我们通过指针访问到b return 0; }
上面例子中a虽然是一个A*类型的指针,但它实际指向的是b类对象,我们知道b类对象里面的虚函数表是重写了父类的get方法,因此a-get()调用的是b的虚函数。



