15.1 动态绑定:
(1)c++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。(两个条件:虚函数,通过基类类型对象的指针或引用调用)
(2)基类的引用(或指针)既可以指向基类对象也可以指向派生类对象。
(3)通过基类的引用或指针,如果调用的是非虚函数,则无论实际对象是何种类型,都执行基类类型所定义的函数,如果调用的是虚函数,具体调用的是哪个类的虚函数在运行时由引用或指针指向的实际对象所确定。
(4) 强制避免动态绑定:通过使用作用域操作符::,可以强制使用某个版本的虚函数,避免动态绑定
15.2 基类和派生类
(1)protected关键字
【1】像private成员一样,protected成员不能被类的用户访问 ; 像public成员一样,protected成员可以被类的派生类访问。
【2】关于protected成员的一个重要性质:派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限
【例】Base类拥有protected成员e,Sub类是Base类的派生类,它定义了一个成员函数func
void Sub::func(const Base &b , const Sub &s){
int e1 = e; //ok
int e2 = s.e // ok
int e3 = b.e //error
}
(2) 关于派生类
【1】派生类中的虚函数声明必须与基类中的定义方式完全匹配,只有一个例外 : 返回对基类类型的引用(或指针)的虚函数,派生类中的虚函数可以返回派生类自身类型的指针或引用。
【2】派生类对象由多个部分组成:派生类本身定义的(非static)成员+基类成员(非static)组成的子对象。
【3】从效果来说,最底层的派生类对象包含其每个直接基类和间接基类的子对象。
【4】如果需要声明(但不实现)一个派生类,则声明包含类名但不包含派生列表。
【5】派生类虚函数调用基类虚函数时,必须显式使用作用域操作符,否则导致无穷递归。这一点与java不同,java通过super关键字调用基类的方法,因为c++支持多继承,所以没有super关键字
(3)公有、私有和受保护的继承
【1】派生类可以进一步限制但不能放松对所继承的成员的访问
【2】对派生类的用户来说,派生类从基类继承的成员的访问权限由该成员的访问标号和派生列表中的基类访问标号共同决定:
a. 如果是公有继承,所有成员在派生类中保持自己在基类的访问级别
b. 如果是受保护继承(protected),基类的public和protected成员在派生类中为protected成员
c. 如果是私有继承(private),基类的所有成员在派生类中为private成员
无论派生列表中使用什么访问标号,所以继承Base的类对Base中的成员具有相同的访问。派生列表中的访问标号只影响 { 派生类用户 } 对派生类继承的成员的访问。
【3】接口继承与实现继承??? P.484
【4】可以使用using Base::member 的形式在派生类中扩大继承来的成员的访问权限。P.484
【5】如果缺省派生列表中的访问标号,class定义的派生类默认是private继承,struct定义的派生类默认为public继承。
【6】友元关系不能继承:
假设类F是类A的友元,类A1是类A的派生类,则F无法访问A1中的受限成员,若F1是F的派生类,F1也无法访问A中的受限成员。
15.3 转换与继承
(1)可以将派生类的对象的引用或指针转换为基类类型的引用或指针,但没有从基类引用(指针)到派生类引用(指针)的转换。即,转换是单向的。
从基类到派生类的自动转换(指针或引用)是不存在的,需要派生类对象时不能使用基类对象。
(2)虽然可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但没有从派生类型对象到基类类型对象的直接转换。即,直接转换只发生于与引用或指针。
(3)区别:
假设Sub是类Base的派生类,现在有函数,void func1(Base &b); void func2(Base b); 以及Base的构造函数 Base(Base &b); 和 赋值操作函数operator=(Base &b);
a.如果用Sub对象s来调用func1,则引用b直接绑定到s,实际上实参是s的引用,发生的是将对Sub对象的引用转换为对Base对象的引用。
b.如果用Sub对象s来调用func2,实际上是用s作为实参调用Base的复制构造函数(Sub对象引用转换为Base对象引用)初始化形参b,发生的是对象复制,而非对象转换。
【例子】
#include
#include
#include
using namespace std;
class Base {
public:
Base(){
cout<<"Base default construct"<
};
Base(const Base &b){
cout<<"Base copy construct"<
};
Base& operator=(const Base &b){
cout<<"Base assign operator"<
return *this;
};
virtual ~Base() {
cout<<"Base destruct base"<
}
};
class Deprived: public Base {
public:
Deprived(){
cout<<"deprived default construct"<
};
virtual ~Deprived(){
cout<<"Deprived destruct base"<
};
};
void func(Base b){
cout<<"func"<
}
int main(int argc, char **argv) {
Deprived d;
func(d);
}
运行结果:
Base default construct
deprived default construct
Base copy construct
func
Base destruct base
Deprived destruct base
Base destruct base
c.如果有Sub对象s,对于表达式 Base b(s); 和 Base b=s ;来说,实际上发生的是引用转换,对Sub对象的引用被转换为对Base对象的引用并作为构造函数或者赋值操作函数的实参。
15.4 构造函数与复制控制
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。如果类不定义自己的默认构造函数和复制控制成员,就使用合成的版本。
(2)派生类构造函数
【1】派生类的默认构造函数实际执行顺序是:先隐式执行基类的默认构造函数初始化基类部分,在执行自己的初始化列表,最后执行构造函数体。
【2】派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员(编译器会显示不存在这样的数据成员)。通常,我们将基类包含在构造函数初始化列表中来间接初始化继承成员:
【例】class Sub : public Base{
private:
string s ;
public:
Sub(string para):s(para),Base(1,""){ };
}
【3】一个派生类只能初始化自己的直接基类。
(3)复制控制和继承
【1】如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分
class Derived : public Base{
public :
Derived(const Derived &d): Base(d){ ... };
};
【2】如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值:
Derived &Derived::operator=(const Derived &rhs){
//赋值操作必须防止自身赋值
if(this!=rhs){
Base::operator=(rhs);
//else
...
}
return *this;
}
【3】派生类析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数,继承层次上的每个析构函数只负责清除自己的成员。对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按继承层次依次向上调用各基类析构函数。
(4)虚析构函数:
因为发生动态绑定的条件之一是调用的是虚函数,所以析构函数必须是虚函数。否则,如果指向动态分配对象的指针的静态类型为基类类型,而实际指向的对象为派生类型,delete该指针时派生类型的析构函数不会得到执行。
像其他虚函数一样,析构函数的虚函数性质都将继承。因此,如果继承层次中根类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数总是虚函数。
构造函数不能是虚函数,因为构造函数运行时,对象的动态类型还不完整。
赋值操作符不建议设为虚函数,不但令人混淆,而且没什么用处。
避免在析构函数和构造函数中调用虚函数 。(原因见 P.497)
15.5 继承情况下的类作用域
(1)名字查找发生在编译阶段:名字的查找是在继承层次中自派生类开始,依次向上的。对象、引用或指针的静态类型决定了对象能够完成的行为,当静态类型与动态类型不同的时候,静态类型仍然决定着可以使用什么成员。
(2)与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。对于函数成员来说,即使函数原型不同,同名的基类成员也会被屏蔽。可以使用作用域操作符访问被屏蔽的基类成员: Base::member (C++没有super关键字)。
(3)继承中的重载:
派生类可以重定义继承来的成员函数(该成员在基类中拥有重载版本),但所有其他的重载版本都会被屏蔽。
【例】
Base类拥有func(int,int,int),func(int,int),func(string)三个版本的重载函数,Deprived类是Base类的派生类,并提供了func(string ,int),则在Deprived中,func的其他版本都被屏蔽了,无法通过Deprived对象重载地使用func ( 如Deprived对象d,调用d.func(1,1)将产生编译错误 )。
如果派生类想通过自身类型使用所有的重载版本,则派生类要么重定义所有重载版本,要么一个也不重定义。但是,可以通过为重载成员提供using声明的方式达到重载基类成员的目的。
如上例,
class Deprived : public Base{
using Base::func(int,int,);
using Base::func(string);
public:
void func(string,int);
void func(int,int,int);
}
此时,Deprived类拥有func的三个重载版本。func(int,int,int)重定义了基类的版本,而func(string,int)则是重载的版本。
15.6 纯虚函数
在函数形参表后面写上=0可以指定该虚函数为纯虚函数 。(类似java的抽象方法)
拥有纯虚函数的类是抽象基类,不能创建抽象基类的对象。
15.7 容器与继承
因为对象不是多态的(多态的条件是使用指针或引用),所以使用容器保存对象时,如果将容器保存的类型声明为基类类型,则在复制对象时,派生类型对象的派生部分将被切掉,只能将基类部分复制进目标对象中(本质是在调用复制构造函数时,将对派生类型的引用转换为对基类类型的引用,而不是转换对象)。为了达到保存多种不同类型的对象的目的,只能保存对象的指针(容器不能保存引用,因为引用不能赋值),或者使用句柄类来包装指针。P.504
15.8 句柄类与继承
阅读(453) | 评论(0) | 转发(0) |