二十、多态

news/2024/10/8 22:40:59 标签: c++, 多态, 纯虚函数, 抽象类

Ⅰ . 多态(polymorphism)

01 多态的概念

多态,就是“多种形态”的意思。

说具体点就是:去完成某个行为时,传不同的对象就会完成不同的行为,可以达到多种形态。

比如买票这个行为,普通人、学生、军人买票是不同的。

普通人必须买全价票,学生可以买学生票,而军热可能就可以优先购买到预留票。

比如我们有一个 BuyTicket 的成员函数,在我们创建普通人、学生和军人三个对象后,它们调用该函数形态结果我们就要设计成不一样的。

02 重写(覆盖)

我们先用代码实现一下刚才的买票场景。

这里当然用的是继承了,将 Student 和 Solider 继承自 Person:

class Person 
{
    
};
class Student : public Person
{

};
class Solider : public Person 
{

};

既然是要设计多态,我们就要为这三个身份设计不同的买票接口

这里用 virtual 虚函数,做到函数名、参数、返回值相同,就能达到“重写”的效果

class Person 
{
public:
//  [virtual] + [返回值] + [函数名] + [参数] 相同 = 构成多态
        👇         👇         👇       👇
      virtual     void     BuyTicket   ()  
      {
          cout << "Person: 买票-全价" << endl;
      }
};
 
class Student : public Person 
{
public:
    // 这里也都相同
    virtual void BuyTicket() 
    {
        cout << "Student: 买票-半价" << endl;
    }
};
 
class Solider : public Person 
{
public:
    // 这里也都相同
    virtual void BuyTicket() 
    {
        cout << "Solider: 优先买预留票-全价" << endl;
    }
};

概念:重写也称为覆盖,即重新改写。

重写是为了将一个已有的事物进行某些改变以适应新的要求。

重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。

最后我们再实现一个 Pay 函数去接受不同的身份。

void Pay(Person* ptr) 
{
    ptr->BuyTicket();
}

代码演示:

class Person 
{
public:
	// 虚函数
	virtual void BuyTicket() 
	{
		cout << "Person: 买票-全价" << endl;
	}
};
class Student : public Person 
{
public:
	// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
	virtual void BuyTicket() 
	{
		cout << "Student: 买票-半价" << endl;
	}
};
class Solider : public Person 
{
public:
	// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
	virtual void BuyTicket() 
	{
		cout << "Solider: 优先买预留票-全价" << endl;
	}
};

/* 接收身份 */
void Pay(Person* ptr) 
{
	ptr->BuyTicket();  // 到底是谁在买票,取决于传来的是谁
}

int main()
{
	int option = 0;
	Person p;
	Student st;
	Solider so;

	do {
		cout << "1.普通人  2.学生  3.军人" << endl;
		cout << "请选择身份:";
		cin >> option;
		switch (option)
		{
		case 1:
			Pay(&p);
			break;
		case 2:
			Pay(&st);
			break;
		case 3:
			Pay(&so);
			break;
		default:
			cout << "输入错误!请重新输入!" << endl;
			break;
		}

	} while (option != -1);

	return 0;
}

运行结果如下:

如果想在用户还可以输入姓名:

cout << "1.普通人  2.学生  3.军人" << endl;
cout << "请选择身份:";
cin >> option;
 
cout << "请输入你的姓名:";
string name;
cin >> name;

我们可以增加一个 _name 的成员变量,并且写好初始化构造:

class Person 
{
public:
	Person(const char* name)
		:_name(name)
	{}
	// 虚函数
	virtual void BuyTicket() 
	{
		cout << _name << ": " << "Person: 买票-全价" << endl;
	}
protected:
	string _name;
};
class Student : public Person 
{
public:
	Student(const char* name)
		:Person(name)
	{}
	// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
	virtual void BuyTicket() 
	{
		cout << _name << ": " << "Student: 买票-半价" << endl;
	}
};
class Solider : public Person 
{
public:
	Solider(const char* name)
		:Person(name)
	{}
	// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)
	virtual void BuyTicket() 
	{
		cout << _name << ": " << "Solider: 优先买预留票-全价" << endl;
	}
};

