一文精通C++ -- 继承

       前言:继承是C++类和对象三大特性中关键的一环,上承封装,下接多态,C++中的继承是一种面向对象编程的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。子类可以使用父类的公共成员函数和变量,也可以重写父类的方法或添加新的方法和变量。继承可以帮助我们重用代码并提高代码的可维护性和可扩展性。相信大部分人都对继承不甚了解,那么,下面,我们就一起来进一步学习继承的相关知识~~

目录

1.继承访问关系

总结

2.基类和派生类对象赋值转换

3.继承中的作用域

4.派生类的默认成员函数

5.继承与(友元&&静态成员)

6.菱形继承

菱形虚继承

多继承经典题

多继承中的指针偏移问题

菱形继承问题

7.继承vs组合

优先使用组合而不是继承


 

1.继承访问关系

       我们知道,派生类有三种继承关系,而基类中通常也具有三种方式的访问限定符,我们需要理清楚在不同的进程关系下派生类继承基类的成员访问,具体如下图,

总结

1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能访问它

2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的

3. 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 = min(成员在基类的访问限定符,继承方式),public > protected > private。

4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式的写出继承方式。

5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

2.基类和派生类对象赋值转换

       派生类对象可以赋值给 基类的对象 / 基类的指针 / 基类的引用,这种赋值和一般的自定义对象的赋值是不同的,因为父类和子类之间是is-a的关系,我们认为在赋值时中间不会产生临时对象。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。基类对象不能赋值给派生类对象。基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(这个地方我们在后续多态的还会继续深入,这里先了解一下)。

3.继承中的作用域

1. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

2. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏(子类同名成员默认情况下隐藏了父类的同名成员)

3. 注意在实际中在继承体系里面最好不要定义同名的成员。

4.派生类的默认成员函数

       6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用

2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

3. 派生类的operator=必须要调用基类的operator=完成基类的复制。

4. 派生类的析构函数会在被调用完成后自动(这个是默认的,我们不用声明)先调用基类的析构函数清理基类成员,在按自己的析构执行。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

5. 一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

5.继承与(友元&&静态成员)

       友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,通俗的说,父亲的朋友并不能碰你的私房钱,你说是吧~ ,这个比较简单,就不再详述,我们来看一下静态成员,基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。这一点和继承普通的父类成员函数比较相似,我们子类只是继承父类成员函数的使用权。

6.菱形继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承有数据冗余和二义性的问题,在上面的结构图中,可以看出,类Assistant中有两份person成员,虽然二义性问题可以利用类的作用域来区分,不会构成严重的错误,但是数据冗余问题是无法解决的。

菱形虚继承

         虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方使用。但是实际中一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

      那到这里有的人可能就会有疑问了?为什么子类对象d的两个类B和C的成员不直接在自己的第一个4字节处保存A成员存储的地址或者偏移量,而要不嫌麻烦的还要通过一层地址指出去,再到外面寻找偏移量呢?

      事实上,对于类B和C来说,当他们采用了虚继承的时候,在每个派生类中会有一个虚基类指针(占四个字节),和虚基类表(不占空间),虚基类指针指向派生类对象中的虚基类成员对象,当虚继承的派生类被当做基类再次虚继承,虚基类指针也会被继承。(因此每个派生类既可以通过虚基类指针,查找虚基类表找到其中的成员)。具体细节我们需要配合多态进行深入探究,后续也会在多态的章节重点讲解。

多继承经典题

多继承中的指针偏移问题

 有如下的代码,试判断p1、p2、p3之间的关系?

class Base1 {
public:
	int _b1;
};
class Base2 {
public:
	int _b2;
};
class Derive : public Base1, public Base2 {
public:
	int _d;
};

int main() {

	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;

	return 0;
}

     如下图,我们可以由切割来得出如下的结构图,p1和p3都指向同一个地址,但是其表示的内容却不一样,p1表示的是base1的大小,p3表示的是整个d对象,而p2就是切割成了指向base2的指针。虽然指针上是p1==p3>p2,实际上他们指向空间的大小却各不相同。

菱形继承问题

以下程序的输出结果是什么?

class A {
public:
	A(char* s) { cout << s << endl; }
	~A() {}
};
class B :virtual public A
{
public:
	B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:
	D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl;
	}
};
int main() {
	char s1[] = "class A";
	char s2[] = "class B";
	char s3[] = "class C";
	char s4[] = "class D";
	//D* p = new D("class A", "class B", "class C", "class D");
	D* p = new D(s1,s2,s3,s4);
	delete p;
	return 0;
}

我们先来给出输出结果:

怎么样,是不是有点懵?没事,且听我娓娓道来~

少装,赶紧说~~~

        咳咳,最先构造A,最后打印D,这个无可厚非,对于B和C谁先被构造,我们知道,构造函数的初始化顺序其实是由类内对象的声明顺序而不是初始化列表顺序或者是赋值语句的顺序来决定的,这点非常重要,所以,继承也是同样的道理,派生类是根据继承声明的顺序而不是初始化列表的顺序来初始化成员的,对于我们的派生类D,其声明的继承顺序是先C后B,所以其构造顺序必然也要先C后B,所以就导致了这样的输出结果。

7.继承vs组合

        public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

优先使用组合而不是继承

      继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。

        但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

        对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性(就像父类的保护成员在子类中不能使用);只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。

       继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

       对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

       实际尽量多去用组合。组合的耦合度低(耦合其实就可以简单的理解为两个对象之间依赖关系的大小,软件工程中,设计工程项目时,一般耦合程度越低越好),代码维护性好。不过继承也有用武之地的,有些关系就适合继承就用继承,另外要实现多态,也必须要继承

本来想和多态一块写的,但是那样篇幅就太大了,所以,多态就下一篇再出叭~

       村上春树说:挫折不会主动说话,却常在暗中帮助你成长,昨日的你承受的有多深,来日的你荣耀就有多高远,扛得住涅槃之痛,才能配得上重生之美。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值