一、虚函数与多态
1.如果将基类中的一个成员函数声明为虚函数(virtual),那么其子类中与该虚函数具有相同原型(返回类型相同、函数名相同、参数表相同、常属性相同)的成员函数就也成为虚函数,
并且和基类中的版本构成覆盖(override)关系。通过指向子类对象的基类指针,或者引用子类对象的基类引用,调用虚函数,实际被调用的将是子类中的覆盖版本。这种特性被称为多态。
2.关于虚函数覆盖
1)基类中的版本必须被声明为虚函数。
2)子类中的覆盖版本,函数名、参数表和常属性,必须和基类中的版本完全相同。
3)如果返回类型为基本类型,则覆盖版本必须和基类版本完全一致。如果返回类型是类类型的指针或引用,那么覆盖版本可以返回基类返回类型的子类。
class A;
class B : public A {};
class X {
virtual A* foo (void) {...}
};
class Y : public X {
B* foo (void) {...}
};
4)子类中的覆盖版本不能比基类中的版本声明可抛出更多的异常。
5)子类的覆盖版本与基类版本的访控属性无关。
class X {
public:
virtual void foo (void) {...}
};
class Y : public X {
private:
void foo (void) {...}
};
X* px = new Y;
px -> foo (); // Y::foo
3.函数名相同:重载、隐藏、覆盖
重载:同一个作用域
覆盖:虚函数、参数表、常属性、返回值一致
隐藏:函数名相同且不构成覆盖
class X {
public:
void foo (int a) {...} // a
virtual void foo (int a, int b) {...} // b
virtual void bar (void) {...} // c
};
class Y : public X {
public:
void foo (int a) {...} // A
void foo (int a, int b) {...} // B
int bar (void) {...} // C
a-b:重载
a-A:隐藏
b-B:覆盖
c-C:错误,编译器试图按覆盖处理,但是发现返回类型不一致,报错。
};
4.多态的必要条件:虚函数及覆盖+指针/引用
class A {
public:
virtual void foo (void) {...}
};
class B : public A {
public:
void foo (void) {...}
};
B b;
A a = b;
a.foo (); // A::foo
A* pa = &b;
pa -> foo (); // B::foo
A& ra = b;
ra.foo (); // B::foo
class X {
public:
X (void) {
bar (); // X::bar
}
~X (void) {
bar (); // X::bar
}
void foo (void) {
bar (); // this -> bar ();
}
virtual void bar (void) {...}
};
class Y : public X {
public:
void bar (void) {..}
};
Y y;
y.foo ();
5.在构造函数和析构函数中调用虚函数没有多态性。
二、纯虚函数、抽象类、纯抽象类
形如:
virtual 返回类型 成员函数名 (参数表) [常属性] = 0;
的虚函数就叫做纯虚函数。
至少包含一个纯虚函数的类就是抽象类。抽象类不能实例化为对象。
除了构造函数、析构函数、静态成员函数以外,全部由纯虚函数构成的抽象类就叫做纯抽象类。
如果一个抽象类的子类没有为其基类中的全部纯虚函数提供覆盖,那么该子类也是抽象类。
注意不要混淆虚基类和抽象基类。
三、动态绑定
1.动态绑定的实现机理——虚函数表
每个包含虚函数的类,一旦被实例化为对象,其中都会包含一个指向虚函数表的指针,虚函数表中保存了每个虚函数的地址。通过一个指向子类对象的基类指针,
或引用子类对象的基类引用调用虚函数时,编译器并不直接生成函数调用指令,相反它会插入一段代码,该代码在程序运行时执行,完成以下工作:
1)确认指针或引用目标对象的类型;
2)通过目标对象的虚函数表指针找到虚函数表;
3)从虚函数表提取特定虚函数版本的函数地址;
4)根据虚函数地址调用正确的实现版本。
以上四步均在运行阶段完成,故谓之动态绑定,亦称运行时绑定。
讨论:
1)虚函数能否内联?否
2)静态成员函数能否被定义为虚函数?否
3)构造函数能否被定义为虚函数?否
4)成员函数形式的运算符函数能否被定义为虚函数?能
5)析构函数能否被定义为虚函数?可以而且很多时候还必须提供虚析构函数!
如果基类的析构函数被定义为虚函数,那么delete一个指向子类对象的基类指针,实际被调用的将是子类的析构函数,而子类的析构函数又会自动调用基类的析构函数,
从而保证子类特有的部分和从基类继承的部分都能得到完整的析构。
一般而言,如果一个基类包含了至少一个虚函数,那么就应该为其定义一个虚析构函数,即使该析构函数什么也不做。
1.如果将基类中的一个成员函数声明为虚函数(virtual),那么其子类中与该虚函数具有相同原型(返回类型相同、函数名相同、参数表相同、常属性相同)的成员函数就也成为虚函数,
并且和基类中的版本构成覆盖(override)关系。通过指向子类对象的基类指针,或者引用子类对象的基类引用,调用虚函数,实际被调用的将是子类中的覆盖版本。这种特性被称为多态。
2.关于虚函数覆盖
1)基类中的版本必须被声明为虚函数。
2)子类中的覆盖版本,函数名、参数表和常属性,必须和基类中的版本完全相同。
3)如果返回类型为基本类型,则覆盖版本必须和基类版本完全一致。如果返回类型是类类型的指针或引用,那么覆盖版本可以返回基类返回类型的子类。
class A;
class B : public A {};
class X {
virtual A* foo (void) {...}
};
class Y : public X {
B* foo (void) {...}
};
4)子类中的覆盖版本不能比基类中的版本声明可抛出更多的异常。
5)子类的覆盖版本与基类版本的访控属性无关。
class X {
public:
virtual void foo (void) {...}
};
class Y : public X {
private:
void foo (void) {...}
};
X* px = new Y;
px -> foo (); // Y::foo
3.函数名相同:重载、隐藏、覆盖
重载:同一个作用域
覆盖:虚函数、参数表、常属性、返回值一致
隐藏:函数名相同且不构成覆盖
class X {
public:
void foo (int a) {...} // a
virtual void foo (int a, int b) {...} // b
virtual void bar (void) {...} // c
};
class Y : public X {
public:
void foo (int a) {...} // A
void foo (int a, int b) {...} // B
int bar (void) {...} // C
a-b:重载
a-A:隐藏
b-B:覆盖
c-C:错误,编译器试图按覆盖处理,但是发现返回类型不一致,报错。
};
4.多态的必要条件:虚函数及覆盖+指针/引用
class A {
public:
virtual void foo (void) {...}
};
class B : public A {
public:
void foo (void) {...}
};
B b;
A a = b;
a.foo (); // A::foo
A* pa = &b;
pa -> foo (); // B::foo
A& ra = b;
ra.foo (); // B::foo
class X {
public:
X (void) {
bar (); // X::bar
}
~X (void) {
bar (); // X::bar
}
void foo (void) {
bar (); // this -> bar ();
}
virtual void bar (void) {...}
};
class Y : public X {
public:
void bar (void) {..}
};
Y y;
y.foo ();
5.在构造函数和析构函数中调用虚函数没有多态性。
二、纯虚函数、抽象类、纯抽象类
形如:
virtual 返回类型 成员函数名 (参数表) [常属性] = 0;
的虚函数就叫做纯虚函数。
至少包含一个纯虚函数的类就是抽象类。抽象类不能实例化为对象。
除了构造函数、析构函数、静态成员函数以外,全部由纯虚函数构成的抽象类就叫做纯抽象类。
如果一个抽象类的子类没有为其基类中的全部纯虚函数提供覆盖,那么该子类也是抽象类。
注意不要混淆虚基类和抽象基类。
三、动态绑定
1.动态绑定的实现机理——虚函数表
每个包含虚函数的类,一旦被实例化为对象,其中都会包含一个指向虚函数表的指针,虚函数表中保存了每个虚函数的地址。通过一个指向子类对象的基类指针,
或引用子类对象的基类引用调用虚函数时,编译器并不直接生成函数调用指令,相反它会插入一段代码,该代码在程序运行时执行,完成以下工作:
1)确认指针或引用目标对象的类型;
2)通过目标对象的虚函数表指针找到虚函数表;
3)从虚函数表提取特定虚函数版本的函数地址;
4)根据虚函数地址调用正确的实现版本。
以上四步均在运行阶段完成,故谓之动态绑定,亦称运行时绑定。
讨论:
1)虚函数能否内联?否
2)静态成员函数能否被定义为虚函数?否
3)构造函数能否被定义为虚函数?否
4)成员函数形式的运算符函数能否被定义为虚函数?能
5)析构函数能否被定义为虚函数?可以而且很多时候还必须提供虚析构函数!
如果基类的析构函数被定义为虚函数,那么delete一个指向子类对象的基类指针,实际被调用的将是子类的析构函数,而子类的析构函数又会自动调用基类的析构函数,
从而保证子类特有的部分和从基类继承的部分都能得到完整的析构。
一般而言,如果一个基类包含了至少一个虚函数,那么就应该为其定义一个虚析构函数,即使该析构函数什么也不做。