封装:把数据和方法放到类里通过访问限定符控制,把不想访问的屏蔽,把想要访问的开给你
继承:类级别的复用,子类可以复用父类的成员
多态:多种形态,去完成某个行为时,当不同对象取完成时,会产生的不同状态
同一件事情,不同的人去做,得到的过程结果不一样
eg:(1)比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优
先买票。—>买票的行为是多态不同的人做这件事情结果不一样
(2)支付宝扫红包,有些人金额大,有些人很大,扫码动作一样,不同的用户得到不一样的红包也是多态
多态构成的条件:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
多态的构成条件:
1必须通过基类的指针或者引用调用虚函数(多态是在父类和子类中产生)
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(也叫覆盖)
虚函数:即被virtual修饰的类成员函数称为虚函数.
虚继承和虚函数没有关系,
切记!!!调用的时候和对象有关以前调用函数是和类型有关谁去调用类型是什么调的就是谁的,现在是对象有关哪个对象取调达到了不同形态,不同人完成完成一件事情达到不一样的形态
class Person {
public:
virtual void BuyTicket() { cout << “买票-全价” << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << “买票-半价” << endl; }
指向person调的是student是因为构成多态跟对象有关,不构成多态跟 本身的类型有关
虚函数重写的两个例外:
1 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用时称为协变
入如下图
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
2. 析构函数的重写(基类与派生类析构函数的名字不同)
3. 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用//析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0; }
personp=new person;
delet p;
调用的是父类的析构函数
personp=new student;new子类给父类
delet p;没有调用子类的析构函数所以要用vitual
切记一个父类指针指向new出来的子类对象–>一定有要用虚函数
一个父类指向new出来的子类对象的析构由两部分子类析构+父类析构(系统调用)
关键字 final 和override
final–>修饰表示该函数不能继承
final放在父类
override(放在子类)帮助检查是否完成重写,如果没有重写就会报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << “Benz-舒适” << endl;}
};
如果没有重写就会报错
重载重写重定义(隐藏)和协变的区别
重载:同一作用域函数名和参数相同
重定义(隐藏):两个函数一个在基类一个在派生类函数名相同—两个基类和派生类的同名函数不构成重写就是重定义(隐藏)
重写:两个函数一个在基类一个在派生类函数名相同参数返回值都相同,两个函数必须是虚函数
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或引用时称为协变
抽象类:
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,而且纯虚函数体现了接口继承.
eg:
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Test()
{
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}
接口继承和实现继承的区别
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
多态的原理:
class Base
{
public:
virtual void Func1()
{
cout << “Func1()” << endl;
}
private:
int _b = 1;
};
cout<<sizeof(Base)<<endl;—>32位操作系统有虚函数4+4=8,64位操作系统16
看监视窗口可以得到
放大仔细观看发现除了有对象b,还有一个_vfptr,而里面存的是函数Func1()的地址
然后根据基类增加一个派生类去继承base,dervice重写func1,func2,然后fuc3没有重写,只是隐藏
class Base{
public:
virtual void Func1()
{
cout << “Base::Func1()” << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << “Derive::Func1()” << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
发现结果如图:
可以看到在基类里面b对象里面的_vptr有两个虚函数Func1,和Func2但是没有Fuc3
然后看对象d,发现它继承的base ,然后Fuc1完成了重写,Fuc2没有重写,也在虚表里面然后在_vptr有固定的位置—>
(1)派生对象d也有虚标指针,d对象由两步分组成一部分是父类继承下来的成员,虚表指针也是,另一部分是自己的成员。
(2)基类b对象和派生类d对象虚表是不一样的,Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
(3) 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
(4)4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
所以可以推出虚函数表的生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
所以可以得出以下结论
(1)虚表存的是虚函数指针,切记不是虚函数(图中函数地址可以看到),虚函数和普通函数一样都存在代码段只是它的指针还存在了虚表里面,对象中存的不是虚表而是虚函数指针,
那么多态的原理是如下:
class Student : public Person {
public:
virtual void BuyTicket() { cout << “买票-半价” << endl; }
};
void Func(Person& p) {
p.BuyTicket();
}
int main()
{
Person chen;
Func(chen);
Student zhang;
Func(zhang);
return 0;
}
通过代码的监视窗口可以看出来
可以看出来p是指向chen对象时,p->BuyTicket在chen的虚表中找到虚函数是Person::BuyTicket。
p是指向zhang对象时,p->BuyTicket在zhang的虚表中找到虚函数
是Student::BuyTicket。而zhang的虚标是通过覆盖chen的虚标,如果还有虚函数则在添加到zhang的虚标的后面
这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
它通过调用不同对象所对应的的类中的续表完成实现多态
所以达到多态的条件:
(1)虚表的覆盖
(2)一个对象的指针或者引用调用虚函数
那么满足多态后函数调用时在什么时候?
看汇编
[eax]就是取eax值指向的内容
[edx]就是取edx值指向的内容
call转化地址
p.BuyTicket();
0136086E mov eax,dword ptr [p]
这里相当于把mike对象头4个字节(虚表指针)移动到了edx—声明在虚表中有4个字节是头指针
01360871 mov edx,dword ptr [eax]
把虚表中的头4字节存的虚函数指针移动到了eax
01360873 mov esi,esp
01360875 mov ecx,dword ptr [p]
01360878 mov eax,dword ptr [edx]
0136087A call eax
call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的
中取找的。
0136087C cmp esi,esp
0136087E call __RTC_CheckEsp (013517B2h)
}
然后看普通的没有虚函数的的则会发现直接call-说明不满足多态条件,普通函数调用直接转化成地址,是在编译时从符号表确认
动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
问题:
inline函数不能,因为inline函数没有地址,无法把地址放到虚函数表中。 - 静态成员不能是虚函数吗因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数不能是虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?并且最好把基类的析构函数定义成虚函数。对应的场景为一个父类指针指向new出来的子类对象–>一定有要用虚函数
- 对象访问普通函数快还是虚函数更快首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数是在编译阶段就生成的,一般情况下存在代码段(常量区)的
8.指向父类调父类指向子类调子类的本质:父类的指针:子类的虚函数表是的虚表的覆盖子类–>切片切出来还是调用父类的虚表只是被覆盖了