本篇文章带你深入学习面向对象设计思想的重要体现之一——多态。 多态在面向对象编程(OOP)中具有深远的意义,它不仅是OOP的三大特性之一,还是实现代码复用、提高程序灵活性和可扩展性的重要手段。
简单来说多态就是多种形态,细说就是当不同的对象去做同一个行为,得到的结果不同。 多态是面向对象编程中的一个核心概念,它允许我们以统一的接口来操作不同的对象。多态意味着 “多种形态”,即多种表现形式或类型。在编程中,多态通常指的是一个接口(或基类)可以有多种实现方式,或者一个对象可以在不同的情境下表现出不同的行为。
比如: 扫码支付,同样是扫码,当扫微信二维码是使用的是微信支付,当扫支付宝二维码时使用的是支付宝支付等。
被virtual
修饰的类成员函数就是虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
};
两个条件缺一不可。
多态是在不同继承关系的类对象去调用同一个函数,产生了不同的行为。 比如同样买火车票,普通人是全价,学生是半价,军人的话是优先买票。这就是同一件事不同人去做得到的结果就不一样。
class Person
{
public:
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
};
class Soldier : public Person
{
public:
virtual void BuyTicket()
{
cout << "优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pe;
Func(pe);
Student st;
Func(st);
Soldier so;
Func(so);
return 0;
}
派生类中有一个和基类完全相同的虚函数(函数名、参数列表、返回类型都相同),称子类的虚函数重写(也叫覆盖)了基类的虚函数。
virtual
,也可以构成重写(继承后基类的虚函数被继承下来了),但规范起见还是不建议省略以下程序的输出结果是什么?
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(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
上面的程序输出结果是:B->1
。
指针p指向B类类型对象,调用由A类继承下来的test函数,在test函数中再调用func
函数,调用的是B类中的func
函数,先输出“B->
”,再打印后面的val,这道题最关键的点就是这里打印的val是A类中func函数参数列表中的val,还是B类中func函数参数列表中的val。
前面我们说过,多态是以统一的接口去操作不同的对象,所以这里即使B类中重写了A类中的func
函数,但重写只是重写了函数的实现,接口用的还是统一的,所以这里虽然执行的是B类中的func
函数,但val
用的还是A类中func
函数参数列表中的val
。
虚函数重写的两个例外:
class A {};
class B : public A {};
class Person
{
public:
virtual A* func() { return new A; }
};
class Student : public Person
{
public:
virtual B* func() { return new B; }
};
virtual
关键字,都与基类的析构函数构成重写。在继承一文中我们提到过,因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor()
。class Person
{
public:
virtual ~Person() { cout << "~Person" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
delete
对象调用析构函数才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数普通情况下析构子类对象:
class Person
{
public:
~Person() { cout << "~Person" << endl;}
};
class Student : public Person
{
public:
~Student() { cout << "~Student" << endl; }
};
int main()
{
Student s;
return 0;
}
我们知道子类对象析构清理先调用子类析构再调用父类析构,没什么问题。而当我们通过父类指针删除子类对象时,会出现问题:
class Person
{
public:
~Person() { cout << "~Person" << endl;}
};
class Student : public Person
{
public:
~Student() { cout << "~Student" << endl; }
};
int main()
{
Person* p = new Student;
delete p;
return 0;
}
我们看到指针p指向的是子类对象,但最后却只调用了基类的析构函数,我们期望的是调用子类的析构函数,这里用多态处理,就可以正常了。
class Person
{
public:
virtual ~Person() { cout << "~Person" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student" << endl; }
};
int main()
{
Person* p = new Student;
delete p;
return 0;
}
从这里我们可以知道,普通调用和多态调用的区别,普通调用看调用者类型,多态调用看指向对象的类型。
析构函数是一个特殊的成员函数,它在对象的生命周期结束时自动被调用,用于执行清理工作,如释放对象所占用的资源。在某些情况下,我们可能需要重写基类中的析构函数:
需要注意的是,即使你不需要在派生类的析构函数中执行任何特定的清理工作,如果你打算通过基类指针来删除派生类对象,并且想要确保派生类对象中的资源被正确释放,你也应该将基类的析构函数声明为虚函数。这样做可以确保当通过基类指针删除派生类对象时,派生类的析构函数也会被调用。
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override
和final
两个关键字,可以帮助用户检测是否重写。
final
:修饰虚函数,表示该虚函数不能被重写class Person
{
public:
virtual ~Person() final { cout << "~Person" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student" << endl; }
};
override
:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写则编译报错class Person
{
public:
virtual ~Person() { cout << "~Person" << endl;}
};
class Student : public Person
{
public:
virtual ~Student() override { cout << "~Student" << endl; }
};
重写也可以调用隐藏,重写是一种特殊的隐藏。
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
抽象类在哪些场景下会使用呢? 假设有一个动物系统,其中包含多种动物如狗、猫、鸟等。这些动物都具有一些共同的行为,如吃和睡。此时,可以定义一个动物抽象类,其中包含eat和sleep方法的声明(其中eat可能为抽象方法,因为不同动物的吃法可能不同;而sleep方法则可能已经在Animal类中给出了具体实现)。然后,狗、猫、鸟等类继承自Animal类,并实现各自的eat方法。 抽象类是实现多态的一种重要手段。通过抽象类和接口,可以实现父类类型的引用指向子类对象,调用方法时根据对象的实际类型执行相应的实现。这种方式可以增加程序的灵活性和可扩展性。
我们通过一道例题来初步认识虚函数表。
在下面的代码中,sizeof(b)
的值是多少?
class Base
{
public:
virtual void func()
{
cout << "func()" << endl;
}
private:
int _a = 1;
};
int main()
{
Base b;
return 0;
}
在不了解虚函数表前,我们可能会根据以往的知识判断sizeof(b)
的值是4,事实上sizeof(b)
的值是8(32位环境下)。
通过观察我们发现b对象的大小是8字节,除了_a
成员,还多一个__vfptr
放在对象的前面(有些平台可能会放到对象的最后面),__vfptr
是一个指针(函数指针),我们叫做虚函数表指针(v代表virtua
,f代表function
)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?
class Base
{
public:
virtual void func1()
{
cout << "func1()" << endl;
}
virtual void func2()
{
cout << "func2()" << endl;
}
void func3()
{
cout << "func3()" << endl;
}
private:
int _a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
Derive d;
return 0;
}
上面我们在Base
中继续增加了一个虚函数和一个普通函数,然后让Derive
继承Base
,在Derive
中只重写func1
函数。
通过观察,可以总结出:
func1
完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。func2
继承下来后是虚函数,所以放进了虚表,func3
也继承下来了,但是不是虚函数,所以不会放进虚表。nullptr
。| 最后有一个问题:虚函数存在哪里?虚表存在哪里?
虚函数不是存在虚表,虚表也不是存在对象中。 虚表存的只是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪里?VS下虚表是存在代码段的。
这里我们再简单总结一下多态的原理:
本篇文章的学习就到这了,如果您觉得在本文中有所收获,还请留下您的三连支持哦~