由于多了一个参数,创建对象时我们可以直接 new:

/* 接收身份 */
void Pay(Person* ptr) 
{
	ptr->BuyTicket();  // 到底是谁在买票,取决于传来的是谁
	delete ptr;
}

int main()
{
	int option = 0;
	//Person p;
	//Student st;
	//Solider so;

	do {
		cout << "1.普通人  2.学生  3.军人" << endl;
		cout << "请选择身份:";
		cin >> option;

		cout << "请输入你的姓名:";
		string name;
		cin >> name;

		switch (option) {
		case 1:
			Pay(new Person(name.c_str()));
			break;
		case 2:
			Pay(new Student(name.c_str()));
			break;
		case 3:
			Pay(new Solider(name.c_str()));
			break;
		default:
			cout << "输入错误!请重新输入!" << endl;
			break;
		}
		cout << endl;
	} while (option != -1);
	return 0;
}

运行结果如下:

03 多态构成的条件

多态是在不同继承关系的类对象中去调用同一个函数,产生了不同的行为。

比如我们刚才的 Student 继承了 Person,Person 买票是全价,但 Student 买票是半价:

注意:继承中想要构成多态,必须满足以下两个条件:

① 必须是子类的虚函数重写成父类函数(重写 = 三同 + 虚函数)

② 必须是父类的指针或引用去调用虚函数

三同:同函数名、同参数、同返回值

虚函数:被 virtual 修饰的类成员函数

代码演示:我们使用引用的方式来接受身份试试:

/* 接收身份 */
void Pay(Person& ref) 
{
	ref.BuyTicket();  // 到底是谁在买票,取决于传来的是谁
}

我们能不能直接在 case 里面创建 Person 、Student 、Solider然后传给Pay呢?

   		switch (option) {    ❌
			case 1:
				Person p(name.c_str());
				Pay(p);
				break;
			case 2:
				Student st(name.c_str());
				Pay(st);
				break;
			case 3:
				Solider so(name.c_str());
				break;
			default:
				cout << "输入错误!请重新输入!" << endl;
				break;
		}

在 switch 里面创建对象时,要放在花括号里面。

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{}

	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
protected:
	string _name;
};

class Student : public Person
{
public:
	Student(const char* name)
		:Person(name)
	{}

	virtual void BuyTicket()
	{
		cout << "学生票" << endl;
	}
};

class Soldier : public Person
{
public:
	Soldier(const char* name)
		:Person(name)
	{}

	virtual void BuyTicket()
	{
		cout << "成人票" << endl;
	}
};

void Pay(Person& ref)
{
	ref.BuyTicket();
}

int main()
{
	int option = 0;

	do {
		cout << "1.普通人  2.学生  3.军人" << endl;
		cout << "请选择身份:";
		cin >> option;

		cout << "请输入你的姓名:";
		string name;
		cin >> name;

		switch (option) {
		case 1: {
			Person p(name.c_str());
			Pay(p);
			break;
		}
		case 2: {
			Student st(name.c_str());
			Pay(st);
			break;
		}
		case 3: {
			Soldier so(name.c_str());
			Pay(so);
			break;
		}
		default:
			cout << "输入错误!请重新输入!" << endl;
			break;
		}
		cout << endl;
	} while (option != -1);
	return 0;
}

运行结果如下:

那如果用对象行不行呢?

不可以,我们说过,必须是父类的指针或引用才能调用虚函数。

对象可以传,但不符合构成多态的条件。

04 协变构成多态

虚函数重写有两个例外,我们先将第一个

观察下面的代码,我们可以看到并没有达到三同的标准,它的返回值是不同的,但依旧构成多态

class A {};
class B : public A {};

class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	virtual B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};

int main()
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	return 0;
}

运行结果如下:

 这就是虚函数重写的一个例外----协变。

但协变也是有条件的,协变的类型必须是父子关系。

当不是父子关系时,就不能协变:

class A {};
class B {};   // 我们取消A与B的父子关系
 
class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	virtual B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};

运行结果如下:error C2555: “Student::f”: 重写虚函数返回类型有差异,且不是来自“Person::f”的协 message : 参见“Person::f”的声明

