C++学习笔记三、C++继承与派生

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++的虚继承

虚继承用于在有菱形继承时,如下图的继承关系:

由于B和C同时继承自A,而D又继承了B和C,那么D中就含有两份A的数据,假如A中有成员变量int a,在使用D对象访问a时,编译器不知道究竟是A->B->D还是A->C->D,在访问时需要在使用时加上类名。 虚继承就解决了这个问题。

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 的值不同。

发布了21 篇原创文章 · 获赞 63 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/u014783685/article/details/84976759