多态:可以理解为一种事务有多种形态,不同的对象可以通过多态的方式去实现不同的事情
多态的前提是先继承,然后才能实现多态。
多态实现的条件:
重写:虚函数+三同
同函数名、同参数类型、同返回类型
重写的内容为派生类对应虚函数{}中的内容。
class Person
{
public:
virtual void Print()
{
cout << "Person" << endl;
}
};
class Student : public Person //多态的前提是继承
{
public:
virtual void Print() //virtual修饰,表示该函数为虚函数
{
cout << "Student" << endl;
}
};
int main()
{
Person p;
p.Print();
Student s;
s.Print();
return 0;
}
此时的virtual实现的虚函数和虚拟继承中的virtual没什么联系,只是都使用该关键字。
虚函数是被virtual修饰的类成员函数,静态成员函数不能被修饰为虚函数。
class A
{
public:
virtual void Print() //虚函数,虚函数是可以支持类中声明,类外定义的。
{
cout<<"A"<<endl;
}
};
虚函数的重写
基类和父类的虚函数,其完全相同,同函数名,同参数、同返回值类型,这样的父子类的虚函数构成重写。
父类的virtual必须有,子类重写的虚函数的virtual可以不带,因为我们继承父类中的该函数,仍然保留虚函数的属性,但是我们最好是带上吧。
虚函数重写的两个特例:
派生类重写基类虚函数时,返回值类型可以不同,基类的返回值类型为基类对象的指针或者引用,派生类的返回值类型为派生类对象的指针或者引用
class A {}; class B : public A {}; class Person { public: virtual Person* Print() //返回值类型也可以为A* { cout << "Person" << endl; return 0; } }; class Student : public Person { public: virtual Student* Print() //返回值类型也可以为B* { cout << "Student" << endl; return 0; } }; void Func(Person& p) { p.Print(); } int main() { Person p; p.Print(); return 0; }
只要基类对应基类,派生类对应派生类即可,两个返回值类型要有继承关系,即构成协变
2.析构函数的重写
当我们手动new一个对象时候,如Person* ptr=new Student; 此时遇到切片,This指针为Person对象,我们使用delete释放空间的时候,只会调用~Person()不会调用Student的析构函数,从而导致内存泄漏。
所以我们对于这种情况,将析构函数重写(虚函数),此时编辑器对于函数名做特殊化处理,设置为destructor,这样就符合重写的三同要求。
协变实际用到不多,基本上没有,析构函数的重写主要是为了兼容前面所学的new和delete内容,然后才会让编辑器去特殊处理函数名为destructor,从而实现重写。
重载:在同一个作用域,函数名相同,参数不同(返回值类型无所谓)
重写(覆盖):基类和派生类中,对于基类的虚函数在派生类中重写,函数名、参数、返回值类型都相同。
重定义(隐藏):在子类中不构成重写的,与父类和子类的同名函数都成为重定义
抽象类就是将类中的虚函数加上=0,只是声明,不会定义,作为接口供子类重写实现函数,这样的函数称为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,子类继承后也不能实例化对象,只能重写纯虚函数,派生类才能实例化出对象。纯虚函数在子类中必须重写,体现接口继承。
class Person
{
public:
virtual void Print() = 0;
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
virtual void Print() override //override来判断是否重写正确
{
cout << "Student" << endl;
}
~Student() { cout << "~Student()" << endl; }
};
void Func(Person& p)
{
p.Print();
}
int main()
{
//Person p; //不允许创建抽象类对象
//Person* ptr = new Person;//不允许指向抽象类本身
Person* p = new Student; //抽象类指针只能指向子类
p->Print();
Student s;
s.Print();
return 0;
}
总结:
- C++抽象类是为子类抽象一个基类,抽象类的主要作用是为子类提供相应的属性和方法,其他如果需要在子类中修改方法,需要将其声明为纯虚函数。
- 抽象类特点为不能实例化对象(但是可以有自己的指针和引用,即可以实现多态),要有纯虚函数供子类重写。
- 含有一个及以上纯虚函数的类称为抽象类。
- 抽象类的指针只能动态指向子类对象,即Person* ptr = new Student;
- 如果子类中没有实现纯虚函数,而只是继承基类的纯虚函数,则这个子类仍然还是一个抽象类。
- 如果子类中给出了基类纯虚函数的实现,则该子类就不再是抽象类,可以建立对象。
实现继承:普通函数的继承。子类继承普通函数,目的是直接调用,是继承函数的实现
接口继承:虚函数的继承。虚函数存在的目的是为了子类重写,继承的是接口,实现需要子类重写,达成多态,继承的是接口。
虚函数的目的就是为了实现多态和接口继承,如果不实现多态,那就不要定义为虚函数。
多态之所以能实现,是因为虚函数和重写,那么虚函数在内存中是如何存储的呢?
虚函数表:存储类中虚函数地址,有虚函数的类至少有一个虚函数表,在vs下虚函数表指针_vfptr放在对象的头部(不同平台不同),_vfptr存放地址,指向虚函数表(虚表)
虚表中只会存放虚函数,普通函数不会放在虚表中,虚函数表的本质是一个存储虚函数指针的指针数组,一般情况下数组最后放置nullptr
子类虚表生成顺序:
- 拷贝父类虚表到子类虚表中
- 将子类重写的虚函数覆盖父类的虚函数
- 子类本身的虚函数按照生命顺序依次加入
虚函数存在哪里呢?虚表在哪里呢?
虚函数和普通函数一样存放在代码段,只是将虚函数地址存放在虚表中,对象中存储的不是虚表,是虚表指针,虚表在vs下是存储在代码段的
多态实现的过程
通过汇编查看多态实现的过程
满足多态的函数调用,不是在编译时确定的,是在运行起来之后到对象中去寻找的,不满足多态的函数调用是编译时确定好的。
动态绑定和静态绑定
静态绑定又称为前期绑定(早绑定),在程序编译期间就确定了程序的行为,称为静态多态,比如函数重载,编译期间就会直到要执行哪一个函数。
动态绑定又称为后期绑定(晚绑定),在程序编辑期间不知道调用哪一个函数,在运行后根据具体拿到的类型确定程序的具体行为,调用具体的函数,称为动态多态
一句话,静态绑定编译器在编译阶段就知道要调用哪一个,动态绑定需要在运行时才知道具体调用哪一个函数。
单继承子类只有一个虚表,多继承有多个虚表,虚表的顺序和继承顺序有关。
我们前文讲解的都是单继承的虚表,直到子类的虚函数是按照声明顺序放在虚表最后的,那么多继承的时候子类的虚函数放在哪一个表中呢?
class Person1
{
public:
virtual void Func1()
{
cout << "Person1::Func1" << endl;
}
virtual void Func2()
{
cout << "Person1::Func2" << endl;
}
int _a;
};
class Person2
{
public:
virtual void Func1()
{
cout << "Person2::Func1" << endl;
}
virtual void Func3()
{
cout << "Person2::Func3" << endl;
}
int _b;
};
class Student : public Person1, public Person2
{
public:
virtual void Func1()
{
cout << "Student::Func1" << endl;
}
virtual void Func2()
{
cout << "Student::Func2" << endl;
}
virtual void Stu()
{
cout << "STU" << endl;
}
};
//_vfptr是函数指针数组的形式
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf("[%d]->%p", i, vTable[i]);
VFPTR f = vTable[i];
f();//调用
}
cout << endl;
}
int main()
{
//得到虚表地址
Student s;
//得到Person1虚表的地址
PrintVTable((VFPTR*)(*(int*)&s));
//得到Person2虚表的地址
//切片
Person2* ptr = &s;
PrintVTable((VFPTR*)*(int*)ptr);
return 0;
}
多继承,假设两个基类,切片之后是如何调用子类重写的函数Func1,调用过程是什么?
class Person1
{
public:
virtual void Func1()
{
cout << "Person1::Func1" << endl;
}
virtual void Func2()
{
cout << "Person1::Func2" << endl;
}
int _a;
};
class Person2
{
public:
virtual void Func1()
{
cout << "Person2::Func1" << endl;
}
virtual void Func3()
{
cout << "Person2::Func3" << endl;
}
int _b;
};
class Student : public Person1, public Person2
{
public:
virtual void Func1()
{
cout << "Student::Func1" << endl;
}
virtual void Func2()
{
cout << "Student::Func2" << endl;
}
virtual void Stu()
{
cout << "STU" << endl;
}
};
int main()
{
Student s;
s.Func1();
Person1* p1 = &s;
p1->Func1();
Person2* p2 = &s;
p2->Func1();
return 0;
}
对于切片后的p1、p2,还是s,子类重写函数Func1,并分别调用的时候,都是使用This指针调用函数,所以由于p1==s!=p2,所以p1和s都是直接找到Func1然后调用,p2需要先通过ecx调整This指针的地址,找到p1位置,然后再去调用。
为什么两个虚表中的Func1函数是一个函数,但是地址不同?
这是因为,第二个虚表中的调度Func1需要对于This指针进行调整然后再去调度,所以拐了个弯,去寻找偏移量,然后修改This,最后才去调用,另外两个虚表中的Func1地址,只是指向Func1的指针。
菱形继承和菱形虚拟继承
菱形继承和菱形虚拟继承+虚函数重写,底层很复杂,我们要避免写出菱形继承
分析:类有四种,A、B1、B2、C
- 当菱形继承时
- 当菱形虚拟继承,但是B1和B2中只是重写A中的虚函数,自身无虚函数
- 当菱形虚拟继承+B1和B2都有各自的虚函数
菱形继承和菱形虚拟继承重写虚函数,此时的底层已经很复杂了,所以我们在以后使用多继承的时候,避免形成菱形继承
多态前提是继承
多态:重写+基类指针或引用
重写:虚函数+三同
为什么只有基类指针和引用能实现多态,基类对象却不行,这是因为切片,基类得到的是派生类中的基类的成员,但无法将父类对象的虚函数进行改变,所以无法实现多态。
多态的两种特殊情况:协变、析构函数(用于new和delete)
常见多态面试题
多态朴素理解为,不同的对象来在同一件事情(例如,买票)得到不同的结果。多态分为静态多态和动态多态,静态多态指函数重载,动态多态是指继承中虚函数重写+基类指针或引用调用重写的虚函数,多态的目的是为了更方便和灵活多种形态的调用。
重载:在同一作用域中,两个相同函数名,不同参数的函数,构成重载
重写:在不同作用域,即父类和子类中,使用虚函数,对于三同(同函数名、同参、同返回类型)函数的实现,构成重写。
重定义:在父类和基类中,非重写的函数,同名构成重定义。
多态是依据函数名修饰规则(同名函数,构成重写)和虚函数表(存放虚函数),简单来讲:多态=虚函数重写+基类指针或引用调度。
可以,inline修饰的函数是可以加上virtual修饰符成为虚函数的,但是同时,编辑器也会否认inline内联,因为虚函数要放在虚函数表中,内联调用是以直接展开的形式。
不可以,静态成员没有This指针,无法访问虚函数表,静态成员的访问形式为类型::成员,且静态成员只有一份,静态成员无法实现多态,也就没有意义,用virtual修饰,在编译阶段,就会报错。
不可以,虚函数表指针是在构造函数初始化列表阶段才会初始化的。
可以,析构函数实现重写,是针对于new和delete这样的函数,在基类实现多态的时候,析构成员能调度子类对象的析构函数,然后调用父类的析构函数。
Person& p=new Student("张三");
delete p;//此时需要Person类中析构函数为虚函数,重写的方式,调用Student类的析构函数,然后在调用Person类的析构函数,放置内存泄漏
如果是普通对象调用这两个函数,是一样快,如果是基类指针或引用对象,普通函数更快。构成多态,虚函数需要通过虚函数表查找,来找到真正要调用的函数,普通函数直接调用即可,所以普通函数的调度更快一点,但是现在电脑的CPU运算很快,基本上体会不到时间差距。
虚函数表是在编译阶段产生的,存放在代码段(常量区)。
菱形继承问题:数据冗余和二义性,虚继承的原理:通过虚基表实现。
抽象类:拥有纯虚函数的类,抽象类是将纯虚函数做为接口使用的,将整个类作为接口。抽象类纯虚函数,相当于强制子类重写该函数,不然无法实例化对象,拥有纯虚函数(继承,没重写)的类都无法实例化对象