继承和动态绑定与抽象一起成为面向对象编程的基础。
C++中用类进行数据抽象,用派生从一个类继承另一个类,动态绑定是编译器能够在运行时决定,是使用基类中定义的函数还是派生类中定义的函数。
面向对象编程的关键思想是多态性。
除构造函数以外,任意非static成员函数都可以是virtual函数。继承层次的基类一般都要定义virtual的析构函数。virtual只能在类内部的声明中出现,不能出现在类外部的定义上。
派生类可以访问基类的public和protected成员,不能访问private成员。用户代码只能访问public成员。但类的友元都可以访问。
派生类型必须对想要重新定义的每个继承成员进行声明,且必须与基类的定义方式完全匹配。但有一个例外:返回对基类型的引用(指针)的虚函数。派生类中虚函数可以返回基类函数返回的类型的派生类的引用或指针。
一旦函数在基类被定义为虚函数,它就一直为虚函数,派生类无法改变这一事实,在派生类中重定义虚函数时,可以使用virtual关键字,也可以不使用。
用作基类的类必须是已定义的。
每个派生类对象都包含基类部分的对象。
派生类虚函数调用基类版本时,必须显式使用作用域操作符。如果没有使用作用域说明符将会导致无穷递归。
虚函数也可以由默认实参,但它们是在编译时确定,与对象的动态类型无关。在同一虚函数的基类和派生类的版本指定不同的默认实参,会导致麻烦。如果通过基类的引用或指针调用虚函数,但实际执行的是派生类中定义的版本,这时传递给派生类虚函数的实参是在基类的虚函数指定的默认实参。
通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明的值。通过派生类的引用或指针调用虚函数,默认实参为在派生类虚函数声明的值。
对类所继承的成员的访问,由基类中的成员访问级别和派生类派生列表中使用的访问标号共同控制。
派生类派生列表决定在派生类中的访问级别:
1:如果是public继承,基类成员保持自己的访问级别。基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。
2:如果是protected继承,基类的public和protected成员在派生类中为protected成员。
3:如果是private继承,基类的public和protected成员为派生类的private成员。
无论访问标号是什么,派生类都可以访问基类对象的public和protected成员。派生列表的访问标号主要是限制使用派生类对象的用户代码对派生类的使用。
派生类使用protected和private继承,限制了基类在派生类的访问级别。某些成员被限制过后,还可以在派生类中恢复,但是只能恢复到原来的级别。
如classbase
{
public:
inta;
protected:
intb;
private:
};
classderived:privatebase
{
public:
usingbase::a;
usingbaseb;//错误,b原来为protected.只能恢复为protected。
protected:
usingbase::b;
};
在对应的位置,使用using声明,可以将其恢复在基类的访问级别。注意使用using声明的位置。
如果不指定派生列表中的访问标号,使用class关键字定义的派生类默认具有private继承。使用struct关键字定义的派生类默认具有public继承。这是class和struct的第二个区别。
友元关系不能继承。基类的友元对派生类的成员没有特殊访问权限。反过来一样。每个类控制自己的友元关系。这就比如老子的朋友只是老子的朋友,儿子的朋友是儿子的朋友,不能因为老子跟儿子有父子关系朋友就共享了。
如果基类定义了static成员,则整个继承层次中只有一个这样的成员。static成员遵循常规访问控制。既可以通过基类访问static成员,也可以通过派生类访问成员。既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
因为派生类对象也是基类的对象,所以存在从派生类型引用(指针)到基类类型的引用(指针)的自动转换。反过来不成立,基类对象不是派生类对象,因为派生类对象拥有基类对象不用有的某些特征。虽然一般可以使用派生类型的对象对基类对象进行初始化或赋值,但,编译器不会自动将派生类对象转换为基类对象。
由于基类的复制构造函数和赋值操作符函数的形参均为基类型的引用,因此可以通过传递派生类的对象对基类进行初始化和赋值。要特别明白派生类对象转换为基类对象的理论基础。
从基类对象到派生类对象的转换是不存在的。原因是基类对象的引用不能转换为派生类对象的引用。也就是说,不能将派生类类型的引用绑定到基类对象上。但是可以显式强转。但错误要自己负责哦。
构造函数和复制控制函数不能继承,每个类定义自己的构造函数和复制控制函数,如果不定义将使用合成版本。
构造函数可以为protected或private,某些类如果需要只希望派生类使用的特殊构造函数,应将其声明为protected。
派生类的构造函数除了初始化自己的成员外,还要初始化基类。派生类的初始化列表只能初始化派生类的成员,不能直接初始化继承的成员。应该使用基类构造函数初始化基类部分的成员。派生类的构造函数首先初始化基类(初始化列表调用基类构造函数),然后按照声明次序初始化派生类的成员。
只能初始化直接基类。
派生类构造函数不能初始化(初始化列表)基类的成员,应不应该对基类成员赋值(构造函数内),虽然可以在构造函数内对基类的成员赋值,但这样做会违反基类的接口。派生类应通过使用基类构造函数(初始化列表),尊重基类初始化意图。
如果派生类定义了自己的复制构造函数和赋值操作符,那么派生类就负责对基类部分以及类自己的成员进行复制或赋值。
如派生类定义自己的复制构造函数,那么应该显式使用基类复制构造函数初始化对象的基类部分。
如:
classBase
{
};
classDerived:publicBase
{
public:
Derived(constDerived&d)
:Base(d)//注意此处。
{
}
};
如果缺少调用基类的复制构造函数的步骤,那么效果就是Base的默认构造函数被用来初始化对象的基类部分。与调用复制构造函数复制的目标大相径庭。新构造的对象将非常奇怪:它的Base部分保存默认值,而Derived部分是另一对象的副本。
赋值操作符与复制构造函数类似:如果派生类定义了自己的复制构造函数,则该操作符必须对基类部分进行显式赋值。
如:Derived&Derived::operator=(constDerived&rhs)
{
if(this!=&rhs)//防止自我赋值。
Base::operator=(rhs);//显式调用基类赋值操作符函数。
///////////////////////////////
}
析构函数的工作与复制构造函数和赋值操作符不同:派生类析构函数不负责撤销基类对象的成员,编译器总是显式调用派生类对象的基类部分的析构函数。每个析构函数只负责清除自己的成员。
对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按继承层次依次向上调用各基类析构函数。
如果析构函数为虚函数,那么通过指针调用时,运行哪个析构函数将因指针所指向的对象类型不同而不同。尤其是当指针为基类类型,而指向派生类类型时,调用delete函数删除指针时,将调用派生类的虚析构函数。
与其他虚函数一样,析构函数的虚函数性也会被继承。
即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
在运行构造函数和析构函数时,对象都不是完整的。如果在它们中调用虚函数,那么运行的是构造函数或析构函数自身类型定义的版本,而不是派生类的版本。如在基类的构造函数调用派生类版本,派生类版本很可能会访问派生类的成员,而派生类的成员此时还并未初始化。如果允许这样的访问,程序很可能会崩溃。
与基类成员同名(同名即可)的派生类成员,将屏蔽对基类成员的直接访问。无论是成员函数还是成员变量。如在派生类访问被派生类成员屏蔽的基类成员,可以使用基类的作用域说明符访问。如Base::a;
要尽量避免在派生类中定义与基类同名的成员。
在派生类作用域定义的同名派生类成员将屏蔽基类成员,即使函数原型不同。
当基类定义了一个重载函数集,派生类如果想通过自身类型使用所有的重载版本,那么它要么重定义所有的重载版本,要么一个也不定义。原因:如果在派生类定义一个重载函数,由于它与基类的重载函数集同名,因此将屏蔽基类函数集中的所有函数,无法达到重载其中一部分函数的目的。解决方法是使用using声明,将该函数的所有重载实例加到派生类的作用域。注意:一个using声明只能指定一个名字,不能指定形参表。usingBase::func;这一句话会将函数的所有重载实例加到派生类作用域,此后派生类就可以只定义确实需要重定义的重载函数,其他版本可以使用继承过来的函数的定义。
派生类重定义基类的虚函数时,要与基类声明的原型完全一致(返回值可以是基类返回值的派生类的指针或引用),否则派生类将屏蔽基类的虚函数。此时通过基类的引用或指针绑定到派生类对象,调用函数时,将调用基类的虚函数,而不是派生类屏蔽后的函数。因为虚函数是在运行时确定的,而派生类没有定义与基类类型原型一致的虚函数,所以将调用基类的版本。
当希望使用容器保存因继承而相关联的对象。不能将容器类型定义为基类型,当加入派生类对象时,派生类对象会被切掉,只保存基类的部分。也不能将容器定义为派生类对象。唯一可行的方法就是将容器定义为基类对象的指针。但是用户必须保证,只要容器存在,指针所指的对象就存在。用户必须保证在容器消失时释放对象。引用更不可能,这都忘了?看顺序容器的介绍吧。在观察者模式时试了这几种方法,只有指针可以,当时还不明白,现在终于明白了。看来有一定的实践对帮助理解还是大有帮助的。