使用引用也是可以的:

class A {};
class B : A {};
 
class Person {
public:
	virtual A& f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};
 
class Student : public Person {
public:
	virtual B& f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};

05 父虚子非虚构成多态

我们现在来看第二个例外:

父类的虚函数没了无法构成多态

class A{};
class B :public A{};
class Person {
public:
	A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	virtual B* f() {
		cout << "virtual B* Student:::f()" << endl;
		return nullptr;
	};
};

int main()
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	return 0;
}

运行结果如下:多态构成失败

 但是子类的虚函数没了却能构成多态

class A{};
class B :public A{};
class Person {
public:
	virtual A* f() {
		cout << "virtual A* Person::f()" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	B* f() {
		cout << "virtual B* Student::f()" << endl;
		return nullptr;
	};
};

int main()
{
	Person p;
	Student s;
	Person* ptr = &p;
	ptr->f();

	ptr = &s;
	ptr->f();

	return 0;
}

运行结果如下:

 为什么可以构成多态呢??

解答:子类虚函数没有写 virtual,但 f 依旧是虚函数,是因为先继承了父类的函数接口声明。

子类继承父类的虚函数是一种接口继承,所以即使子类的 virtual 没写,它也是虚函数,符合多态条件。

总结:父类为虚函数,子类继承父类的情况下,即使不声明 virtual 也能构成多态

06 析构函数的重写

观察下面的代码:

class Person 
{
public:
	~Person() 
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person 
{
public:
	~Student() 
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person p;
	Student s;

	return 0;
}

运行结果如下:

 这三行分别是谁的呢?

解答:第一行和第二行是 Student s 的,第三行是 Person p 的。我们先来看析构顺序,Student s是后定义的,析构顺序是后定义的先析构。根据子类对象析构先子后父,调用子类的析构函数结束后自动调用父类的析构函数,所以第一行的 ~Student()和第二行的 ~Person()都是 Student 的,随后第三行的 ~Person()是 Person p 自己的。

现在两个析构函数默认是隐藏关系,因为它们的函数名会被统一处理成 destructor

但如果用 virtual 修饰 ~Person,我们知道,如果这里加了 virtual ,不管 ~Student 加不加 virtual ,子类都会跟着父类变成 virtual,那么现在这两个析构函数的关系就变了

class Person 
{
public:
	virtual ~Person() 
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person 
{
public:
	~Student() 
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	// delete 调用 Person 的析构,对这个也没有影响
	Person* ptr1 = new Person;
	delete ptr1;

	// 但是对这样的场景会产生影响
	Person* ptr2 = new Student;
	delete ptr2;


	return 0;
}

运行结果如下:

 如果这里不加 virtual ,~Student 是没有调用析构的。

比如下面这个场景,我们希望 delete 谁调用的就是谁的析构:

class Person 
{
public:
	~Person() 
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person 
{
public:
	// 隐藏(重定义)关系 -> 重写(覆盖) 关系
	~Student() 
	{
		cout << "~Student()" << endl;
		delete[] _name;
		cout << "delete: " << (void*)_name << endl;
	}

private:
	char* _name = new char[10] { 'h', 'e', 'l', 'l', 'o' };
};

int main()
{
	// 我们期望 delete ptr 调用析构函数是一个多态调用
	Person* ptr = new Person;
	delete ptr;   // ptr->destructor() + operator delete(ptr)

	ptr = new Student;
	delete ptr;   // ptr->destructor() + operator delete(ptr)

	return 0;
}

运行结果如下:

 Student 并没有析构!

我们加上 virtual 试试:

class Person 
{
public:
	virtual ~Person() 
	{
		cout << "~Person()" << endl;
	}
};

运行结果如下:

