我们在多态(1)中说到,多态就是使用父类指针访问子类函数,可以使得代码更加的简便。并且举了一个喂食动物的例子加以说明,我们使用代码进行展示。
enum class _ANIMALS_TYPE {
CAT,
DOG,
ANIMAL_COUNT
};
class Animal {
public:
Animal(_ANIMALS_TYPE type, int age);
void eat()const;
private:
_ANIMALS_TYPE type; // 动物类型
int age; // 动物年龄
};
class CAT : public Animal {
public:
CAT(_ANIMALS_TYPE type, int age);
void eat()const;
};
class DOG : public Animal {
public:
DOG(_ANIMALS_TYPE type, int age);
void eat()const;
};
/*
喂养动物的时候,实现多态。 --> 使用父类指针(Animal指针)指向子类对象。(Cat和Dog)
*/
void feedAnimal(const Animal* animal) {
// 调用动物吃的功能
animal->eat();
}
int main(void) {
CAT cat(_ANIMALS_TYPE::CAT,5);
DOG dog(_ANIMALS_TYPE::DOG, 6);
/* 传入子类对象指针 */
feedAnimal(&cat);
feedAnimal(&dog);
system("pause");
return 0;
}
Animal::Animal(_ANIMALS_TYPE type, int age)
{
this->type = type;
this->age = age;
}
void Animal::eat()const
{
cout << "动物吃食物" << endl;
}
CAT::CAT(_ANIMALS_TYPE type, int age) : Animal(type,age)
{
}
void CAT::eat() const
{
cout << "猫猫吃猫粮" << endl;
}
DOG::DOG(_ANIMALS_TYPE type, int age):Animal(type,age)
{
}
void DOG::eat() const
{
cout << "狗狗吃狗粮" << endl;
}
代码分析:
1. 代码中我们使用c++新增的枚举类型,表示我们养的动物是什么,上面养了猫和狗。
2. 我们定义了两个类猫和狗,都继承了动物类,并且重写了吃的方法(吃自己想吃的食物)。
3. 然后我们可以定义了一个全局函数(也可以创建一个人类,封装这个喂食的方法),用来喂食我们所养的动物。
4. 我们使用了多态的思想,使用Animal类的指针作为参数,然后我们传入参数(相应动物类的对象),通过父类指针去访问子类中继承的方法。(此处就是使用animal访问dog和cat中重写的eat函数)。
问题:
1. 虽然上面使用多态的特性合情合理,但是当我们运行代码的时候出现了问题,发现使用父类指针访问子类对象中重写的eat()方法时,会发现输出的时: "动物吃食物",也就是说使用父类指针调用eat()方法时并没有调用子类中重写的函数,而是调用的父类中的方法。
原因:
上面的问题的原因是由于指针调用函数的机制造成的。
指针调用函数的机制: 指针调用函数,是根据指针的类型进行调用的,就是Animal类的指针调用函数时,是调用Animal类中的函数,同理CAT和DOG类的指针,调用的是其类中的方法。
上面我们虽然使用父类指针指向了子类对象,而且在子类中重写了eat()函数,但是由于父类指针的类型是Animal类型的,调用eat()函数的时候,也是调用的是Animal类中的函数,所以会打印出"动物吃食物"。
虚函数
有上面的问题,多态就是空谈,c++使用虚函数解决了上面的问题。
语法:
在父类的函数声明(只在函数声明前加,函数定义前是不需要加的)中加上virtual,那么这个函数就是虚函数了。
enum class _ANIMALS_TYPE {
CAT,
DOG,
ANIMAL_COUNT
};
class Animal {
public:
Animal(_ANIMALS_TYPE type, int age);
virtual void eat()const;
private:
_ANIMALS_TYPE type; // 动物类型
int age; // 动物年龄
};
class CAT : public Animal {
public:
CAT(_ANIMALS_TYPE type, int age);
void eat()const;
};
class DOG : public Animal {
public:
DOG(_ANIMALS_TYPE type, int age);
void eat()const;
};
/*
喂养动物的时候,实现多态。 --> 使用父类指针(Animal指针)指向子类对象。(Cat和Dog)
*/
void feedAnimal(const Animal* animal) {
// 调用动物吃的功能
animal->eat();
}
int main(void) {
CAT cat(_ANIMALS_TYPE::CAT,5);
DOG dog(_ANIMALS_TYPE::DOG, 6);
/* 传入子类对象指针 */
feedAnimal(&cat);
feedAnimal(&dog);
system("pause");
return 0;
}
Animal::Animal(_ANIMALS_TYPE type, int age)
{
this->type = type;
this->age = age;
}
void Animal::eat()const
{
cout << "动物吃食物" << endl;
}
CAT::CAT(_ANIMALS_TYPE type, int age) : Animal(type,age)
{
}
void CAT::eat() const
{
cout << "猫猫吃猫粮" << endl;
}
DOG::DOG(_ANIMALS_TYPE type, int age):Animal(type,age)
{
}
void DOG::eat() const
{
cout << "狗狗吃狗粮" << endl;
}
代码分析:
1. 上面的代码和这里的代码是类似的,我们只是在此处代码上加上了virtual关键字,说明此时父类的eat()函数就是虚函数了。
2. 当我们加上virtual的时候,再去运行代码,输出就不一样了。输出: "猫猫吃猫粮","狗狗吃狗粮"。也就说明,我们在喂食的函数中,使用animal的指针访问的eat()函数,是其子类重写的函数。
原因:
1. 上面的例子就可以看出虚函数的存在让多态有了意义(可以使用父类指针指向子类继承的函数)
2. 那为什么虚函数就能够让多态有意义呢?
其实是,如果父类存在虚函数,那么编译器就会在对象的内存的开头添加一个虚指针(vptr),这个虚指针指向一个虚函数表(vtable),这个虚函数表中存放了所有虚函数的地址。(按照声明的顺序存放)
这样当我们使用指针访问函数的时候,编译器会找虚函数表中是否存在这个函数,如果存在就通过虚函数表的地址找到函数的位置(也就是调用虚函数表中的指针)。
3. 当子类继承了存在虚函数的父类
我们知道,子类继承父类的时候,父类中的数据也会被继承,包括虚指针,并且虚函数表中的内容也会复制过来,
如果不对父类中继承来的属性和核函数,那么内存就是这样的,但是当我们对父类继承来虚函数进行重写,那么就会发生改变,编译其会将虚函数表进行修改。
如图,我们在子类中重写了eat()方法,编译器就会自动修改虚函数表中的数据,原来从父类继承过来的虚函数表,地址是指向父类的eat()函数的,当我们在CAT类中重写了eat()函数时,编译器就会将对应函数修改成CAT类中eat()函数所在的地址。
4. 怎样去调用?
我们前面说到,指针调用函数是根据自己的类型调用类型中相符的函数,当加上虚函数之后,在指针访问虚函数的时候,会根据虚指针,查找要访问的虚函数的地址,根据虚函数表的地址来找到相应的函数。
我们前面说过,使用父类指针指向子类对象是,可以理解为使用子类中继承了父类的成员中的数据去创建一个父类对象,其内部的值和子类中这些成员的值是一样的(虚指针也是)。在加上虚函数的时候,就又多拷贝一个虚指针(因为虚指针在最上面)。
当我们使用指针去访问虚函数的时候,自然就会去虚函数表中寻找函数的位置,但是此时虚函数表中的eat()函数的位置,已经被编译器修改成了子类的eat()函数的地址,所以这时候去调用eat()函数就是子类中重写的函数。
5. 使用工具查看类的内存模型
前面说过: 项目右键 -> 属性 -> c/c++ -> 命令行 -> 写上/d1 reportSingleClassLayout类名,重新编译之后就能显示出类的内存模型来了。
6. 子类继承以及重写
父类的虚函数,子类继承之后就也是虚函数了。
在子类重写虚函数的时候,函数声明前可以加virtual也可以不加,因为加不加都是虚函数重写(继承过来就是虚函数)
多继承的虚函数
其实和单继承是类似的,只是根据继承的顺序,拷贝属性。
如果,假设我们上面的CAT类又继承了一个Animal2的类,类内部有一个属性name,有一个虚函数virtual void play()const;
看上面的图,其实和前面的是类似的。 多继承了一个类的属性
纯虚函数和抽象类
纯虚函数
纯虚函数就是当我们在父类实现虚函数时,并不会有太大的作用,这样的话如果在父类实现的话就会白白占用资源(即使是空实现)。
就比如说,我们代码中的eat()函数,在Animal类实现的话其实没什么用,因为吃东西其实是针对具体的实际动物(例如: 猫和狗之类的),所以我们在猫类和狗类中去实现eat()函数更加有意义。
而且我们又希望实现多态,这时候我们就可以将父类的虚函数设置为纯虚函数,就可以了。
上面的eat()函数就是纯虚函数,形如: virtual void eat() const = 0;
如果把父类声明为纯虚函数,那么它的子类:
1)要么对纯虚函数进行完整的实现 (常用)
2)继续将其设置为纯虚函数
3)啥也不写(和(2)一样,但是写上更好)
抽象类
存在纯虚函数的类就是抽象类。 抽象类是不能定义对象的。
1. 子类继承和实现
如果父类中存在纯虚函数,那么子类继承过来这个函数也是纯虚函数,也就是说如果子类不对继承过来的纯虚函数进行实现,那么它也是一个抽象类,不能创建对象。
父类的纯虚函数被子类继承需要在子类中进行实现,否则子类也是一个抽象类。
当然子类中也可以拥有自己的纯虚函数。
总结:
1. 使用虚函数时候才能真正的实现多态。
2. 如果想要使用父类指针调用子类重写的函数,就需要在父类中的函数声明前加上virtual关键字。
3. 只有在继承(父类和子类的关系)中,才能使用多态(父类指针指向子类对象,得有父类和子类) 。
4. 父类中存在虚函数,子类继承之后也是虚函数。而且,只有在子类中重写了虚函数之后,编译器才会去修改对应的虚函数表中的内容。 不然子类中的虚函数表就是复制父类中的。
5. 有纯虚函数的类称为抽象类,抽象类不能定义对象,同理,子类继承之后,也是纯虚函数。 需要进行实现。
6. 简单来理解指针调用函数的情况,加上virtual,指针指向谁(子类对象)就调用谁(对应类)中的方法。不加virtual,指针类型是什么,就调用哪个类中的方法。