C++ 虚函数基础

   理一下虚函数相关的基础概念,以及虚函数的各种使用场景。包括虚函数使用时的一些较佳实践,和一些需要注意的地方。

目录

虚函数介绍

   虚函数是基类希望其派生类进行覆盖的函数。当使用指针或引用调用虚函数时,会检查指针指向的是基类还是派生类,并执行相应版本的函数。

class Base
{
public:
    virtual void function1() {
        std::cout << "Base" << std::endl;
    };
};

class Derived : public Base
{
public:
    virtual void function1() override {
        std::cout << "Derived" << std::endl;
    };
};

int main() {
    Derived d1;
    Base *p = &d1;
    p->function1(); //  "Derived"
    return 0;
}

   这就是虚函数的基本用法,用指针可以调用到相应类中的同名同参函数。这也是面向对象的多态性,相同的接口表现出不同行为。在 C++ 的面向对象体系中,子类可以重写 (override)父类的函数,使得子类在同样的接口表现出不一样的行为。

使用虚函数

虚函数的一些规则:
1. 继承类中重写 (override) 的虚函数的函数名与形参列表必须一致,返回类型也要一致

  1. 一旦一个函数被定义为 virtual,则该函数在整个继承体系里都是虚函数,即使子类的函数没有 virtual 关键字

  2. virtual 会去掉类内函数的内联属性 (inline),因为内联函数可能会被编译器优化掉,不能在运行中动态确定位置。

  3. 在 C++11 的新标准中,引入了 overridefinal,使用这两个限定符可以获得编译器的帮助。

使用 override 限定符显式重载
class Base
{
public:
  virtual void function();
};

class Derived : public Base
{
public:
  virtual void fnuction();
};

  在上面这个例子里,Derived 类的函数名写错了,但编译器会认为 Derived 类声明了一个新的函数。这个问题只有等到运行时才会被暴露出来。

class Derived : public Base
{
public:
  virtual void fnuction() override; // compile error, function is not an override
};

  如果添加了限定符 override,则编译器会报错说没有可以重写的函数。virtual 关键字虽然可以省略,但也推荐写上,可以减少代码阅读的困难。

使用 final 限定符限制继承
class Base
{
public:
  virtual void function() final;
};

class Derived : public Base
{
public:
  virtual void function(); // compile error, cannot override
};

  假如不需要再继承此函数,可以使用 final 来获得编译器的帮助。overridefinal 的使用规则和 const 差不多,都可以帮助我们把问题暴露在编译期。只要有需要,尽量都加上。

需要注意的地方
1. 默认参数是静态绑定的
class Base
{
public:
    virtual void function(int a = 0) {
        std::cout << "Base : " << a << std::endl;
    };
};

class Derived : public Base
{
public:
    virtual void function(int b = 1) override {
        std::cout << "Derived : " << b << std::endl;
    };
};

int main() {
    Derived d;
    Base *p = &d;
    p->function();  // Derived : 0
    return 0;
}

  虚函数的默认实参是在编译期决定的,当编译器发现调用的参数个数少了,就把默认参数传入。所以在这个例子里,p 的静态类型为 Base *,编译器根据其静态类型预先分配好了默认参数,程序又在运行时根据其动态类型找到了 Derived 的虚函数,所以结果为 Derived : 0

最好不要在重写虚函数时的更改默认实参,如果非要有默认实参,可以在非虚函数中指定默认参数,然后在非虚函数中调用虚函数

更不要在子类中隐藏非虚函数的默认实参,最好是不要在子类隐藏非虚函数

2. 返回参数不同的虚函数

  这是虚函数的一个特例,如果父类返回的是一个类的指针或引用,重写的函数可以返回其继承类的指针或引用。虽然允许这样写,但重写的函数仍然只会返回父类的指针或引用。

class Base
{
public:
    virtual Base* getPointer() {
        std::cout << "get Base" << std::endl;
        return this;
    }
    void printType() {
        std::cout << "Base" << endl;
    }
};

class Derived : public Base
{
public:
    virtual Derived* getPointer() override {    // covariant return type
        std::cout << "get Derived" << std::endl;
        return this;
    }
    void printType() {
        std::cout << "Derived" << endl;
    }
};

int main()
{
    Derived d;
    Base *b = &d;
    d.getPointer()->printType(); // get Derived, Derived
    b->getPointer()->printType(); // get Derived, Base
    return 0;
}

getPointer 函数返回的分别是 Base *Derived *。使用 d 对象直接调用函数时,首先调用了 Derived::getPointer(),得到了 Derived * 类型的 this 指针,再通过这个指针调用了非虚函数 Derived::printType()

使用指针 b 调用对象时,因为Base::getPointer()是虚函数,所以会调用Derived::getPointer(),虽然这个函数返回 Derived *,但因为这个虚函数的基类版本返回的是 Base *,所以返回的 this 会被转换成 Base *,导致之后调用了 Base::printType()

3. 调用特定版本的虚函数

可以通过作用域限定符 :: 调用特定版本的虚函数。

d.getPointer()->Base::getPointer()->printType();
//  get Derived, get Base, Base

通过 d 得到了 Derived * 类型的指针,但可以通过 Base:: 作用域限定符去调用 Base::getPointer

什么场合使用虚函数?
1. 虚析构

  如果一个类会被继承,那么它的析构函数需要是虚函数。

来看一个例子

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

class Derived : public Base
{
private:
    int *p;
public:
    Derived(int size) {
        p = new int[size];
    }
    ~Derived() {
        std::cout << "~Derived" << std::endl;
        delete[] p;
    }
};

