【C++取经之路】继承

目录

继承的概念及定义

单继承的格式

继承方式和访问限定符 

继承后子类访问基类成员的权限

基类和派生类对象赋值转换

切片

继承中的作用域

引申:重载和隐藏的区别

派生类的默认成员函数

继承与友元

继承与静态成员

如何实现一个不能被继承的类

复杂的菱形继承及虚拟继承

虚拟继承

虚拟继承解决数据冗余和二义性的原理

多继承中指针偏移问题

继承的总结和反思


继承的概念及定义

在C++中,继承是一种面向对象编程的重要特性,它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的成员变量(通常称为属性)和成员函数(通常称为方法)。通过这种方式,派生类可以重用基类的代码,并且可以添加或覆盖基类中的方法。

根据继承的基类的数量,C++中的继承可以分为单继承和多继承。

单继承:指一个派生类只从一个基类派生的情况。

多继承:指一个派生类从多个基类派生的情况。

单继承:

多继承:

单继承的格式

class 子类名 :继承方式 父类 { };

这里借助一段简单的代码来帮助理解。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "Peter";
	int _age = 18;
};

class Student : public Person
{
	int _stuid;//学号
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

先解释一些基本的概念,后面再通过调试感受继承。 

继承方式和访问限定符 

继承后子类访问基类成员的权限

1)如果子类通过public继承(公有继承)自父类,那么基类(父类)的public成员在子类中为public成员,在子类的外部可以直接访问。

2)如果子类通过public继承(公有继承)自父类,那么基类(父类)的protected成员在子类中为protected成员,在子类的外部不可以直接访问。

这里列举了这两个例子,剩下的可以从上表中看出。

总结:

1)基类的private成员无论以什么方式继承再派生类中都是不可见的。这里的不可见指的是:基类的私有成员还是被继承到派生类中,但是语法       上限制派生类对象不管是在类里面还是在类外面都不能去访问它。

2)基类的private成员在派生类中是不能被直接访问的。如果基类成员不想在派生类外直接被访问,但是需要在派生类中能直接被访问,那么可        以在基类中定义为protected。可见,保护成员限定符就是为继承而生的。

3)通过上述表格,可以发现,基类的私有成员在子类中都是不可见的。基类的其它成员在子类的访问方式 = min(该成员在基类的访问限定符,        继承方式),其中,public > protected > private。

4)如果不显式写继承方式,那么使用class时,默认为私有继承,使用struct时,默认为公有继承。但最好显式的写出继承方式。

5)实际应用中一般使用的都是public继承。因为protected/private继承下来的成员都只能在派生类里使用,实际中扩展维护性不强。

好了,基本的概念已经解释完毕,下面通过调试看看继承。

可以看到,通过派生类创建的对象s中,不仅有派生类自己的成员_stuid,还有继承自基类的成员_name和_age。至于如何去修改 _name和_age的值,后面再说吧~

基类和派生类对象赋值转换

只是文字描述很抽象,还是上代码吧~

class Person
{
protected:
	string _name; //姓名
	string _sex;  //性别
	int _age;     //年龄
};

class Student : public Person
{
public:
	int _No;	 //专业课排名
};

void test()
{
	Student sobj;

	//1.子类对象可以赋值给父类的对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.父类对象不能赋值给子类对象
	sobj = pobj;

	//3.父类的指针可以通过强制类型转换赋给派生类指针
	Student* ps1 = (Student*)pp;

	pp = &pobj;
	Student* ps2 = (Student*)pp;//这种情况也可以,但是存在越界访问的问题
}

int main()
{
	test();
	return 0;
}

将父类对象赋值给子类对象,编译器报错如下:

 总结:

 ● 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法,叫切片,下面会解释~

 ● 基类对象不能赋值给派生类对象。

 ● 基类的指针可以通过强制类型转换赋值给派生类的指针,但不一定安全。

切片

先来看一张图,就知道为什么叫做切片了~

把子类赋值给父类,就相当于把红色部分切过去赋给父类。所以说切片这个说法很形象~

上面说到,基类对象不可以赋值给派生类对象,通过上图,我们可以这么理解:基类有的派生类都有,因而派生类可以赋值给基类,但是,派生类有的基类未必有,所以基类不可以赋值给派生类。 

继承中的作用域

1)在继承体系中,基类和派生类都有独立的作用域。

2)如果基类和派生类中有同名成员,那么派生类成员将屏蔽父类的同名成员,这种情况叫隐藏,也叫重定义。

3)基类和派生类中,对于成员函数,只要函数名相同,就构成隐藏。

