c++三大特性之多态

发布时间:2024年01月09日

目录

1. 多态的概念

2. 多态的定义及实现

????????2.1多态的构成条件

????????2.2 虚函数

????????2.3虚函数的重写

????????????????2.3.1虚函数重写的两个例外:

????????????????????????????????????????1.析构函数的重写

????????????????????????????????????????2.协变

????????2.4 C++11 override 和 final

??????????????????????? ? 2.5重载,覆盖(重写),隐藏(重定义)的对比

3. 抽象类

4.附上一个经典例题


1. 多态的概念

1.1 概念
????????多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

2. 多态的定义及实现

2.1多态的构成条件

  • 1. 必须通过基类的指针或者引用调用虚函数

  • 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2 虚函数
  • 虚函数:即被virtual修饰的类成员函数称为虚函数。
class Person {
public:
     virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

注意:这里使用的虚函数与继承中的virtual修饰虚继承没有任何关系,只是共用了一个关键字而言

2.3虚函数的重写
????????虚函数的 重写(覆盖) 派生类中有一个跟基类完全相同的虚函数 ( 派生类虚函数 基类虚函数 1.返回值类型、2.函数名字、3.参数列表 完全相同 ) ,称子类的虚函数重写了基类的虚函数。
class Person
{
public:
	virtual void BuyTicket() { cout << "买票 - 全价" << endl; }
};

class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "买票 - 半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

调用的是基类的虚函数还是子类的虚函数?

  • 1.不满足多态,看调用者的类型,调用这个类型的成员函数

  • 2.满足多态,看指向对象的类型,调用这个类型的成员函数

????????举例1:不构成多态,未满足重写条件,调用的类型是什么就调用对应的函数。这里虽然不满足重写,但是满足隐藏(继承的知识点),但是这里不会有隐藏关系的体现,因为子类调用才会用隐藏

举例2:构成多态,指向对象的类型是什么就调用对应的虚函数

另外,有人说c++难学,有一点的原因,比如说下面的语法坑!

1.子类继承基类,可以不写 virtual 关键字 ,同样满足多态 (不推荐这样书写,很不规范)

? ? ? ? ?可以这样认为,父类不写 virtual,肯定就不是虚函数,那么子类继承肯定也不是虚函数;

? ? ? ? ?但是在这个地方,我们认为子类重写了这个虚函数,重写体现的是接口继承,重写继承父类这个函数的实现继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),所以就算是子类不加 virtual 关键字,也认为是虚函数,因为父类就是虚函数,子类继承也是虚函数,当然,也可以认为是个例外。

2.3.1虚函数重写的两个例外:

1.析构函数的重写(基类与派生类析构函数的名字不同)

??????? ?如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

有什么作用呢?可以看看接下来的例子,

? ? ? ? 示例:

? ? ? ? 这里并不符合我们的预期,我们主张谁申请空间资源,就由谁来释放,但是这里却不一样,并且很明显造成了内存泄露。为什么会出现这个结果呢?

? ? ? ? 因为父类析构函数不加 virtual 的情况下,子类析构函数和父类析构函数构成隐藏关系。为什么构成隐藏,就是因为编译器会对析构函数名进行特殊处理,处理成 destrutor();

????????而为什么调用的是父类的析构而不是子类的析构,因为这里不满足多态,所以会根据调用者的类型去调用对应的成员函数。并且这里是子类通过切片赋值给了父类。建议看一下c++三大特性之一 继承

????????所以只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证指向的空间正确的调用析构函数。

这里可能也会有人有疑问,为什么这里子类的析构,还会调用基类?

????????因为继承规定,派生类对象析构清理先调用派生类析构再调基类的析构,举例来说:假如父类中有指针指向一块空间,先析构父类,这时候如果子类去访问就会出现野指针的行为,但是先析构子类就不会出现这样的情况。因为c++规定:父类不能访问子类 (这个也可以简单理解一下,就是包含与被包含的关系,子类一定包括父类,但是父类不一定包括子类,也可以把父类理解成是子类的一个特殊成员)

2.协变(基类与派生类虚函数返回值类型不同)
?? ??????派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
简单来说就是基类与子类的虚函数返回值不同也能构成重写,但是必须是父子关系的指针或者引用。
举例:不是父子关系的指针/引用(报错)

例2:满足父子关系的指针或者引用(程序正常运行)

例3:满足父子关系的指针或者引用(程序正常运行)

2.4 C++11 override 和 final
?????? ??从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了? override? final 两个关键字,可以帮助用户检测是否重写。
  • 1. final:修饰虚函数,表示该虚函数不能再被重写

?final其实没多大意义,因为虚函数就是为重写而生的,重写就是为了多态而生的,所以虚函数不重写根本没多大意义。

  • 2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3. 抽象类

3.1 概念
?? ??????在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
示例:

抽象类的意义:

  • 一个类型在现实中没有对应的实体,我们就可以定义一个抽象类

  • 纯虚函数的另一个意义也可以理解为强制了重写

示例:

4.经典例题

补充知识点:

????? ???普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

//请问以下程序运行结果是什么?
//A: A->0 B : B->1 C : A->1 D : B->0 E : 编译出错 F : 以上都不正确

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();

	return 0;
}

????????解析:首先要看这里是否构成多态。不满足多态---看调用者的类型,调用这个类型的成员函数? ? ? ?满足多态---看指向对象的类型,调用这个类型的成员函数

?构成多态还有两个条件:
1.虚函数的重写---三同(函数名,参数,返回值)? ?

2.必须是基类指针或者引用调用虚函数
?

答案是:? B? ??

因为这里首先是构成多态的,为什么不是 D?因为重写是接口继承,重写继承父类这个函数的实现,子类不仅继承了函数的实现,还继承了缺省值

变形1:

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};

class B : public A
{
public:
	virtual void func(int val = 0) { std::cout << "B->" << val << std::endl; }
	virtual void test() { func(); }
};

int main(int argc, char* argv[])
{

	B* p = new B;
	p->test();

	return 0;
}

//A: A->0 B : B->1 C : A->1 D : B->0 E : 编译出错 F : 以上都不正确

变形2:

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

class B : public A
{
public:
	virtual void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{

	A* p = new B;
	p->test();

	return 0;
}
//A: A->0 B : B->1 C : A->1 D : B->0 E : 编译出错 F : 以上都不正确

?蛮有意思,可以玩一下,看看结果。


文章来源:https://blog.csdn.net/m0_73969113/article/details/135415967
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。