学习是一项持续的投资,永远不
会
白费——本杰明·富兰克林
第13章:类继承
一个基类和派生类
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。以下是一个简单的基类:
class Person
{
private:
string name;
public:
Person(){name="none";};
Person(string s)
{
name=s;
}
~Person(){};
void show()
{
cout<<name;
}
}
这是一个表示人的简约的类,接下来我们用这个人来派生一个学生类:
class Student:public Person
{
private:
int grade;
public:
Student():Person()
{
grade=0;
}
Student(string s,int g):Person(s)
{
grade=g;
}
void s_show()
{
show();
cout<<grade;
}
}
- 要使用类继承,在类声明中需要使用如下格式:
class 派生类:public/protected/private 基类
其中public表示公有继承,protected表示保护继承,private表示私有继承,在本章我们只考虑公有继承。
- 在继承中,派生类自动包含基类的所有属性和方法,但派生类中的方法不能直接访问基类的私有成员,而要通过基类提供的方法来访问,例如:
Student():Person()
{
grade=0;
}
这是Student类的默认构造函数。对于派生类和基类的逻辑关系,可以用下面这张图来表示:
所以当要构造一个Student类的时候,从概念上要先构造出其中的Person类,所以要在构造函数执行到函数体之前将Person类初始化,即必须选用列表初始化语法。而且因为派生类不能直接访问基类的私有属性,因此下面的代码是错误的:
Student():name("none")//不能直接访问name属性
{
grade=0;
}
- 使用时,可以用基类指针来指向派生类:
int main(void)
{
Person* p[2];
p[0]=Person();//指向一个Person类对象
p[1]=Student();//也可以指向一个Student类对象
}
注意,这一规则是单向的,也就是说不能把派生类指针指向基类,这是因为编译器看到Person类指针后,会认为这个指针指向的对象拥有Person类声明的方法和属性,下面的代码显然是错误的:
int main(void)
{
Student* s[2];
s[0]=Student();//可以通过编译
s[1]=Person();//不能通过编译
s[0]->s_show();//因为s[0]指向的是一个Student类对象,自然可以调用这个方法
s[1]->s_show();//但s[1]指向一个Person对象,因此显然是不行的
另外,如果用Person类指针指向了Student类对象,也只能用对象中从Person类继承下来的方法:
int main(void)
{
Person* p[2];
p[0]=Person();//指向一个Person类对象
p[1]=Student();//也可以指向一个Student类对象
p[1]->s_show();//错误,不能通过编译,即使指向一个Studnet类对象
}
对于形参为指向基类的指针的函数,也可以用派生类的地址作为实参,因此也可以用派生类对象来初始化基类对象(实际上是使用了复制构造函数),实际效果为将派生类对象中的基类部分拷贝到新的基类对象上。
公有继承的逻辑关系:is-a
当派生类is a基类时,适合使用公有继承,这就是公有继承的逻辑关系,例如在上面的例子中,Student is a Person.
多态公有继承
在弄清Person类和Student类的关系后,我们可以把这种关系扩展到其他职业:
同样的,因为teacher is a person,headmaster is a person,也可以使用公有继承。可以试着自己写一写代码,笔者的代码如下:
class Person
{
private:
string name;
public:
Person(){name="none";};
Person(string s)
{
name=s;
}
virtual ~Person(){};
virtual void show()
{
cout<<name;
}
}
class Student:public Person
{
private:
int grade;
public:
Student():Person()
{
grade=0;
}
Student(string s,int g):Person(s)
{
grade=g;
}
virtual void show()
{
Person::show();
cout<<grade;
}
}
class Teacher:public Person
{
private:
int number;//负责哪个班级的班号,简单即可
public:
Teacher():Person()
{
number=0;
}
Teacher(string s,int n):Person(s)
{
number=n;
}
virtual void show()
{
Person::show();
cout<<number;
}
}
class HeadTeacher:public Person
{
private:
int s_number;//负责哪个学校的代号
public:
HeadTeacher():Person()
{
number=0;
}
HeadTeacher(string s,int n):Person(s)
{
s_number=n;
}
virtual void show()
{
Person::show();
cout<<s_number;
}
}
这里需要注意的就是virtual关键字。被virtual关键字当要将基类的方法在派生类中重写时,可以使用这个关键字。当使用这个关键字时,如果一个对象由指针引用,那么这个对象调用函数时,所调用的函数由这个对象的实际类决定。另外,因为进行了函数重写,因此在调用基类的方法时需要使用::运算符。下面是这四个类的测试代码:
Person* p[4];
p[0]=Person();
p[1]=Student();
p[2]=Teacher();
p[3]=HeadTeacher();
p[0]->show();//调用Person类中的show函数
p[1]->show();//调用Student类中的show函数
p[2]->show();//调用Teacher类中的show函数
p[3]->show();//调用HeadTeacher类中的show函数
另外,如果将函数的声明和定义分开,那么virtual关键字只在类声明的时候加上即可。
用一个基类指针的数组指向不同派生类的对象,当执行同样的代码时,实际行为会随被执行的对象类型而变化,这就是多态。多态保证了代码的重用性,使得程序员不需要频繁修改测试代码,所以多态也是继封装和继承后OOP中第三个重要的思想。
最后,在Person类中的构造函数也被声明为虚函数,这是因为如果不这样声明,那么编译器会按照指针类型而非实际对象类型来析构数组p所指向的对象,这会导致内存泄漏,所以即使Person类析构函数不进行任何操作,也要声明为虚函数。
我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!如果觉得好的话,可以关注一下,我会在将来带来更多更全面的知识讲解!