4)在继承体系里,最好不要定义同名成员。

关于隐藏,这里还想再更详细的说一遍。

隐藏:通常指在派生类中定义了与基类中某个成员同名的成员,从而导致基类中的那个成员在派生类的作用域内被隐藏,这并不意味着基类中的成员被删除或不可访问,而是说,在派生类的作用域内,直接访问将访问到的是派生类中的成员,除非使用 基类 :: 基类成员 显式访问。

下面运行一段代码验证结论:

class Person
{
protected:
	string _name = "小李子";
	int _num = 111;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "_name:" << _name << endl;
		cout << "直接访问_num:" << _num << endl;
		cout << "指定访问_num:" << Person::_num << endl;
	}
protected:
	int _num = 999;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

引申:重载和隐藏的区别

派生类的默认成员函数

● 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数(不传参也能调),则必须在派生类构     造函数的初始化列表显式调用。

文字描述太抽象了,还是上代码吧~

基类中没有默认构造函数

class Person
{
public:
	Person(const int& num) :_num(num) {}
protected:
	int _num;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "num" << _num << endl;
	}
protected:
	string _name = "李华";
	int _age = 18;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

  

正确操作: 

class Person
{
public:
	Person(const int& num) :_num(num) {}
protected:
	int _num;
};

class Student : public Person
{
public:
	//基类没有默认构造函数,需要在派生类的初始化列表显式调用基类的构造函数
	Student(): Person(1){}
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "num:" << _num << endl;
	}
protected:
	string _name = "李华";
	int _age = 18;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

 

上面没讲如何修改继承来的属性,其实这种做法就可以修改继承来的属性了~

● 派生类的拷贝构造函数必须调用基类的拷贝构造函数来完成基类部分的拷贝。

● 派生类的operator=(赋值重载)必须调用基类的赋值重载来完成基类部分的赋值。

● 派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类的资源。

● 派生类对象初始化先调用基类构造再调派生类构造。

● 派生类对象析构清理先调用派生类的析构再调用基类的析构。

以下是我在学习继承过程中做的一些笔记,大概是关于要不要写派生类默认构造的问题,希望对你有用~

1)如果基类有默认构造函数,那么派生类可以选择不提供构造函数,此时,在派生类中,编译器会自动生成一个默认构造,该构造函数会调用        基类的默认构造函数

2)如果需要初始化派生类的特有成员变量,那么应该在派生类中提供构造函数

3)派生类自动生成的拷贝构造函数调用基类的拷贝构造函数来完成基类部分的拷贝,对派生类特有的成员变量执行浅拷贝。

下面这张图是派生类对象和基类对象的构造函数、析构函数执行顺序:

上面已经演示了派生类的构造函数调用基类构造函数初始化基类部分的代码,下面将演示如何在派生类中调用基类的拷贝构造函数处理基类部分的拷贝。

class Person
{
public:
	Person():_name(""), _age(0) {}
	Person(const Person& p) :_name(p._name), _age(p._age){}
protected:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student():Person(),_id("") {}
	Student(const Student& s) : Person(s), _id(s._id) {}
protected:
	string _id;// 学号
};

int main()
{
	Student s;
	return 0;
}

直接将s传过去给Person的拷贝构造函数,因为父类的拷贝构造函数会通过切片来拿到父类的那部分。 赋值重载函数等也是同理。

继承与友元

友元关系不能继承,也就是说基类友元不能访问派生类的私有和保护成员。请看代码~

class Student;//声明

class Person
{
public:
	friend void DisPlay(const Person& p, const Student& s);
protected:
	string _name;
};

class Student : public Person
{
protected:
	int _No;
};

void DisPlay(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._No << endl;   //尝试访问派生类的保护成员
}

int main()
{
	Person p;
	Student s;
	DisPlay(p, s);
	return 0;
}

可以看到,Person类对象中的保护成员_name在友元声明后,是可以在类外访问的,但是继承自Person类的Student类对象中的保护成员_No并不能访问到。说明:基类友元不能访问派生类的私有和保护成员。

继承与静态成员

基类定义了static静态成员,则整个继承体系里只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

请看代码:

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name;
public:
	static int _count; //统计人数
};

int Person::_count = 0; //初始化

class Student : public Person
{
protected:
	int _No;
};

class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};

void Test()
{
	Student s1;
	Student s2;
	Student s3;

	Graduate s4;

	cout << "人数:" << Person::_count << endl;
	Student::_count = 0;
	cout << "人数:" << Person::_count << endl;
}

int main()
{
	Test();
	return 0;
}

