继承
继承的基本概念继承的概念继承的定义继承的格式继承的方式继承基类成员访问方式的变化 基类与派生类的对象赋值转换继承中的作用域派生类中的默认成员函数继承与友元继承中的静态成员菱形继承菱形虚拟继承继承的总结
继承的基本概念
继承的概念
继承机制是面向对象程序设计中一种使代码得到复用的重要手段,让程序员可以在原有类的特性的基础上机型扩展,增加功能,产生新的类,新的类称为派生类,原有的类叫做基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
例如,对于人这个类而言,都会有姓名和年龄,对于学生这个类而言它还具有一些特有的属性,比如说学号。所以,定义学生类的时候可以复用人这个类。
class Person{public:void Print(){cout << "name : " << _name << " age : " << _age << endl;}protected:int _age = 18;string _name = "小明";};class Student : public Person{protected:string _id = "20230101";};int main(){Student st1;st1.Print();return 0;}
派生类继承了来自基类的成员,这些继承的成员都会成为派生类的一部分。像上面的Student类来说,并没有定义Print()这个函数,但是由于其继承了Person类,所以在Student类中也会有Print这个成员。在监视窗口下可以看到派生类对象的内部构成。
继承的定义
继承的格式
继承的方式
注意:继承方式与访问限定符是两套体系。
继承基类成员访问方式的变化
这里有个记忆的技巧,就是继承方式与基类的访问限定符与较小者。
public > protected > private
总结:
基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式。在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强。
student以private的继承方式继承Person这个基类:
class Person{public:void Print(){cout << "name : " << _name << " age : " << _age << endl;}private:int _age = 18;string _name = "小明";};class Student : public Person{public:void st_print(){cout << "name : " << _name << " age : " << _age << " id :" << _id << endl;}protected:string _id = "20230101";};int main(){Student st1;st1.st_print();return 0;}
可以看到,编译时会报错,由于_name,_age都是基类的私有成员,在派生类中是不可见的不管是在类内还是在类外都是不能被访问的。但是在监视窗口可以看到它们确实被继承下来了,只是语法规定它们在派生类中是不可见的。
基类与派生类的对象赋值转换
派生类的对象可以赋值给基类的对象/指针/引用。有个形象地说法叫做切片,意思就是将派生类中继承自基类的那部分切割出来。基类的对象不可以赋值给派生类基类的指针或者引用可以通过强势类型转换的方式赋值给派生类的指针或者引用。
class Person{public:void Print(){cout << "name : " << _name << " age : " << _age << endl;}protected:int _age = 18;string _name = "小明";};class Student : public Person{public:int _a = 10;protected:string _id = "20230101";};int main(){//Student st1;//st1.Print();Person* pp = new Person;Student* ps = (Student*)pp;cout << ps->_a;return 0;}
可以看到打印出来的是随机值,这是因为基类对象中就没有这个_a变量,你是强制的将基类对象的地址赋值给了派生类指针,去访问派生类中特有的内容。这种做法是不安全的。
继承中的作用域
继承体系中,基类和派生类有独立的作用域。派生类和基类中存在同名的成员时,在派生类中会屏蔽基类中的同名成员,这种情况叫做隐藏也叫做重定义。但是可以使用 基类::基类成员的这种方式显示的访问。class A{public:int _a = 1;};class B : public A{public:int _a = 2;void Print(){cout << "_a : " << _a << endl;}};int main(){B b;b.Print();return 0;}
如果这里想打印的是从基类中继承的_a变量,那么就要显示的访问。
class B : public A{public:int _a = 2;void Print(){cout << "_a : " << A::_a << endl;}};
要注意的是,如果是成员函数构成重定义,只需要函数名相同即可。 class A{public:void Print(int a = 0){cout << "a is : " << a << endl;}};class B : public A{public:void Print(){cout << "this is B " << endl;}};int main(){B b;b.Print();return 0;}
这里要想访问基类的Print函数,也要进行显示的访问。
int main(){B b;b.A::Print();return 0;}
在实际中在继承体系中最好不要使用同名成员。 派生类中的默认成员函数
派生类的构造函数必须调用基类的构造函数来初始化基类的那一部分成员。如果基类没有默认的构造函数,那么就要在派生类的构造函数初始化列表阶段显示调用基类的构造函数。class A{public:A(){cout << "A()" << endl;}};class B : public A{public:B(){cout << "B()" << endl;}};int main(){B b;return 0;}
派生类的拷贝构造函数必须调用基类的拷贝构造函数来拷贝基类的那部分成员。 class A{public:A(){cout << "A()" << endl;}A(const A& a){cout << "A(const A& a)" << endl;}};class B : public A{public:B(){cout << "B()" << endl;}B(const B& b):A(b){cout << "B(const B& b)" << endl;}};int main(){B b;B b1(b);return 0;}
派生类的赋值运算符重载函数必须调用基类的赋值运算符重载函数来对基类的那部分成员进行赋值。 class A{public:A(){cout << "A()" << endl;}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){if (this != &a){cout << "operator=(const A& a)" << endl;}return *this;}};class B : public A{public:B(){cout << "B()" << endl;}B(const B& b):A(b){cout << "B(const B& b)" << endl;}B& operator=(const B& b){if (this != &b){A::operator=(b);cout << "operator=(const B& b)" << endl;}return *this;}};int main(){B b;B b1(b);b1 = b;return 0;}
派生类的析构函数调用完,会自动调用基类的析构函数做数据的清理。这样才能保证派生类对象先析构,基类对象后析构,因为基类对象先构造,派生类对象后构造。 class A{public:A(){cout << "A()" << endl;}A(const A& a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){if (this != &a){cout << "operator=(const A& a)" << endl;}return *this;}~A(){cout << "~A()" << endl;}};class B : public A{public:B(){cout << "B()" << endl;}B(const B& b):A(b){cout << "B(const B& b)" << endl;}B& operator=(const B& b){if (this != &b){A::operator=(b);cout << "operator=(const B& b)" << endl;}return *this;}~B(){cout << "~B()" << endl;}};int main(){B b;B b1(b);b1 = b;return 0;}
派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。多态中要对析构函数进行重写,而重写的要求之一是函数名相同,为此编译器会将析构函数的名字变为destrutor(),所以基类的构造函数不加virtual的情况下,派生类中的析构函数与基类中的析构函数构成隐藏。 继承与友元
友元关系不能继承,也就是说基类的友元函数不饿能访问派生类的私有和保护成员。
class Student;class Person{public:friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名};class Student : public Person{protected:int _stuNum; // 学号};void Display(const Person& p, const Student& s){cout << p._name << endl;cout << s._stuNum << endl;}int main(){Person p;Student s;Display(p, s);return 0;}
继承中的静态成员
由于static成员是属于整个类的并不属于某个对象或实例,所以一旦基类中定义了static成员,则整个继承体系中只有只有这一个static成员,无论实例化出多少对象。
class A{public:void Print(){cout << "_a : " << _a << endl;}static int _a;};int A::_a = 10;class B : public A{public:void func(){++_a;}};int main(){A a;B b;a.Print();A::_a = 100;b.Print();B::_a = 1000;a.Print();return 0;}
菱形继承
单继承: 一个子类只有一个直接父类。
多继承: 一个子类有两个及以上的直接父类。
菱形继承: 菱形继承是多继承的一种特殊情况,就是子类的两个直接父类又有共同的直接父类,可能这句话比较绕,画个图来表示一下。
从图中就能很明显的看出来,D类中有两份相同的A类中的数据,分别是从B类和C类中继承下来的,这无疑就造成了数据冗余和二义性的问题。如果这样的话,D类中使用继承自A类中的成员时要加B:: 或者 C:: 要不然编译器都不知道你到底想用从B类继承下来的那份还是从C类中继承下来的那份。这虽然能解决二义性的问题,但是数据冗余仍然无法解决,所以就出现了菱形虚拟继承这一概念。
class A{protected:int _a = 1;};class B : public A{};class C : public A{};class D : public B, public C{public:void print(){cout << "B::_a" << B::_a << endl;cout << "C::_a" << C::_a << endl;}};
在监视窗口可以看到d这个对象中有两份_a这个变量。
菱形虚拟继承
为了解决菱形继承中的数据冗余和二义性的问题,提出了虚拟继承这一概念,B类与C类通过虚拟继承的方式继承自A类,这样D类在多继承B类和C类的时候,就不会出现数据冗余和二义性的问题了。
class A{public:int _a = 1;};class B : virtual public A{};class C : virtual public A{};class D : public B, public C{public:void print(){cout << "_a" << C::_a << endl;}};int main(){D d;d._a = 100;cout << "d.B::_a : " << d.B::_a << "d.C::_a : " << d.C::_a << endl;return 0;}
可以看到,我们可以通过d对象直接修改_a的值,并且继承自B类中的_a与继承自C类中的_a的值都被修改了,这充分的证明通过虚拟继承的方式导致在D类中只有一份_a这个变量,解决了数据冗余和二义性的问题。
但这种做法的原理是什么呢?我们通过内存窗口来看一下。
class A{public:int _a = 1;};class B : virtual public A{public:int _b = 2;};class C : virtual public A{public:int _c = 3;};class D : public B, public C{public:int _d = 4;void print(){cout << "_a" << C::_a << endl;}};
可以看到d对象中有继承自B类和C类的一个指针,这个指针指向了一张表,这个表里存出了到一个偏移量,这个偏移量就是到从从A类继承的成员变量_a。这个指针叫做虚基表指针,虚基表指针指向的表叫做虚基表,这张表里存储了到A的偏移量。
即使A类中有多个成员变量,只要有到第一个变量的偏移量根据对齐规则就能顺次找到其他变量。并且D类的对象是共用这张虚基表的。
int main(){D d;D d1;d.B::_a = 5;d.A::_a = 5;return 0;}
可以看到不论修改B类中的_a 还是C类中的_a,到头来都是同一个_a。
即使通过虚继承的方式可以解决菱形继承中的数据冗余和二义性的问题,但是并不断推荐这样用。
继承的总结
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。继承和组合 public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。优先使用对象组合,而不是类继承 。继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。
什么是菱形继承?菱形继承的问题是什么?
两个子类继承同一个父类,而又有子类同时继承这两个子类。
数据冗余和二义性。
什么是菱形虚拟继承?它是如何解决数据冗余和二义性的?
两个子类通过虚拟继承的方式继承自同一个父类,它们的子类在多继承自这两个类,这样就形成了菱形虚拟继承。
菱形虚拟继承会在子类对象中原本会有冗余的成员变量只出现一份,并产生一个虚基表指针,这个指针指向一个虚基表,表中存储了到最初的那个类的变量的偏移量,这样便解决了数据冗余和二义性的问题。