C++ 参考手册
- C++11
- C++14
- C++17
- C++20
- C++ 编译器支持情况表
- 独立与宿主实现
- C++ 语言
- 变量模板(C++14 起)
- 整数字面量
- 聚合初始化
- 比较运算符
- 默认比较(C++20 起)
- 转义序列
- for 循环
- while 循环
- 用户定义转换
- SFINAE
- 主函数
- ASCII 码表
- 标识符
- 类型
- 内存模型
- 对象
- 基本概念
- 表达式
- 声明
- 初始化
- 函数
- 语句
- 类
- 运算符重载
- 模板
- 异常
- 事务性内存
- 占位符类型说明符 (C++11 起)
- decltype 说明符
- 函数声明
- final 说明符 (C++11 起)
- override 说明符(C++11 起)
- 引用声明
- 移动构造函数
- 移动赋值运算符
- 枚举声明
- constexpr 说明符(C++11 起)
- 列表初始化 (C++11 起)
- 构造函数与成员初始化器列表
- using 声明
- nullptr,指针字面量
- 基础类型
- 类型别名,别名模版 (C++11 起)
- 形参包
- 联合体声明
- 字符串字面量
- 用户定义字面量 (C++11 起)
- 属性说明符序列(C++11 起)
- Lambda 表达式 (C++11 起)
- noexcept 说明符 (C++11 起)
- noexcept 运算符 (C++11 起)
- alignof 运算符(C++11 起)
- alignas 说明符 (C++11 起)
- 存储类说明符
- 基于范围的 for 循环 (C++11 起)
- static_assert 声明
- 隐式转换
- 代用运算符表示
- 自增/自减运算符
- 折叠表达式(C++17 起)
- 类模板实参推导(C++17 起)
- 模板形参与模板实参
- if 语句
- inline 说明符
- 结构化绑定声明 (C++17 起)
- switch 语句
- 字符字面量
- 命名空间
- 求值顺序
- 复制消除
- consteval 说明符 (C++20 起)
- constinit 说明符 (C++20 起)
- 协程 (C++20)
- 模块 (C++20 起)
- 约束与概念 (C++20 起)
- new 表达式
- do-while 循环
- continue 语句
- break 语句
- goto 语句
- return 语句
- 动态异常说明
- throw 表达式
- try 块
- 命名空间别名
- 类声明
- cv(const 与 volatile)类型限定符
- 默认初始化
- 值初始化(C++03 起)
- 零初始化
- 复制初始化
- 直接初始化
- 常量初始化
- 引用初始化
- 值类别
- C++ 运算符优先级
- 布尔字面量
- 浮点字面量
- typedef 说明符
- 显式类型转换
- static_cast 转换
- dynamic_cast 转换
- const_cast 转换
- reinterpret_cast 转换
- delete 表达式
- 构造函数与成员初始化器列表
- this 指针
- 访问说明符
- 友元声明
- virtual 函数说明符
- explicit 说明符
- 静态成员
- 默认构造函数
- 复制构造函数
- 复制赋值运算符
- 析构函数
- 类模板
- 函数模板
- 显式(全)模板特化
- 汇编声明
- C++ 的历史
- 作用域
- 生存期
- 定义与单一定义规则(ODR)
- 名字查找
- 有限定的名字查找
- 无限定的名字查找
- 如同规则
- 未定义行为
- 翻译阶段
- 常量表达式
- 赋值运算符
- 算术运算符
- 逻辑运算符
- 成员访问运算符
- 其他运算符
- sizeof 运算符
- typeid 运算符
- 指针声明
- 数组声明
- 语言链接
- 详述类型说明符
- 默认实参
- 变长实参
- 实参依赖查找
- 重载决议
- 重载函数的地址
- 注入类名
- 非静态数据成员
- 非静态成员函数
- 嵌套类
- 派生类
- 空基类优化
- 抽象类
- 位域
- 转换构造函数
- 成员模板
- 模板实参推导
- 部分模板特化
- sizeof... 运算符
- 待决名
- 函数 try 块
- 扩充命名空间 std
- 字母缩写
- RAII
- 三/五/零之法则
- PImpl
- 零开销原则
- 类型
- 隐式转换
- 注释
- C++ 关键词
- 预处理器
- C++ 标准库头文件
- 具名要求
- 功能特性测试 (C++20)
- 工具库
- 类型支持(基本类型、RTTI、类型特性)
- 概念库 (C++20)
- 错误处理
- 动态内存管理
- 日期和时间工具
- 字符串库
- 容器库
- 迭代器库
- 范围库 (C++20)
- 算法库
- 数值库
- 输入/输出库
- 文件系统库
- 本地化库
- 正则表达式库
- 原子操作库
- 线程支持库
- 实验性 C++ 特性
- 有用的资源
- 索引
- std 符号索引
- 协程支持 (C++20)
- C++ 关键词
virtual 函数说明符
virtual 说明符指定非静态成员函数为虚函数并支持动态调用派发。它只能在非静态成员函数的首个声明(即当它于类定义中声明时)的 声明说明符序列 中出现。
解释
虚函数是可在派生类中覆盖其行为的成员函数。与非虚函数相反,即使没有关于该类实际类型的编译时信息,仍然保留被覆盖的行为。当使用到基类的指针或引用来处理派生类时,对被覆盖的虚函数的调用,将会调用定义于派生类中的行为。当使用有限定名字查找(即函数名出现在作用域解析运算符 ::
的右侧)时,此行为被抑制。
#include <iostream> struct Base { virtual void f() { std::cout << "base\n"; } }; struct Derived : Base { void f() override { // 'override' 可选 std::cout << "derived\n"; } }; int main() { Base b; Derived d; // 通过引用调用虚函数 Base& br = b; // br 的类型是 Base& Base& dr = d; // dr 的类型也是 Base& br.f(); // 打印 "base" dr.f(); // 打印 "derived" // 通过指针调用虚函数 Base* bp = &b; // bp 的类型是 Base* Base* dp = &d; // dp 的类型也是 Base* bp->f(); // 打印 "base" dp->f(); // 打印 "derived" // 非虚函数调用 br.Base::f(); // 打印 "base" dr.Base::f(); // 打印 "base" }
细节
若某个成员函数 vf
在类 Base
中被声明为 virtual
,且某个直接或间接派生于 Base
的类 Derived
拥有一个下列几项与之相同的成员函数声明
- 名字
- 形参列表(但非返回类型)
- cv 限定符
- 引用限定符
则类 Derived
中的此函数亦为虚函数(无论其声明中是否使用关键词 virtual
)并覆盖 Base::vf(无论其声明中是否使用单词 override
)。
要覆盖的 Base::vf
不需要可见(可声明为 private,或用私有继承继承)。
class B { virtual void do_f(); // 私有成员 public: void f() { do_f(); } // 公开接口 }; struct D : public B { void do_f() override; // 覆盖 B::do_f }; int main() { D d; B* bp = &d; bp->f(); // 内部调用 D::do_f(); }
每个虚函数都有其最终覆盖函数,它是进行虚函数调用时所执行的函数。基类 Base
的虚成员函数 vf
是最终覆盖函数,除非派生类声明或(通过多重继承)继承了覆盖 vf
的另一个函数。
struct A { virtual void f(); }; // A::f 是虚函数 struct B : A { void f(); }; // B::f 覆盖 A::f in B struct C : virtual B { void f(); }; // C::f 覆盖 A::f in C struct D : virtual B {}; // D 不引入覆盖函数,B::f 在 D 中为最终 struct E : C, D { // E 不引入覆盖函数,C::f 在 E 中为最终 using A::f; // 非函数声明,仅令 A::f 能为查找所见 }; int main() { E e; e.f(); // 虚调用调用 C::f,e 中的最终覆盖函数 e.E::f(); // 非虚调用调用 A::f,它在 E 中可见 }
若一个函数拥有多于一个最终覆盖函数,则程序非良构:
struct A { virtual void f(); }; struct VB1 : virtual A { void f(); // 覆盖 A::f }; struct VB2 : virtual A { void f(); // 覆盖 A::f }; // struct Error : VB1, VB2 { // // 错误:A::f 在 Error 中拥有两个最终覆盖函数 // }; struct Okay : VB1, VB2 { void f(); // OK:这是 A::f 的最终覆盖函数 }; struct VB1a : virtual A {}; // 不声明覆盖函数 struct Da : VB1a, VB2 { // Da 中,A::f 的最终覆盖函数是 VB2::f };
具有相同名字但不同形参列表的函数并不覆盖同名的基类函数,但会隐藏它:在无限定名字查找检查派生类的作用域时,查找找到该声明而不再检查基类。
struct B { virtual void f(); }; struct D : B { void f(int); // D::f 隐藏 B::f(错误的形参列表) }; struct D2 : D { void f(); // D2::f 覆盖 B::f(它不可见也不要紧) }; int main() { B b; B& b_as_b = b; D d; B& d_as_b = d; D& d_as_d = d; D2 d2; B& d2_as_b = d2; D& d2_as_d = d2; b_as_b.f(); // 调用 B::f() d_as_b.f(); // 调用 B::f() d2_as_b.f(); // 调用 D2::f() d_as_d.f(); // 错误:D 中的查找只找到 f(int) d2_as_d.f(); // 错误:D 中的查找只找到 f(int) }
若函数以说明符 struct B { virtual void f(int); }; struct D : B { virtual void f(int) override; // OK,D::f(int) 覆盖 B::f(int) virtual void f(long) override; // 错误:f(long) 不覆盖 B::f(int) }; 若函数以说明符 struct B { virtual void f() const final; }; struct D : B { void f() const; // 错误:D::f 试图覆盖 final B::f }; |
(C++11 起) |
非成员函数和静态成员函数不能为虚函数。
函数模板不能被声明为 virtual
。这只适用于自身是模板的函数——类模板的常规成员函数可被声明为虚函数。
虚函数(无论是声明为 virtual 者还是覆盖函数)不能有任何关联制约 struct A { virtual void f() requires true; // 错误:受制约的虚函数 };
|
(C++20 起) |
在编译时替换虚函数的默认实参。
协变返回类型
若函数 Derived::f
覆盖 Base::f
,则其返回类型必须要么相同要么为协变(covariant)。当满足所有下列要求时,两个类型为协变:
- 两个类型均为到类的指针或引用(左值或右值)。不允许多级指针或引用。
-
Base::f()
的返回类型中被引用/指向的类,必须是Derived::f()
的返回类型中被引用/指向的类的无歧义且可访问的直接或间接基类。 -
Derived::f()
的返回类型必须有相对于Base::f()
的返回类型的相等或较少的 cv 限定。
Derived::f
的返回类型中的类必须要么是 Derived
自身,要么必须是于 Derived::f
声明点的某个完整类型。
进行虚函数调用时,最终覆盖函数的返回类型被隐式转换成所调用的被覆盖函数的返回类型:
class B {}; struct Base { virtual void vf1(); virtual void vf2(); virtual void vf3(); virtual B* vf4(); virtual B* vf5(); }; class D : private B { friend struct Derived; // Derived 中,B 是 D 的可访问基类 }; class A; // 前置声明的类是不完整类型 struct Derived : public Base { void vf1(); // 虚函数,覆盖 Base::vf1() void vf2(int); // 非虚函数,隐藏 Base::vf2() // char vf3(); // 错误:覆盖 Base::vf3,但具有不同且非协变的返回类型 D* vf4(); // 覆盖 Base::vf4() 并具有协变的返回类型 // A* vf5(); // 错误:A 是不完整类型 }; int main() { Derived d; Base& br = d; Derived& dr = d; br.vf1(); // 调用 Derived::vf1() br.vf2(); // 调用 Base::vf2() // dr.vf2(); // 错误:vf2(int) 隐藏 vf2() B* p = br.vf4(); // 调用 Derived::vf4() 并将结果转换为 B* D* q = dr.vf4(); // 调用 Derived::vf4() 而不将结果转换为 B* }
虚析构函数
虽然析构函数是不继承的,但若基类声明其析构函数为 virtual
,则派生的析构函数始终覆盖它。这使得可以通过指向基类的指针 delete 动态分配的多态类型对象
class Base { public: virtual ~Base() { /* 释放 Base 的资源 */ } }; class Derived : public Base { ~Derived() { /* 释放 Derived 的资源 */ } }; int main() { Base* b = new Derived; delete b; // 进行对 Base::~Base() 的虚函数调用 // 由于它是虚函数,故它调用的是 Derived::~Derived(), // 这就能释放派生类的资源,然后遵循通常的析构顺序 // 调用 Base::~Base() }
此外,若类是多态的(声明或继承了至少一个虚函数),且其析构函数非虚,则删除它是未定义行为,无论不调用派生的析构函数时是否会导致资源泄漏。
一条有用的方针是,任何基类的析构函数必须为公开且虚,或受保护且非虚。
在构造和析构期间
当从构造函数或从析构函数中直接或间接调用虚函数(包括在类的非静态数据成员的构造或析构期间,例如在成员初始化器列表中),且对其实施调用的对象是正在构造或析构中的对象时,所调用的函数是构造函数或析构函数的类中的最终覆盖函数,而非进一步的派生类中的覆盖函数。 换言之,在构造和析构期间,进一步的派生类并不存在。
当构建具有多个分支的复杂类时,在属于一个分支的构造函数内,多态被限制到该类及其基类:若它获得了指向这个子层级之外的某个基类子对象的指针或引用,且试图进行虚函数调用(例如通过显式成员访问),则行为未定义:
struct V { virtual void f(); virtual void g(); }; struct A : virtual V { virtual void f(); // A::f 是 V::f 在 A 中的最终覆盖函数 }; struct B : virtual V { virtual void g(); // B::g 是 V::g 在 B 中的最终覆盖函数 B(V*, A*); }; struct D : A, B { virtual void f(); // D::f 是 V::f 在 D 中的最终覆盖函数 virtual void g(); // D::g 是 V::g 在 D 中的最终覆盖函数 // 注意:A 在 B 之前初始化 D() : B((A*)this, this) { } }; // B 的构造函数,从 D 的构造函数调用 B::B(V* v, A* a) { f(); // 对 V::f 的虚调用(尽管 D 拥有最终覆盖函数,D 也不存在) g(); // 对 B::g 的虚调用,在 B 中是最终覆盖函数 v->g(); // v 的类型 V 是 B 的基类,虚调用如前调用 B::g a->f(); // a 的类型 A 不是 B 的基类,它属于层级中的不同分支。 // 尝试通过这个分支进行虚调用导致未定义行为, // 即使此情况下 A 已完成构造 // (它在 B 之前构造,因为它在 D 的基类列表中先于 B 出现) // 实践中,对 A::f 的虚调用会试图使用 B 的虚成员函数表, // 因为它在 B 的构造期间是活跃的 }
参阅
- 派生类和继承模式
- override 说明符 (C++11 起)
- final 说明符 (C++11 起)