这说明了整个继承体系里只有一个_count,Student中的_count一改,Person中的_count也跟着改,因为它们就是同一个。

也可以通过监视窗口看看_count的地址。

 

_count从0变到3的过程中,_count的地址始终不变,说明只有一个_count。

如何实现一个不能被继承的类

可以通过将构造函数和析构函数声明为protected和private来阻止其他类继承该类,但是其他类仍然可以通过友元关系来继承它。用final关键字修饰类,可以彻底防止被继承,格式如下。

class A final
{
public:
    int _a;
};

复杂的菱形继承及虚拟继承

上面讲的全是单继承,从这部分开始,将讲到多继承中的一种特殊情况——菱形继承。

这张图描述的就是菱形继承。 直接上代码~

class Person
{
public:
	string _name;
};

class Student : public Person
{
protected:
	int _No;
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程
};

void Test()
{
	Assistant a;
}

int main()
{
	Test();
	return 0;
}

监视窗口:

可以看到,一个a对象中,有两份_name,这就是菱形继承带来的问题——数据冗余和二义性

请看下面代码的运行结果:

class Person
{
public:
	string _name;
};

class Student : public Person
{
protected:
	int _No;
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程
};

void Test()
{
	Assistant a;
	a._name = "Peter";
}

int main()
{
	Test();
	return 0;
}

 二义性,就是不知道要访问哪一个,例如此处的_name,到底是访问继承自Student中的_name还是访问继承自Teacher中的_name不明确。

解决二义性的一个方法是:指定访问哪个父类的成员。

a.Student::_name = "Peter"; / /指定访问

虽然这种方式可以解决二义性问题,但是数据冗余问题并没有得到解决。 

那么有没有可以根治菱形继承数据冗余和二义性的方法呢?有的,虚拟继承就是为了解决这一问题而生的。

虚拟继承

虚拟继承的格式:

class 子类名 :virtual 继承方式 父类

 上代码验证一下虚拟继承是否可以解决数据冗余和二义性的问题~

class Person
{
public:
	string _name = "李华";
};

class Student : virtual public Person
{
protected:
	int _No;
};

class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; //主修课程
};

void Test()
{
	Assistant a;
	a._name = "张三";
}

int main()
{
	Test();
	return 0;
}

监视窗口:

 

执行第76行后:

可以看到,执行完第36行后,_name全被改为了“张三”,说明虽然监视窗口上显示3个_name,但是它们的地址是一样的,也就是说,对象a中只有一个_name。这样就解决了菱形继承带来的数据冗余和二义性问题。这里我没说明白, 如果还有疑问,请看原理部分~

虚拟继承解决数据冗余和二义性的原理

为了了解虚拟继承的原理,这里给出一个简化的菱形继承体系,再借助内存窗口观察对象成员模型。

这里通过内存窗口再次看看数据冗余和二义性的问题~

class A
{
public:
	int _a;
};

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

class C : public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

内存窗口:

这一个整体为一个D的对象,该对象继承自B和C。通过这张图,可以看到该对象中既有一份来自B的_a,又有一份来自C的_a(观察数值就可以发现),这很好的展现了菱形继承的问题。

下面,将上述代码改为虚拟继承,再通过内存窗口看看是否还存在数据冗余和二义性的问题。

可以看到,_a只有一份了,并且放在了最后,所以数据冗余和二义性的问题不存在了。C中的_a一改B中的也跟着改了,说明B和C共用同一个_a,问题是,B和C是如何找到公共的_a呢?还有个疑问,上图未框起来的部分究竟是何物?这两部其实是都是地址,下面会通过内存窗口看看它们里面的存的内容是什么。

注意这里的计算:

0x0137F970 + 20(十进制) = 0x0137F984(十六进制的计算)

0x0137F978 + 12(十进制) = 0x0137F984

对比加粗的结果和上图中用粉红色框起来的地址,发现计算结果和粉红色框起来的地址是一样的。这并不偶然,其实这就是虚拟继承解决数据冗余和二义性的原理了。上面只是一个引子,接下来总结原理~

接上面的问题——B和C是如何找到公共的_a的,其实是通过虚基表指针(vptr),也就是上图红色框起来的两个地址,虚基表指针指向同一张表,叫虚基表(vtable),虚基表里存的是偏移量,通过偏移量就可以找到_a。这里只是针对上面的测试代码进行说明,下面换种说法,尽量不针对某种情形,而是适用于广泛的场景~

什么是虚基表?

在讲适应性更广的说法之前,先了解一下虚基表,因为会用到它。

