继承知识总结
理解:继承本质上就是一种复用,比如B类想要用A类里面的成员,就可以继承A,那么A就是基类,B就是派生类。B拥有A的所有成员,还可以额外实现A没有的函数或变量,B范围上是大于等于A的。
使用
(1)格式:class 类名 :继承方式 基类类名(以上面的A类B类为例,派生类B-> class B : public A)
(2)继承方式与访问权限:与访问限定符一样,继承方式也有三种:public,protected,private。如果是private继承,那么基类的所有成员在派生类中都不可见(不能用),如果是其他两种继承方式,就拿继承方式和基类的访问限定符比较范围,谁范围小谁就是派生类的访问范围。比如用public继承,基类的所有访问限定范围都小于等于public,那么派生类就可以访问所有基类成员。(要注意protected的范围是类里面能访问,类外面不能)(使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显式的写出继承方式)(还有一点就是基类的友元不是派生类的友元)
(3)定义在类里面的静态成员变量:基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。静态成员变量是属于类的,无论创建多少个类的对象,静态成员变量只有一份拷贝,修改静态成员变量的值会影响所有对象。有一道判断题:基类对象中包含了所有基类对象的成员。这句话是错的,因为静态成员变量属于类而非对象,不被包含。
(4)基类与派生类的转换:public继承的派⽣类对象可以赋值给基类的指针 / 基类的引⽤,基类指针或引⽤指向的是派⽣类中切出来的基类那部分,比如B b; A* ptr=&b; ptr指向的就是B中属于A的那部分。这在一些需要将派生类传给用基类指针做参数的函数里会很有用。
(5)隐藏:派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,注意,同名就隐藏,不管类型,如果访问要显示访问--> 基类::基类成员
(6)多继承:就是⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派⽣类成员在放到最后⾯。(例(假设多定义了一个class C),class B:public A,public C),用一个经典问题加深理解:
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;//问:p1,p2,p3是什么关系?return 0;
}
这里,答案是p1== p3 != p2。解析:p1和p2虽然都是其父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2, 由于p1对象是第一个被继承的父类类型,所以它的地址与子类对象的地址p3所指位置都为子类对象的起始位置,因此p1==p3。
有多继承就可能会有菱形继承,就是有一个多继承的派生类,该派生类的基类的基类是同一个基类就构成菱形继承,这会导致数据冗余和二义性问题,为了解决这一点,又有了虚拟继承,就在该派生类的几个基类的继承方式前面加上virtual就行。
继承类模板的主要四种默认成员函数
注:在理解层面,在派生类中可以把基类看做一个整体,一种特殊的自定义类型。
(1)构造函数:派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤,如果已经有默认构造,派生类没有指针以及动态开辟空间的情况下也不用显示写构造。下面演示一下显⽰构造。
class A//基类
{
public:A(const char* name = "peter"): _name(name){}
private:string _name;
}
class B : public A
{
public:B(const char* name = "peter"):A(name), //将基类当做一个整体,不能是_name(name)_num(0){}
private:int _num;
}
构造的顺序是先基类后派生类(先父后子)
(2)拷贝构造:派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
//还是上面那个例子,这是B的拷贝构造
B(const B& s):A(s), _num(s ._num){}
(3)赋值重载:派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
//B的赋值重载
B& operator=(const B& s)
{if (this != &s){// 构成隐藏,所以需要显⽰调⽤A::operator=(s);_num = s ._num;}return *this ;
}
(4)析构函数:派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。所以派生类的析构里不用主动去调基类的析构。
~B()//因为基类会去调自己的析构,不用写,
{} //而B里又没有需要手动释放的,就什么都不用写
继承和组合(补充)
public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。由于组合的耦合度低,代码维护性好,加上这两种实际效果相似,能用组合就尽量用组合。