C++学习 day11

发布于 18 天前  64 次阅读


Day 11:多态、虚函数和动态类型转换

本节代码位置:

2-代码/day11-多态/62-多态示例.cpp
2-代码/day11-多态/63-多态示例2.cpp
2-代码/day11-多态/64-final示例.cpp
2-代码/day11-多态/65-虚函数的默认值示例.cpp
2-代码/day11-多态/66-虚析构函数示例.cpp
2-代码/day11-多态/67-虚函数调用过程验证.cpp
2-代码/day11-多态/68-动态类型转换示例.cpp
2-代码/day11-多态/多态练习.cpp

1、多态的概念

多态指的是:

不同对象收到相同消息,产生不同的行为。

简单理解:

同一个函数调用,根据对象真实类型不同,执行不同版本的函数。

例如:

Shape* p 指向 Rect,对 p->area() 求矩形面积。
Shape* p 指向 Circle,对 p->area() 求圆形面积。

C++ 中多态主要分两类:

编译时多态:编译阶段就能确定调用哪个函数。
运行时多态:运行阶段根据对象真实类型决定调用哪个函数。

编译时多态:

函数重载
运算符重载

运行时多态:

通过虚函数实现。

2、虚函数

virtual 修饰的成员函数叫虚函数。

基本写法:

class Base
{
public:
    virtual void print() const
    {
        cout << "hello" << endl;
    }
};

派生类中重新定义基类虚函数,叫函数重写,也叫函数覆盖。

class Derived : public Base
{
public:
    void print() const
    {
        cout << "world" << endl;
    }
};

重写要求:

函数名相同。
参数列表相同。
返回类型相同或满足协变返回。
const 属性也要一致。

重点:

基类函数是 virtual,派生类重写时即使不写 virtual,也自动是虚函数。
但实际写代码时建议配合 override,让编译器帮忙检查。

3、动态绑定

62-多态示例.cpp 中:

void foo(Base *p)
{
    p->print();
}

调用:

foo(new Base);    // 输出 hello
foo(new Derived); // 输出 world

原因:

print() 是虚函数。
p 是基类指针。
当 p 指向 Base 对象时,调用 Base::print()。
当 p 指向 Derived 对象时,调用 Derived::print()。

运行时多态的条件:

1. 必须有继承关系。
2. 基类中有虚函数。
3. 派生类重写基类虚函数。
4. 通过基类指针或基类引用调用虚函数。

注意:

如果不是通过基类指针或引用调用,通常不会体现运行时多态。
如果基类函数没有 virtual,通过 Base* 调用时会调用 Base 的函数。

4、纯虚函数和抽象类

如果基类只想规定接口,不想提供具体实现,可以写纯虚函数。

格式:

class Shape
{
public:
    virtual double area() const = 0;
};

纯虚函数:

函数声明后面写 = 0。
表示这个函数没有普通实现,要求派生类自己实现。

抽象类:

包含纯虚函数的类叫抽象类。
抽象类不能直接创建对象。

错误示例:

Shape s; // 错误,Shape 是抽象类

派生类如果没有实现所有纯虚函数:

派生类仍然是抽象类,也不能创建对象。

5、形状多态练习

练习要求:

编写形状基类 Shape。
派生 Rect 和 Circle。
用多态方式计算各种形状对象的面积和周长。

基类:

class Shape
{
public:
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
    virtual ~Shape() {}
};

这里:

area() 是纯虚函数,求面积。
perimeter() 是纯虚函数,求周长。
~Shape() 是虚析构函数,方便通过基类指针正确释放派生类对象。

矩形:

class Rect : public Shape
{
public:
    Rect(double w, double h) : m_w(w), m_h(h) {}

    double area() const override
    {
        return m_w * m_h;
    }

    double perimeter() const override
    {
        return 2 * (m_w + m_h);
    }

private:
    double m_w;
    double m_h;
};

圆形:

class Circle : public Shape
{
public:
    Circle(double r) : m_r(r) {}

    double area() const override
    {
        return 3.14 * m_r * m_r;
    }

    double perimeter() const override
    {
        return 2 * 3.14 * m_r;
    }

private:
    double m_r;
};

统一接口:

void foo(Shape *p)
{
    cout << "面积: " << p->area() << endl;
    cout << "周长: " << p->perimeter() << endl;
    delete p;
}

调用:

foo(new Rect(100, 200));
foo(new Circle(100));

结果:

Rect 面积:100 * 200 = 20000
Rect 周长:2 * (100 + 200) = 600
Circle 面积:3.14 * 100 * 100 = 31400
Circle 周长:2 * 3.14 * 100 = 628

重点:

foo() 的参数是 Shape*,但可以接收 Rect* 和 Circle*。
p->area() 和 p->perimeter() 会根据实际对象类型动态绑定。

6、override 和 final

override 用于说明派生类函数重写了基类虚函数。

class Rect : public Shape
{
public:
    double area() const override
    {
        return m_w * m_h;
    }
};

好处:

如果函数名、参数列表、const 属性写错,编译器会报错。
可以防止本来想重写,结果写成了新的普通函数。

final 可以修饰虚函数,也可以修饰类。

修饰虚函数:

class Base
{
public:
    virtual void bar() final {}
};

class Derived : public Base
{
public:
    // void bar() override {} // 错误,final 函数不能被重写
};

