这篇文章重点介绍C++中的多态特性。前面我们知道了,派生类中可以调用基类中的方法,对于同名的函数我们有隐藏的相关概念。但是现实可能存在一个问题,就是基类中的方法和派生类中的方法是不同的,不同的对象调用的方法我们希望是不同的行为。这种复杂的行为我们称之为多态,形象地来说,就是具有不同的形态,方法的行为会根据上下文不同。
像刚才上面讲到的,多态形象地来说就是多种形态,具体地来说就是完成完成一个任务有不同的状态,不同的对象会产生不同的状态。
下面可以举一个例子:
举个例子:
车站售票处推出根据购票身份不同从而出售与身份相对应的车票。
推出三种方案:
1. 普通成人:排队购票,每人100¥
2. 学生 :排队购票,每人 50 ¥
3. 军人 :购买预留票,无需排队,每人100¥上面就是一个典型的多态案例,不同的身份购买到了不同的车票。我们尝试使用代码实现。
有两个重要的机制用于实现多态:
我们来看一下下面这段代码:
class Person
{
public:
Person(const char* name = "张三")
:_name(name)
{}
//这个地方使用virtual关键字
virtual void BuyTicket()
{
cout << _name << "购票,需要排队,每人 100 ¥" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name)
:_name(name)
{}
virtual void BuyTicket()
{
cout << _name << "购票,需要排队,每人 50 ¥" << endl;
}
private:
string _name;
};
class Soldier : public Person
{
public:
Soldier(const char* name)
:_name(name)
{}
virtual void BuyTicket()
{
cout << _name << "购买预留票,不需要排队,每人 100 ¥" << endl;
}
private:
string _name;
};
void Buy(Person* p)
{
p->BuyTicket();
}
int main()
{
Person p("张三");
Buy(&p);
Student st("张同学");
Buy(&st);
Soldier so("报国");
Buy(&so);
return 0;
}运行结果:


虚函数是指被virtual关键字修饰的函数:
virtual void BuyTicket()
{
cout << _name << "购票,需要排队,每人 100 ¥" << endl;
}虚函数的重写也叫覆盖,派生类(子类)有一个和基类完全相同的虚函数(也就是派生类的函数名、返回参数列表都完全相同),称此为子类的虚函数重写了基类的虚函数:
class Person
{
public:
virtual void BuyTicket(){}
};
class Student : public Person
{
public:
//派生类也可以不写virtual 但是不推荐
virtual void BuyTicket(){}
};
class Soldier : public Person
{
public:
//派生类也可以不写virtual 但是不推荐
virtual void BuyTicket(){}
};注意:子类的虚函数就算没有添加virtual也同样构成重写(基类的虚函数在子类继承之后依然保持虚函数属性),但是这种写法并不规范。
派生类重写基类虚函数时,返回值类型两者并不相同(指基类虚函数返回基类对象的指针或引用,派生类虚函数返回子类对象的指针或引用),称之为协变:
class A
{};
class B : public A
{};
class Person {
public:
virtual A* f()
{
cout << "A* f() " << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* f()
{
cout << "B* f() " << endl;
return nullptr;
}
};
int main()
{
Person p;
Student s;
Person* ptr = &p;
ptr->f();
ptr = &s;
ptr->f();
return 0;
}这种情况同样是构成重写的:

如果基类的析构函数已经是一个虚函数了,那么派生类的析构函数有没有virtual关键字,都与基类的构造函数构成重写。这里虽然名字并不相同,似乎违背了重写的一个定义,但是本质上是编译器知道这是一个重写的情况下,对于函数的名字统一为destructor:
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p = new Person;
Person* st = new Student;
delete p;
delete st;
return 0;
}
注意:只有派生类的析构函数重写了基类的析构函数,下面的delete调用析构函数才能构成多态,换句话说,就是基类析构函数一定要使用virtual关键字。
下面给出一段程序,回答它的输出是什么?
A: A->0 B: B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main()
{
B* p = new B;
p->test();
return 0;
}我们分析一下这道题:

final用于修饰虚函数,表示它不能再被重写。
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{ cout << "Benz-舒适" << endl; }
};
这里大家可能会有一个疑问,既然都是用了virtual关键字,那我的目的不就是希望能够重写然后形成多态吗?为什么还需要有一个final来限制虚函数的重写?其实我在学习的时候也有这个疑问,后来发现,其实这两者并不矛盾。
这里其实更准确地说,final使用了限制函数的进一步重写,也就是说virtual + final 的组合意思是:这个函数曾经参与多态机制(所以是virtual),但现在我们锁定了它的实现,不允许在进一步的派生类中修改(所以是final)。
检查派生类时候重写了基类的虚函数,如果没有重写需要报错。
class Car
{
public:
virtual void Drive(){}
};
class Benz : public Car
{
public:
virtual void Drive() override
{
cout << "Benz-舒适" << endl;
}
};加入我们一旦去掉基类的virtual,override就会提示不能进行重写:

总的来说,override 是C++11引入的关键字,它的作用正是为了确保能够正确重写基类成员。

在虚函数后面写上 = 0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类。
抽象类不能被实例化,抽象类的派生类同样不能被实例化,只有重写纯虚函数,派生类才能被实例化。也就是说纯虚函数规范了派生类必须会被重写,这一点在接口继承有很重要的体现。
//抽象类 -- 不能实例化出对象
//在现实生活一般没有具体对应的实体
//间接功能:子类必须进行重写才能实例化出对象
class Car
{
public:
//纯虚函数 -- 抽象
//没有对象可以调用 子类必须重写才能调用
virtual void Drive() = 0;
//实现没有价值,因为没有对象会调用他
virtual void Drive() = 0
{
cout << " Drive()" << endl;
}
};
很明显证实了我们上面说到的,抽象类是不能被实例化的。
我们接下来,在派生类中重写基类中的纯虚函数:
class Car
{
public:
void f()
{
cout << "f()" << endl;
}
virtual void Drive() = 0;
};
class BMW : public Car
{
public:
//进行了重写
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
BMW b;
b.Drive();
b.f();
return 0;
}
普通的函数继承是一种实现继承,目的是为了继承基类的函数,然后拥有该函数,并且使用这个函数;而虚函数的继承体现的是一种接口继承,目的是为了派生类重写,构成多态,继承的是接口。所以如果不使用多态,就不要把函数定义成虚函数。
这里给大家看一道很经典的笔试题:
//32位下 sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
Base b;
return 0;
}首先我们打印出的结果应该8(64位操作系统打印出来的是16我们稍后解释),但这显然不符合我们我们在类和对象中学到的,sizeof()只会计算类中的成员变量,而没有成员函数。
通过调试我们发现对象b中不仅有_b,还多了一个_vfptr,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function),一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

然后我们试着多添加几个虚函数,看一看它的储存机制:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
virtual void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
不难发现一个虚函数对应一个指针,这些指针都存储子啊_vfptr中。
我在对比着来看普通函数:

不难发现,普通函数并没有存储在虚表中。
总结:
针对上面的代码我们继续深挖一点,如果我使用一个派生类去继承包含虚函数的基类,这个派生类的实例化对象又会是怎么样的呢?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
virtual void Func3()
{
cout << "Func3()" << endl;
}
void Func4()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}通过调试我们发现:
总结:

(本篇完)