一.什么是多态?
通俗点说,多态就是多种形态,具体点说,就是不同的对象,去做同一个行为,产生了不一样的结果,比如抢红包,抢票这些都是多态行为,在C++中,多态就是不同继承关系的类对象,去调用同一个函数,产生了不同的行为,比如Person类对象去调用买票函数,就是买票全价,而Student类对象去调用买票函数。就是买票半价。
二.多态的实现
2.1多态的构成条件
在继承中,构成多态需要两个条件:
1.必须通过基类的引用或指针调用虚函数
2.被调用的函数必须是虚函数,并且在子类中,该虚函数必须被重写(三同:参数,返回值,函数名相同,然后完成函数过程的重写,虚函数标识符可以不写,因为函数的特性已经从父类继承下来了)
2.2虚函数
class customer
{
public:
virtual void check()
{
cout << "普通顾客无折扣" << endl;
}
};
2.3虚函数的重写
虚函数重写(覆盖):子类中含有一个跟父类中完全相同的虚函数,即函数名,函数类型,函数参数完全相同,但是函数的实现可以不同,成为子类的虚函数重写了基类的虚函数。
class customer
{
public:
virtual void check()
{
cout << "普通顾客无折扣" << endl;
}
};
class VIP : public customer
{
public:
virtual void check()
{
cout << "VIP顾客打五折" << endl;
}
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
/*void check() { cout << "VIP顾客打五折" << endl; }*/
};
2.4虚函数重写的两个例外
1.协变(子类函数和父类函数的返回值类型不同)
子类重写父类的虚函数,它与父类的虚函数的返回值类型是不一样的,它的虚函数的返回值类型是子类的指针或者引用,而它的父类的虚函数的返回值类型是父类的指针或者引用,这成为协变。
void Test()
{
class customer
{
public:
virtual customer* check()
{
cout << "普通顾客无折扣" << endl;
return new customer;
}
};
class VIP : public customer
{
public:
virtual VIP* check()
{
cout << "VIP顾客打五折" << endl;
return new VIP;
}
};
VIP v;
customer c;
customer& c1 = v;
customer& c2 = c;
c1.check();
c2.check();
}
2. 析构函数的重写(基类与派生类析构函数的名字不同)
void Test()
{
class father
{
public:
virtual ~father()
{
cout << "~father()" << endl;
}
};
class son :public father
{
public:
virtual ~son()
{
cout << "~son()" << endl;
}
};
//动态申请的子类对象,如果给了父类对象管理,那么需要析构函数是虚函数
//当父类和子类的析构函数都是虚函数的时候,用父类指针指向子类对象在堆上开辟的空间,
//然后当要释放父类指针指向的堆上的空间的时候,就会把子类和父类的空间都释放了,这就正确调用析构函数了
//普通对象,不论析构函数是否是虚函数,是否完成重写,都正确调用析构函数了
father* s = new son;//operator new + 构造函数
father* f = new father;
delete s; //析构函数 + delete
delete f;
}
2.5 重写(覆盖),重载,隐藏(重定义)的对比
2.6 C++override和final
1.final:修饰虚函数,表示该虚函数不能被重写(这个是写在基类对象的虚函数的后面)
void Test()
{
class a
{
public:
virtual void buy() final
{
cout << "a购买成功" << endl;
}
};
class b: public a
{
virtual void buy()
{
cout << "b购买成功" << endl;
}
};
a* c = new b;
c->buy();
}
出现报错:
2.override:检查子类的虚函数是否重写了父类的虚函数,如果没有重写就报错(这个写在子类对象的虚函数的后面)
void Test()
{
class a
{
public:
virtual void buy()
{
cout << "a购买成功" << endl;
}
};
class b: public a
{
virtual void buy() override
{
cout << "b购买成功" << endl;
}
};
a* c = new b;
c->buy();
}
三.抽象类
3.1概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。(纯虚函数的类,本质上强制子类去完成虚函数的重写,override只是在语法上检查是否完成重写)
如果没有重写纯虚函数,会报错,并且抽象类不能实例化对象,如果实例化,就会报错:
void Test()
{
class father
{
public:
virtual void buy() = 0
{}
};
class son :public father
{
public:
};
father f;
son s;
}
正确的做法:
void Test()
{
class father
{
public:
virtual void buy() = 0
{
}
};
class son :public father
{
public:
virtual void buy()
{
cout << "son购买成功" << endl;
}
};
son s;
s.buy();
father* f1 = new son;
f1->buy();
}
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四.多态的原理
4.1虚函数表
在讲解多态的原理之前,首先要知道什么是虚函数表。在64位的VS运行下面的代码:
void Test()
{
class father
{
public:
virtual void buy()
{
}
private:
int a = 0;
char ch = 'a';
};
father f;
cout << sizeof(f) << endl;
}
我们发现,father类的大小是16字节,但是,这个类成员只有4字节的int类型变量和1字节的char类型变量,按照空间对齐原则,这两个变量在类中所占有的空间就只有8字节,那为什么会出现father类的大小是16字节呢??
为了搞清楚这个问题,我们打开监视窗口后,发现了father类里面多了一个存放该类虚函数地址的表指针,如下图所示:
由于在64位的VS中,指针所占用的字节大小为8字节,所以这个father类的大小就是16字节,那上面的问题就迎刃而解了。
由上面我们知道,含有虚函数的类中,会有一个存放该类虚函数地址的表指针,这个指针指向该类的虚函数表,而虚函数地址就存放在这个表中,由此,我们知道了存放该类虚函数地址的表就称为虚函数表。
那问题来了,虚函数表指针在类继承中是怎么运作的呢?以及它是在多态的实现中起了什么作用呢?
class father
{
public:
virtual void func1()
{
cout << "father的func1函数" << endl;
}
virtual void func2()
{
cout << "father的func2函数" << endl;
}
private:
int a = 0;
};
class son : public father
{
public:
virtual void func1()
{
cout << "son的func1函数" << endl;
}
virtual void func3()
{
cout << "son的func3函数" << endl;
}
private:
int b = 1;
};
void Func(father &p)
{
p.func1();
}
void Test()
{
father f;
son s;
Func(f);
Func(s);
}
以上面这段代码为例,son类的父亲是father类,运行代码,通过监视窗口,我们发现,father类对象f的虚函数表是这样子的:
而son类对象s的虚函数表是这样子的:
从这两个表,我们不难看出,son类继承了father类的虚函数表,如果,子类有对父类的虚函数进行了重写,那么子类就会将子类的虚函数地址覆盖到父类虚函数表中中对应的虚函数地址,由此知道,重写是语法层的说法,而覆盖则是原理层的说法。
由此,我们知道了在继承中,虚函数表所起的作用,那么虚函数表在多态的实现上又是怎么运作的呢??
我们还是以上面的代码为例,说明虚函数表在多态的实现中起的作用。
由上面的图,可以看到,func1函数的调用是一串指令,不管传进来的是father类,还是son类,都运行的是同一份指令,但是我们发现,father类传入Func函数中和son类传入Func函数中的运行结果是完全不一样的,那就不难想象,根据Func函数传进来的不同的类,调用不同的func1函数,所以在汇编语言下,我们才能看到func1函数的调用是一串指令。
我们再来看,虚函数表中,father类的func1函数的指针地址,和son类的func1函数的指针地址,它们是不一样的,然后通过反汇编查看到,这边两个地址转跳到了各自的func1函数的首地址。
也就是说,在Func函数中,由于传进来的参数是该类的父类的引用,当子类对象传进来的时候,就会造成切片(父类的引用指向了子类对象中的父类的那一部分),如果再去调用它的虚函数,这样子,就会去虚函数表中,找到该虚函数的地址,然后调用这个虚函数;如果是父类对象传进来,那道理也是一样的。所以说,多态的原理就是:根据父类引用或父类指针指向的对象不同,而去虚表中找到对应的虚函数,然后调用,从而产生不同的效果。
注意一下几个点:
1.不可以将子类对象直接拷贝赋值给父类对象,因为这样子产生的父类对象不是子类中父类部分的别名,而是一个子类中父类部分的拷贝,这样子就不会构成虚函数的重写和覆盖了,它的虚表,与产生它的子类的虚表毫无关系。
2.同一个类型的对象它们指向的虚表是一样的。
3.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后(被编译器隐藏了,看不见)。
4.虚函数表存放在常量区(代码段)的
4.2 单继承中的虚函数表
class father
{
public:
virtual void func1()
{
cout << "father的func1函数" << endl;
}
virtual void func2()
{
cout << "father的func2函数" << endl;
}
private:
int a = 0;
};
class son : public father
{
public:
virtual void func1()
{
cout << "son的func1函数" << endl;
}
virtual void func3()
{
cout << "son的func3函数" << endl;
}
private:
int b = 1;
};
观察下图中的监视窗口中我们发现看不见func3。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?下面我们在内存窗口找出虚表中的函数。
father类的虚函数表:
son类的虚函数表:
4.3多继承中的虚函数表
class father1
{
public:
virtual void func1()
{
cout << "father1的func1函数" << endl;
}
virtual void func2()
{
cout << "father1的func2函数" << endl;
}
private:
int a1 = 0;
};
class father2
{
public:
virtual void func1()
{
cout << "father2的func1函数" << endl;
}
virtual void func2()
{
cout << "father2的func2函数" << endl;
}
private:
int a2 = 0;
};
class son : public father1, public father2
{
public:
virtual void func1()
{
cout << "son的func1函数" << endl;
}
virtual void func3()
{
cout << "son的func3函数" << endl;
}
private:
int b = 1;
};
多继承时,son类重写了father1和father2虚函数func1,但是虚表中重写的func1的地址却不一样,但是没关系,他们最终调用同一个函数。