一、多态的概念
二、多态的定义和实现
2.1 多态的构成条件
2.1.1 实现多态的两个重要条件
1.必须基类指针或者引⽤调⽤虚函数
2.被调⽤的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
这两个条件缺一不可,缺少了任何一个都不能实现多态,如果不是基类的指针或引用调用虚函数,那便指向不了派生类,而派生类没有重写基类的虚函数,派生类就没有不同的函数,多态的不同形态效果就达不到,也就无法实现多态。
如上图所示,Student类的虚函数重写了基类Person的虚函数,且调用虚函数的都是基类的引用或指针,那么这就构成了多态,指向哪个对象,就调用哪个对象的虚函数。
结果:
2.1.2 虚函数
我们在上面提到了虚函数,接下来我们来讲讲什么是虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
如上述代码所示,成员函数BuyTicket就是一个虚函数。
2.1.3 虚函数的重写
我们在上面也讲到了虚函数的重写,那么什么是虚函数的重写呢?
虚函数的重写就是派⽣类中有⼀个跟基类完全相同的虚函数,这里的完全相同有三同(返回类型相同、函数名相同以及参数列表相同),这样才能说派生类的虚函数重写了基类的虚函数。
这里要注意的是,在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,这样也可以构成重写,语法上是允许的,但是不建议这样写,都加上virtual能够使代码更加的规范。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
//void BuyTicket() //构成重写
virtual void BuyTicket() //构成重写
{
cout << "买票-半价" << endl;
}
};
2.1.4 析构函数的重写
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
我们看上面的代码,此时我们并未将基类的析构函数设置成为虚函数,此时不构成重写,也就不构成多态,我们来看看代码的执行结果。
可以看到,我们原本想将新创建的对象A和B删掉,但我们执行发现结果却是调用了两次基类的析构函数,这就导致了B对象所申请的内存没被释放掉,造成内存泄漏。所以析构函数要设计成重写,防止这种情况下的内存泄漏。
当我们将基类的析构函数设置成虚函数时,再次执行程序,就可以发现执行了B对象的析构函数,这样就不会造成内存泄漏了,这里调用两次基类的析构函数是因为B继承了A,所以B既要调用自己的析构函数也要调用基类的析构函数。
2.1.5 override和final关键字
override关键字可以帮助我们检测是否进行了重写,加了override关键字就可以在编译时检测出来是否进行了重写,不需要执行完程序看到结果才知道没有进行重写。而final关键字可以不让派生类重写基类的这个虚函数。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B() override
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
我们未将基类的析构函数设为虚函数,那么在编译时就会报错,说没有重写任何函数。
class A
{
public:
virtual ~A() final
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
我们在基类的虚函数后加上final关键字,此时编译就会报错,说不能被B的虚函数重写。
2.1.6 重载、重写和隐藏的对比
1.重载:
a. 重载要求两个函数要在同一作用域。
b. 重载要求两个函数的函数名相同,参数不同,参数的类型或者个数不同,返回值可同可不同。
2. 重写:
a. 重写要求两个函数分别在继承体系中的基类和派生类的作用域内。
b. 三同(返回类型相同、函数名相同以及参数列表相同)
c. 两个函数都必须是虚函数。
3. 隐藏:
a. 隐藏要求两个函数分别在继承体系中的基类和派生类的作用域内。
b. 隐藏要求函数名相同。
c. 两个函数只要不构成重写,就是隐藏。
d. 基类和派生类成员变量相同也叫隐藏。
三、纯虚函数和抽象类
class Person
{
public:
virtual void BuyTicket() = 0
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket(int a)
{
cout << "买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
在上面的代码中我们将基类Person中的虚函数设为了纯虚函数,那么基类Person就是抽象类,不能实例化出对象,修改派生类Student虚函数的参数列表使派生类Student继承了Person但未对Person的虚函数进行重写,所以派生类Student也是一个抽象类,所以这两个类都不能够实例化出对象。
结果:
四、多态的原理
4.1 虚函数表指针
我们先来看一道题目:
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
这一题根据学的内存对齐的规则来算,应该很快就能得出结果为8个字节,但结果真得是8字节吗?
我们可以看到,类Base的大小为12个字节,可是按照内存对齐的规则来算应该是8个字节才对,这其中是因为除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能 会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要 被放到这个类对象的虚函数表中,虚函数表也简称虚表。
我们可以通过调试来查看__vfptr。
我们来看下面的代码:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
class Base1 : public Base
{
public:
virtual void Func1()
{
cout << "Base1::Func1()" << endl;
}
};
int main()
{
Base b;
Base1 b1;
//cout << sizeof(b) << endl;
return 0;
}
我们在原有代码的基础上增加了一个派生类Base1继承基类Base,在Base类中增加一个虚函数Func2,派生类Base1重写基类Base的Func1。我们通过调试来观察他们两个类的虚表。
通过上图我们可以发现,两个类所创建出的对象的虚表地址不同,说明两个类所创建出的对象拥有各自的虚表,而派生类Base1重写了基类的Func1,所以两个虚表中的Func1的地址不同,这也是为什么我们能够实现多态,实现传什么对象就调用哪个对象的虚函数的原因,重写也叫覆盖的原因也是这么来的,重写后的虚函数用一个新的地址将旧的地址覆盖,而未重写的虚函数Func2的地址则是一样的,通过继承继承下来的。
这时我们再对代码进行修改,每个类多创建一个对象,来观察他们的虚表地址。
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
class Base1 : public Base
{
public:
virtual void Func1()
{
cout << "Base1::Func1()" << endl;
}
};
int main()
{
Base b;
Base b_1;
Base1 b1;
Base1 b1_1;
//cout << sizeof(b) << endl;
return 0;
}
我们每个类都创建了两个对象,下面我们通过调试来观察他们的虚表地址。
可以发现,相同类所创建出的对象是共用一个虚表的。
4.2 多态的原理
4.2.1 多态是如何实现的
由上图可知,满足多态后,是通过指向对象的虚表来确定虚函数的地址,这两个虚表中的虚函数的地址都是不同的,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。
4.2.2 动态绑定和静态绑定
静态绑定:对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
静态绑定汇编:
从上面两张图可以看出,动态绑定是编译运行到ptr指向对象的虚函数表中确定调用的函数地址,而静态绑定不满足多态条件,编译器直接确定调用函数地址,只会调用Person类的BuyTicket函数。
4.2.3 虚函数表
1. 基类对象的虚函数表中存放基类所有虚函数的地址。
2. 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表 指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基 类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独立的。
3. 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
4. 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分。
5. 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
4.3 虚表和虚函数的存储位置
1. 虚函数的存储位置和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。
2. 虚表的存储位置严格来说C++标准并没有明确规定,我们通过下面的代码来验证一下。
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2()
{
cout << "Base::func2" << endl;
}
void func5()
{
cout << "Base::func5" << endl;
}
protected:
int a = 1;
};
class Derive : public Base
{
public:
// 重写基类的func1
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func1" << endl;
}
void func4()
{
cout << "Derive::func4" << endl;
}
protected:
int b = 2;
};
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
通过打印虚表的地址来判断虚表的存储位置。
从上图我们可以看出,虚表的存储地址与常量区的地址十分接近,所以我们能够判断在vs下虚函数表是存储在常量区(代码段)的
五、总结
以上就是关于多态的讲解了,多态要能理解构成重写和多态的条件,同时对于虚表的理解和多态是如何实现的也要掌握,希望以上所讲能够对你有所帮助,感谢阅读。