多态1
父类指针, 子类指针
(1)父类指针可以指向子类对象, 是安全的, 开发中经常用到(继承方式必须是public)
(2)子类指针指向父类对象是不安全的.(因为不安全, 所以编译器会报错)
#include <iostream>
using namespace std;
class Person {
public:
int m_age;
};
class Student : public Person {
public:
int m_score;
};
int main()
{
Person *p = new Student();
p->m_age = 10;
// 安全
/*
Student *p = (Student*) new Person();
p->m_age = 10;
p->m_score = 10;;
不安全
*/
return 0;
}
安全原因:
person父类指针能访问的范围只有age(父类里面定义的成员变量), 而Student对象一定有age.不会超出申请的堆空间的范围.
父类能访问的东西都是能保证在子类的对象里面能找到的.所以用父类指针去访问子类对象的内存, 肯定是安全的.
因为父类指针能访问的内存范围肯定是在子类对象的内存范围之内.
不安全原因:
p->m_socre = 100; 会找到age后面连续的4个字节赋值, 而后面的4个字节没有申请, 不属于你, 可能是别的对象的, 会把别的对象的数据覆盖掉.
多态
默认情况下, 编译器只会根据指针类型调用对应的函数, 不存在多态
多态是面向对象非常重要的一个特性
定义:同一操作作用于不同的对象, 可以有不同的解释, 产生不同的执行结果
在运行时, 可以识别出真正的对象类型, 调用对象子类的函数
多态的要素
(1)子类重写父类的成员函数(override)
(2)父类指针指向子类对象
(3)利用父类指针调用重写的成员函数
多态2-虚函数
C++中的多态通过虚函数(virtual function)来实现
虚函数:被virtual修饰的成员函数
只要在父类中声明为虚函数, 子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "Animal::speak()" << endl;
}
virtual void run() {
cout << "Animal::run()" << endl;
}
};
class Dog : public Animal {
public:
// 重写(override)
void speak() {
cout << "Dog::speak()" << endl;
}
void run() {
cout << "Dog::run()" << endl;
}
};
class Cat : public Animal {
void speak() {
cout << "Cat::speak()" << endl;
}
void run() {
cout << "Cat::run()" << endl;
}
};
class Pig : public Animal {
void speak() {
cout << "Pig::speak()" << endl;
}
void run() {
cout << "Pig::run()" << endl;
}
};
void liu(Animal *p) {
p->speak();
p->run();
}
int main()
{
liu(new Dog());
liu(new Cat());
liu(new Pig());
return 0;
}
多态3-虚表
虚函数的实现原理是虚表, 这个虚表里面存储着最终需要调用的虚函数地址, 这个虚表也叫虚函数表.
虚表(x86环境的图)
一旦多了虚函数, cat对象就会多4个字节, 根据这4个字节存储的地址值, 就可以找到虚表的存储空间, 然后取出这个存储空间中存储的函数地址, 然后调用函数.(Call 函数地址)就可以调用cat的speak, cat 的run, 也就是说它提前就把Cat的speak函数地址, run函数地址放到了虚表中.
多态4-虚表的汇编分析
调用speak()
// 调用speak
Animal *cat = new Cat();
cat->speak();
// ebp-8 是指针变量cat的地址
mov eax, dword ptr [ebp-8]
// 根据指针变量的地址, 找到指针变量的存储空间, 取出存储空间里面的东西, 也就是Cat对象的地址给eax
// 所以eax是Cat对象的地址
mov edx, dword ptr [eax]
// 根据Cat对象的地址值, 找到Cat对象的存储空间, 取出4个字节出来赋值给edx
// 取出Cat对象最前面的4个字节(虚表的地址)给edx
mov eax, dword ptr [edx]
// 根据edx的地址值, 找到edx的存储空间, 取出虚表的前4个字节(Cat::speak的函数地址)赋值给eax
call eax
// call Cat::speak 调用函数
调用run()
// 调用run
Animal *cat = new Cat();
cat->run();
// ebp-8 是指针变量cat的地址
mov eax, dword ptr [ebp-8]
// 所以eax是Cat对象的地址
mov edx, dword ptr [eax]
// 根据Cat对象的地址值, 找到Cat对象的存储空间, 取出4个字节出来赋值给edx
// 取出Cat对象最前面的4个字节(虚表的地址)给edx
mov eax, dword ptr [edx+4]
// 根据edx+4之后的地址值, 从这个地址值开始找到它的存储空间, 取出4个字节(Cat::run的函数地址)赋值给eax
// 跳过虚表的最前面4个字节, 在取出4个字节(Cat::run的函数地址)赋值给eax
call eax
// call Cat::run 调用函数
多态5-虚表的作用
Pig有Pig的虚表, Cat有Cat 的虚表
编译器在编译cat->speak(); 时只知道cat是Animal*类型, 不知道cat是new Cat(), 因为new Cat()是在运行时, 才知道的, 所以采用了虚表, 即把对象的函数地址跟对象绑在一起.
多态6-虚表的细节
第一个Cat对象和第二个Cat 对象最前面的4个字节是一样的, 它们的虚表的地址是一样的, 它们是同一张虚表.(合理, 没有必要搞多份因为虚表的内容是一样的)
即所有的Cat对象(不管在全局区, 栈, 堆)共用同一份虚表.
(2)如果父类里面有两个虚函数, 子类里面只重写了一个, 那编译器就会把父类里面的虚函数和子类里面的虚函数放到虚表里.
(3)有虚函数就有虚表
多态7-调用父类的成员函数
当子类重写父类的成员函数时, 如果想要调用父类的成员函数的内容.直接Animal::speak();
class Animal {
public:
virtual void speak() {
cout << "Animal::speak()" << endl;
}
};
class Cat : public Animal {
public:
void speak() {
Animal::speak(); // 调用父类的成员函数
cout << "Cat::speak()" << endl;
}
};
多态8-虚析构函数
(1)如果存在父类指针指向子类对象的情况, 应该将析构函数声明为虚函数(虚析构函数)
(2)这样delete父类指针时, 才会调用子类的析构函数, 保证析构的完整性.
多态9-纯虚函数, 抽象类
纯虚函数: 没有函数体却初始化为0的虚函数, 用来定义接口规范.
抽象类(Abstract Class) : 含有纯虚函数的类就叫做抽象类.
抽象类不可以实例化(不可以创建对象)
抽象类也可以包含非纯虚函数, 成员变量
如果父类是抽象类, 子类没有重写所有的纯虚函数, 那么这个子类依然是抽象类.
如果子类重写了所有的纯虚函数, 那么这个子类不是抽象类.
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() = 0;
virtual void run() = 0;
};
class Cat : public Animal {
public:
void speak() {
cout << "1" << endl;
}
void run() {
cout << "22" << endl;
}
};
int main()
{
Animal *cat = new Cat();
cat->speak();
cat->run();
return 0;
}