C++类的继承

继承是很大程度上解决了代码重复的问题。比如狗和猫都属于动物,我们需要在猫和狗的类里面写上动物年龄,动物名字等。但是动物都可以有年龄和起名称,这个时候我们写个动物类,把动物都有的属性写进去,狗和猫继承动物类就好了,就不需要对狗和猫的类里面分别写年龄和名字了。
本篇博客主要记录以下问题:
一、基本语法和继承方式
二、继承中的对象模型
三、继承中对象的构造析构顺序
四、继承同名成员的处理方式(包括普通成员、静态成员)
五、多继承语法
六、棱形继承

一、基本语法和继承方式

1.基本语法

继承的基本格式为:
class 子类名 :继承方式 父类名
还允许一个类继承多个类,多继承语法格式为:
class 子类名 : 继承方式 父类名1,继承方式 父类名2…

2.继承方式

继承方式分为公有(public)继承,保护(protect)继承,私有(private)继承。
不同的继承方式,继承的成员的属性会发生变化,如下图:
在这里插入图片描述
总结:
(1)首先,不管哪种继承方法,父类的私有成员子类都没办法访问。
(2)子类以公有方式继承父类的话,父类的公有成员在子类中依旧是公有属性,保护成员依旧是保护属性。
(3)子类以私有方式继承父类的话,父类的公有成员和保护属性成员都统一为保护属性。

二、继承中的对象模型

代码:

class A
{
public:
	int a_a;
protected:
	int a_b;
private:
	int a_c;
};

class B :public A
{
	int Geta_a()
	{
		return a_a;
	}
	int Geta_b()
	{
		return a_b;
	}
	/*//不可访问a_c,因为a_c是父类的私有属性,子类不可访问
	int Geta_c()
	{
		return a_c;
	}
	*/
private:
	int b_a;
};

父类的私有成员,子类无法访问。无法访问不代表没有继承。父类的所有成员,子类都会完全继承下来,只不过编译器将继承下来的私有成员给隐藏了,不允许子类访问。
比如我们用类大小看一下:

int main()
{
	cout << "sizeof(class B) = " << sizeof(B) << endl;
	return 0;
}

输出结果如下:
在这里插入图片描述
类B的大小为16字节。类B自己有一个int类型成员。那么还有12个字节都是从父类继承下来的。所以父类三个成员都被子类继承了,包括不允许子类访问的私有属性。
我们可以用vs2019的开发者工具进行查看:
在这里插入图片描述
发现从A类继承下来的是有私有成员a_c的,只不过编译器对其进行了隐藏不让访问而已。

总结:
父类的所有非静态成员,子类都会完全继承下来,只不过编译器将继承下来的私有成员给隐藏了,不允许子类访问。

三、继承中的构造和析构顺序

代码:

class A
{
public:
	A()
	{
		cout << "这里是A构造函数" << endl;
	}
	~A()
	{
		cout << "这里是A析构函数" << endl;
	}
public:
	int a_a;
protected:
	int a_b;
private:
	int a_c;
};

class B :public A
{
public:
	B()
	{
		cout << "这里是B构造函数" << endl;
	}
	~B()
	{
		cout << "这里是B析构函数" << endl;
	}
private:
	int b_a;
};

int main()
{
	B b;
	return 0;
}

在这里插入图片描述
发现是类A先调用构造函数的,这是因为只有A类先调用构造函数,类B构造的对象才能拿到类A的成员。

四、继承同名成员的处理方式

继承会引发一些问题,比如说如果子类有和父类相同名称的成员,或者说子类继承了两个父类,两个父类有同名的成员,这样的话,使用成员的时候,编译器会犯难,不知道要使用哪一个成员。这里我们拿子类和父类同名成员时的处理方式。

1.继承普通同名成员的处理方式

1.继承普通同名成员变量的处理方式

class A
{
public:
	int a;
	int b;;
private:
	int c;
};

class B :public A
{
public:
	int a;
};

int main()
{
	B b;
	b.a = 10;    //B类里面本身的变量a
	b.A::a = 20;//B类继承的成员变量a
	cout << b.a << "   " << b.A::a << endl;

	b.b = 20;    //B类继承的成员变量b
	b.A::b = 30; //B类继承的成员变量b
	cout << b.b << "   " << b.A::b << endl;

	return 0;
}

运行结果:
在这里插入图片描述
总结:
(1)当访问的成员变量不是同名成员变量时,直接以对象点的形式访问就可以。
(2)当访问的成员变量是同名成员变量时,以对象点的方式访问本类本身的同名成员变量,以对象点+作用域的方式访问父类的成员变量。

2.继承普通同名成员函数的处理方式
代码:

class A
{
public:
	void fun()
	{
		cout << "this is A fun()" << endl;
	}
	void fun(int tmp)
	{
		cout << "this is fun(int)" << endl;
	}
public:
	int a;
	int b;;
private:
	int c;
};

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

int main()
{
	B b;
	b.fun();    //B类fun()
	//b.fun(10);//error
	b.A::fun();//A类fun()
	b.A::fun(10);//A类fun(int)

	return 0;
}

