目录
3.1 C++继承时的名字遮蔽
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
3.2 C++继承时的内存模型
有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。例子:
#include <stdio.h>
#include <string.h>
class TestA
{
public:
int m_a;
int m_b;
};
class TestB : public TestA
{
public:
int m_c;
};
int main()
{
TestB obj_b;
memset((void *)&obj_b, 0, sizeof(obj_b));
obj_b.m_a = 10;
obj_b.m_b = 20;
obj_b.m_c = 30;
int *pTB = (int *)&obj_b;
printf("%d\n", *pTB);
*pTB++;
printf("%d\n", *pTB);
*pTB++;
printf("%d\n", *pTB);
return 0;
}
输出:
10
20
30
可见,假设obj_b的起始地址为0x1100,那么它的内存分布如下图所示:
3.3 有成员变量遮蔽时的内存分布
当基类的成员变量被遮蔽时,仍然会留在派生类对象的内存中,派生类新增的成员变量始终排在基类的后面,例子:
#include <stdio.h>
#include <string.h>
class TestA
{
public:
int m_a;
int m_b;
};
class TestB : public TestA
{
public:
int m_b;
int m_c;
};
int main()
{
TestB obj_b;
memset((void *)&obj_b, 0, sizeof(obj_b));
printf("sizeof(TestB) = %d\n", sizeof(TestB));
obj_b.m_a = 10;
obj_b.m_b = 20;
obj_b.m_c = 30;
int *pTB = (int *)&obj_b;
printf("%d\n", *pTB);
*pTB++;
printf("%d\n", *pTB);
*pTB++;
printf("%d\n", *pTB);
*pTB++;
printf("%d\n", *pTB);
return 0;
}
输出:
sizeof(TestB) = 16
10 (m_a)
0 (TestA::m_b)
20 (m_b)
30 (m_c)
可以看出,派生类对象会包含所有基类的成员变量,而且新增的成员变量始终排在基类成员变量的后面。
3.4 多继承
当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。
#include <stdio.h>
class A
{
public:
int m_a;
int m_b;
};
class B
{
public:
int m_a;
};
class C : public A, public B
{
public:
void show();
};
void C::show()
{
printf("m_a:%d m_b:%d", m_a, m_b);
}
int main()
{
C objc;
objc.m_a = 10;
objc.m_b = 20;
objc.show();
return 0;
}
此时编译会报错,因为A和B都有成员变量m_a,在使用时编译器不知道使用哪一个,这时就需要加类名和域解析符::,修改如下:
void C::show()
{
printf("m_a:%d m_b:%d\n", A::m_a, m_b);
}
int main()
{
C objc;
objc.A::m_a = 10;
objc.m_b = 20;
objc.show();
return 0;
}
输出:
m_a:10 m_b:20
3.5 多继承时的内存模型
#include <stdio.h>
class A
{
public:
int m_a;
int m_b;
};
class B
{
public:
int m_b;
int m_c;
};
class C : public A, public B
{
public:
int m_a;
int m_c;
int m_d;
};
int main()
{
C objc;
objc.A::m_a = 10;
objc.A::m_b = 20;
objc.B::m_b = 30;
objc.B::m_c = 40;
objc.C::m_a = 50;
objc.C::m_c = 60;
objc.C::m_d = 70;
int *pC = (int *)&objc;
printf("%d\n", *pC);
pC++;
printf("%d\n", *pC);
pC++;
printf("%d\n", *pC);
pC++;
printf("%d\n", *pC);
pC++;
printf("%d\n", *pC);
pC++;
printf("%d\n", *pC);
pC++;
printf("%d\n", *pC);
return 0;
}
输出:
10
20
30
40
50
60
70
假如&obj为0x1000,那么objc的内存模型如下图所示:
3.6 C++的虚继承
虚继承用于在有菱形继承时,如下图的继承关系:
3.7 C++向上转型
3.7.1 基类对象和派生类对象之间的赋值
在将派生类对象赋值给基类对象时,内存转换如下:
3.7.2 基类指针和派生类指针之间的赋值
对象指针只是指向了对象的数据,也就是对象的内存模型开始处。
编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。将上面的代码修改一下:
#include <stdio.h>
class A
{
public:
int m_a;
int m_b;
};
class B
{
public:
int m_b;
int m_c;
};
class C : public A, public B
{
public:
int m_a;
int m_c;
int m_d;
};
int main()
{
C objc;
objc.A::m_a = 10;
objc.A::m_b = 20;
objc.B::m_b = 30;
objc.B::m_c = 40;
objc.C::m_a = 50;
objc.C::m_c = 60;
objc.C::m_d = 70;
A *pA = &objc;
B *pB = &objc;
printf("pA = %p pA->m_a = %d pA->m_b = %d\n", pA, pA->m_a, pA->m_b);
printf("pB = %p pB->m_b = %d pB->m_c = %d\n", pB, pB->m_b, pB->m_c);
printf("&objc = %p objc.m_a = %d objc.m_c = %d objc.m_d = %d\n", &objc, objc.m_a, objc.m_c, objc.m_d);
return 0;
}
输出:
pA = 0x7ffc7219c940 pA->m_a = 10 pA->m_b = 20
pB = 0x7ffc7219c948 pB->m_b = 30 pB->m_c = 40
&objc = 0x7ffc7219c940 objc.m_a = 50 objc.m_c = 60 objc.m_d = 70
可以看出pA和pB的值不一样,该对象内存布局如下:
在将objc的地址赋给pA时,objc的起始地址和A类子对象的起始地址是同一个地址,pA = &objc = 0x7ffc7219c940,而pB = &objc时,从内存布局中可以看到,B类子对象相对于objc对象偏移了8字节,这时编译器内部就会将B类子对象的地址0x7ffc7219c948赋值给pB。
首先要明确的一点是,对象的指针必须要指向对象的起始位置。对于 A 类来说,它们的子对象的起始地址和 C 类对象一样,所以将 &objc 赋值给 pA时不需要做任何调整,直接传递现有的值即可;而 B 类子对象距离 C 类对象的开头有一定的偏移,将 &objc 赋值给 pB 时要加上这个偏移,这样 pB 才能指向 B 类子对象的起始位置。也就是说,执行pB=objc语句时编译器对 pB 的值进行了调整,才导致 pA、pB 的值不同。