 结论:如果设计的类可能会作为父类,析构函数最好设计成虚函数,即加上 virtual。

07 final 关键字(C++ 11)

如果我有个虚函数,但不希望它被人重写,那么就可以使用 final 这个关键字:

class Car 
{
public:
	// 我是虚函数,但我不想被人重写,怎么办?
	virtual void Drive() 
	{}
};

class Benz : public Car 
{
public:
	// Drive 被人重写了
	virtual void Drive() 
	{
		cout << "Benz-舒适" << endl;
	}
};

这种情况,就可以把 C++ 11 的 final 关键字放在函数末尾:

class Car 
{
public:
	virtual void Drive() final 
	{}
};

class Benz : public Car 
{
public:
	virtual void Drive() 
	{
		cout << "Benz-舒适" << endl;
	}
};

运行结果:(报错)

E1850   无法重写“final”函数 "Car::Drive"

final 不仅能让虚函数不能被重写,还可以把类的继承功能砍掉。

之前我们是通过将构造函数私有化来实现一个不能被继承的类

class Car 
{
public:
private: // 将构造函数私有化
	Car() {}
};

class Benz : public Car 
{
public:
	virtual void Drive() 
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{

	return 0;
}

这样运行不会报错,因为它并不阻止继承行为,还是会给子类继承。

这种方式是简介来实现,当你创建子类对象的时候才会报错:

int main()
{
	Benz car; ❌
 
	return 0;
}

间接让子类创建不了对象,因为子类的构造函数必须调用父类的构造函数进行初始化,

父类的构造函数被私有化,子类不可见。

通过这样的方式间接实现了不能被继承的类。

上面是 C++ 98 的间接方式,在 C++ 11 final 出现后有了更直观的实现方式:

// C++11
class Car final 
{
public:
	Car() {}
};
 
// 此时就不能被继承了
class Benz : public Car ❌ 
{
public:
	virtual void Drive() 
    {
		cout << "Benz-舒适" << endl; 
	}
};

将 final 放在类名后,该类就不能被继承,不用创建对象就可以报错、检查出来。

总结:final 的两个作用:

① 让虚函数不能被重写

② 让类不能被继承

08 override 关键字(C++ 11)

相信大家体会到了 C++ 对函数重写的要求很严格,

但我们难免会犯错,有时候可能因为函数名次序写反导致无法构成重写,

这种错误在编译期间不会报错,只有在程序运行时发现没有得到预期结果,

才能将问题找出来。

final 是禁止重写,override 是必须重写

代码演示:override 可以帮助你检查重写

// C++11
class Car 
{
public:
	virtual void Drive() {}
};

// override 写在子类中:要求严格检查是否完成重写,如果没有重写就报错
class Benz : public Car 
{
public:
	virtual void Drive() override 
	{
		cout << "Benz-舒适" << endl;
	}
};

如果没有重写,在编译时就会报错:

// C++11
class Car 
{
public:
	virtual void Drive() {}
};

// override 写在子类中:要求严格检查是否完成重写,如果没有重写就报错
class Benz : public Car 
{
public:
	virtual void Dirve() override 
	{
		cout << "Benz-舒适" << endl;
	}
};

运行结果如下:

 有了 override 修饰,像如果没加 virtual 或 参数不同、函数名次序写反都会在编译时报错。

在想要重写的地方加上 override,如果你大意了不小心没构成重写,

它能直接报错。

总结:override 写在子类中,会严格检查是否完成重写,如果没有就会报错。

09 重载、覆盖、隐藏的对比

Ⅱ . 抽象类(Abstract Class)

01 纯虚函数抽象类

在虚函数的后面写上 =0,我们称这个虚函数为纯虚函数

包含纯虚函数的类,就是抽象类(也叫接口类)。

/* 抽象类 */
class Car 
{
public:
	virtual void Drive() = 0;  // 纯虚函数
};

抽象类不能实例化出对象,子类即使继承后也不能实例化对象。

只有重写纯虚函数,子类才能实例化出对象:

/* 抽象类 */
class Car 
{
public:
	virtual void Drive() = 0;
};

// 如果父类是抽象类,子类必须重写才能实例化
class BMW : public Car 
{
public:
	virtual void Drive() 
	{   // 重写
		cout << "BMW-操控性" << endl;
	}
};

int main()
{
	BMW b;
	b.Drive();

	return 0;
}

运行结果如下:

总结:抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。

02 抽象类指针

虽然父类是抽象类不能定义对象,但是可以定义指针。

定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:

 

/* 抽象类 */
class Car 
{
public:
	virtual void Drive() = 0;
};

class Benz : public Car 
{
public:
	virtual void Drive() 
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	Car* pBenz1 = new Benz;
	pBenz1->Drive();

