虚函数
虚函数是通过基类指针或引用来实现多态性的一种方法。为了确保在运行时正确调用派生类中的函数,虚函数需要在基类中有一个具体的实现,即使这个实现可能从未被直接调用。这样做的原因包括:
- 基类实例的使用:虽然大多数情况下,基类的实例不会直接使用虚函数,但在某些情况下,基类的实例可能会被创建和使用。在这些情况下,必须有一个默认的实现,以便程序可以正常运行。
-
派生类的重载:虚函数提供了一个默认的实现,这样如果派生类没有重载这个虚函数,仍然可以调用基类的实现。
纯虚函数
纯虚函数是一种特殊的虚函数,表示在基类中没有具体实现,但必须在派生类中被重载。纯虚函数主要用于定义接口(抽象类),使得基类不能被实例化。尽管如此,C++允许为纯虚函数提供一个定义。
纯虚函数之所以可以拥有函数体,是因为 C++ 允许为纯虚函数提供一个默认的实现。这种机制虽然不常见,但在某些情况下非常有用。纯虚函数拥有函数体的主要原因和用途如下:
- 提供默认行为:尽管纯虚函数强制派生类必须提供自己的实现,但基类仍然可以提供一个默认行为。派生类可以选择调用这个默认实现,而不是完全重新定义行为。
- 代码重用:在基类中实现一些通用功能,派生类可以通过调用基类的实现来重用这些代码。这减少了代码的重复,并确保不同派生类中的通用行为一致。
- 接口与实现分离:纯虚函数主要用于定义接口,然而在某些情况下,基类也需要包含一部分实现逻辑。通过提供纯虚函数的实现,基类可以提供这些逻辑而不会破坏接口的纯粹性。
具有函数体的纯虚函数
virtual void fun() = 0;
这行代码的含义只是代表当前这个类是一个抽象类,不允许拥有实例,但是我们仍然可以为纯虚函数指定函数体,通常情况下纯虚函数不需要函数体,是因为我们一般情况下不会调用这个函数,仅仅会调用派生类的相应函数。因此如下语法是正确的。
class Base {
public:
Base(){}
virtual void fun() = 0;
};
void Base::fun(){
cout << "i am Base::fun()" << endl;
}
但是,在语法上,纯虚函数的函数定义不能直接写入类声明中(内联函数的写法)。
这样做,虽然一般情况下不会调用Base::fun()
函数,但是这种写法是有意义的,例如多个派生类需要相同的默认实现,如果我们在基类定义了纯虚函数的函数体,那么就可以避免代码重复。如下便是一个示例:
#include <iostream>
#include <string>
using namespace std;
// 基类
class Animal {
public:
virtual void makeSound() const = 0;
// 提供一个公共接口,调用纯虚函数
void describe() const {
cout << "This animal says: ";
makeSound();
}
};
void Animal::makeSound() const {
cout << "Some generic animal sound" << endl;
}
// 派生类 - 狗
class Dog : public Animal {
public:
void makeSound() const override { cout << "Woof" << endl; }
};
// 派生类 - 猫
class Cat : public Animal {
public:
void makeSound() const override { cout << "Meow" << endl; }
};
// 派生类 - 鸟
class Bird : public Animal {
void makeSound() const override { Animal::makeSound(); }
};
int main() {
Dog dog;
Cat cat;
Bird bird;
Animal* animals[3] = {&dog, &cat, &bird};
for (auto & animal : animals)
animal->describe();
return 0;
}
析构函数能否为纯虚?
如果Base
类中,没有合适的虚函数可以设置为纯虚函数,那么能否将析构函数设置为纯虚函数?
答案是可以的,首先纯虚函数只是一种特殊的虚函数,它只不过特殊在派生类中必须实现该虚函数,那么对于析构函数来说,在派生类中显然是不可能重写基类的析构函数的,那么在这种情况下,我们必须定义纯虚析构函数的函数体。也就是如下方式:
class Base {
public:
Base(){}
virtual ~Base() = 0;
};
Base::~Base(){
cout << "i am Base::~Base()" << endl;
}
也可以从另外一个角度分析,在继承关系下,对象的创建和销毁存在一个函数调用链,一个对象的生成需要依次从基类开始,逐步向下调用子类的构造函数;一个对象的销毁,需要从派生类开始,逐步向上调用父类的析构函数。举例分析如下:
class Base {
public:
Base(){}
virtual ~Base() = 0;
};
Base::~Base(){
cout << "i am Base::~Base()" << endl;
}
class Derived : public Base {
public:
Derived(){}
virtual ~Derived(){};
};
Derived
的析构函数里面隐含调用了Base
的析构函数,而如果Base
的析构函数缺少函数体,这显然会出现错误。因此当析构函数是纯虚函数时,必须提供函数定义。
派生类显式重载基类函数是如何确定的?
在通常情况下,编译器是通过函数的签名来确定要重写的虚函数。
函数的签名包括函数名、参数的类型和顺序(不包括返回类型)。举例如下:
#include <iostream>
using namespace std;
class Base {
public:
Base() {}
virtual void fun() { cout << "void Base::fun()" << endl; };
virtual void fun(int i) { cout << "void Base::fun(int)" << endl; };
virtual double fun(double f) { cout << "double Base::fun(double)" << endl; return f;};
};
class Derived : public Base {
public:
Derived() {}
virtual void fun() { cout << "void Derived::fun()" << endl; };
virtual void fun(int i) { cout << "void Derived::fun(int)" << endl; };
virtual double fun(double f) { cout << "double Derived::fun(double)" << endl; return f;};
};
int main() {
Base * ptr = new Derived;
ptr->fun();
ptr->fun(2);
ptr->fun(1.1);
return 0;
}
void Derived::fun()
void Derived::fun(int)
double Derived::fun(double)
但是,如果将double Derived::fun(double f)
的返回值类型修改为int
会发生什么情况?会发生如下报错:
return type is not identical to nor covariant with return type "double" of overridden virtual function "Base::fun"
// translate : 返回类型与重写虚拟函数 "Base::fun" 的返回类型 "double" 既不相同,也不协变
也不协变
,协变是什么意思,这其实是C++的一种返回值类型机制。
协变返回类型
协变返回类型(Covariant Return Types),C++允许在子类中重写虚函数时返回类型可以是基类返回类型的派生类指针或引用。这么说起来或许有些拗口,下面通过一个例子来分析:
#include <iostream>
using namespace std;
class Base {
public:
virtual Base* clone() { return new Base(*this); }
virtual void print() { cout << "Base" << endl; }
};
class Derived : public Base {
public:
virtual Derived* clone() override { return new Derived(*this); }
virtual void print() override { cout << "Derived" << endl; }
};
void demonstrateClone(Base& base) {
Base* clonedBase = base.clone();
clonedBase->print();
delete clonedBase;
}
int main() {
Base base;
Derived derived;
demonstrateClone(base); // 输出 "Base"
demonstrateClone(derived); // 输出 "Derived"
return 0;
}
如上代码,可以编译运行且不发生报错。原因在于Derived::clone()
的返回类型是Base
类的派生类Derived
的引用,这是C++的返回值类型机制,即协变返回类型,这种特性可以在一定程度上提高代码的灵活性和可读性,特别是在对象关系的上下文中。
但当如果返回的类型是非基类返回类型的派生类型的指针或引用时,就会发生报错。