运行结果:
在这里插入图片描述
当子类与父类有同名成员函数的时候,发现哪怕父类的成员函数和子类的成员函数形参列表不同(比如父类的fun(int)和子类的fun()明显不同,构成了函数重载),但是子类对象却不能以对象点的方式调用父类函数fun(int)。这是由于只要子类有同名的成员函数,那么父类的同名成员函数都会被隐藏,统一不能以对象点的方式调用,必须以对象点加作用域的方式调用。
总结:
(1)如果子类和父类不存在同名的函数,那么直接以对象点的方式调用即可。
(2)如果子类和父类的成员函数的函数名相同,那么对于父类的所有同名函数,子类必须以对象点+作用域的方式调用父类的同名函数。此时子类以对象点的方式调用子类本身的同名函数。

2.继承同名静态成员的处理方式

首先,要知道静态成员只有一个,继承的话也是只有一个,即子类和父类的对象都共用同一个静态成员。
如下验证代码:

class A
{
public:
	static int a;
	int b;
};

int A::a = 10;

class B :public A {};

int main()
{
	A a;
	cout << a.a << endl;

	B b;
	cout << b.a << endl;

	b.a = 20;
	cout << a.a << "   " << b.a << endl;
	cout << &a.a << "   " << &b.a << endl;

	return 0;
}

在这里插入图片描述
通过类B对象修改静态成员变量的值,类A的静态成员变量值也会修改,打印出来地址,两个类的对象都是用同一个静态成员变量。
1.继承同名静态成员变量的处理方式
代码:

class A
{
public:
	static int a;
	int b;
};

int A::a = 10;

class B :public A 
{
public:
	static int a;
};

int B::a = 20;

int main()
{
	B b;
	cout << b.a << endl;  //访问的是子类本身的静态成员变量
	cout << b.A::a << endl;//访问的是父类的静态成员变量
	cout << B::A::a << endl;//访问的是父类的静态成员变量 

	return 0;
}

在这里插入图片描述
总结:
发现有同名静态成员变量时,和同名普通成员变量处理方式一样。只不过对于静态成员变量,多了一个直接用域名访问的方式B::A::a。

2.继承同名静态成员函数的处理方式

class A
{
public:
	static void fun()
	{
		cout << "this is A fun()" << endl;
	}
	static void fun(int tmp)
	{
		cout << "this is A fun(int)" << endl;
	}
public:
	int a;
	int b;;
private:
	int c;
};

class B :public A
{
public:
	static void fun()
	{
		cout << "this B fun()" << endl;
	}
};

int main()
{
	B b;

	//B类本身的静态fun()函数
	b.fun();
	B::fun();

	//调用继承的静态fun函数
	b.A::fun();
	b.A::fun(10);
	B::A::fun();
	B::A::fun(10);
	return 0; 
}

运行结果:
在这里插入图片描述
总结:
发现有同名静态成员函数时,和同名普通成员函数处理方式一样。只不过对于静态成员函数,多了一个直接用域名访问的方式B::A::fun()。
但是注意,两个"::"的意义不同,第一个代表用类名访问,第二个是代表A类作用域下。

五、棱形继承(钻石继承)

1.棱形继承的概念

两个派生类继承一个基类,又有一个派生类继承这两个派生类。
如下图:
在这里插入图片描述
可能举例不太恰当,但是就是这么个意思。

2.棱形继承带来的问题

1.羊继承了动物的数据,驼同样继承了动物的数据,羊驼继承了羊和驼的数据,继承本身是有继承性的,那么羊驼就有了两份动物的数据。
2.羊驼继承自动物的数据有两份,造成了资源浪费。
总的来说就是相同作用的数据有两份,就这么个问题。
平常中,是不允许这样继承的,尽量避免多继承和棱形继承的发生。

class Animal 
{
public:
	int m_age;
};

class Sheep :public Animal {};

class Tuo :public Animal {};

class SheepTuo :public Sheep, public Tuo {};

int main()
{

	SheepTuo st;
	//st.m_age;//error,产生二义性,编译器不知道用Sheep的m_age还是Tuo的m_age
	st.Sheep::m_age; //访问Sheep的m_age
	st.Tuo::m_age;     //访问Tuo的m_age

	return 0;
}

类SheepTuo的m_age有两份,但是只需要一份就够了,所以资源浪费。
在这里插入图片描述
可以清楚的看见Sheep 和Tuo 分别从Animal继承了m_age,而SheepTuo 又继承了Sheep 和Tuo,所以SheepTuo有两份不同作用域下的m_age。我们要做的就是让SheepTuo的m_age只有一份。

下面解决数据有两份的问题。

3.解决棱形继承带来的问题

利用虚继承解决棱形继承问题。
虚继承,即在继承方式前面再加一个virtual关键字。

class Animal 
{
public:
	int m_age;
};

class Sheep :virtual public Animal {};

class Tuo :virtual public Animal {};

class SheepTuo :public Sheep, public Tuo {};

在这里插入图片描述
vbptr是虚指针,由于Sheep和Tuo都是虚继承的方式继承Animal的,所以这俩类只是各多了一个虚指针,指向对应的vbtable,vbtable是虚表,Sheep和Tuo的虚指针会查虚表,记录偏移量,比如Sheep的虚指针地址偏移量为0,Sheep的虚表里面的记录的偏移量是8,0+8就是Animal的m_age的地址偏移量;同理对于Tuo来说4+4也是Animal的m_age的地址偏移量。这样的话,SheepTuo就只包含一个m_age。那么如果把SheepTuo继承Sheep和Tuo的方式都改为虚继承呢?那么SheepTuo自己也会有一个虚指针,这个虚指针指向一个虚表,虚表里放着从Sheep和Tuo还有Animal继承过来的内容。此时仍旧只有一份Animal的m_age,如下图:
在这里插入图片描述

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值