编程八股文——C/C++中虚函数性质和使用

发布时间:2023年12月21日

C/C++中虚函数性质和使用

/*BrassPlus 是 Brass 的子类, ViewAcct() 是两个类中都有的方法。
由于 bp是父类指针,如果基类不用虚方法那么就会调用基类的 ViewAcct() 方法
若在基类中将 ViewAcct() 声明为虚,则 bp->ViewAcct() 根据对象类型(BrassPlus)调用 BrassPlue::ViewAcct()方法*/
BrassPlus ophelia;
Brass *bp;
bp = &ophelia;
bp->ViewAcct();  // 是调用子类还是父类的 ViewAcct() 方法?
  • 当且仅当通过指针或引用调用虚函数时,才会在运行过程解析该调用,也只有在这种情况下对象的动态类型有可能与静态类型不同。

  • 在派生类中覆盖某个虚函数时,可以再次使用virtual关键字说明函数性质,但这并非强制要求。因为一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。

  • 在派生类中覆盖某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

  • 派生类可以定义一个与基类中的虚函数名字相同但形参列表不同的函数,但编译器会认为该函数与基类中原有的函数是相互独立的,此时派生类的函数并没有覆盖掉基类中的版本。这往往会发生错误,因为我们原本希望派生类可以覆盖基类中的虚函数,但是一不小心把形参列表写错了。想调试并发现这样的错误非常困难,C++11允许派生类使用override关键字显式地注明虚函数。如果override标记了某个函数,但该函数并没有覆盖已存在的虚函数,编译器将报告错误。override位于函数参数列表之后。

struct B
{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};

struct D1 : B 
{
    void f1(int) const override;    // 正确:f1() 与基类中的 f1() 匹配
    void f2(int) override;      	// 错误:B 没有形如 f2(int) 的函数
    void f3() override;     // 错误:f3() 不是虚函数
    void f4() override;     // 错误:B 没有名为 f4() 的函数
}

与禁止类继承类似,函数也可以通过添加final关键字来禁止覆盖操作

struct D2 : B
{
    // 从 B 继承 f2() 和 f3(),覆盖 f1(int)
    void f1(int) const final;   // 不允许后续的其他类覆盖 f1(int)
};
struct D3 : D2
{
    void f2();	// 正确:覆盖从 B 继承而来的 f2()
    void f1(int) const;	// 错误:D2 已经将 f1 声明成 final。
}
  • finaloverride关键字出现在形参列表(包括任何const或引用修饰符)以及尾置返回类型之后。

  • 虚函数也可以有默认实参,每次函数调用的默认实参值由本次调用的静态类型决定。如果通过基类的指针或引用调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。

  • 如果虚函数使用默认实参,则基类和派生类中定义的默认实参值最好一致。

回避虚函数

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫执行虚函数的某一个特定版本。使用作用域运算符::可以强制执行虚函数的某个版本,不进行动态绑定。

// 强行调用 Quote 的 net_price 函数而不管 baseP 的动态类型到底是什么
double undiscounted = baseP->Quote::net_price(42);

通常情况下,只有成员函数或友元中的代码才需要使用作用域运算符来回避虚函数的动态绑定机制。

如果一个派生类虚函数需要调用它的基类版本,但没有使用作用域运算符,则在运行时该调用会被解析为对派生类版本自身的调用,从而导致无限递归。

虚函数原理

虚函数是通过虚函数表实现的:如果一个类中有一个虚函数,则系统会为这个类分配一个指针成员指向一张虚函数表(vtbl),表中每一项指向一个虚函数地址,虚函数表实际上就是一个函数指针数组。

虚函数是否可以内联

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译期间编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

虚析构函数

// Employee 是基类,Singer 是派生类
Employee *pe = new Singer;
...
delete pe;  // call ~Employee() or ~Singer()?
  • 如果基类中的析构函数不是虚的,就只调用对应于指针类型(Employee)的析构函数,但实际中,是想调用派生类的析构函数。
  • 如果基类的析构函数是虚的,将调用相应对象类型(Singer)的析构函数,然后自动调用基类的析构函数。
  • 因此,使用虚析构函数可以保证正确的析构函数序列被调用。
// Employee 是基类,Singer 是派生类
Singer *pe = new Singer;
...
delete pe;  // 先调用 ~Singer() 再调用 ~Employee(),
			// 因为 pe 是 Singer* 类型指针,而不是 Employee* 类型指针。
文章来源:https://blog.csdn.net/qq_44814825/article/details/135122861
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。