多态
C/C++
是静态编译语言。
在编译时,编译器自动根据指针的类型判断执行的是一个什么样的对象
- 在编译此函数的时候,编译器可能不知道指针究竟指向了什么
- 编译器没有理由报错
- 于是,编译器认为安全的做法是编译到父类的
print
函数,因为父类和子类肯定都有相同的print
函数
如下:
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int a)
{
this->a = a;
cout<<"Parent a = "<<a<<endl;
}
void print() //子类的和父类的函数名字一样
{
cout<<"Parent print a ="<<a<<endl;
}
protected:
private:
int a ;
};
class Child : public Parent
{
public:
Child(int b) : Parent(10)
{
this->b = b;
cout<<"Child b"<<b<<endl;
}
void print() //virtual 父类写了virtual,子类可写 可不写
{
cout<<"Child print b:"<<b<<endl;
}
protected:
private:
int b;
};
void howToPrint(Parent *base)
{
base->print(); //一种调用语句 有多种表现形态...
}
void howToPrint2(Parent &base)
{
base.print();
}
int main(int argc, char* argv[])
{
Parent *base = NULL;
Parent p1(20);
Child c1(30);
cout << "===================" <<endl;
base = &p1;
base->print(); //执行父类的打印函数
base = &c1;
base->print(); //执行谁的函数 ? //面向对象新需求
cout << "===================" <<endl;
{
Parent &base2 = p1;
base2.print();
Parent &base3 = c1; //base3是c1 的别名
base3.print();
}
//函数调用
howToPrint(&p1);
howToPrint(&c1);
howToPrint2(p1);
howToPrint2(c1);
cout<<"hello..."<<endl;
return 0;
}
事件执行结果
Parent a = 20
Parent a = 10
Child b30
===================
Parent print a =20
Parent print a =10
===================
Parent print a =20
Parent print a =10
Parent print a =20
Parent print a =10
Parent print a =20
Parent print a =10
hello...
从执行结果可以看出,无论指针指向子类对象还是执行父类对象,最终调用的print
都是父类的函数
如果有以下需求:
如果父类指针指向父类,调用父类的print
函数,指针指向子类,调用子类的print
函数
编译器的做法不是我们期望的
根据实际的对象类型来判断重写函数的调用
如果父类指针指向的是父类对象则调用父类中定义的函数
如果父类指针指向的是子类对象则调用子类中定义的重写函数
解决方案
C++
中通过virtual
关键字对多态进行支持
使用viertual
声明的函数被重写后就可展现多态特性
多态实例:
#include <iostream>
using namespace std;
class Parent
{
public:
Parent(int a)
{
this->a = a;
cout<<"Parent a = "<<a<<endl;
}
virtual void print() //子类的和父类的函数名字一样
{
cout<<"Parent print a ="<<a<<endl;
}
protected:
private:
int a ;
};
class Child : public Parent
{
public:
Child(int b) : Parent(10)
{
this->b = b;
cout<<"Child b"<<b<<endl;
}
virtual void print() //virtual 父类写了virtual,子类可写 可不写
{
cout<<"Child print b:"<<b<<endl;
}
protected:
private:
int b;
};
void howToPrint(Parent *base)
{
base->print(); //一种调用语句 有多种表现形态...
}
void howToPrint2(Parent &base)
{
base.print();
}
int main(int argc, char* argv[])
{
Parent *base = NULL;
Parent p1(20);
Child c1(30);
cout << "===================" <<endl;
base = &p1;
base->print(); //执行父类的打印函数
base = &c1;
base->print(); //执行谁的函数 ? //面向对象新需求
cout << "===================" <<endl;
{
Parent &base2 = p1;
base2.print();
Parent &base3 = c1; //base3是c1 的别名
base3.print();
}
//函数调用
howToPrint(&p1);
howToPrint(&c1);
howToPrint2(p1);
howToPrint2(c1);
cout<<"hello..."<<endl;
return 0;
}
执行结果:
Parent a = 20
Parent a = 10
Child b30
===================
Parent print a =20
Child print b:30
===================
Parent print a =20
Child print b:30
Parent print a =20
Child print b:30
Parent print a =20
Child print b:30
hello...
从执行结果来看,使用virtual
关键字之后,父类指针执行子类对象就调用子类的函数,执行父类的对象就调用父类的函数。
封装
突破了C语言函数的概念
继承
代码复用,,,
多态
多态可以使用未来的代码
多态成立的三个条件:
- 要有继承
- 要有函数重写
- 要有父类指针(父类引用)指向子类对象
多态是设计模式的基础,多态是框架的基础
静态联编和动态联编
- 静态联编是指一个程序模块、代码之间互相关联的过程
- 静态联编(
static binding
),是程序的匹配、连接在编译阶段实现,也称为早期匹配,重载函数使用静态联编 - 动态联编是指程序联编推迟到运行时进行,所以称为晚期联编(迟绑定)。
switch
语句和if
语句就是动态联编的例子。 - 理论联系实际
- C++与 C 相同,是静态编译型语言
- 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
- 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编
对多态的理解
多态的实现效果
多态:同样的调用语句有多种不同的表现形态;
多态实现的三个条件
有继承、有 virtual 重写、有父类指针(引用)指向子类对象。
多态的 C++实现
virtual 关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用;而
是要根据指针所指向的实际对象类型来判断如何调用
多态的理论基础
动态联编 PK 静态联编。根据实际的对象类型来判断重写函数的调用。
多态的重要意义
设计模式的基础 是框架的基石。
实现多态的理论基础
函数指针做函数参数
C 函数指针是 C++至高无上的荣耀。C 函数指针一般有两种用法(正、反)。
多态原理探究
与面试官展开讨论
重写、重载的理解
函数重载
必须在同一个类中进行
子类无法重载父类的函数,父类同名函数将被名称覆盖
重载是在编译期间根据参数类型和个数决定函数调用
函数重写
必须发生于父类与子类之间
并且父类与子类中的函数必须有完全相同的原型
使用 virtual 声明之后能够产生多态(如果不使用 virtual,那叫重定义)
多态是在运行期间根据具体对象的类型决定函数调用
问题:定义一个空的类型,里面没有任何的成员变量或者成员函数,对这个类型进行 sizeof 运算,结果是?
结果是1,因为空类型的实例不包含任何信息,按道理 sizeof 计算之后结果是0,但是在声明任何类型的实例的时候,必须在内存占有一定的空间,否则无法使用这些实例,至于占据多少内存大小,由编译器决定。
继续问:如果在这个类型里添加一个构造函数和析构函数,那么结果又是多少?
还是1,因为我们调用构造函数和析构函数,只需要知道函数的地址即可,而这些函数的地址只和类型相关,和类型的实例无关,编译器不会为这两个函数在实例内添加任何额外的信息。
继续问:如果把析构函数变为虚函数呢?结果是多少?
c++编译器发现了类型里有虚函数,,就会为这个类型生成一个虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针,在32位机器,指针类型大小是4字节,结果是4,64位机器中,指针大小是8字节,结果是8。
C++
多态实现原理
当类中声明虚函数时,编译器会在类中生成一个虚函数表
虚函数表是一个存储类成员函数指针的数据结构
虚函数表是由编译器自动生成与维护的
virtual
成员函数会被编译器放入虚函数表中
存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)
虚函数的效率要低很多。
说明 2:
出于效率考虑,没有必要将所有成员函数都声明为虚函数
证明VPTR
函数的存在
#include <iostream>
using namespace std;
class Test
{
private:
/* data */
public:
Test
(/* args */);
~Test
();
};
Test::Test(/* args */)
{
}
Test::~Test()
{
}
class Parent1
{
public:
Parent1(int a=0)
{
this->a = a;
}
void print()
{
cout<<"parent"<<endl;
}
private:
int a;
};
class Parent2
{
public:
Parent2(int a=0)
{
this->a = a;
}
virtual void print()
{
cout<<"parent"<<endl;
}
private:
int a;
};
int main(int argc, char*argv[])
{
printf("sizeof(Parent):%zd sizeof(Parent2):%zd \n", sizeof(Parent1), sizeof(Parent2));
cout<<"hello..."<<endl;
cout << sizeof(Test) << endl;
return 0;
}
函数输出:
sizeof(Parent):4 sizeof(Parent2):16
hello...
1
可以看出,没有任何成员变量的类大小为1,有单个int
成员变量的类大小为4,但是增加虚函数之后大小就变为了16
构造函数中能调用虚函数,实现多态吗
1)对象中的 VPTR 指针什么时候被初始化?
对象在创建的时,由编译器对 VPTR 指针进行初始化
只有当对象的构造完全结束后 VPTR 的指向才最终确定
父类对象的 VPTR 指向父类虚函数表
子类对象的 VPTR 指向子类虚函数表
参考:[C++编译器多态实现原理](https://www.cnblogs.com/kubixues
heng/p/4353907.html)