- 1. 多态的概念
- 2. 多态的定义及实现
- 2.1. 多态的条件
- 2.2. 虚函数
- 2.3. 虚函数的重写
- 2.4. 虚函数重写的两个例外
- 2.5. C++11的override和final
- 2.6. 重载、重写、重定义的对比
- 3. 抽象类
- 3.1. 概念
- 3.2. 实现继承和接口继承的对比
- 4. 多态的原理
- 4.1. 虚函数表
- 4.2. 多态原理
- 4.3. 动态绑定和静态绑定
- 4.4. 多继承的虚表
1. 多态的概念
在C++中,多态性是面向对象编程的一个重要概念,它允许不同的对象对同一个消息做出不同的响应。多态性使得程序具有灵活性和可扩展性,能够根据具体的对象类型来选择不同的行为。
C++中的多态性通过虚函数(Virtual Function)和基类指针或引用来实现。虚函数是在基类中声明为虚函数的函数,它可以在派生类中重写(覆盖)以实现特定的行为。基类指针或引用可以指向派生类的对象,并通过虚函数来调用相应的方法。
简单的说就是不同的对象做相同的事,会有不同的行为。举个例子,有人去买票,假如他是个普通老百姓,那么他买票就得是全价。假如他是学生,拿着学生证买票,那么他买票就半价。假如他是个军人,那么他买票就可以优先。
下面是一个简单的示例,说明C++中多态性的概念:
class Shape
{
public:
virtual void draw()
{
cout << "draw a Shape" << endl;
}
};
class Circle : public Shape
{
public:
virtual void draw()
{
cout << "draw a Circle" << endl;
}
};
class Rectangle : public Shape
{
public:
virtual void draw()
{
cout << "draw a Rectangle" << endl;
}
};
int main()
{
Shape* s1 = new Circle();
Shape* s2 = new Rectangle();
s1->draw();
s2->draw();
Shape* s3 = new Shape();
s3->draw();
return 0;
}
在上面的示例中,shape1和shape2是基类指针,分别指向Circle和Rectangle的对象。当调用shape1->draw()时,实际上调用的是Circle类中重写的draw()函数,同样,当调用shape2->draw()时,实际上调用的Rectangle类中重写的draw()函数。
2. 多态的定义及实现
2.1. 多态的条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Circle继承了Shape。Shape对象画图形,Circle对象画圆。
在继承中要实现多态的两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
2.2. 虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Shape
{
public:
virtual void draw()
{
cout << "draw a Shape" << endl;
}
};
2.3. 虚函数的重写
虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了父类的虚函数。
class Shape
{
public:
virtual void draw()
{
cout << "draw a Shape" << endl;
}
};
class Circle : public Shape
{
public:
//void draw() //这样省略virtual的写法也是允许的
virtual void draw()
{
cout << "draw a Circle" << endl;
}
};
class Rectangle : public Shape
{
public:
//void draw() //只要父类的虚函数写了virtual关键字,子类继承下来都可以不写
virtual void draw()
{
cout << "draw a Rectangle" << endl;
}
};
2.4. 虚函数重写的两个例外
虚函数的重写/覆盖有三同:返回值相同,参数相同,函数名相同。
但是有两个例外。
- 协变
子类重写父类的虚函数时,返回值可以不相同,但是子类虚函数的返回值和父类虚函数的返回值要构成父子类关系,且返回的类型必须是父类的指针或者引用。
class A{};
class B : public A {};
class Shape
{
public:
virtual A* draw()
{
cout << "draw a Shape" << endl;
}
};
class Circle : public Shape
{
public:
virtual B* draw()
{
cout << "draw a Circle" << endl;
}
};
- 析构函数的重写
假如在父类的析构函数加上virtual关键字,那么即使子类的析构函数不加virtual关键字,此时子类和父类的析构函数也会形成重写,即使函数名不相同。这是因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名字统一变成了destructor。
class Shape
{
public:
virtual void draw()
{
cout << "draw a Shape" << endl;
}
virtual ~Shape()
{
cout << "delete Shape()" << endl;
}
};
class Circle : public Shape
{
public:
virtual void draw()
{
cout << "draw a Circle" << endl;
}
~Circle()
{
cout << "delete Circle()" << endl;
}
};
int main()
{
Shape* s1 = new Shape();
Shape* s2 = new Circle();
delete s1;
delete s2;
return 0;
}
输出结果:
2.5. C++11的override和final
- final:修饰虚函数,表示该虚函数不能再被重写
class Shape
{
public:
virtual void draw () final
{
cout << "draw a Shape" << endl;
}
};
class Circle : public Shape
{
public:
virtual void draw()
{
cout << "draw a Circle" << endl;
}
};
报错结果:
- override 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Shape
{
public:
virtual void draw ()
{
cout << "draw a Shape" << endl;
}
};
class Circle : public Shape
{
public:
virtual void draw() override
{
cout << "draw a Circle" << endl;
}
};
2.6. 重载、重写、重定义的对比
- 重载是同一作用域,函数名相同,参数不同
- 重写(Override)是指派生类中重新定义(覆盖)基类中已经声明为虚函数的函数
- 重定义(Redeclaration)是指在派生类中重新定义(覆盖)基类中的函数,但不使用virtual关键字
3. 抽象类
3.1. 概念
在C++中,抽象类(Abstract Class)是指包含至少一个纯虚函数的类。纯虚函数是通过在函数声明末尾添加= 0来声明的,表示该函数没有实现,需要在派生类中进行重写。
**抽象类不能被实例化,只能作为基类来派生其他类。**它的主要目的是为了提供一个通用的接口,定义了一组纯虚函数,要求派生类必须实现这些函数。抽象类的存在可以约束派生类的行为,确保派生类具有某些特定的功能或行为。
class Shape
{
public:
virtual void draw() = 0{}
};
class Circle : public Shape
{
public:
virtual void draw()
{
cout << "draw a Circle" << endl;
}
};
class Rectangle : public Shape
{
public:
virtual void draw()
{
cout << "draw a Rectangle" << endl;
}
};
int main()
{
Shape* s1 = new Circle;
Shape* s2 = new Rectangle;
s1->draw();
s2->draw();
return 0;
}
3.2. 实现继承和接口继承的对比
在C++中,实现继承(Implementation Inheritance)和接口继承(Interface Inheritance)是两种不同的继承方式。
- 实现继承(也称为类继承):实现继承是指派生类继承基类的成员和实现。派生类可以使用基类中的成员变量和成员函数,并且可以重写基类中的虚函数。实现继承通过使用关键字
public
、protected
或private
来指定基类的访问权限。
示例:
class Base {
public:
int publicVar;
void publicFunc();
protected:
int protectedVar;
void protectedFunc();
private:
int privateVar;
void privateFunc();
};
class Derived : public Base {
// 派生类继承了Base类的成员和实现
};
在上面的示例中,派生类Derived
通过public
关键字继承了Base
类的成员和实现。这意味着Derived
类可以访问Base
类的public
成员和实现,但无法直接访问Base
类的protected
和private
成员。
- 接口继承:接口继承是指派生类只继承基类的纯虚函数,而不继承成员变量或实现。接口继承通常用于定义一组接口规范,要求派生类实现这些接口。接口继承通过使用关键字
public
来指定基类的访问权限,并将基类中的成员函数声明为纯虚函数。
示例:
class Interface {
public:
virtual void pureVirtualFunc() = 0;
};
class Derived : public Interface {
public:
void pureVirtualFunc() override {
// 派生类实现接口中的纯虚函数
}
};
在上面的示例中,Interface
类是一个接口类,它只包含一个纯虚函数pureVirtualFunc()
。派生类Derived
通过public
关键字继承了Interface
类,并实现了接口中的纯虚函数。
接口继承的目的是为了实现多态性,通过基类指针或引用来操作派生类对象,实现统一的接口调用。接口继承也可以用于定义一组接口规范,要求派生类实现这些接口。
4. 多态的原理
4.1. 虚函数表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
int num = sizeof(b);
cout << num << endl;
return 0;
}
输出结果:
一个类的大小,是计算成员变量的大小,然后根据内存对齐来确定的。但是Base类中只有一个int,大小应该为4字节,但是为什么会是8呢?
通过调试,可以看到Base类中不仅只有_b一个成员,还有一个指针。
vs2019的平台这里默认是32位的,所以指针的大小是4个字节,再加上原来的成员变量,大小就是8个字节。
其中这里多出来的_vfptr成为虚函数表,是一个函数指针数组。
4.2. 多态原理
class Base
{
public:
virtual void func()
{
cout << "Base::func()" << endl;
}
int _a;
};
class Derived : public Base
{
public:
virtual void func()
{
cout << "Derived::func()" << endl;
}
int _b;
};
int main()
{
Base b;
Derived d;
return 0;
}
通过调试可以发现,继承之间的关系和多态的原理如下图:
_vfptr中存放的就是Base的虚函数的函数指针。
那么当Derived继承了Base,也会继承这个虚表。
但是这两个虚表的地址是不一样的,内容也有所不同
但也有相同的内容,里面有一个是Base的函数指针,另一个却变成了Derived的函数指针。那么就证明了Derived继承Base,也会继承了虚表,此时的虚表是由Base的虚表拷贝一份到Derived的虚表,如果Derived重写了基类中的某个函数,那么就会覆盖Derived虚表中的函数指针。
4.3. 动态绑定和静态绑定
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
输出结果:
基类指针或引用可以指向派生类的对象,并根据对象的实际类型来动态地选择调用相应的虚函数。这样,在运行时可以根据对象的实际类型来调用正确的函数,实现多态性。如果直接使用基类对象调用虚函数,将无法实现多态性,只能调用基类中的函数。成为动态绑定,因为他是在运行的时候,然后根据对象的类型来确定使用哪个函数,完成多态。
C++的静态绑定是指在编译时确定函数调用的具体实现。它是通过函数的静态类型来确定调用哪个函数的过程。
4.4. 多继承的虚表
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
运行结果:
通过上图可以知道,子类重写了的虚函数的函数地址会覆盖父类原来虚函数的函数地址,而子类没有重写的虚函数会存放在第一个类的虚表当中。