1 虚函数和纯虚函数
虚函数,之所以说是虚的,说的是在派生类中,可以覆盖基类中的虚函数;相对于虚函数来说,没有 virtual 修饰的函数可以叫做实函数,实函数就不能被覆盖。虚函数是实现多态的核心。虚函数和纯虚函数比较的话,虚函数可以在派生类中被覆盖,但是虚函数也是有自己的实现的,可以被直接调用;纯虚函数没有自己的实现,在派生类中可以被覆盖,并且必须实现。包含纯虚函数的类是抽象类,不能创建对象,如果抽象类的派生类中没有实现纯虚函数 ,那么派生类也是抽象类,不能创建对象。
虚函数和纯虚函数并没有严格的优劣之分。
从实际使用中,纯虚函数有一个优点,假如一个基类中的函数,派生类中必须实现自己的逻辑,而不能使用基类中的逻辑,那么就可以使用纯虚函数,这样在派生类中如果忘记实现了,那么编译器就会提示错误,起到了一个约束的作用;如果用虚函数实现,那么派生类中忘记实现的话,编译器也不会报错,起不到约束提醒的作用。
纯虚函数有以下两点:
(1)纯虚函数的声明方式
函数声明之后加 = 0,而不是花括号
(2)抽象类不能创建对象,抽象类的派生类如果没有实现所有的纯虚函数,派生类也是抽象类
(3)抽象类可以定义指针,并且可以使用派生类的指针给它赋值
如下是使用抽象类的一个例子:
#include <iostream>
#include <string>class Phone {
public:Phone() {std::cout << "Phone()" << std::endl;}~Phone() {std::cout << "~Phone()" << std::endl;}virtual void Call() = 0;virtual void SendMessage(std::string msg) = 0;
};class Apple : public Phone {
public:Apple() {std::cout << "Apple()" << std::endl;}~Apple() {std::cout << "~Apple()" << std::endl;}virtual void Call() {std::cout << "Apple Call()" << std::endl;}virtual void SendMessage(std::string msg) {std::cout << "apple send msg: " << msg << std::endl;}
};class Oppo : public Phone {
public:Oppo() {std::cout << "Oppo()" << std::endl;}~Oppo() {std::cout << "~Oppo()" << std::endl;}virtual void Call() {std::cout << "Oppo Call()" << std::endl;}
};class Vivo : public Phone {
public:Vivo() {std::cout << "Vivo()" << std::endl;}~Vivo() {std::cout << "~Vivo()" << std::endl;}virtual void Call() {std::cout << "Vivo Call()" << std::endl;}virtual void SendMessage(std::string msg) {std::cout << "vivo send msg: " << msg << std::endl;}
};int main() {// 不能创建 Phone 对象,因为 Phone 是抽象类// Phone phone;// 不能创建 Oppo 对象,因为 Oppo 没有实现 Phone 中的 SendMessage 函数// 所以 Oppo 也是抽象类// Oppo oppo;std::cout << "sizeof(Phone) = " << sizeof(Phone) << std::endl;std::cout << "sizeof(Apple) = " << sizeof(Apple) << std::endl;std::cout << "sizeof(Oppo) = " << sizeof(Oppo) << std::endl;std::cout << "sizeof(Vivo) = " << sizeof(Vivo) << std::endl;Phone *phone;Apple apple;Vivo vivo;phone = &apple;phone->Call();phone->SendMessage("this is apple");phone = &vivo;phone->Call();phone->SendMessage("this is vivo");return 0;
}
运行结果如下:
抽象类中也有虚表,从上边的打印来看,sizeof(Phone) 计算出来的结果是 8。
2 构造函数调用虚函数
2.1 基类构造函数调用虚函数,调用的是基类的虚函数
在构造函数中调用虚函数,基类构造函数调用虚函数调用的是基类的虚函数还是调用的子类的虚函数;派生类的构造函数中调用虚函数,调用的是基类的虚函数还是调用的自己的虚函数。
使用下边的代码来做一下实验,基类是 Base,有一个虚函数 VDo(),派生类是 Derived1,覆盖了基类的虚函数 VDo()。在 Base 的构造函数中调用了虚函数 VDo(),在 Derived1 的构造函数中也调用了虚函数 VDo()。在 main 函数中创建一个 Derived1 对象。
#include <iostream>
#include <string>class Base {
public:Base() {std::cout << "Base()" << std::endl;VDo();}~Base() {std::cout << "~Base()" << std::endl;}void Do() {std::cout << "Base() Do()" << std::endl;}virtual void VDo() {std::cout << "Base() VDo()" << std::endl;}
};class Derived1 : public Base {
public:Derived1() {std::cout << "Derived1()" << std::endl;VDo();}~Derived1() {std::cout << "~Derived1()" << std::endl;}virtual void VDo() {std::cout << "Derived1() VDo()" << std::endl;}
};int main() {Derived1 d1;return 0;
}
如下是打印的日志,从日志可以看出来,在构造 Derived1 的时候首先要构造 Base。在 Base 构造函数中调用的 VDo() 是 Base 中的,在 Derived1 的构造函数中调用的 VDo 是 Derived1 中的。
当派生类构造的时候,首先构造基类,然后再构造派生类。在调用基类构造函数的时候,派生类还没构造,还没有初始化,所以在基类构造函数中调用的虚函数是基类中的虚函数。
派生类构造函数中调用的虚函数是派生类的虚函数。
2.1 基类和派生类之间的引用传递,指针传递,值传递
引用传递和指针传递都能体现多态,值传递无法体现多态。
#include <iostream>
#include <string>class Base {
public:Base() {std::cout << "Base()" << std::endl;VDo();}~Base() {std::cout << "~Base()" << std::endl;}void Do() {std::cout << "Base() Do()" << std::endl;}virtual void VDo() {std::cout << "Base() VDo()" << std::endl;}
};class Derived1 : public Base {
public:Derived1() {std::cout << "Derived1()" << std::endl;VDo();}~Derived1() {std::cout << "~Derived1()" << std::endl;}virtual void VDo() {std::cout << "Derived1() VDo()" << std::endl;}
};void CallDo(Base &b) {std::cout << "CallDo(Base &b)" << std::endl;b.VDo();
}void CallDo(Base *b) {std::cout << "CallDo(Base *b)" << std::endl;b->VDo();
}void CallDoValue(Base b) {std::cout << "CallDoValue(Base b)" << std::endl;b.VDo();
}int main() {Derived1 d1;std::cout << std::endl;CallDo(d1);std::cout << std::endl;CallDo(&d1);std::cout << std::endl;CallDoValue(d1);std::cout << std::endl;return 0;
}
程序运行结果,使用值传递的时候,调用的函数还是 Base 中的 VDo()。
2.2 虚函数重载
如下代码 Base 是基类,Derived 是派生类。Base 中有两个虚函数 func1() 和 func2(),Derived 中也有两个虚函数 func1() 和 func2()。Derived 中的 func1() 和 Base 中的 func1() 的形参列表是不一样的,所以 Derived 中的 func1 不会覆盖 Base 中的 func1。
#include <iostream>
#include <string>class Base {
public:Base() {std::cout << "Base()" << std::endl;}~Base() {std::cout << "~Base()" << std::endl;}virtual void func1(int a) {std::cout << "Base()::func1(int a), a = " << a << std::endl;}virtual void func2(int a = 100) {std::cout << "Base()::func2(int a = 100), a = " << a << std::endl;}
};class Derived : public Base {
public:Derived() {std::cout << "Derived()" << std::endl;}~Derived() {std::cout << "~Derived()" << std::endl;}virtual void func1(double a) {std::cout << "Derived()::func1(double a), a = " << a << std::endl;}virtual void func2(int a = 200) {std::cout << "Derived()::func2(int a = 200), a = " << a << std::endl;}
};typedef void (*PF)(int);
typedef void (*PF1)(double);
int main() {Base *b = new Derived;Derived *d = new Derived;std::cout << "----------------" << std::endl;b->func1(10);b->func1(1.123);b->func2();std::cout << "----------------" << std::endl;d->func1(10);d->func1(1.123);d->func2();return 0;
}
运行结果如下:
(1)派生类中的虚函数表如下
第一个表项是 Base 中的 func1。
第二个表项在基类中是 Base 中的 func2,在派生类中,Derived 中的 func2 对 Base 中的 func2 进行了覆盖。
第三个表象是派生类中的 func1,因为派生类中的 func1 和 Base 中的 func1 形参不一样,所以不会对 Base 中的 func1 进行覆盖。
(2)运行结果分析
① 使用 Base 类型的指针或者 Base 类型的引用,当指针指向 Derived 对象的时候,调用 func1(),不管传参是 int 类型还是 double 类型,都是调用的 Base 中的 func1()。当传参是 double 类型的时候也不是调用的 Derived 中的 func1,也就是派生类和基类形不成重载。
② 使用 Base 类型的指针或者引用,当指针指向 Derived 对象的时候,调用 func2,调用的函数是 Derived 中的 func2,但是默认参数还是 Base 中初始化的。这个现象让人看起来有点奇怪,函数和默认参数不是配套的。
③ 使用 Derived 指针或者引用,调用的 func1() 都是 Derived 中的函数,不管入参是 int 还是 double。
④ 使用 Derived 指针或者引用,调用 func2() 调用的是 Derived 中的 func2(),并且形参默认是 200。