int main()
{
    Derived *d = new Derived(100);
    Base *b = d;
    delete b; // ~Base
    return 0;
}

上面这个例子中,delete b 时,bBase * 类型,通过 b 去析构其指向的 Derived 类,因为析构函数不是虚函数,调用的是 Base 的析构函数,并没有清理掉 Derived 类的成员,造成了内存泄漏。

  所以,会被继承的类必须要使用虚析构。不用来继承的类不需要虚析构,因为虚函数比普通函数要消耗更多的性能,这一点之后会说。

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

STL 容器都是非虚析构,最好不要继承

2. 在类中其他地方使用虚函数

构造函数可以被声明成虚函数吗?需要被声明成虚函数吗?

  构造函数并不能被声明成虚函数。在一个对象构造时,这个对象的类型还不是完整的(有成员还没有生成完),并不能确定这个对象是什么类型。而虚函数是要通过对象的动态类型来解析的,显然不能将虚函数的机制运用在构造函数上。

构造与析构函数内部可以使用虚函数吗?

   如上所述,在对象构造时,该类型还不完全,构造函数内部的函数会被静态地当作该类型的成员函数。虽然可以在构造函数中使用虚函数,但意义不大。

   析构的时候也是类似的,析构会先调用子类的析构,再调用基类的析构。如果在这期间调用虚函数,可能会调用已经析构了的子类对象的函数,这是很危险的。所以析构函数里的虚函数也会被静态地当作该类型的成员函数。

静态成员函数可以是虚函数吗?

  静态成员函数 (static member function) ,动态多态,这两个从命名上来说就没有交集。静态成员函数是编译期绑定的,其在调用的时候不会传入 this 指针,也无从判别其动态类型了。

在非虚函数里调用虚函数会怎么样?

  类的成员函数在被调用时都会得到 this 指针(除了静态成员函数),在成员函数内部调用虚函数,等同于通过 this 指针去调用虚函数。例如在 func1() 中调用 virtual func2()func2() 会是动态绑定的,要注意的是, func1()还是静态绑定的。

输出流运算符需要是虚函数吗?

  通常,输出一个类对象需要一个输出流的友元函数 (friend),如果不使用虚函数,那就需要给继承体系中每一个子类都添加一个友元函数。但是,友元函数并不是类的成员函数,不能给友元函数添加 virtual 关键字。

  可以采用在非虚函数里调用虚函数的方法,在基类友元函数的函数体中调用另一个自定义的输出虚函数。就可以保证整个继承体系都能被输出了。

class Base
{
public:
    friend std::ostream& operator<<(std::ostream &out, const Base &b)
    {
        return b.print(out);
    }

    virtual std::ostream& print(std::ostream& out) const
    {
        out << "Base";
        return out;
    }
};

class Derived : public Base
{
public:
    virtual std::ostream& print(std::ostream& out) const override
    {
        out << "Derived";
        return out;
    }
};
3. 纯虚函数与抽象类

  在面向对象编程中,经常会有抽象概念,比如说视频文件,而我们播放的是某种格式的视频文件。

class VideoFile
{
public:
    virtual void play() {
        std::cout << "VideoFile" << std::endl;
    }
};

class mp4 : public VideoFile {
public:

};

int main()
{
    mp4 m;
    m.play(); // Video File
    return 0;
}

  假如忘记写 mp4 的播放函数了,然后又播放了mp4,这样就会调用基类的 play 函数。而且,我们也不希望基类函数的 play 有任何行为,但如果基类的 play 函数体为空,我们连播放 mp4 出错了都不知道。

  C++ 提供了纯虚函数来解决这个问题。

class VideoFile
{
public:
    virtual void play() = 0;
};
class mp4 : public VideoFile {
public:
    // compile error : pure virtual function Videofile::play() has no override
};

int main()
{
    VideoFile v; // compile error : object of abstract class type is not allowed
    return 0;
}

  在一个类中定义了纯虚函数后,该类就会被编译器视为抽象类,不能生成该类的对象。另外,编译器也会强制要求抽象类的继承类 override 纯虚函数。之前说的问题都可以通过纯虚函数来解决。

  纯虚函数是为了抽象类而存在的,抽象类一般都是基类,只为了提供接口,便于继承。没有成员的抽象类也被称为接口类。

纯虚函数也可以有函数体,但只能放在类外,不能嵌入。

class VideoFile
{
public:
    virtual void play() = 0;
};

void VideoFile::play() {
    std::cout << "File format is not supported" << std::endl;
}

class mp4 : public VideoFile {
public:
    virtual void play() override {
        VideoFile::play();
    }
};

int main()
{
    VideoFile v; // compile error : object of abstract 
    mp4 m;
    m.play(); // File format is not supported
    return 0;
}

  有函数体的纯虚类仍然不能生成对象,但可以为继承类提供一个默认实现,可以手动调用。

纯虚析构必须要有定义

  当子类被析构时,必定会调用父类的析构函数,即使父类的析构是纯虚析构,也需要定义函数体,否则链接器会报错。

小小的总结一下

什么时候该选用纯虚函数呢?

  • 基类不需要实例化
  • 子类基本都需要 override 基类的虚函数时
  • 抽象类的析构函数必须是纯虚函数

什么时候不用虚函数呢?

  • 不需要多态特性时
  • 不是基类,不要虚析构
函数类型功能
纯虚函数只继承接口
虚函数继承接口和一份默认实现
非虚函数继承接口和一份强制实现(不要隐藏非虚函数)

返回目录

返回目录

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值