目录
具体什么是多态在前言中已经提到, 正文部分不做赘述。
一、多态的相关概念
1.1虚函数
被virtual关键字修饰的成员函数叫做虚函数。 例如:
//A位基类
class A
{
public:
virtual void func() //定义一个虚函数
{
cout << "Afunc()" << endl;
}
};
需要注意的是, 对于构造函数和析构函数来说。 析构函数可以是虚函数, 但是构造函数不可以是虚函数。
1.2虚函数的重写
虚函数的重写就是: 在派生类当中, 有一个和基类中某一个虚函数函数头的虚函数(函数头就是:函数的返回值, 函数名, 函数的参数列表)。 这个时候就会构成虚函数的重写, 即 子类重写了基类的虚函数。
//A位基类
class A
{
public:
virtual void func()
{
cout << "Afunc()" << endl;
}
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
virtual void func() //注意, 这里的virtual可以不写, 因为编译器默认这里是加了virtual的
{
cout << "Bfunc()" << endl;
}
};
需要注意的是, 上图中派生类的func可以不加virtual, 因为基类的func是虚函数, 编译器会默认派生类中和他函数头相同的函数也是虚函数。
1.3虚函数重写的两个例外
协变:派生类在重写基类的虚函数的时候, 与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用, 派生类虚函数返回派生类对象的指针或者引用的时候, 成为协变。
//A位基类
class A
{
public:
virtual A* func()
{
cout << "Afunc()" << endl;
}
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
virtual B* func()
{
cout << "Bfunc()" << endl;
}
};
//C类继承A类
class C : public A
{
virtual C* func()
{
cout << "Cfunc" << endl;
}
};
析构函数的重写: 基类析构函数如果加了virtual, 那么说明基类的析构函数为虚函数。 这个时候如果派生类的析构函数也就变成了虚函数。 那么成不成为虚函数对于析构函数来说有什么不同呢?
首先我们需要知道的是, 在一个普通的类之中, 编译器其实将析构函数统一处理成为了destructor。
然后, 对于一个派生类来说, 如果它的析构函数不是虚函数。 当我们使用父类的指针构成多态时, 只会析构派生类的一部分:
//A位基类
class A
{
public:
virtual A* func()
{
cout << "Afunc()" << endl;
}
//其他动态内存分配的空间
//int* ...
//double* ...
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
virtual B* func()
{
cout << "Bfunc()" << endl;
}
//其他动态内存分配的空间
//int* ...
//double* ...
};
void test_func(A* p)
{
p->func();
}
int main()
{
C c;
A* p = nullptr;
p = &c;
delete p;
return 0;
}
如上图, 假如delete p, 那么就只能释放属于C类自己的那一部分。那么属于A类的那一部分将得不到释放。
但是, 如果我们对A类的析构函数使用虚函数。 那么派生类的析构函数也变成了虚函数, 这个时候如果再形成多态。delete p就能将A类和C类都释放掉。
//A位基类
class A
{
public:
virtual A* func()
{
cout << "Afunc()" << endl;
}
virtual ~A()
{}
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
virtual B* func()
{
cout << "Bfunc()" << endl;
}
virtual ~B()
{}
};
1.4override 和 final 的使用
先谈override, override是用来检验某个虚函数是否构成了重写。如果没有构成重写, 那么编译器就会报错。
如下为构成重写:
//A位基类
class A
{
public:
virtual void func()
{
cout << "Afunc()" << endl;
}
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
virtual void func() override
{
cout << "Bfunc()" << endl;
}
};
如下为没有构成重写:
//A位基类
class A
{
public:
void func()
{
cout << "Afunc()" << endl;
}
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
virtual void func() override
{
cout << "Bfunc()" << endl;
}
};
二、重载、重写、隐藏(重定义)的区别
三、如何构成多态
要形成多态有两个条件:
如下为一个多态的实例:
其实, 多态的应用场景多为这样:
//A位基类
class A
{
public:
virtual void func()
{
cout << "Afunc()" << endl;
}
};
//B类继承A类
class B : public A
{
public:
virtual void func()
{
cout << "Bfunc()" << endl;
}
};
//C类继承A类
class C : public A
{
virtual void func()
{
cout << "Cfunc" << endl;
}
};
void test_func(A* p)
{
p->func();
}
int main()
{
C c;
B b;
test_func(&b);
test_func(&c);
return 0;
}
这样, 通过传送不同类型的对象给test_func函数, 就能构成多态。
四、抽象类
如果一个虚函数后面加上 =0, 那么这个虚函数就是纯虚函数, 并且包含这个纯虚函数的类叫做抽象类。
抽象类不能实例化对象。
//A位基类
class A
{
public:
virtual void func() = 0;
};
int main()
{
A a;
return 0;
}
但是A的派生类如果重写了纯虚函数, 那么就可以这个派生类就可以实例化处对象。
但是如果A的派生类没有重写纯虚函数, 那么这个派生类同样不能实例化处对象。
//A位基类
class A
{
public:
virtual void func() = 0;
};
//B类继承A类
class B : public A
{
public:
//重写A类的func函数
};
int main()
{
B b;
return 0;
}
五、普通继承和接口继承
普通继承:在继承体系中, 派生类继承了基类的函数, 能够直接使用的是普通继承, 这类继承继承的是基类函数的实现。
接口继承:如果继承了基类的虚函数, 并且重写实现了多态。 那么就是一种接口继承, 多态的体系是一种接口的继承, 具体的函数实现是由派生类自己实现的。
六、静态绑定和动态绑定
静态绑定: 静态绑定又被成为前期绑定, 当程序在编译的时候确定的要调用的函数, 确定了程序要执行的行为, 这个过程成为静态多态。 比如我们使用的函数重载就是静态的多态。
动态绑定: 动态绑定又被成为后期绑定, 当程序在编译之后也就是运行期间根据不同的对象调用不同的函数。 这个过程叫做动态多态, 也就是多态。
------------------------------------------------------
ps: 本篇内容没有讲解多态的原理, 因为多态的原理其实就是虚函数表。 而虚函数表的详细讲解博主之前已经写过一篇: 总结虚函数表机制——c++多态底层原理-CSDN博客 。
在这篇文章中, 博主用自己的理解讲解的虚函数表的机制与实现。 写的不甚严谨, 但是里面的结论却是博主通过调试一步一步验证的来的。感兴趣的友友们可以看一下。
后续补带有虚函数的类的内存大小的计算(暂时有点模糊, 先不写, 而且最近考试比较多。可能要等暑假才能补上这一板块)。