目录
一. 多态
多态是什么
多态:简单的说就是不同类型的对象调用相同的函数产生的结果是不同的。
二. 多态的构成条件
-
虚函数重写
-
父类型的指针或者引用调用虚函数
1. 虚函数
虚函数就是一个函数前面加一个关键字 virtual 关键字,这样就是虚函数了,但是虚函数只能是成员函数才能是虚函数,而全局函数不可以成为虚函数。
virtual void fun()
{
cout << "fun()" << endl;
}
int main()
{
return 0;
}
只有成员函数才可以是虚函数。
class A
{
public:
virtual void fun()
{
}
};
这样就是没有问题的。
2. 虚函数重写(覆盖)
我们的父类和子类只有函数三同才可以构成虚函数重写。
三同:函数名相同,参数相同,返回值相同,然后还需要是虚函数。
只有满足三同并且还都是虚函数才能构成重写。
class person
{
public:
virtual void buyTicket()
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
virtual void buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
上面代码就是一个重写。
3. 父类型的引用或者指针调用
void fun(person& p)
{
p.buyTicket();
}
int main()
{
person p;
student s;
fun(p);
fun(s);
return 0;
}
上面就是多态了。
如果我们破坏了里面的任意一个条件,那么就是不构成多态的,我们下面破坏掉虚函数的条件,让父类不是虚函数。
class person
{
public:
void buyTicket()
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
virtual void buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
void fun(person& p)
{
p.buyTicket();
}
int main()
{
person p;
student s;
fun(p);
fun(s);
return 0;
}
此时我们的调用就是普通的 person对象 调用 buyTicket 函数。
我们在破坏一个其他的条件,返回值。
class student : public person
{
public:
virtual void buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
void fun(person& p)
{
p.buyTicket();
}
此时编译都不通过。
我们在破坏一个参数的条件。
class person
{
public:
virtual void buyTicket(int a)
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
virtual void buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
此时编译也是不通过的,如果改变的是子类的参数,那么编译通过但是调用时不成功的。
4. 多态的特殊情况
在多态里面,是由一些特殊情况的
1) 子类可以不加 virtual 关键字
class person
{
public:
virtual void buyTicket()
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
void buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
我们的多态也是成功的,所以在多态这里我们的子类时可以不加 virtual 关键字的,但是基类时需要加的。
2) 协变
协变就是参数可以不同,但是我们是有条件的,我们的协变只能是返回父类和子类的指针或者是引用。
class person
{
public:
virtual person& buyTicket()
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
virtual student& buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
而且我们必选是同时是指针或者是引用,我们也不仅是可以是自己的父类和子类,我们也可以是其他的父类和子类。
class A
{};
class B : public A
{};
class person
{
public:
virtual A& buyTicket()
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
virtual B& buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
而我们的返回值必须是父类返回父类,子类返回子类。
class person
{
public:
virtual B& buyTicket()
{
cout << "void person::buyTicket()" << endl;
}
};
class student : public person
{
public:
virtual A& buyTicket()
{
cout << "void student::buyTicket()" << endl;
}
};
三. 关键字
1. virtual
virtual 关键字:
- 虚继承:在继承的时候使用该关键字
- 虚函数:在函数前面加 virtual 关键字
2. final
final 关键字:
- 最终类:在类的后面加 final 表示该类无法被继承
class A final
{};
class B : public A
{};
这里就是我们不能对 A 进行继承
3. override
override 关键字:
- 检查是否完成重写:在派生类中使用,加在想要被重写的函数后面
- 静态检查:在编译时静态检查,确保派生类中重写了基类的虚函数
class A
{
public:
void fun()
{}
};
class B : public A
{
public:
void fun() override
{}
};
许哦一这里只需要完成重写就可以了。
class A
{
public:
virtual void fun()
{}
};
class B : public A
{
public:
void fun() override
{}
};
四. 多态的原理
1. 虚函数表指针
在前面的菱形虚拟继承继承中,如果有重定义的函数或者变量,那么里面是有一个虚基表的,这个是虚函数表指针,不要弄混了,下面看一下虚函数表指针。
在有虚函数的类中,该类都会有一个虚函数表指针,该指针就是指向该类中的虚函数的。
class A
{
public:
virtual void fun()
{}
int _a;
};
int main()
{
A a;
cout << sizeof(A) << endl;
return 0;
}
这里的 sizeof 是8,所以这里的 A 肯定不只有一个 _a 的成员变量,还有一个虚函数表指针,这个是一个指针,所以在 32 位和 64 位下的环境是不同的。
下面1看一下该类的内部是怎么样的。
看一下这个指针里面的内容。
现在 a 这个对象里面只有一个虚函数,所以该表里面只存储一个,而虚函数表指针的本质,就可以理解为一个函数指针数组,该函数指针数组里面存储的就是一个个虚函数,如果还有其他的虚函数,那么还是会放在该类的这个虚函数表里面。
虚函数表:每个类共同使用一个虚函数表。
下面看一下相同类型的对象使用的虚函数表是否相同。
2. 调用原理
多态是怎么样被调用的?
多态是需要父类的对象或者是指针,然后调用重写的虚函数。
在父类的指针或者引用看来,它自己指向的对象模型就是一个父类对象。
class A
{
public:
virtual void fun()
{
cout << "A::fun()" << endl;
}
int _a;
};
class B : public A
{
public:
virtual void fun()
{
cout << "B::fun()" << endl;
}
int _b;
};
int main()
{
B b;
return 0;
}
这里看一下B 类型对象的对象模型
所以在父类的引用或者指针看来就是一个父类的对象,但是调用为什么会形成多态?
看一下这个虚表指针里面的内容。
B 类型的对象里面的虚表指针里面是重写后的自己的虚函数,所以在调用的时候可以调用到自己的函数。
那么为什么B 类型的对象里面的虚表指针里面是自己的函数地址呢?下面分为3步。
- 在多态中,派生类首先会把基类的虚函数表拷贝下来。
- 如果派生类中对基类的虚函数进行了重写,那么就会将虚函数表中的基类的被重写的虚函数替换成自己的虚函数的地址。
- 如果自己也有自己的虚函数,就将自己的虚函数添加到虚函数表中。
多态为什么不能是父类的对象调用?
多态为什么不能是父类的对象呢?
int main()
{
B b;
A a = b;
}
现在将b 赋值给a,那么现在的a里面的虚表是父类的虚表还是子类的虚表?
当然是自己的,因为在父子类在进行赋值的时候,虚表是不会进行赋值的,如果虚表进行赋值的话,那么现在的 a 里面的虚表就是B 类型的虚表,那么a 对象调用虚函数的时候是调用的就是派生类的函数了,所以在进行赋值的时候虚表不能赋值,由于虚表在赋值的时候不能不拷贝,所以多态调用的时候不能是父类的对象。
int main()
{
B b;
A a;
b._a = 10;
a = b;
}
通过这段代码测试一下,这里将里面的 _a 变量赋值时为例更好的查看。
这里看到虚表时没有变化的。
为什么需要是虚函数呢?
前面说过了,虚函数会放到虚表里面,那么如果不是虚函数的话,那么当然是不会放在虚表里面的,所以如果函数都不在虚表里面即使是父类的指针或者是引用在虚表里面也找不到应该重写后的虚函数,所以也就没法实现多态。
五. 总结
多态
- 多态的调用需要父类的引用或者是指针调用
- 调用的函数必须是虚函数进行重写
- 虚函数重写的条件是三同(函数名相同,返回值,参数)
- 在多态这里子类是可以不加 virtual 的,只要父类加了,那么子类与父类的函数又是三同,就构成多态
- 协变,协变就是返回值可以不同,但是返回值必须是父类返回父类的指针或者是引用,子类必须返回子类的指针或者是引用,而且返回的父子类只要是父子类既可以,不一定是自己的父子类