12.0 虚函数的作用
虚函数是实现C++多态特性的关键功能。
引出问题:
有一个animal
的基类,包含可以打印动物叫声的函数speak()
。
每种动物的叫声函数speak
都不一样,比如派生类cat
的speak()
,dog
的speak()
都不一样。
如果想打印不同的动物的叫声,则针对每种动物实例化几个具体的对象,然后对每种动物调用speak()
函数。
问题来了,如果有10种动物呢,30种动物,100种动物呢?
#include <iostream>
int main()
{
Cat cats[]{ { "Fred" }, { "Misty" }, { "Zeke" } };
Dog dogs[]{ { "Garbo" }, { "Pooky" }, { "Truffle" } };
for (const auto &cat : cats)
std::cout << cat.getName() << " says " << cat.speak() << '\n';
for (const auto &dog : dogs)
std::cout << dog.getName() << " says " << dog.speak() << '\n';
return 0;
}
解决思路:
由于它们的基类animal
也都有speak
的函数,那能不能实例化完对象之后,放到一个animal
的循环里调用animal
的speak
函数呢?但是这个函数不执行,去执行子类各个动物里面的speak
函数呢?
当然可以,这种speak
函数,就是虚函数。
在举一个例子:
不妨假设现在在开发一个动物园自动管理系统,那么一个很自然的想法就是,建立一个Animal容器,存放所有Animal类,然后遍历容器,给他们喂食喂水。但很显然的问题出现了,Tiger类和Snake类成员不一定相同,大小更不一定相同,无法放到一个普通的容器里。只有一种办法可行,那就是指针。C++给出的解决方案是,派生类指针可以被转化为父类指针,于是Tiger指针和Snake指针都可以被转化为Animal指针,这样就可以:
for(auto it: zoo)
{
it->eat();
it->drink();
}
然而由于it是Animal指针,实际调用的是Animal类的方法。为了能让派生类指针被转化为父类指针后仍然调用子类方法,虚函数出现了。虚函数是在运行期绑定的,目的就是为了让派生类转为父类指针后,仍然可以去执行派生类的方法。
12.1 虚函数的定义
虚拟函数是一种特殊类型的功能,调用它时,解析为所述基类和派生类之间存在的函数的最衍生版本。这种能力称为多态性。如果派生函数具有与该函数的基本版本相同的签名(名称,参数类型以及是否为const)且返回类型相同,则将其视为匹配项,并且覆盖掉父类函数。这种功能称为覆盖。
要使函数虚拟,只需将“ virtual”关键字放在函数声明之前。
#include <iostream>
#include <string_view>
class Base
{
public:
virtual std::string_view getName() const { return "Base"; } // note addition of virtual keyword
};
class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};
int main()
{
Derived derived;
Base &rBase{ derived };
std::cout << "rBase is a " << rBase.getName() << '\n';
return 0;
}
输出:
rBase is a Derived
下面的例子充分说明函数的调用会一直查询到子类的子类。
#include <iostream>
#include <string_view>
class A
{
public:
virtual std::string_view getName() const { return "A"; }
};
class B: public A
{
public:
virtual std::string_view getName() const { return "B"; }
};
class C: public B
{
public:
virtual std::string_view getName() const { return "C"; }
};
class D: public C
{
public:
virtual std::string_view getName() const { return "D"; }
};
int main()
{
C c;
A &rBase{ c };
std::cout << "rBase is a " << rBase.getName() << '\n';
return 0;
}
输出:
C
下面这个例子说明基类和派生类必须保持完全一致,才会被虚函数认为是可覆盖,可继承中查询。
#include <iostream>
#include <string_view>
class A
{
public:
virtual std::string_view getName() const { return "A"; }
};
class B: public A
{
public:
// Note: Functions in B, C, and D are non-const.
virtual std::string_view getName() { return "B"; }
};
class C: public B
{
public:
virtual std::string_view getName() { return "C"; }
};
int main()
{
C c;
A &rBase{ c };
std::cout << rBase.getName() << '\n';
return 0;
}
该函数输出:A。因为A类中虚函数是有关键字const
,B和C中没有,所以不被认为可以继承覆盖。
如果基类中虚函数标了关键字
virtual
,但是派生类中没有,派生类也会被认为是虚函数。
① 构造函数和析构函数中不要使用虚函数
#include <iostream>
#include <string_view>
class A
{
public:
A() { std::cout << getName(); } // note addition of constructor
virtual std::string_view getName() const { return "A"; }
};
class B : public A
{
public:
virtual std::string_view getName() const { return "B"; }
};
class C : public B
{
public:
virtual std::string_view getName() const { return "C"; }
};
int main()
{
C c;
return 0;
}
答案:A。因为构造C的时候会先构造A,此时调用getname()
,但是此时B和C还没有构造,因此不能往下调用。
② 虚函数可以不可以滥用
答案自然是:不能。
从功能上讲,把基类所有的函数都设置成虚函数,百利而无一害,但是主要问题是效率,调用虚拟函数调用要比解决常规调用花费更长的时间。此外,编译器还必须为每个具有一个或多个虚函数的类对象分配一个额外的指针。
12.2 override
和final
关键字
① override
在继承的过程中,虚函数必须返回类型、函数名等均保持一致才能才能实现对父类虚函数的覆盖,如果不对应,编译以然可以通过,但是达不到想要的效果,所以添加override
关键字的使用可以在编译阶段就进行检查。
使用替代说明符不会降低性能,并且有助于避免意外错误。因此,我们强烈建议您在编写的每个虚函数覆盖中使用它,以确保您实际上覆盖了您认为具有的函数。
class A
{
public:
virtual const char* getName1(int x) { return "A"; }
virtual const char* getName2(int x) { return "A"; }
virtual const char* getName3(int x) { return "A"; }
};
class B : public A
{
public:
virtual const char* getName1(short int x) override { return "B"; } // compile error, function is not an override
virtual const char* getName2(int x) const override { return "B"; } // compile error, function is not an override
virtual const char* getName3(int x) override { return "B"; } // okay, function is an override of A::getName3(int)
};
int main()
{
return 0;
}
② final
在链式继承关系中,有时候希望某个类是继承的终点,想标识这个类不能被继承。就可以在类名的后面添加该关键字。
示例如下:
class A
{
public:
virtual const char* getName() { return "A"; }
};
class B final : public A // 标识符添加在这
{
public:
virtual const char* getName() override { return "B"; }
};
class C : public B // compile error: cannot inherit from final class
{
public:
virtual const char* getName() override { return "C"; }
};
③ 协变返回类型
在一种特殊情况下,派生类虚函数重写可以具有与基类不同的返回类型,并且仍被视为匹配重写。如果虚函数的返回类型是指向类的指针或引用,则覆盖函数可以返回指向派生类的指针或引用。这些称为协变返回类型。这是一个例子:
#include <iostream>
class Base
{
public:
// This version of getThis() returns a pointer to a Base class
virtual Base* getThis() { std::cout << "called Base::getThis()\n"; return this; }
void printType() { std::cout << "returned a Base\n"; }
};
class Derived : public Base
{
public:
// Normally override functions have to return objects of the same type as the base function
// However, because Derived is derived from Base, it's okay to return Derived* instead of Base*
Derived* getThis() override { std::cout << "called Derived::getThis()\n"; return this; }
void printType() { std::cout << "returned a Derived\n"; }
};
int main()
{
Derived d;
Base *b = &d;
d.getThis()->printType(); // calls Derived::getThis(), returns a Derived*, calls Derived::printType
b->getThis()->printType(); // calls Derived::getThis(), returns a Base*, calls Base::printType
return 0;
}
12.3 析构函数虚拟化
问题引出:
通过虚函数,派生类指针被转化为父类指针后,通过父类函数调用子类的函数,可能存在一些新建变量或者可变数组等,但父类的实例进行删除时,如果不对子类这些使用的内存进行删除,就会造成内存泄漏。
示例:
该例子中子类的可变数组m_array
在父类实例删除后并没有删掉。
#include <iostream>
class Base
{
public:
~Base() // note: not virtual
{
std::cout << "Calling ~Base()\n";
}
};
class Derived: public Base
{
private:
int* m_array;
public:
Derived(int length)
{
m_array = new int[length];
}
~Derived() // note: not virtual (your compiler may warn you about this)
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived *derived { new Derived(5) };
Base *base { derived };
delete base;
return 0;
}
所以就像多态一样,析构函数也必须变成虚函数,这样删除父类时也多态的删掉子类。如例:
#include <iostream>
class Base
{
public:
virtual ~Base() // note: virtual
{
std::cout << "Calling ~Base()\n";
}
};
class Derived: public Base
{
private:
int* m_array;
public:
Derived(int length)
{
m_array = new int[length];
}
virtual ~Derived() // note: virtual
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived *derived { new Derived(5) };
Base *base { derived };
delete base;
return 0;
}
- 如果某个类是要被继承的父类,请确保析构函数是虚拟的。
- 如果您不打算继承这个父类,则将该类标记为最终类
final
。这将首先防止其他类从其继承,而不会对该类本身施加任何其他使用限制。
与普通的虚拟成员函数一样,如果基类函数是虚拟的,则无论是否指定了此类,所有派生的替代都将被视为虚拟的。