一、多态的概念
多态通俗来讲,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子:比如说买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价票;军人买票时,是优先买票。
下面这段代码就是多态的例子:
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
protected:
int _b;
};
void Test(A* a)
{
a->func();
}
int main()
{
A a;
Test(&a);
B b;
Test(&b);
return 0;
}
传A类对象的指针,调用的是A类的func方法,传B类对象的指针,调用的是B类的func方法。
二、多态的定义与实现
1、多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
在继承中想要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
2、虚函数
虚函数:即被virtual修饰的类成员函数称之为虚函数。
class A
{
public:
virtual void func()//虚函数
{
cout << "A::func()" << endl;
}
protected:
int _a;
};
3、虚函数的重写
虚函数的重写(覆盖):派生类中有一个和基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
class A
{
public:
virtual void func()//基类的虚函数
{
cout << "A::func()" << endl;
}
protected:
int _a;
};
class B : public A
{
public:
virtual void func()//派生类重写了基类的虚函数
{
cout << "B::func()" << endl;
}
//void func()这样写也行
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也
可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属
性),但是该种写法不是很规范,不建议这样使用*/
protected:
int _b;
};
void Test(A* a)
{
a->func();
}
虚函数重写有两个例外:
①协变(基类与派生类虚函数返回值不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。但是基类虚函数返回基类对象的指针或者引用,派生类返回虚函数返回派生类对象的指针或者引用时,称之为协变。
class A
{
};
class B : public A
{
};
class Person
{
public:
virtual A* func()//返回A类对象的指针,A类是B类的基类
{
cout << "Person::func" << endl;
return new A;
}
};
class Student :public Person
{
public:
virtual B* func()//与基类虚函数返回值不同,B类是A类的派生类
{
cout << "Student::func" << endl;
return new B;
}
};
void Test(Person* a)
{
a->func();
}
int main()
{
Person p;
Test(&p);
Student s;
Test(&s);
return 0;
}
②析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论加不加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,但是编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
在什么情况下我们需要让析构函数称为虚函数呢?看下面这段代码
class A
{
public:
~A()
{
cout << "A::~A()" << endl;
_a = 0;
}
protected:
int _a = 100;
};
class B :public A
{
public:
~B()
{
cout << "B::~B()" << endl;
_b = 0;
}
protected:
int _b = 199;
};
int main()
{
A* a1 = new A;
A* a2 = new B;
delete a1;
cout << "----------" << endl;
delete a2;
return 0;
}
在释放a1指向的空间时,调用了A类对象的析构函数,这是正确的。但是在释放a2指向的空间时,也调用了A对象的析构函数。因为a2是基类指针,在指向派生类对象时会进行切割,此时这个指针只能看到属于基类的那一部分,所以不会调用B类对象的析构函数。
a2指向的的是B类对象动态开辟的空间,如果不调用B类对象的析构函数,可能会造成内存泄漏
此时我们只需要将析构函数设置为虚函数即可解决这个问题。
class A
{
public:
virtual ~A()
{
cout << "A::~A()" << endl;
_a = 0;
}
protected:
int _a = 100;
};
class B :public A
{
public:
virtual ~B()
{
cout << "B::~B()" << endl;
_b = 0;
}
protected:
int _b = 199;
};
int main()
{
A* a1 = new A;
A* a2 = new B;
delete a1;
cout << "----------" << endl;
delete a2;
return 0;
}
将析构函数设置成虚函数,如果基类指针指向的是基类对象,就调用基类的析构函数,如果指向的是派生类对象,就调用派生类的析构函数。
4、C++11 override和final
C++对函数重写的要求比较严格,但是在有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间不会报错。因此C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
①final:修饰虚函数,表示该虚函数不能被重写
class A
{
public:
virtual void fucnc() final
{
cout << "A::fucnc()" << endl;
}
};
class B :public A
{
public:
virtual void fucnc()//此时重写会报错
{
cout << "B::fucnc()" << endl;
}
};
②override:修饰派生类的虚函数,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class A
{
public:
virtual void fucnc()
{
cout << "A::fucnc()" << endl;
}};
class B :public A
{
public:
virtual void fucnc(int a) override//此时报错,因为它没有重写基类的某个虚函数
{
cout << "B::fucnc()" << endl;
}
};
5、重载、重写(覆盖)、重定义(隐藏)的对比
三、抽象类
1、概念
在虚函数后面写上=0,则这个函数称为纯虚函数。包含纯虚函数的类叫做抽象类(也叫做抽象类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象。纯虚函数规范了派生类必须重写(这是一种间接的强制派生类去重写),否则派生类无法实例化出对象。另外纯虚函数更体现出了接口继承。
class A//抽象类
{
public:
virtual void fucnc() = 0//纯虚函数
{
cout << "A::fucnc()" << endl;
}};
class B :public A
{
public:
virtual void fucnc()//重写了基类的纯虚函数
//如果不重写,则继承了基类的纯虚函数,则B类也是个抽象类
{
cout << "B::fucnc()" << endl;
}
};
int main()
{
A* a = new A;//报错
//A类是抽象类,不能实例化出对象
A* a = new B;//正常
//B类重写了纯虚函数,所以B类不是抽象类,可以实例化出对象
return 0;
}
2、接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。如果不实现多态,不要把函数定义成虚函数。
四、多态的原理
1、虚函数表
看下面这段代码,请问sizeof(A)的结果是多少?答案是16。
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
protected:
int _a;
};
int main()
{
A a;
cout<<sizeof(a);
return 0;
}
通过监视窗口发现a对象除了_a成员变量外,还多了一个_vfptr放在对象的最前面(有些平台可能放到对象的最后面),对象中这些指针我们称之为虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。上面a对象中包含一个指针和一个整形变量,再结合内存对齐,所以最后sizeof(a)出来的结果是16。
再进一步看下面这段代码
//新增了B类继承A类
//在B类中重写func1
//A类新增虚函数func2和普通函数func2
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
void func3()
{
cout << "A::func3()" << endl;
}
protected:
int _a;
};
class B :public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
protected:
int _b;
};
int main()
{
A a;
cout << sizeof(a) << endl;
B b;
cout << sizeof(b) << endl;
return 0;
}
通过观察和测试,我们发现了以下几点:
- 派生类对象b中也有一个虚表指针,b对象由两部分组成,一部分是基类继承下来的成员,虚表指针就属于这一部分。另一部分就是自己的成员。
- 基类a对象和派生类b对象的虚表是不一样的,这里我们发现func1完成了重写,所以b的虚表中存的是重写的B::func1,所以虚函数重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
- func2继承下来后是虚函数,所以放进了虚表,func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后放了一个nullptr。
- 总结一下派生类的虚表生成:①先将基类中的虚表内容拷贝一份到派生类虚表中 ②如果派生类重写了基类的某个虚函数,用派生类自己的虚函数覆盖虚函数表中基类的虚函数 ③派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
- 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都存在代码段,只是它的指针存到了虚表中。另外,对象存的不是虚表,存的是虚表指针。那么虚表存在哪里?答案是在vs下是存在代码段的。
2、多态的原理
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
protected:
int _a = 1;
};
class B :public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
protected:
int _b = 2;
};
void Test(A* p)
{
p->func();
}
int main()
{
A a;
Test(&a);
B b;
Test(&b);
return 0;
}
①观察上图的红色箭头可以观察到,p指向a对象时,p->func()在a的虚表中找到的虚函数是A::func()。
②观察上图的蓝色箭头可以观察到,p指向b对象时,p->func()在b的虚表中找到的虚函数是B::func()。
③这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
④通过下图的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中去找的。不满足多态的函数调用时是确认好的。
五、单继承与多继承关系的虚表函数
1、单继承中的虚表函数
class A
{
public:
virtual void func1() { cout << "A::func1()" << endl; }
virtual void func2() { cout << "A::func2()" << endl; }
protected:
int _a = 1;
};
class B :public A
{
public:
virtual void func1() { cout << "B::func1()" << endl; }
virtual void func3() { cout << "B::func3()" << endl; }
virtual void func4() { cout << "B::func4()" << endl; }
protected:
int _b = 2;
};
int main()
{
A a;
B b;
return 0;
}
观察上图的监视窗口发现b对象的虚表中没有func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。我们可以用下面的这个函数来打印虚表中的函数。
void PrintVTable(VFPTR vtable[])
{
cout << "虚表地址>>" << vtable << endl;
for (int i = 0;vtable [i] != nullptr;i++)
{
printf("这是第%d个虚函数,地址是%p --> ", i, vtable [i]);
vtable[i]();
}
cout << endl;
}
int main()
{
A a;
B b;
//取出a、b的前8个字节,就是虚表指针
//虚函数表的本质就是一个存虚函数指针的指针数组,数组的最后放了一个nullptr
//1、先取b的地址,强转成一个long long*的指针
//2、再解引用取值,就得到了指向虚表的指针
//3、再强转成VFPTR*,因为虚表就是一个存VFPTR类型的数组
//4、虚表指针传递给PrintVTable进行打印虚表
//5、注意,这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,
//虚表最后面没有放nullptr,导致越界。只需要清理解决方案,再编译就好了
cout << "a的虚表:" << endl;
PrintVTable((VFPTR*)(*(long long*)&a));
cout << "b的虚表:" << endl;
PrintVTable((VFPTR*)(*(long long*)&b));
return 0;
}
2、多继承中的虚函数表
class A1
{
public:
virtual void func1() { cout << "A1::func1()" << endl; }
virtual void func2() { cout << "A1::func2()" << endl; }
protected:
int _a1 = 1;
};
class A2
{
public:
virtual void func1() { cout << "A2::func1()" << endl; }
virtual void func2() { cout << "A2::func2()" << endl; }
protected:
int _a2 = 1;
};
class B :public A1,public A2
{
public:
virtual void func1() { cout << "B::func1()" << endl; }
virtual void func3() { cout << "B::func3()" << endl; }
protected:
int _b = 2;
};
typedef void(*VFPTR)();//函数指针类型
void PrintVTable(VFPTR vtable[])
{
cout << "虚表地址>>" << vtable << endl;
for (int i = 0;vtable [i] != nullptr;i++)
{
printf("这是第%d个虚函数,地址是%p --> ", i, vtable [i]);
vtable[i]();
}
cout << endl;
}
int main()
{
A1 a1;
A2 a2;
B b;
cout << "a1的虚表:" << endl;
PrintVTable((VFPTR*)(*(long long*)&a1));
cout << "a2的虚表:" << endl;
PrintVTable((VFPTR*)(*(long long*)&a2));
cout << "b的第一个虚表:" << endl;
PrintVTable((VFPTR*)(*(long long*)&b));
cout << "b的第二个虚表:" << endl;
PrintVTable((VFPTR*)(*(long long*)((A1*)&b + 1)));
return 0;
}
观察上图看一看出:多继承派生类的新增的虚函数放在第一个继承基类的虚函数表中。
完结。。。。