多态的含义
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如:有一个考试类,学生a和学生b跟考试都是has-a的关系所以继承了考试类,但是学生a和学生b的学习情况不同,所以得到的分数也不一样。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
//这个应该写成接口的
class exam
{
public:
exam(int score = 0)
:_score(score)
{
}
void printScore()
{
cout << _score;
}
protected:
int _score;
};
class stuA:public exam
{
public:
stuA(string name,int score)
:exam(score)
, _name(name)
{
}
void printName()
{
cout << _name;
}
void virtual printScore()
{
cout << _score << endl;
}
protected:
string _name;
};
class stuB : public exam
{
public:
stuB(string name,int score)
:exam(score)
,_name(name)
{
}
void printName()
{
cout << _name ;
}
void virtual printScore()
{
cout << _score << endl;
}
protected:
string _name;
};
int main()
{
exam* e1 = new stuA("jaxsen",150);
exam* e2 = new stuB("sofia", 100);
e1->printScore();
cout << endl;
e2->printScore();
return 0;
}
多态的条件:1. 虚函数的重写 2. 父类的指针或引用去调用虚函数
虚函数(条件1)
虚函数:即被virtual修饰的类成员函数称为虚函数
虚函数的重写
重写条件:三同(函数名、参数、返回值)
重写条件的例外:
1.协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
2.析构函数的重写(基类与派生类析构函数的名字不同)如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
补充:派生类重写的虚函数可以不加上virtual(建议加上)
案例:为什么这里切片赋值之后析构少析构了student? 如何解决?
错误代码:
/*基类*/
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
void setName(string name)
{
_name = name;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name; // 姓名
};
/*派生类*/
class Student : public Person
{
public:
Student(const char* name, int num)
:Person(name)
//派生类的构造函数要分成两个部分来看到,要显示调用基类的构造函数和自己的成员变量初始化
,_num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s)//这里的s切片传入基类(派生类赋值给基类)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);
//operator=(s); 为什么不能这么去写?
//因为这里的函数名字与父类相同,会发生隐藏这里只会重复调用operator=,不停地发生函数递归,直到stack溢出
_num = s._num;
}
return *this;
}
void printname()
{
cout << _name << endl;
}
~Student()
{
/*由于多态的原因,析构函数统一会被处理成destructor()(同一个函数名),父子类的析构函数会被隐藏*/
//~Person(); == destructor() == ~Student
//Person::~Person();
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Person* p = new Person();
delete p;
p = new Student("jaxsen",1);
delete p;
return 0;
}
因为这里因为继承的关系析构函数被处理为统一的函数名destructer(),如果不加上virtual虚函数修饰构成重写的话,就会构成隐藏(在什么类就用那个类的对应同名函数)但是,在上面这个情景(父类指针指向子类对象)下,导致没有对student析构,就会导致内存泄露的错误,所以这里要用virtual修饰person的析构函数即可解决问题。最好同时修饰student的析构
多态调用与普通调用(条件2)
class Person {
public:
virtual void BuyTicket() { cout << "Person::买票-全价" << endl; }
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
// 重载
// 隐藏/重定义
// 虚函数重写
void BuyTicket() { cout << "Student::买票-半价" << endl; }
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
main()函数这样调用会输出什么?没有传指针或者引用,并没有构成多态,所以输出都是全价
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
main()函数这样调用会输出什么?传入的是引用或者指针,构成多态,所以输出的是一个全价 一个半价
结论:
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int dat = 0) { std::cout << "B->" << dat << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
问:这段代码输出的是什么?为什么?
输出的结果为B->1。首先,B继承了A, p的类型是B* 指针指向B对象,p调用test(),然后去p的类域里面找,没有找到 (问:如果找到了如果像下面的代码1那样,又会输出什么?答案是输出B::test(),因为这不是基类指针或引用调用所以不构成重载,这里是隐藏) 去到基类里面去找,找到之后执行test()的内容 (问:这里在test()里面调用func()的是什么指针?答案是A * 因为虽然调用的指针类型是B * 传来的指向内容是new B但是不会改变原本类域中的this指针,所以还是A ) 然后就是作为基类类型的指针调用fun(),fun()被virtual修饰,并且派生类重写了该函数内容然后就调用子类函数,然后这里还有一个关键点,为什么输出的是B->1 而不是B->0,因为重写只会重写内容是相当于用基类的外壳(返回值、参数类型(不关心名字)、函数名)去执行派生类的内容,而缺省值也包括在基类外壳中,就好比代码2
//代码1
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int dat = 0) { std::cout << "B->" << dat << std::endl; }
void test()
{
cout << "B::test()" << endl;
}
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
//代码2
virtual void func(int 名字xxx = 1) { std::cout << "B->" << 名字xxx << std::endl; }
三个概念的对比
重载:
1.两个函数在同一个作用域
2.函数名和参数不同重写(覆盖):
1.两个函数分别在基类和派生类的作用域
2.函数名、参数、返回值必须相同
3.两个函数必须是虚函数重定义(隐藏):
1.两个函数分别在基类和派生类的作用域
2.函数名相同
3.两个基类和派生类的同名函数不构成重写就是重定义
final和override(C++11)
final: 修饰类,不能被继承、修饰虚函数,不能被重写
override:修饰派生类的虚函数,检查是否完成重写,如果被修饰了没有完成重写就会报错。
抽象类
在虚函数的后面写上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
什么时候使用抽象类?把众多类的公共特征提取出来作为类
比如:下面这个代码,奔驰和宝马都是汽车,那么可以把汽车的共同点作为一个类,这个car类就可以写成抽象类
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 << "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;
cout<<sizeof(d)<<endl;
return 0;
}
问上面代码输出什么?
答案是12,为什么?如图:
问:这里vfptr是什么呢?答:是虚函数表指针用于寻址找虚函数表
如图:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
- 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的
验证虚函数表和虚函数存在哪个空间?
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;
};
void Func()
{
;
}
int main()
{
/*Base b;
Derive d;
cout << sizeof(d) << endl;*/
Base b1;
//Base b2;
static int a = 0;
int b = 0;
int* p1 = new int;
const char* p2 = "hello world";
printf("静态区:%p\n", &a);
printf("栈:%p\n", &b);
printf("堆:%p\n",p1);
printf("代码段:%p\n",p2);
printf("虚表:%p\n", *((int*)&b1));
printf("虚函数地址:%p\n", &Base::Func1);
printf("普通函数地址:%p\n",Func);
return 0;
}
从输出即可证实虚函数和虚函数表存放在代码段(常量区)
多态的实现
先给出结论:子类的虚函数会覆盖掉继承过来的父类虚函数表中的虚函数
- 内存1监视窗口:基类对象b
- 内存2监视窗口:派生类对象d
- 内存3监视窗口: 基类对象b的虚函数表
- 内存4监视窗口:派生类对象d的虚函数表
如图:
这里的内存显示是上面代码的,我们可以看到在内存3的监视窗口中虚函数表的第一个地址为0x00081514,而到了内存4窗口的虚函数表中第一个地址为0x00081528, 其实这里就是子类的虚函数会覆盖掉继承过来的父类虚函数表中的虚函数,从而实现多态,也解释了怎么根据指向去实现派生类调用派生类,指向基类调用基类的原理(base)。
为什么要父类的指针和引用才能实现多态?对象不行?
对于一个指针类型来说,一个类型决定这个指针 解引用的大小、自增自减的步长,然后对于之前的切片的知识,一个子类赋值给一个父类会发生切片就会把父类的那一部分切出来,赋值给父类 (补充知识点:对于非继承关系的不同类型赋值,会产生临时变量,而如果要引用的话,比如: A a ; B b; const A& a = b
这需要加const因为临时变量具有常性,但是C++对于继承关系的不同类型赋值作了特殊处理,不会产生临时变量,并且发生切片)而这里的切片包括了虚函数表,所以能够根据引用别名或指针指向去实现多态。
那为什么对象不行呢?因为对象与对象之间的赋值是拷贝,这里拷贝只会拷贝子类中属于父类的一部分,但是不会拷贝虚函数表指针,因为如果拷贝了虚函数表指针,就会导致不一定能使用父类的虚函数,这里的父类对象虚函数表被赋值了。而指针与指针之间的赋值只会改变指向。
不同类的没有重写的虚函数表是否一样? 不一样,虽然子类的父类继承的虚函数没有重写,没有发生改变,但是还是使用同一个虚函数。
同类的虚函数表是否一样?一样。
虚函数的地址一定会被放进类的虚函数表吗?是
打印虚表代码:
// 打印虚表
typedef void (*VFUNC)();
//void PrintVFT(VFUNC a[])
void PrintVFT(VFUNC* a)
{
for (size_t i = 0; a[i] != 0; i++)
{
printf("[%d]:%p->", i, a[i]);
VFUNC f = a[i];
f();
//(*f)();
}
printf("\n");
}
int main()
{
void (*f1)();
VFUNC f2;
cout << sizeof(long long) << endl;
Base b;
PrintVFT((VFUNC*)(*((long long*)&b)));
Derive d;
X x;
// PrintVFT((VFUNC*)&d);
//64位用 long long 32位用int
PrintVFT((VFUNC*)(*((long long*)&d)));
PrintVFT((VFUNC*)(*((long long*)&x)));
return 0;
}
多继承的虚函数表
继承多少个基类就会有多少个虚函数表,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
- 什么是多态?
静态动态:函数重载
动态多态:1、父类的指针或者引用调用虚函数 2、虚函数完成重写 - inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去,这里值得注意的是inline修饰虚函数,多态调用时,才会从失去inline属性,否则还是inline函数 - 静态成员可以是虚函数吗?
答:不能,编译报错,其实静态成员函数就是被类域限制的全局函数,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 - 构造函数可以是虚函数吗?
答:不能,编译报错,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 - 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。 - 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 - 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。