C++多态
多态的概念
概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个例子子:
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
多态的定义及实现
多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
//1. 虚函数的重写 -- 三同(函数名,参数,返回值)
//2. 父类指针或者引用去调用
class Person
{
public:
virtual void BuyTicket(int price = 100)
{
cout << "买票 - 全价" << price <<endl;
}
};
class Student : public Person
{
public:
//重写/覆盖
//接口继承,继承了父类这个函数声明,重写了函数实现
virtual void BuyTicket(int price = 50)
{
cout << "买票 - 半价" << price << endl;
}
};
void Func1(Person& p)
{
//1.不满足多态 -- 看调用者的类型,调用这个类型的成员函数
//2.满足多态 -- 看指向的对象的类型,调用这个类型的成员函数
p.BuyTicket();
//只有子类对象去调用才会体现隐藏关系的体现
}
void Func2(Person* p)
{
//1.不满足多态 -- 看调用者的类型,调用这个类型的成员函数
//2.满足多态 -- 看指向的对象的类型,调用这个类型的成员函数
p->BuyTicket();
//只有子类对象去调用才会体现隐藏关系的体现
}
我们可以看到,这里通过一个基类的对象的引用或者指针,因为赋值兼容,就可以实现传递不同的对象调用的不同的函数,不同的人去做同一件事情,达到结果不一样,这就是多态。
多态允许我们编写更加通用和可复用的代码。 通过定义一个基类的公共接口,我们可以让不同的派生类以不同的方式实现这些接口。这样,当我们在代码中需要处理这些不同类型的对象时,只需要通过基类的引用或指针即可,而无需关心具体的实现细节。
多态的一个应用
(面试常考)
class Person
{
public:
~Person()
{
cout << "~Person" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
delete _ptr;
_ptr = nullptr;
cout << "~Student() : " << _ptr << endl;
}
private:
int* _ptr = new int[10];
};
可以看到这里子类的析构函数只调用了父类的析构函数,如果子类成员中有资源没有释放,就很容易造成内存泄漏,所以,这里我们并不希望通过按照指针或者对象的类型调用,而是按照指向的对象调用。
所以,这里为了实现多态,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
。
所以,这里应该这样写。
class Person
{
public:
virtual ~Person()
{
cout << "~Person" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
delete _ptr;
_ptr = nullptr;
cout << "~Student() : " << _ptr << endl;
}
private:
int* _ptr = new int[10];
};
虚函数重写的两个例外:
基类与派生类析构函数的名字不同
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写
class Person
{
public:
virtual void BuyTicket(int price = 100)
{
cout << "买票 - 全价" << price <<endl;
}
};
class Student : public Person
{
public:
//重写/覆盖
//接口继承,继承了父类这个函数声明,重写了函数实现
void BuyTicket(int price = 50)
{
cout << "买票 - 半价" << price << endl;
}
};
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现(函数的全部内容)。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口(函数的声明),目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class Person
{
public:
virtual Person* BuyTicket()
{
cout << "买票 - 全价" << endl;
return this;
}
};
class Student : public Person
{
public:
//重写/覆盖
//接口继承,继承了父类这个函数声明,重写了函数实现
virtual Student* BuyTicket()
{
cout << "买票 - 半价" << endl;
return this;
}
};
其它的父子类也是可以的
class A
{};
class B : public A{};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "买票 - 全价" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
//重写/覆盖
//接口继承,继承了父类这个函数声明,重写了函数实现
virtual B* BuyTicket()
{
cout << "买票 - 半价" << endl;
return nullptr;
}
};
下面我们分析一个例子,加深对多态的理解。
class A
{
public:
virtual void func(int val = 1)
{
cout << "A -> " << val << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B -> " << val << endl;
}
};
这里我们显示的看一下,类似于下面。
继承并不会改变参数类型,所以这里是A*,并不会因为继承就变成B*,并不是拷贝一份函数下来,而是在调用函数的时候找不到会到父类中查找。
virtual void test(A* this)
{
//所以这里this指针指向的对象是一个new的B对象
this->func();
}
test(p);
所以这里满足多态的条件,这里虚函数重写的是实现,就是cout << "B -> " << val << endl;
继承的是接口,就是virtual void func(int val = 1)
,包括缺省参数。
class A
{
public:
virtual void func(int val = 1)
{
cout << "A -> " << val << endl;
}
};
class B : public A
{
public:
void func(int val = 0)
{
cout << "B -> " << val << endl;
}
virtual void test()
{
func();
}
};
这里不满足多态,和虚函数的重写就没有关系,所以,但不满足多态的时候不用考虑虚函数的重写,就是正常调用。
C++11 override 和 final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
final:修饰虚函数,表示该虚函数不能再被重写。
例如,父类 Benz的虚函数 Drive被final修饰后就不能再被重写了,子类若是重写了父类的 Drive函数则编译报错。
class Car
{
public:
virtual void Drive() final
{
}
};
class Benz
{
public:
//因为final关键字所以不能被重写
virtual void Dirve()
{
cout << "goog" << endl;
}
};
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
例如,子类Car和Benz的虚函数Drivet被override修饰,编译时就会检查子类的这两个Drive函数是否重写了父类的虚函数,如果没有则会编译报错。
class Car
{
public:
virtual void Drive() {}
};
//检查是否完成重写
class Benz :public Car
{
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
重载、覆盖(重写)、隐藏(重定义)的对比
抽象类
抽象类的概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
//纯虚函数 -- 抽象类 -- 不能实例化出对象
virtual void Dirve() = 0;
};
//一个类在现实中没有没有对应的实体,就可以定义一个抽象类
class BMW : public Car
{
public:
//强制了子类进行重写
virtual void Dirve()
{
cout << "BMW" << endl;
}
};
抽象类的应用
一个类在现实中没有没有对应的实体,就可以定义一个抽象类,比如:动物的叫,因为动物是一个抽象的类别,有各种各样的动物,可以动物定义成一个抽象的类,动物的叫声写成纯虚函数。
class Animal
{
public:
virtual void sound() const = 0;
};
class Cat : public Animal
{
virtual void sound() const
{
cout << "喵喵~~" << endl;
}
};
class Dog : public Animal
{
virtual void sound() const
{
cout << "汪汪~~" << endl;
}
};
void AnimalSound(const Animal& anm)
{
anm.sound();
}
运行结果如图: