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 做安全向下转换。






Comments | NOTHING