为了实现虚拟继承,C++引入了虚基表(vtable)的概念。虚基表用于记录虚基类(即被虚拟继承的基类)在派生类中的偏移量。

当一个类被声明为虚拟继承时(例如测试代码中的B和C),编译器会为该类生成一个虚基表指针(测试代码中B和C各有一个),当访问虚基类的成员(例如测试代码中的A)时,编译器会先通过vptr找到虚基表,然后根据虚基表中的偏移量定位虚基类在派生类中的实际位置,从而正确的访问虚基类成员。

原理部分的最后,根据测试代码画出一张图,来帮助理解虚拟继承的原理。

多继承中指针偏移问题

当一个基类指针指向一个派生类对象时,这个指针本身只包含该基类部分的地址。这句话很抽象,请看代码~

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };

class Derive : public Base1, public Base2
{
public:
	int _d;
};


int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

下面来分析p1、p2、p3的指向。

 

p1和p3指向Base1的起始位置,但是范围却不相同,p1只包函基类Base1(红色部分),p3包函整个部分。p2指向Base2的起始位置,只包含基类Base2(橙色部分)。 

继承的总结和反思

继承,它是一种“is-a”的关系,也就是说派生类是一个特殊的基类。优先使用组合而不是继承,还有,尽量不要写多继承,尤其是菱形继承,会坑自己~


完~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/706676.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【SkiaSharp绘图03】SKPaint详解(一)BlendMode混合模式、ColorFilter颜色滤镜

文章目录 SKPaintSKPaint属性BlendMode获取或设置混合模式SKBlendMode 枚举成员效果预览 Color/ColorF获取或设置前景色ColorFilter 颜色滤镜CreateBlendMode 混合模式CreateColorMatrix 颜色转换CreateCompose 组合滤镜CreateHighContrast 高对比度滤镜CreateLighting 照明滤镜…

Java最新面试题(全网最全、最细、附答案)

一、Java基础 1、基础概念与常识Java 语言有哪些特点? 简单易学&#xff08;语法简单&#xff0c;上手容易&#xff09;&#xff1b;面向对象&#xff08;封装&#xff0c;继承&#xff0c;多态&#xff09;&#xff1b;平台无关性&#xff08; Java 虚拟机实现平台无关性&a…

千万级流量冲击下,如何保证极致性能

1 简要介绍 随着互联网的快速发展&#xff0c;网络应用的流量规模不断攀升&#xff0c;特别是在电商大促、明星直播、重大赛事、头条热搜等热点事件中&#xff0c;秒级100w请求成为了常态。在这样的流量冲击下&#xff0c;如何确保系统稳定、高效地处理每一个请求&#xff0c;为…

抖某音号解封释放实名

##抖音账号封禁后如何解封呢 我相信&#xff0c;做过抖音&#xff0c;或者正在做抖音的朋友&#xff0c;都曾面临一种尴尬至极的局面&#xff0c;辛辛苦苦做起来的账号&#xff0c;或者刚刚准备好的账号&#xff0c;在一时之间&#xff0c;竟然被抖音官方封禁了&#xff01; 实…

ubuntu下使用cmake编译opencv4.8.0+ffmpeg4.2.2+cuda11.1

1.源码下载 &#xff08;1&#xff09;下载ffmpeg4.2.2、opencv4.8.0源码&#xff0c;这里提供一个百度网盘地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1pBksr0_RtKL0cM6Gsf2MGA?pwdcyai 提取码&#xff1a;cyai &#xff08;2&#xff09;解压所有文件 例…

小而美的算法技巧:前缀和数组

小而美的算法技巧&#xff1a;前缀和数组 类似动态规划。 class NumArray {private int[] preSum;public NumArray(int[] nums) {preSumnew int[nums.length1];//preSum[0]的前缀和为0for(int i1;i<preSum.length;i){preSum[i]nums[i-1]preSum[i-1];//先计算累加和}}publi…

Git进阶使用(图文详解)

文章目录 Git概述Git基础指令Git进阶使用一、Git分支1.主干分支2.其他分支2.1创建分支2.2查看分支1. 查看本地分支2. 查看远程分支3. 查看本地和远程分支4. 显示分支的详细信息5. 查看已合并和未合并的分支 2.3切换分支1. 切换到已有的本地分支2. 创建并切换到新分支3. 切换到远…

毕业年薪20w起!25届最近5年南京信息工程大学自动化考研院校分析

南京信息工程大学 目录 一、学校学院专业简介 二、考试科目指定教材 三、近4年考研分数情况 四、近4年招生录取情况 五、最新一年分数段图表 六、历年真题PDF 七、初试大纲复试大纲 八、学费&奖学金&就业方向 一、学校学院专业简介 二、考试科目指定教材 1、…

