目录
1、多态的概念
多态意思就是一个行为的多种状态。具体讲就是当不同对象去完成同一行为时呈现不同结果。
举例说明:动物园买门票,大人、小孩的票价时不同的。还有在手机软件上订房时,不同人通过同一软件订同一种房间的时候价格可能是不同的。买票和订房这一行为就是一种多态性为。
2、多态的定义与实现
多态的构成条件
多态是在继承关系下的类对象去调用同一个函数,产生了不同的行为。
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人票" << endl;
}
};
class Child :public Person
{
public:
virtual void BuyTicket()
{
cout << "儿童票" << endl;
}
};
//必须通过父类的指针或者引用才能去调用虚函数
void Func(Person& p)
{
p.BuyTicket();
}
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person p;
Child c;
Person* ptr;
ptr = &p;//指向父类对象,调的就是父类虚函数
Func(ptr);//通过指针去调用
ptr = &c;//指向子类对象,调的就是子类重写的虚函数
Func(ptr);//通过指针去调用
Func(p);//通过引用去调用
Func(c);//通过引用去调用
return 0;
}
虚函数
虚函数:被virtual修饰的成员函数就是虚函数
class Person
{
public:
//BuyTicket就是虚函数
virtual void BuyTicket()
{
cout << "成人票" << endl;
}
};
虚函数的重写
虚函数的重写也叫做覆盖,注意和重载、继承里的隐藏区分开来,派生类中会有一个和基类完全一样的虚函数,派生类的虚函数的函数名、返回值、参数列表与基类虚函数一致,函数的实现部分需要派生类自己写,这种行为就称作重写了基类的虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人票" << endl;
}
};
class Child :public Person
{
public:
//不加virtual也可以,不过不建议这样写,不易于区分
//void BuyTicket()
virtual void BuyTicket()
{
cout << "儿童票" << endl;
}
};
虚函数的重写有两个特殊情况:
1、协变(基类和派生类的虚函数返回值不同)
class Person
{
public:
//virtual Person* BuyTicket()
virtual Person& BuyTicket()
{
cout << "成人票" << endl;
return *this;
}
};
class Child :public Person
{
public:
//virtual Person* BuyTicket()
//virtual Child* BuyTicket()
//virtual Person& BuyTicket()
virtual Child& BuyTicket()
{
cout << "儿童票" << endl;
return *this;
}
};
派生类重写基类虚函数时,返回值可以是指针或者引用。父类虚函数返回值是父类指针的话,子类重写后返回值可以是子类指针或父类指针;父类虚函数返回值是父类引用时,子类重写后返回值可以是子类引用或者父类引用。注意指针和引用返回值不同。
2、析构函数的重写
如果父类的析构函数被设置成虚函数,那么子类的虚函数无论加不加virtual,都与父类的析构函数构成重写。因为编译器最后都对析构函数的名称做了特殊处理,编译后的析构函数名都是destructor
class Person {
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
//p2指向子类对象,子类对象析构自己的时候
//会先调用自己的析构函数再去调用父类的析构函数
delete p2;
return 0;
}
C++11中的override和final
1、override,检查派生类是否重写了基类的虚函数,如果没有重写就会报错
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人票" << endl;
}
};
class Child :public Person
{
public:
//不加override可以看到函数名写错了,但是编译不报错
//加上override写错后编译会不通过
//virtual void BuyTicketl()//override
virtual void BuyTicket()override
{
cout << "儿童票" << endl;
}
};
2、final,修饰虚函数,标识该虚函数不能被继承
class Person
{
public:
//加上final后派生类重写虚函数会报错无法重写
virtual void BuyTicket()final
{
cout << "成人票" << endl;
}
};
class Child :public Person
{
public:
virtual void BuyTicket()//err
{
cout << "儿童票" << endl;
}
};
重载、隐藏(重定义)、重写(覆盖)的区别
重载:
- 重载前提是指函数名相同,参数列表的个数或者参数顺序不同或参数类型不同,与返回值无关。
- 重载必须在同一作用域。
隐藏:
- 通常是派生类和基类有同名成员,继承后构成隐藏。
- 不在同一作用域,分别位于派生类和基类中。
- 只要名字相同就构成隐藏。
- 参数不同,不论有无virtual,基类的函数都会被隐藏。
- 参数相同,基类函数没有virtual,基类函数被隐藏;有virtual就是多态了。
重写:
- 通常是派生类重写基类虚函数发生的。
- 不在同一作用域,分别位于派生类和基类中。
- 参数相同,函数名相同,返回值相同(协变例外)。
- 基类虚函数必须用virtual修饰,虚函数不能是静态的,所以不能用static修饰。
- 重写函数的访问权限可以不同,例如基类的虚函数是private,派生类重写的时候写成public也是可以的。
3、抽象类
概念
在虚函数后面加上=0,这个函数就变成了纯虚函数,纯虚函数没有实现部分。含有纯虚函数的类称为抽象类,抽象类不能实例化对象。派生类如果继承后不重写纯虚函数的话,也同样会成为抽象类。纯 虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Person
{
public:
virtual void BuyTicket() = 0;
};
class Child :public Person
{
public:
virtual void BuyTicket()
{
cout << "儿童票" << endl;
}
};
int main()
{
//Person p;//err
Child ch;
return 0;
}
接口继承和实现继承
普通函数的继承就是实现继承,派生类继承了基类的函数,且只继承了函数的实现。
虚函数的继承就是接口继承,派生类继承的只是基类虚函数的接口,目的为了重写实现多态,体现的就是接口继承。
4、多态的原理
虚函数表
上一篇博客中提到的虚基表主要是菱形继承里出现的,不要和这里的虚函数表混淆。
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人票" << endl;
}
private:
int _pAge = 18;
};
class Child :public Person
{
public:
virtual void BuyTicket()
{
cout << "儿童票" << endl;
}
private:
int _cAge = 12;
};
int main()
{
Person p;
Child ch;
return 0;
}
可以看到基类对象中不只有_pAge这个成员变量,还有一个_vfptr,这里的v标识virtual,f标识function,所以_vfptr是虚函数表指针,实际就是一个函数指针数组,里面每个数据都是函数指针,指向虚函数。每个含有虚函数的类中至少都有一个虚函数表指针。
说明:
- 派生类的虚表生成:①先将基类中的虚表内容拷贝一份到派生类虚表中②如果派生类重写了基类的某个虚函数,那么就用重写的覆盖虚表中基类的那个虚函数③如果派生类自己也有一个新增加的虚函数,那么派生类自己的虚函数按声明顺序存入继承的虚函数表后面。
- 假设基类有一个Func()函数,该函数不是虚函数,派生类继承后不会将该函数放在虚函数表中。
- 虚函数表本质是函数指针数组,在内存中最后面放了一个nullptr。
多态原理
前面说到多态的函数调用必须通过父类的指针或者引用才能调用,那么为什么呢?
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人票" << endl;
}
private:
int _pAge = 18;
};
class Child :public Person
{
public:
virtual void BuyTicket()
{
cout << "儿童票" << endl;
}
private:
int _cAge = 12;
};
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person p;
Child ch;
Func(&p);
Func(&ch);
return 0;
}
- 通过学习上面的虚函数表,大致就能知道,当Func传过去的是Person对象p时,会去p对象里存的虚函数表里找BuyTicket()函数,所以调用的就是基类的ButTicket(),子类同理。
- 之所以Func()参数不用子类指针或者引用,也是因为之前说过的切割、切片,父类不能给子类赋值,而子类可以给父类赋值。所以不能用子类做参数。
- Func函数不能使用父类对象做参数是因为实参传给形参会发生拷贝,这样就找不到原有的虚表,通过指针或者引用就没有这种问题了。
- 通过上面的分析可以知道,多态的函数调用是一种运行时调用,而不是编译时就确定的。
动态绑定和静态绑定
- 静态绑定意味着在编译时就确定了程序的行为,又称为静态多态;函数重载就是的静态绑定。
- 动态绑定就是在程序运行过程中确定程序的行为,又称为动态多态。