多态
1.问题引出
子类定义了与父类中原型相同的函数会发生什么?
- 父类指针/引用指向父类对象和子类对象
#include <iostream>
using namespace std;
/*基类*/
class Parent
{
public:
void func()//基类和派生类函数原型一样
{
cout << "i am parent" << endl;
}
protected:
private:
};
/*派生类*/
class Child: public Parent
{
public:
void func()//基类和派生类函数原型一样
{
cout << "i am child" << endl;
}
protected:
private:
};
/*基类指针作函数参数*/
void print(Parent *tmp)
{
tmp->func();
}
/*基类引用作函数参数*/
void print(Parent & tmp)
{
tmp.func();
}
int main(void)
{
Parent * ptr = NULL;
Parent p1;//构造基类对象
Child c1;//构造派生类对象
/*指针方式调用同名函数*/
ptr = &p1;
ptr->func();
ptr = &c1;
ptr->func();
print(&p1);
print(&c1);
/*饮用方式调用同名函数*/
Parent & quote = p1;
quote.func();
Parent & quote2 = p1;
quote2.func();
print(p1);
print(c1);
system("pause");
return 0;
}
结论:当父类指针/引用指向子类对象的时候,如果有同名函数,默认调用父类的成员函数
- 父类指针/引用指向父类对象且子类指针/引用指向子类对象
#include <iostream>
using namespace std;
/*基类*/
class Parent
{
public:
void func()//基类和派生类函数原型一样
{
cout << "i am parent" << endl;
}
protected:
private:
};
/*派生类*/
class Child: public Parent
{
public:
void func()//基类和派生类函数原型一样
{
cout << "i am child" << endl;
}
protected:
private:
};
int main(void)
{
Parent p1;//构造基类对象
Child c1;//构造派生类对象
p1.func();
c1.func();
system("pause");
return 0;
}
结论:子类指针/引用指向子类对象的时候,如果有同名函数,子类函数会将父类函数覆盖
要通过子类对象调用被覆盖的同名父类成员函数需要显示的加上父类名和作用域解析符。
c1.Parent::func();
引出了一个矛盾:当赋值兼容性原则(父类指针/引用指向子类对象)和函数重写(父类和子类有相同函数原型的成员函数)发生在一起,父类指针/引用只会调用父类成员函数。
C/C++是静态编译型语言
在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象
现象产生的原因
赋值兼容性原则遇上函数重写 出现的一个现象
1 没有理由报错
2 对被调用函数来讲,在编译器编译期间,我就确定了,这个函数的参数是p,是Parent类型的。。。
3 静态链编
void howToPrint(Parent* p)
{
p->print();
}
1、在编译此函数的时,编译器不可能知道指针 p 究竟指向了什么。
2、编译器没有理由报错。
3、于是,编译器认为最安全的做法是编译到父类的print函数,因为父类和子类肯定都有相同的print函数。
面向对象新需求
根据实际的对象类型来判断重写函数的调用
- 如果父类指针指向的是父类对象则调用父类中定义的函数
- 如果父类指针指向的是子类对象则调用子类中定义的重写函数
2.解决方案
- C++中通过virtual关键字对多态进行支持
- 使用virtual声明的函数被重写后即可展现多态特性
实际案例
#include <iostream>
using namespace std;
//HeroFighter AdvHeroFighter EnemyFighter
class HeroFighter
{
public:
virtual int power() //C++会对这个函数特殊处理
{
return 10;
}
};
class EnemyFighter
{
public:
int attack()
{
return 15;
}
};
class AdvHeroFighter : public HeroFighter
{
public:
virtual int power()
{
return 20;
}
};
class AdvAdvHeroFighter : public HeroFighter
{
public:
virtual int power()
{
return 30;
}
};
//多态威力
//1 PlayObj给对象搭建舞台 看成一个框架
//15:20
void PlayObj(HeroFighter *hf, EnemyFighter *ef)
{
//不写virtual关键字 是静态联编 C++编译器根据HeroFighter类型,去执行 这个类型的power函数 在编译器编译阶段就已经决定了函数的调用
//动态联编: 迟绑定: //在运行的时候,根据具体对象(具体的类型),执行不同对象的函数 ,表现成多态.
if (hf->power() > ef->attack()) //hf->power()函数调用会有多态发生
{
printf("主角win\n");
}
else
{
printf("主角挂掉\n");
}
}
//多态的思想
//面向对象3大概念
//封装: 突破c函数的概念....用类做函数参数的时候,可以使用对象的属性 和对象的方法
//继承: A B 代码复用
//多态 : 可以使用未来...
//多态很重要
//实现多态的三个条件
//C语言 间接赋值 是指针存在的最大意义
//是c语言的特有的现象 (1 定义两个变量 2 建立关联 3 *p在被调用函数中去间接的修改实参的值)
//实现多态的三个条件
//1 要有继承
//2 要有虚函数重写
//3 用父类指针(父类引用)指向子类对象....
void main()
{
HeroFighter hf;
AdvHeroFighter Advhf;
EnemyFighter ef;
AdvAdvHeroFighter advadvhf;
PlayObj(&hf, &ef);
PlayObj(&Advhf, &ef);
PlayObj(&advadvhf, &ef) ; //这个框架 能把我们后来人写的代码,给调用起来
cout<<"hello..."<<endl;
system("pause");
}
void main1401()
{
HeroFighter hf;
AdvHeroFighter Advhf;
EnemyFighter ef;
if (hf.power() > ef.attack())
{
printf("主角win\n");
}
else
{
printf("主角挂掉\n");
}
if (Advhf.power() > ef.attack())
{
printf("Adv 主角win\n");
}
else
{
printf("Adv 主角挂掉\n");
}
cout<<"hello..."<<endl;
system("pause");
return ;
}
3.工程意义
多态的思想
面向对象3大概念
封装: 突破c函数的概念….用类做函数参数的时候,可以使用对象的属性 和对象的方法
继承: A B 代码复用
多态: 可以使用未来…
4.成立条件
实现多态的三个条件
C语言 间接赋值 是指针存在的最大意义
是c语言的特有的现象 (1 定义两个变量 2 建立关联 3 *p在被调用函数中去间接的修改实参的值)
实现多态的三个条件
- 1 要有继承
- 2 要有虚函数重写
- 3 用父类指针(父类引用)指向子类对象….
多态是设计模式的基础,多态是框架的基础
5.理论基础
- 联编是指一个程序模块、代码之间互相关联的过程。
- 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。
重载函数使用静态联编。 - 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。
switch 语句和 if 语句是动态联编的例子。 - 理论联系实际
1、C++与C相同,是静态编译型语言
2、在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
3、由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象
从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。
6.本质剖析
6.1 多态实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
- 当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局vptr指针;当进行howToPrint(Parent *base)函数时,C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可。)
- VPTR一般作为类对象的第一个成员
说明1:
通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。
说明2:
出于效率考虑,没有必要将所有成员函数都声明为虚函数
说明3 :C++编译器,执行HowToPrint函数,不需要区分是子类对象还是父类对象.只需要根据父类对象指针找到VPTR成员指针,再通过虚函数表找到实际对应的成员函数即可。
#include <iostream>
using namespace std;
//多态成立的三个条件
//要有继承 虚函数重写 父类指针指向子类对象
class Parent
{
public:
Parent(int a=0)
{
this->a = a;
}
virtual void print() //1 动手脚 写virtal关键字 会特殊处理 //虚函数表
{
cout<<"我是爹"<<endl;
}
virtual void print2() //1 动手脚 写virtal关键字 会特殊处理 //虚函数表
{
cout<<"我是爹"<<endl;
}
private:
int a;
};
class Child : public Parent
{
public:
Child(int a = 0, int b=0):Parent(a)
{
this->b = b;
}
virtual void print()
{
cout<<"我是儿子"<<endl;
}
private:
int b;
};
void HowToPlay(Parent *base)
{
base->print(); //有多态发生 //2 动手脚
//效果:传来子类对 执行子类的print函数 传来父类对执行父类的print函数
//C++编译器根本不需要区分是子类对象 还是父类对象
//父类对象和子类对象分步有vptr指针 , ==>虚函数表===>函数的入口地址
//迟绑定 (运行时的时候,c++编译器才去判断)
}
void main01()
{
Parent p1; //3 动手脚 提前布局
//用类定义对象的时候 C++编译器会在对象中添加一个vptr指针
Child c1; //子类里面也有一个vptr指针
HowToPlay(&p1);
HowToPlay(&c1);
cout<<"hello..."<<endl;
system("pause");
return ;
}
6.2 证明VPTR的存在
利用sizeof运算符判断有无virtual关键字的类的大小。
#include <iostream>
using namespace std;
class A
{
public:
void printf()
{
cout<<"aaa"<<endl;
}
protected:
private:
int a;
};
class B
{
public:
virtual void printf()
{
cout<<"aaa"<<endl;
}
protected:
private:
int a;
};
void main()
{
//加上virtual关键字 c++编译器会增加一个指向虚函数表的指针 。。。
printf("sizeof(a):%d, sizeof(b):%d \n", sizeof(A), sizeof(B));
cout<<"hello..."<<endl;
system("pause");
return ;
}
6.3 构造函数中调用虚函数
这个问题实际上就是VPTR指针的分步初始化问题。
- 对象在创建的时,由编译器对VPTR指针进行初始化
- 只有当对象的构造完全结束后VPTR的指向才最终确定
- 父类对象的VPTR指向父类虚函数表
- 子类对象的VPTR指向子类虚函数表
#include <iostream>
using namespace std;
//构造函数中调用虚函数能发生多态吗?
class Parent
{
public:
Parent(int a=0)
{
this->a = a;
print();
}
virtual void print()
{
cout<<"我是爹"<<endl;
}
private:
int a;
};
class Child : public Parent
{
public:
Child(int a = 0, int b=0):Parent(a)
{
this->b = b;
print();
}
virtual void print()
{
cout<<"我是儿子"<<endl;
}
private:
int b;
};
void HowToPlay(Parent *base)
{
base->print(); //有多态发生 //2 动手脚
}
void main()
{
Child c1; //定义一个子类对象 ,在这个过程中,在父类构造函数中调用虚函数print 能发生多态吗?
//c1.print();
cout<<"hello..."<<endl;
system("pause");
return ;
}
7.面试题集锦
7.1 关于函数重载、重写、重定义
函数重载
- 必须在同一个类中进行
- 子类无法重载父类的函数,父类同名函数将被名称覆盖
- 重载是在编译期间根据参数类型和个数决定函数调用
- 静态联编
函数重写
- 必须发生于父类与子类之间
- 并且父类与子类中的函数必须有完全相同的原型
- 使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
- 多态是在运行期间根据具体对象的类型决定函数调用
当父类和子类有相同的函数名、变量名出现,发生名称覆盖(子类的函数名,覆盖了父类的函数名。)
子类和父类的同名函数绝对不可能重载,如果原型不是完全相同则不属于重写和重定义,他们之间的关系只能说是函数覆盖。
#include <iostream>
using namespace std;
//重写 重载 重定义
//重写发生在2个类之间
//重载必须在一个类之间
//重写分为2类
//1 虚函数重写 将发生多态
//2 非虚函数重写 (重定义)
class Parent
{
//这个三个函数都是重载关系
public:
void abc()
{
printf("abc");
}
virtual void func()
{
cout<<"func() do..."<<endl;
}
virtual void func(int i)
{
cout<<"func() do..."<<i<<endl;
}
virtual void func(int i, int j)
{
cout<<"func() do..."<<i<< " "<<j<<endl;
}
virtual void func(int i, int j, int m , int n)
{
cout<<"func() do..."<<i<< " "<<j<<endl;
}
protected:
private:
};
class Child : public Parent
{
public:
void abc()
{
printf("child abc");
}
/*
void abc(int a)
{
printf("child abc");
}
*/
virtual void func(int i, int j)
{
cout<<"func(int i, int j) do..."<<i<< " "<<j<<endl;
}
virtual void func(int i, int j, int k)
{
cout<<"func(int i, int j) do.."<< endl;
}
protected:
private:
};
//重载重写和重定义
void main()
{
//: error C2661: “Child::func”: 没有重载函数接受 0 个参数
Child c1;
//c1.func();
//子类无法重载父类的函数,父类同名函数将被名称覆盖
c1.Parent::func();
//1 C++编译器 看到func名字 ,因子类中func名字已经存在了(名称覆盖).所以c++编译器不会去找父类的4个参数的func函数
//2 c++编译器只会在子类中,查找func函数,找到了两个func,一个是2个参数的,一个是3个参数的.
//3 C++编译器开始报错..... error C2661: “Child::func”: 没有重载函数接受 4 个参数
//4 若想调用父类的func,只能加上父类的域名..这样去调用..
c1.func(1, 3, 4, 5);
//c1.func();
//func函数的名字,在子类中发生了名称覆盖;子类的函数的名字,占用了父类的函数的名字的位置
//因为子类中已经有了func名字的重载形式。。。。
//编译器开始在子类中找func函数。。。。但是没有0个参数的func函数
cout<<"hello..."<<endl;
system("pause");
return ;
}
7.2 为什么定义虚析构函数
在什么情况下应当声明虚函数
- 构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
- 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
//虚析构函数
class A
{
public:
A()
{
p = new char[20];
strcpy(p, "obja");
printf("A()\n");
}
virtual ~A()
{
delete [] p;
printf("~A()\n");
}
protected:
private:
char *p;
};
class B : public A
{
public:
B()
{
p = new char[20];
strcpy(p, "objb");
printf("B()\n");
}
~B()
{
delete [] p;
printf("~B()\n");
}
protected:
private:
char *p;
};
class C : public B
{
public:
C()
{
p = new char[20];
strcpy(p, "objc");
printf("C()\n");
}
~C()
{
delete [] p;
printf("~C()\n");
}
protected:
private:
char *p;
};
//只执行了 父类的析构函数
//向通过父类指针 把 所有的子类对象的析构函数 都执行一遍
//向通过父类指针 释放所有的子类资源
void howtodelete(A *base)
{
delete base; //这句话不会表现成多态 这种属性
}
/*
void howtodelete(B *base)
{
delete base; //这句话不会表现成多态 这种属性
}
*/
void main()
{
C *myC = new C; //new delete匹配
//
delete myC; //直接通过子类对象释放资源 不需要写virtual
//howtodelete(myC);
cout<<"hello..."<<endl;
system("pause");
return ;
}
7.3 父类和子类指针的步长
1) 铁律1:指针也只一种数据类型,C++类对象的指针p++/–,仍然可用。
2) 指针运算是按照指针所指的类型进行的。
p++《=》p=p+1 //p = (unsigned int)basep + sizeof(*p) 步长。
3) 结论:父类p++与子类p++步长不同;不要混搭,不要用父类指针++方式操作数组。
#include <iostream>
using namespace std;
//结论:
//多态是用父类指针指向子类对象 和 父类步长++,是两个不同的概念
class Parent
{
public:
Parent(int a=0)
{
this->a = a;
}
virtual void print()
{
cout<<"我是爹"<<endl;
}
private:
int a;
};
//成功 ,一次偶然的成功 ,必然的失败更可怕
class Child : public Parent
{
public:
/*
Child(int a = 0, int b=0):Parent(a)
{
this->b = b;
print();
}
*/
Child(int b = 0):Parent(0)
{
//this->b = b;
}
virtual void print()
{
cout<<"我是儿子"<<endl;
}
private:
//int b;
};
void HowToPlay(Parent *base)
{
base->print(); //有多态发生 //2 动手脚
}
void main411()
{
Child c1; //定义一个子类对象 ,在这个过程中,在父类构造函数中调用虚函数print 能发生多态吗?
//c1.print();
Parent *pP = NULL;
Child *pC = NULL;
Child array[] = {Child(1), Child(2), Child(3)};
pP = array;
pC = array;
pP->print();
pC->print(); //多态发生
pP++;
pC++;
pP->print();
pC->print(); //多态发生
pP++;
pC++;
pP->print();
pC->print(); //多态发生
cout<<"hello..."<<endl;
system("pause");
return ;
}
7.4 关于多态的理解
- 多态的实现效果
多态:同样的调用语句有多种不同的表现形态; - 多态实现的三个条件
有继承、有virtual重写、有父类指针(引用)指向子类对象。 - 多态的C++实现
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用 - 多态的理论基础
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。 - 多态的重要意义
设计模式的基础 是框架的基石。可以将未来的代码适用于以前开发的框架。 - 实现多态的本质
函数指针(虚函数表指针VPTR)做函数参数
C函数指针是C++至高无上的荣耀。C函数指针一般有两种用法(正、反)。
7.5 C++编译器是如何实现多态
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表
- 虚函数表是一个存储类成员函数指针的数据结构
- 虚函数表是由编译器自动生成与维护的
- virtual成员函数会被编译器放入虚函数表中
- 当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局vptr指针;当进行howToPrint(Parent *base)函数是,C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可。)
- VPTR一般作为类对象的第一个成员
7.6 类的每个成员函数是否都可以声明为虚函数,为什么?
通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了调用的函数。在效率上,虚函数的效率要低很多。
出于效率考虑,没有必要将所有成员函数都声明为虚函数
7.7 构造函数中调用虚函数能实现多态吗?为什么?
vptr指针的初始化是分步骤完成的,所以不能实现多态。
7.8 虚函数表指针(VPTR)被编译器初始化的过程,你是如何理解的?
1.对象在创建的时,如果对象所属的类中有虚函数,则编译器会自动为该对象创建VPTR指针,并对VPTR指针进行初始化
2.只有当对象的构造完全结束后VPTR的指向才最终确定
3.父类对象的VPTR指向父类虚函数表
4.子类对象的VPTR指向子类虚函数表
虚函数表是在编译期间就创建了的!编译器一旦检测到类里面声明了虚函数,则为该类创建一个属于该类的虚函数表。
当定义一个父类对象的时候比较简单,因为父类对象的VPTR指针直接指向父类虚函数表。
但是当定义一个子类对象的时候就比较麻烦了,因为构造子类对象的时候会首先调用父类的构造函数然后再调用子类的构造函数。当调用父类的构造函数的时候,此时会创建Vptr指针(也可以认为Vptr指针是属于父类的成员,所以在子类中重写虚函数的时候virtual关键字可以省略,因为编译器会识别父类有虚函数,然后就会生成Vptr指针变量),该指针会指向父类的虚函数表;然后再调用子类的构造函数,此时Vptr又被赋值指向子类的虚函数表。
上面的过程是Vptr指针初始化的过程。
这是因为这个原因,在构造函数中调用虚函数不能实现多态。