C++学习随笔11 面向对象编程(4)- 多态

时间:2021-12-30 17:26:33

面向对象编程三个基本特征: 1. 封装 2. 继承 3. 多态

一个操作随着所传递或捆绑的对象类型的不同能够做出不同的反应,其行为模式称为多态。(一般地,一个容器,其元素是基类对象的指针或引用,才有多态可言)

  • 虚函数(采用虚函数会影响一些程序的效率)

1. 多态条件:仅仅对于对象的指针和引用的间接访问,才会发生多态现象。

 1 class Base{                                            //含有虚函数的类比不用虚函数的类多了一个指针的空间,指向类中的虚函数表,会影响程序效率  2 public:
 3      virtual void fn(){cout<<"In Base class\n";}       //加上virtual编译器会做滞后处理  4 };
 5 
 6 class Sub : public Base{
 7 public:
 8      virtual void fn(){cout<<"In Sub class\n";}        //可以省略virtual  9 };
10 
11 void test(Base &b){               void test(Base b){
12      b.fn();                           b.fn();
13 }//传递引用,会触发多态              }//值传递,子类对象变成基类对象,不触发多态
14 
15 int main()
16 {
17     Base bc;
18     Sub  sc;
19     test(bc);
20     test(sc);
21 }

2. 重载与覆盖:

覆盖:子类重定义父类的虚函数,导致不同类对象的该成员函数操作表现出不同的行为,称为成员函数的覆盖。(函数声明必须完全相同,包括:函数名、返回类型(协变)、参数类型、个数和顺序都必须完全相同),否则,成员函数即使加上virtual也不会被编译器做滞后处理。

3. 若干限制:

  1. 只有类的成员函数才能声明为虚函数;(虚函数仅适用于有继承关系的对象)
  2. 静态成员函数不能是虚函数;(静态成员函数不受对象的捆绑)
  3. 内联函数不能是虚函数;(内联函数不能在运行中动态地确定其位置)
  4. 构造函数不能是虚函数;
  5. 析构函数可以是虚函数且通常声明为虚函数;
  • 纯虚函数

抽象类的用途是被继承,定义抽象类就是在类定义中至少声明一个纯虚函数。所谓纯虚函数是指被标明为不具体实现的虚函数。纯虚函数的声明形式是在虚函数声明形式后跟“=0”抽象类是不允许有实例对象的,即不能由抽象类创建对象。但我们可以用抽象类的指针和引用来进行多态编程。

 

  • 纯虚析构函数

有些时候,你想使一个类成为抽象类,但刚好又没有任何纯虚函数。怎么办?所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数
这里是一个例子:
class awov {
public:
virtual ~awov() = 0; // 声明一个纯虚析构函数
};
这个类有一个纯虚函数,所以它是抽象的,而且它有一个虚析构函数,所以不会产生析构函数问题。但这里还有一件事:必须提供纯虚析构函数的定义
awov::~awov() {} // 纯虚析构函数的定义
这个定义是必需的,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要产生对~awov的调用,所以要保证为它提供函数体。如果不这么做,链接器就会检测出来,最后还是得回去把它添上

总结:

当定义一个指向子类实例父类指针的时候,内存中实例化了子类,由于子类继承了父类,因此内存中的子类里包含父类的所有成员.但由于声明的是父类指针,因此该指针不能够访问子类的成员.而只能访问父类的成员.然而在父类里可以声明纯虚函数和定义虚函数.使用父类指针访问虚函数或纯虚函数的时候,访问到的是子类里重写的函数. 当然,对于虚函数,如果子类里没有对其重写的话,仍然访问到父类里定义的虚函数.可见虚函数和纯虚函数的却别仅仅在于:纯虚函数没有定义,只有声明

再有一个要注意的是析构函数要声明为虚函数,这样在delete父类指针的时候,才会调用实例化的子类的虚函数,否则只会调用父类的析构函数,造成子类的剩余部分没被释放,从而造成内存的泄漏.

 

  • 类型转换

1. 动态转换(dynamic_cast):dynamic_cast操作是专门针对有虚函数的继承结构来的,它将基类指针转换成想要的子类指针,以做好子类操作的准备。

注:

dynamic_cast操作所针对的基类指针(即圆括号中的表达式),如果所指向的对象中不包含有想要的子类对象,则将得到0值结果:

1 Savings s("8288", 1000);
2 Account* pa = &s;                           //先将s转化为基类指针
3 Checking* pc = dynamic_cast<Checking*>(pa); //pc为0,因为Saving类和Checking类都是Account的子类但不互相继承,所以pa指向的Saving实体不包含Check对象,所以pc为0 

2. 静态转换(static_cast): 

  1. static_cast转换并不是专门针对指针的,只要是相关类型的转换,都可以操作。无非它主要针对的是确定的类型,而不是针对多态。关于多态的类型转换由dynamic_cast去做。
  2. 由于void* 到任何类型的指针都可以进行相融性转换,所以,可以用static_cast来进行。
  3. static_casttype(表达式)的“防盗性”更强,因为可以通过指针值的非0判断,可以避免转换后的操作失常。

3. 常量转换(const_cast):去掉常量性的转换操作,从const type类型转换到type类型

1 const char* max(const char* s1, const char* s2){
2      return strcmp(s1, s2)>0 ? s1 : s2;
3 }
4 
5 int fn(){
6     char* p = max("hello", "world");       //
7     
8     char* p = const_cast<char*>(max("hello", "world"));    //
9 }

4. 重解释转换(一般用来转换不同类型指针): reinterpret_cast<type>(表达式)

指针的类型表明不同类型的指针,其类型不同,是不能互相赋值的。

例如:

float f = 34.5;

int *ip = &f;        //错:float的地址不能赋给int指针

但从地址值的本质来说,无非是用二进制表示的整数而已。因此从内存空间位置性而言,不同类型的指针又是统一的,都是二进制的地址。

int *ip = reinterpret_cast<int*>(&f);

该地址的空间不管放着什么数据,都将它作为整形看待。