	Benz* pBenz2 = new Benz;
	pBenz2->Drive();

	return 0;
}

运行结果如下:

 

03 纯虚函数的实现问题

纯虚函数也是可以实现的:

/* 抽象类 */
class Car 
{
public:
	// 实现没有价值,因为压根没有对象会调用它
	virtual void Drive() = 0 
	{      // 纯虚函数
		cout << "Drive()" << endl;
	}
};

但是,纯虚函数的实现没有什么太大意义,因为根本就没人能用它。

04 关于接口继承和实现继承

普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。

虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,

达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。


http://www.niftyadmin.cn/n/5694895.html

相关文章

用强互作用力抵消电磁力:一种假想的物理机制

用强互作用力抵消电磁力&#xff1a;一种假想的物理机制 摘要 在现代物理学中&#xff0c;强相互作用力和电磁力是两种不同的基本力。强相互作用力主要作用于夸克和胶子之间&#xff0c;而电磁力则在电荷之间产生。由于强相互作用力的强度远大于电磁力&#xff0c;我们提出一…

(C语言贪吃蛇)15.贪吃蛇吃食物

目录 前言 注意事项⚠️ 效果预览 实现方法 运行效果 新的问题&#x1f64b; 最终效果 总结 前言 我们上一节实现了解决了贪吃蛇不合理走位的情况&#xff0c;不理解的再回去看看(传送门&#xff1a;解决贪吃蛇不合理走位)&#xff0c;那么贪吃蛇自然是要吃食物的啊&…

【Vue】Vue2(2)

文章目录 1 数据代理1.1 回顾Object.defineproperty方法1.2 何为数据代理1.3 Vue中的数据代理 2 事件处理2.1 事件的基本使用2.2 事件修饰符2.3 键盘事件 1 数据代理 1.1 回顾Object.defineproperty方法 <!DOCTYPE html> <html><head><meta charset&quo…

Crypto虐狗记---”你“和小鱼(五)

前言&#xff1a;剧情五 提示&#xff1a; 一种食物&#xff1f; 一种食物——培根&#xff1a;&#xff08;A B 也暗示是培根加密&#xff09; cyberpeace{attackanddefenceworldisinteresting} 密码学笔记——培根密码 - ILK - 博客园 (cnblogs.com)

当x趋于零时,零乘以无穷的极限等于多少

当x趋于零时&#xff0c;零乘以无穷的极限是未定义。‌ 在数学中&#xff0c;0乘以无穷大&#xff08;0 ∞&#xff09;是一个未定义的表达式&#xff0c;因为它涉及到两个相互矛盾的概念&#xff1a;0乘以任何有限数都等于0&#xff0c;而无穷大乘以任何非零数都应该是无穷大…

聊聊Mysql的MVCC

1 什么是MVCC&#xff1f; MVCC&#xff0c;是Multiversion Concurrency Control的缩写&#xff0c;翻译过来是多版本并发控制&#xff0c;和数据库锁一样&#xff0c;他也是一种并发控制的解决方案。 我们知道&#xff0c;在数据库中&#xff0c;对数据的操作主要有2种&#…

C++——STL简介

目录 一、什么是STL 二、STL的版本 三、STL的六大组件 没用的话..... 不知不觉两个月没写博客了&#xff0c;暑假后期因为学校的事情在忙&#xff0c;开学又在准备学校的java免修&#xff0c;再然后才继续开始学C&#xff0c;然后最近打算继续写博客沉淀一下最近学到的几周…

MFC工控项目实例二十三模拟量输入设置界面

承接专栏《MFC工控项目实例二十二主界面计数背景颜色改变》 1、在SenSet.h文件中添加代码 #include "BtnST.h" #include "ShadeButtonST.h"/ // SenSet dialogclass SenSet : public CDialog { // Construction public:SenSet(CWnd* pParent NULL); //…