修饰类:

class Demo final
{
};

// class Test : public Demo {}; // 错误,final 类不能被继承

总结:

override:我正在重写基类虚函数,请编译器检查。
final:到此为止,不允许继续重写或继承。

7、虚函数的默认参数

虚函数可以有默认参数,但默认参数是静态绑定的。

示例:

class Base
{
public:
    virtual void foo(int x = 0)
    {
        cout << "Base.foo.x = " << x << endl;
    }
};

class Derived : public Base
{
public:
    void foo(int x = 1) override
    {
        cout << "Derived.foo.x = " << x << endl;
    }
};

通过基类指针调用:

void bar(Base *p)
{
    p->foo();
}

bar(new Derived);

结果:

调用的是 Derived::foo()。
但是默认值使用的是 Base 中的 0。
输出 Derived.foo.x = 0。

通过派生类对象调用:

Derived d;
d.foo();

结果:

使用 Derived 中的默认值 1。
输出 Derived.foo.x = 1。

重点:

虚函数调用动态绑定。
默认参数静态绑定。
所以通过 Base* 调用时,默认值看 Base。

8、虚析构函数

如果一个类要作为基类,并且可能通过基类指针删除派生类对象,基类析构函数应该写成虚函数。

class Base
{
public:
    virtual ~Base()
    {
        cout << "~Base()" << endl;
    }
};

派生类:

class Derived : public Base
{
public:
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
};

使用:

Base *p = new Derived;
delete p;

如果基类析构函数是虚函数:

先调用 ~Derived()
再调用 ~Base()

如果基类析构函数不是虚函数:

通过 Base* delete 派生类对象时,可能只调用 ~Base()。
派生类资源可能无法释放,造成资源泄漏。

注意:

构造函数不能是虚函数。
在构造函数和析构函数中调用虚函数,不会发生正常的动态绑定。

9、虚函数表和虚指针

如果一个类中有虚函数,编译器会为这个类生成虚函数表。

虚函数表:vtable,保存虚函数地址。
虚指针:vptr,对象内部隐藏的指针,指向该类的虚函数表。

示例:

class Base
{
public:
    virtual void foo();
    virtual void bar();
};

大致结构:

Base 的虚函数表:
索引 0 -> Base::foo()
索引 1 -> Base::bar()

如果派生类重写虚函数:

class Derived : public Base
{
public:
    void foo() override;
    void bar() override;
};

派生类虚函数表中对应位置会被替换:

Derived 的虚函数表:
索引 0 -> Derived::foo()
索引 1 -> Derived::bar()

通过基类指针调用虚函数时:

1. 先根据对象找到隐藏的 vptr。
2. 通过 vptr 找到对象真实类型对应的 vtable。
3. 根据虚函数在表中的索引取出函数地址。
4. 调用对应函数。

所以:

Base* p = new Derived;
p->foo();

最终能调用 Derived::foo()

代价:

有虚函数的对象通常会多一个隐藏的 vptr。
对象大小会变大一点。
虚函数调用比普通函数调用多一次查表过程。

10、dynamic_cast 动态类型转换

dynamic_cast 用于多态类型之间的安全向下转换。

多态类型:

包含虚函数的类型。

向上转换:

Base *p = new Derived1;

派生类指针转基类指针,通常可以自动完成。

向下转换:

Derived1 *q = dynamic_cast<Derived1 *>(p);

基类指针转派生类指针,需要显式转换。

dynamic_cast 的特点:

运行时检查 p 实际指向的对象是不是目标类型。
如果类型匹配,转换成功。
如果类型不匹配,转换失败。

指针转换失败:

Derived2 *q2 = dynamic_cast<Derived2 *>(p);
if (q2 == nullptr)
{
    cout << "bad cast!" << endl;
}

结果:

指针转换失败返回 nullptr。

引用转换失败:

引用转换失败会抛出 std::bad_cast 异常。

使用建议:

如果必须从 Base* 判断真实派生类型,再调用派生类特有接口,可以用 dynamic_cast。
如果能通过虚函数解决,优先使用虚函数。

11、Day11 总结

1. 多态是同一接口在不同对象上产生不同行为。
2. C++ 运行时多态通过虚函数实现。
3. virtual 修饰的成员函数叫虚函数。
4. 派生类重新定义基类虚函数叫重写。
5. 运行时多态需要通过基类指针或基类引用调用虚函数。
6. 纯虚函数写成 virtual 函数声明 = 0。
7. 含有纯虚函数的类是抽象类,不能直接创建对象。
8. override 用于检查派生类是否真的重写了基类虚函数。
9. final 可以禁止虚函数继续重写,也可以禁止类继续被继承。
10. 虚函数默认参数是静态绑定,通过 Base* 调用时默认值看 Base。
11. 作为多态基类时,析构函数通常要写成 virtual。
12. 构造函数不能是虚函数。
13. 有虚函数的类通常有 vtable,对象内部通常有 vptr。
14. dynamic_cast 用于多态类型之间的安全向下转换。
15. 指针 dynamic_cast 失败返回 nullptr,引用失败抛出 std::bad_cast。

一句话记忆:

Day11 重点:virtual 让 Base* 调用时看对象真实类型;抽象类管接口,override 防写错,虚析构防泄漏,dynamic_cast 做安全向下转换。

"When faced with uncertainty, ask the spring breeze."