一、多态基础
1.1 静态编联与动态编联
class Shape
{
public:
void show() const {
cout << "area: " << get_area() << endl;
}
float get_area() const { return 0; }
};
class Rectangle : public Shape
{
public:
Rectangle(float w, float h)
: _width(w), _height(h) {}
float get_area() const { return _width * _height; }
private:
float _width;
float _height;
};
class Circle : public Shape {
public:
Circle(float r) : _radius(r) {}
float get_area() const { return _radius * _radius * 3.14; }
private:
float _radius;
};
int main()
{
Rectangle rect(1, 2);
Circle cir(1);
Shape &sp1 = rect;
Shape &sp2 = cir;
sp1.show(); // 输出0
sp2.show(); // 输出0
return 0;
}
运行上面代码发现,矩形和圆求出的面积都为0,与我们预期不符合,这是为什么呢,原因在于使用静态编联。
静态编联(早绑定,静态绑定) :编译期间就决定了具体调用哪个函数体,即使没有主程序,也知道程序中各个函数体之间的调用关系。
/*
这段代码采用的是静态编联,使用sp1调用show函数,show函数体内调用了get_area成员函数,
调用非静态成员函数时编译器会传入隐藏this指针,因为this是指向Shape,
所以调用Shape::get_area函数而不是Rectangle::get_area函数
*/
Shape &sp1 = rect;
sp1.show();】
动态编联(晚绑定,动态绑定):在运行期间决定具体调用哪个函数体。动态编联有多种实现方式,大多数编译器使用虚机制(虚函数和虚函数表)。
1.2 多态
使用基类的指针或引用调用同一方法时,产生不同的行为,这是一种动态多态,大多数C++编译器通过虚函数实现。下文中提到的多态如没有单独说明,则都是指通过虚函数实现的动态多态。
多态构成必须满足下面两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(override)
1.3 虚函数
被virtual
修饰的类非静态成员函数就是虚函数。
class A
{
public:
virtual void f(){}; //这是一个虚函数
};
虚函数说明:
• 静态成员函数、构造函数、拷贝构造函数不能是虚函数
• 析构函数可以是虚函数
• 赋值函数通常不定义为虚函数
• 访问控制可以任意(public、protected、private )
1.4 虚函数重写(override)
虚函数重写(override):派生类中有一个跟基类完全相同的虚函数(函数名、返回值类、参数列表相同)。
说明:派生类中virtual
关键字可以不写,但不推荐。
class Base
{
public:
virtual void show() { cout << "Base" << endl; }
};
class Derived : public Base
{
public:
void show() { cout << "Derived" << endl; } // 派生类可以不写virtual关键字
};
int main() {
Derived b;
Base &ref = b;
ref.show(); // 输出 "Derived"
return 0;
}
在实际开发过程中,基类与派生类可能不是同一个人写的,如果基类是虚函数,而派生类忘记写virtual
关键字没构成多态可能带来一些不可预料问题。因此,基类是虚函数的情况下,派生类不强制需要写virtual
关键字,具有一定安全作用。
1.4.1 虚函数重写两个例外
虚函数重写有两个例外,一个是协变,一个是析构函数的重写,这两种也构成虚函数重写。
协变:派生类中重写(override)基类方法时,返回类型可以是基类方法返回类型(类型必须为指针或引用)的派生类。协变可以使代码更加灵活。
实现协变需满足以下条件:
- 基类中的函数必须是虚函数
- 派生类中重写的函数必须具有相同的函数签名(函数名、参数列表和常量性)。
- 派生类中重写的函数的返回类型必须是基类函数返回类型的派生类型。
class Base
{
public:
virtual Base &test()
{
cout << "Base" << endl;
return *this;
}
};
class Derived : public Base
{
public:
virtual Derived &test() // 协变
{
cout << "Derived" << endl;
return *this;
}
};
int main() {
Derived b;
Base &ref = b;
ref.test(); // 输出 "Derived"
return 0;
}
前文中提到析构函数是可以为虚函数的,尤其是存在继承关系的时候,建议将基类析构函数定义为虚函数。
class Base
{
public:
~Base() { cout << "~Base()" << endl; }
};
class Derived : public Base
{
public:
~Derived() { cout << "~Derived()" << endl; }
};
int main() {
Base *p = new Derived;
delete p;
return 0;
}
输出:
~Base()
调用delete p
时,我们本意根据指针(引用)指向的对象类型来选择对应的析构函数,但结果是根据指针(引用)的类型的来选择对应的析构函数,导致对象没有正确的析构,存在资源泄漏。有人立马想到可以将基类析构函数定义为虚函数解决这个问题,但是重写条件之一是函数名要相同。基类析构函数与派生类析构函数名明显不相同,编译器为了支持析构函数能为虚函数,对析构函数名做了特殊处理,编译后析构函数的名统一处理成destructor。
class Base
{
public:
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
virtual ~Derived()
{
cout << "~Derived()" << endl;
}
};
int main()
{
Base *p = new Derived;
delete p;
return 0;
}
输出
~Derived()
~Base()
通过将基类析构函数定义为虚函数,解决继承体系中析构问题。