C++面向对象的三大特征: 封装,继承,多态。接下来我们就转入对继承的深入学习啦。
目录
1.继承的概念和定义
1.1继承的概念:
1.2继承的定义 :
1.21书写格式
1.22继承关系和访问限定符
2基类和派生类赋值转换
3.继承中的作用域
3.1成员函数和成员变量的隐藏:
4.子类的默认成员函数:
5.继承中的友元和静态成员
5.1友元
5.2静态成员
6.继承的方式
6.1三大继承方式
6.2菱形虚拟继承
继承(inheritance)机制是面向对象程序设计使代码可以复用(避免设计重复)的最重要的手段。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。
举个例子:学校中人员可以分三类:学生,老师,后勤 。 学生有姓名+学号,老师有姓名+工号,那我们在封装这三个类是会不会有大量重复呢?
它的书写格式:
接下来简单写一个继承类试一下。
/父类Person class Person { public: void print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } //protected: string _name ; // 姓名 int _age ; // 年龄 }; //子类Student class Stu : public Person { protected: int _stuid; // 学号 }; //子类Teacher class Tea : public Person { protected: int _jobid; // 工号 }; int main() { Stu s; s._age = 20; s.print(); Tea t; t._name = "张三"; t._age = 44; t.print(); }
- 在学类和对象时我们学到了访问限定符:public,protected,private这三个。
- 在继承中也有三种继承关系::public,protected,private。
- 这个继承关系更好理解,就比如李华同学从他家长那继承了车房等,但是他的爸爸有一些属于自己的小秘密会告诉李华吗?显然这些小秘密被他父亲保密,不会被李华继承。
- 大家想一下:访问限定符和继承关系结合会有几种方法?9种 9=3*3.
- 一般情况下,我们用到的是public继承关系,其他两种绝绝绝大部分下很难用到,但是笔试就喜欢这种很难见到了,我们了解一下。
- . 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 一般我们取两者中访问权限最小的那个.(public,protected,取protected)。
我们具体看一下公有和私有继承。
2基类和派生类赋值转换
在类和对象中我们能让类给类赋值,在继承中也有派生类给派生类赋值,基类给基类赋值。那派生类能不能给基类赋值?
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。前提是要public公有继承。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
- 子类给父类赋值有三种
class A { public: protected: int _length, _weight; }; class B : public A { public: protected: int _length, _weight, _size; }; int main() { B b; A a(b); //类给类赋值 A& s = b; //类给引用 A* ptr = &b; //类给指针赋值 }
一般每个函数都有一个作用域,出了他的范围就不起作用了,其实父类和子类有各自的作用域。
- 基类与派生类的作用域关系
1、每个类定义自己的作用域,在这个作用域内我们定义类的成员;
2、派生类的作用域位于基类作用域之内;
- 成员函数调用
1、名字冲突与继承:派生类能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字;
2、通过作用域运算符来使用隐藏的成员:
3、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
4、需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 成员函数的隐藏:
class A { public: void func() { cout << "A::func" << endl; } protected: int _length,_weight,_size; }; class B : public A { public: void func() { cout << "B::func" << endl; } protected: int _length, _weight, _size; }; int main() { B tmp; tmp.func(); tmp.A::func(); }
在这个继承类中,基类有func函数,派生类中也有func函数,对象tmp在访问func时,子类会隐藏父类的func函数,注意,这这两个func函数不是函数重载,因为这俩都不在一个作用域。
- 成员变量的隐藏:
class A { protected: int _length=175; int _weight=70; int _size=6; }; class B : public A { public: void print() { cout << "身高: " <<_length<< endl; cout << "体重: " << _weight << endl; cout << "个数: " << _size << endl; cout << "体重: " << A::_weight << endl; } protected: int _length = 172; int _weight = 73; int _size = 5; };
如果我们指定作用域,就会去调用相关作用域那个 成员变量。
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
- 1、派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 2、派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 3、派生类的operator=必须要调用基类的operator=完成基类的复制。
- 4、派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 5、派生类对象初始化先调用基类构造再调派生类构造。
- 6、派生类对象析构清理先调用派生类析构再调基类的析构。
- 7、因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
(三分苦师兄总结的很全面,大家可以去看看大佬写的博客,水平特别高)
我们先完成一个父类的。
class A { public: A(int length = 170, int weight = 70) //构造函数,初始化列表进行初始化 :_length(length) ,_weight(weight) { cout << "A()" << endl; } ~A() //析构函数 { cout << "~A()" << endl; } A(const A& s) //拷贝构造函数用来进行拷贝 :_length(s._length) , _weight(s._weight) { cout << "A(const A& s)" << endl; } A& operator=(const A& p) { cout << "A& operator=(const A& p)" << endl; if (&p != this) { _weight = p._weight; _length = p._length; } return *this; } protected: int _length; int _weight; }; class B : public A { public: protected: int _size = 5; };
派生类对于系统默认的成员函数如何使用?
- 对于自己的成员,就像类和对象那样处理。
- 对于继承的基类成员(length,weight),必须调用父类的构造函数初始化。
如果基类没有写构造函数。那派生类如何初始化继承的成员函数呢?
在派生类中写一个构造函数,继承的成员函数用初始化列表进行初始化。很明显这种方法行不通。派生类是整体处理他的,自己内部成员调用自己的构造函数,继承的要调用父类的构造函数。
如何规范些派生类的构造函数? 1.我们用初始化列表进行初始化但是格式有些区别: 2. 对于继承基类成员初始化:而是基类+随意一个基类成员名即可。 3. 对于自身成员初始化 :和基类初始化列表初始化一样。
接下来我们把派生类也给完善一下。
class A { public: A(int length = 170, int weight = 70) //构造函数,初始化列表进行初始化 :_length(length) ,_weight(weight) { cout << "父类A1()" << endl; } ~A() //析构函数 { cout << "父类~A()" << endl; } A(const A& s) //拷贝构造函数用来进行拷贝 :_length(s._length) , _weight(s._weight) { cout << "A(const A& s)" << endl; } A& operator=(const A& p) { cout << "A& operator=(const A& p)" << endl; if (&p != this) { _weight = p._weight; _length = p._length; } return *this; } protected: int _length; int _weight; }; class B : public A { public: B( int length, int weight, string name) :A(_length) //继承父类的成员(length,weight) ,_name(name) //自己的成员 { cout << "B()" << endl; } //拷贝构造 B(const B& s) :A(_length) ,_name(s._name) { cout << "B(const B& s)" << endl; } //operator=运算符重载 B& operator=(const B& p) { if (this != &p) { //子类operator=和父类operator=构成隐藏,为了避免无线递归,要加上指定父类作用域 A::operator=(p);//调用基类的operator=完成基类成员的赋值 _length = p._length;//完成派生类的赋值 } cout << "B& operator=(const B& p)" << endl; return *this; } //为了保证析构顺序(先子后父)。子类的析构函数完成后会自动调用父类的析构函数,所以不需要我们显示调用 ~B() { cout << "~B()" << endl; }//自动调用父类的析构函数 protected: string _name ; }; int main() { B tmp(175, 73,"jhg"); //A a(tmp); }
- 友元关系不能被继承。
- 根据友元性质,父类友元可以直接访问父类的所有成员,也可以访问子类的共有成员,但不能访问子类私有成员和保护成员。
class B; //前置声明类B,要不友元调用a成员会报错。 class A { public: friend void func(const A& a, const B& b); protected: int _length, _weight; string _name; }; class B : public A { public: protected: int _length, _weight, _size; }; //友元函数 void func(const A& a,const B& b) { cout <
注意,一定要前置声明一下友元含有的子类参数的那个类。class B;即可
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。class B; //前置声明类B,要不友元调用a成员会报错。 class A { public: A() { _count++;} public: //设成共有的 int _length, _weight; string _name; static int _count; //计数 }; int A:: _count = 0; class B : public A { public: protected: int _length, _weight, _size; }; int main() { B b; A a; cout << b._count << endl; //2 cout << B::_count << endl; //2 cout <
- 1.单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
A是B的父类,B是C的父类。
- 2.多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 3.菱形继承 :菱形继承是多继承的一种特殊情况。
菱形继承有两种方法:数据冗杂,二义性。
class B; //前置声明类B,要不友元调用a成员会报错。 class A { public: A() {} public: //设成共有的 int _length, _weight; }; class B : public A { public: int _size; }; class C : public A { public: string name; }; class D : public B, public C { public: int _count; }; int main() { D d; d._weight; d._length = 10; }
- 二义性——编译器不知道要访问那个weight,length。
解决方法:指定类域。
解决了二义性但是D的数据冗杂还是没有解决。接下来我们解决下它。
为了解决数据冗杂这个问题,提出了虚拟继承:两个直接父类的继承方式访问限定符前加vitual:
class A { public: A() {} public: //设成共有的 int _length, _weight; }; class B : virtual public A { public: int _size; }; class C : virtual public A { public: string name; }; class D : public B, public C { public: int _count; }; int main() { D d; d._length=179; d.B::_length = 178; d.C::_length = 170; }
地址这玩意有的时候不准,那不妨看一下内存是咋回事?
步骤:调试+逐语句(F11),在重新打开调试+窗口+内存,监视我们也可以一并打开。
我们为了好观察,把各类的变量做了简单处理。
class A { public: A() {} public: //设成共有的 int _a; }; class B : virtual public A { public: int _b; }; class C : virtual public A { public: int _c; }; class D : public B, public C { public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; }
使用菱形虚拟继承内存分布如下:
我们根据地址看出来,A在B,C的下面,此时A是属于他俩的。但是我们咋找的a呢。
B不是有两行吗,第一行是指针指向一张表这个表叫虚机表。这个指针叫虚机表指针。
第二行存的是数据。虚机表中第二行是不是有个数据(偏移量)编译器就是通过这个偏移量来找A.
C的情况和B一样。
我们看一下菱形虚拟继承原理图:
虚拟菱形继承相比较于菱形继承,D对象t的直接父类B和C中存储的不再是A的成员,而是A的偏移量地址,通过该指针找到虚基表之后,计算偏移,就能知道A成员存放的地址,
位于B和C成员的下边,不再像普通继承那样位于B和C成员的上边,这时候A既不属于B也不属于C。
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。如A和B,他们之间是强关联关系。(动物,人 。动物<------人)
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。他们之间是弱关联关系。 (轮胎,车。轮胎------>车)
- 优先使用对象组合,而不是类继承 。为了降低对象之间的关联度
- 继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。父类所有非私有成员对子类都可见,父类的改变会影响子类,父类的封装对子类不太起作用。
- 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。(一般我们说的继承都是公共继承)。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装。子类只能使用父类的共有成员 ,子类和父类关联度低。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。