掌握WhoisAPI,提升域名管理的效率

在互联网时代&#xff0c;域名管理是网站运营中非常重要的一环。通过域名&#xff0c;我们能够轻松访问和识别不同的网站。然而&#xff0c;域名的注册和管理也是一项复杂的任务&#xff0c;特别是对于大规模拥有许多域名的企业来说。为了提升域名管理的效率&#xff0c;我们可…

边缘计算网关在智慧厕所远程监测与管理的应用

随着智慧城市建设的不断深入&#xff0c;城市公共设施的智慧化管理成为了提升城市品质和居民生活质量的关键建设。公厕作为城市基础设施的重要组成部分&#xff0c;其管理效率和卫生状况直接影响着市民的日常生活体验。在公厕设施建设背景下&#xff0c;边缘计算网关技术的应用…

ansible离线安装docker

docker简介&#xff1a; Docker 是一个开源的应用容器引擎&#xff0c;它允许开发者打包他们的应用以及应用的运行环境到一个可移植的容器中。这个容器可以在任何支持Docker的机器上运行&#xff0c;确保了应用在不同环境中的一致性。 网上有很多在线ansible安装docker的&…

Base64编码方式的介绍及其编码解码

一、Base64是什么 Base64是一种用于将二进制数据编码为ASCII字符的编码方式&#xff0c;主要目的是为了能够在文本环境中传输和存储二进制数据。这种编码方式广泛应用于电子邮件、HTTP协议和其他需要传输或存储二进制数据的地方。 二、发明Base64编码的原因 Base64编码的发明解…

猫狗识别(超详细版)(py代码)

猫狗识别&#xff08;一&#xff09; 二、视频识别 用OpenCV和Tkinter构建的视频识别猫狗的应用程序。它允许用户从文件对话框中选择一个视频文件&#xff0c;然后在Tkinter窗口中播放视频&#xff0c;并使用Haar级联分类器实时检测视频中的猫和狗。 1.导入所需的库&#xff…

QT--DAY1

不使用图形化界面实现一个登陆界面 #include "widget.h"Widget::Widget(QWidget *parent): QWidget(parent) {//设置窗口标题this->setWindowTitle("登录界面");//设置窗口大小this->resize(535,410);//固定窗口大小this->setFixedSize(535,410)…

北京多商入驻app开发项目的主要优势及功能

多商入驻app开发项目的定义 随着电子支付技术的不断成熟&#xff0c;全国各地的消费者通过网络在线上购物的频率越来越高&#xff0c;为此&#xff0c;多商入驻app开发项目应用而生。各商家也纷纷开始申请入驻商城平台&#xff0c;开设自己的店铺。 图片来源&#xff1a;unspl…

MAVEN-SNAPSHOT和RELEASE

一、快照版本SNAPSHOT和发布版本RELEASE区别 快照版本SNAPSHOT和发布版本RELEASE区别-CSDN博客 在使⽤maven过程中&#xff0c;我们在开发阶段经常性的会有很多公共库处于不稳定状态&#xff0c;随时需要修改并发布&#xff0c;可能⼀天就要发布⼀次&#xff0c;遇到bug时&am…

网络编程(三)UDP TFTP协议

文章目录 一、 UDP&#xff08;一&#xff09;概述&#xff08;二&#xff09;流程 二、收发函数&#xff08;一&#xff09;recvfrom&#xff08;二&#xff09;sendto 三、实现一个简单的udp服务端和客户端四、实现tftp客户端协议 一、 UDP &#xff08;一&#xff09;概述 …

vue 中多个表单元素控一个校验规则

1. 场景一 <el-form-itemlabel"确认时长方式"prop"preSubResourceDurationDay" ><div class"confirmDurationDay">最晚使用日期前<el-input-numberv-model"form.preSubResourceDurationDay":precision"0"cla…

为什么需要负样本

假如我们只有正样本&#xff0c;模型在最开始训练的时候都是错误的&#xff0c;随着模型的迭代&#xff0c;准确率逐渐从0到1&#xff0c;最终将所有的样本都判别成正样本&#xff0c;也就是都在线的上方。 但真实的场景中有正有负&#xff0c;例如我们要做一个猫狗分类器&…

jsp 实验20

三、源代码以及执行结果截图&#xff1a; NewFile.jsp <% page import "java.io.*" %> <% page contentType"text/html" %> <% page pageEncoding "utf-8" %> <jsp:useBean id"english" class "web.Engli…