定义
多态(polymorphism
)是面向对象编程语言的一大特点,而虚函数是实现多态的机制。其核心理念是通过基类访问派生类定义的函数。
多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定。
对于虚函数,子类可以(也可以不)重新定义基类的虚函数,该行为称为复写(override)
虚函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
#include <iostream> #include <string>
class Pet { public: std::string GetName() { return "pet"; } };
class Dog : public Pet { private: std::string m_Name; public: Dog(const std::string& name) : m_Name(name) {}
std::string GetName() { return m_Name; } };
int main() { Pet* e = new Pet(); std::cout << e->GetName() << std::endl;
Dog* Peter = new Dog("Peter"); std::cout << Peter->GetName() << std::endl;
return 0; }
|
编译执行:
1 2
| g++ -o test.o no_virtual.cpp ./test.o
|
输出:
pet
Peter
这和我们想的一样,如果我们使用多态的概念来编写上面的代码:
在main里面加上:
1 2 3 4 5 6 7 8 9 10 11 12 13
| int main() { Pet* e = new Pet(); std::cout << e->GetName() << std::endl;
Dog* Peter = new Dog("Peter"); std::cout << Peter->GetName() << std::endl; Pet* pet = Peter; std::cout << pet->GetName() << std::endl;
return 0; }
|
输出:
pet
如果我们这样编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <string>
class Pet { public: std::string GetName() { return "pet"; } };
class Dog : public Pet { private: std::string m_Name; public: Dog(const std::string& name) : m_Name(name) {}
std::string GetName() { return m_Name; } };
void PrintName(Pet* pet) { std::cout << pet->GetName() << std::endl; }
int main() { Pet* e = new Pet(); PrintName(e);
Dog* Peter = new Dog("Peter"); PrintName(Peter); return 0; }
|
我们期望输出的是:pet Peter
输出:
pet
pet
这是因为到了调用方法时,都会调用这个类型的方法,因为PeintName
这个函数是属于Pet
类型的指针,调用这个函数,将会调用Pet
中的getName
如果我们希望C++可以根据我们传入的类型不同自动来选择对应的函数,这时就需要用到虚函数了。
虚函数引入了动态分配,使用虚函数表(VTable)
来实现编译,虚函数表包含基类中所有虚函数的映射,以便我们能在运行时映射它们到正确的覆写函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <iostream> #include <string>
class Pet { public: virtual std::string GetName() { return "pet"; } };
class Dog : public Pet { private: std::string m_Name; public: Dog(const std::string& name) : m_Name(name) {} std::string GetName() override { return m_Name; } };
void PrintName(Pet* pet) { std::cout << pet->GetName() << std::endl; }
int main() { Pet* e = new Pet(); PrintName(e);
Dog* Peter = new Dog("Peter"); PrintName(Peter); return 0; }
|
这时就能输出:pet Peter
需要特别说明下override
是C++11新特性,我们可以加上也可以不加上,不过最好还是加上,这可以避免我们覆写基类函数中没有定义的函数。
纯虚函数
在很多情况下,基类本身生成对象是不合理的,例如,宠物Pet
作为一个基类可以派生出猫,狗等子类。
纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就
变成了虚函数。
定义纯虚函数是为了实现一个接口,用来规范派生类的行为。
含有纯虚函数的类称为抽象类,它不能生成对象(创建实例),只能创建它的派生类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
#include <iostream> #include <string>
class Pet { public: virtual std::string GetName() = 0; };
class Dog : public Pet { private: std::string m_Name; public: Dog(const std::string& name) : m_Name(name) {} std::string GetName() override { return m_Name; } };
void PrintName(Pet* pet) { std::cout << pet->GetName() << std::endl; }
int main() { Pet* e = new Pet(); PrintName(e);
Dog* Peter = new Dog("Peter"); PrintName(Peter); return 0; }
|
改进过后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
#include <iostream> #include <string>
class Printable { public: virtual std::string GetClassName() = 0; };
class Pet : public Printable { public: virtual std::string GetName() { return "pet"; } std::string GetClassName() override { return "Pet"; }
};
class Dog : public Pet { private: std::string m_Name; public: Dog(const std::string& name) : m_Name(name) {} std::string GetName() override { return m_Name; } std::string GetClassName() override { return "Dog"; } };
void PrintName(Pet* pet) { std::cout << pet->GetName() << std::endl; }
void Print(Printable* obj) { std::cout << obj->GetClassName() << std::endl; }
int main() { Pet* e = new Pet();
Dog* Peter = new Dog("Peter"); Print(e); Print(Peter); return 0; }
|
输出:
Pet
Dog
对于Print
函数,接受printable
对象,它不关心实现的是什么类(多态性)。
如果你没有实现GetClassName
这个函数,你就不能实例化这个类
1 2 3 4 5 6 7
| class A : Printable { std::string GetClassName() override { return "A"; } }
int main() { Print(new A()); }
|
注意
静态函数可以声明为虚函数吗?
静态函数不可以声明为虚函数,同时也不能被const
和volatile
关键字修饰
static
成员函数不属于任何类对象或类实例,所以给此函数加上virtual
也是没有意义的。
虚函数依靠vptr
和vtable
来处理。vptr
是一个指针,在类的构造函数中创建生成,并且只能用this
指针来访问它,静态成员函数没有this
指针,所以
无法访问vptr
。
构造函数可以为虚函数吗?
构造函数不可以声明为虚函数,同时除了inline,explicit
之外,构造函数不允许使用其他任何关键字。
尽管虚函数表vtable
是在编译阶段已经建立的,但指向虚函数表的指针vptr
是在运行阶段实例化对象时才产生的。如果类含有虚函数,编译器会在构造函数中
添加代码来创建vptr
。问题来了,如果构造函数是虚的,那么它需要vptr
来访问vtable
,可这个时候vptr
还没产生。因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型是必须明确的。因此,构造函数没有
必要被声明为虚函数。
析构函数可以为虚函数吗?
析构函数可以声明为虚函数。
虚函数可以被内联吗?
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表示多态性的时候不能被内联。
内联是在编译期间,而虚函数的多态性是在运行期,编译器无法知道运行期间调用哪个代码,因此虚函数表现为多态时,不可以被内联。