简单派生
继承是组织类的一种特殊方式,所有面向对象的语言都支持这种方式,它使得类能够以多种不同的方式共享代码,并且可以揭示类之间的自然关系。它也可以使设计良好的类更具有可复用性。
为了使用继承,需要将一组相关类的共同性质放置到一个基类中,然后由它派生出其它更专门化的类。每一个派生类都继承于基类的所有成员,当然也可以根据需要重写或扩展基类中的每一个函数。从一个共同的基类继承各种成员大大简化了派生类,利用某种设计模式,还可以去除冗余代码。事实上,当消除一组相关类中重复的代码块时,就应当推荐使用继承。
重构是一个提高软件设计的过程,其中的一个步骤就是将相似的代码块转换成对可复用函数的调用。
派生类中的两个函数重写了基类中对应的函数。这种派生类函数必须与它所重写的基类函数具有相同的签名 (由函数名和形参类表构成,暂不考虑const重载) 和返回类型。
基类的成员初始化
由于每一个Undergrad都为Student,所以只要创建一个Undergrad对象,就必须同时创建并初始化一个Student对象,此外,还必须要调用Student构造函数来初始化,继承于基类的私有成员变量。
在构造函数的成员初始化列表中,可以将基类名当成一个隐式的派生类成员。它在派生类成员初始化之前首先进行初始化,寻找合适的初始化函数,如果找不到,则报错。
分析一下Undergrad构造函数的初始化列表。参数表中包含的Student数据成员的值为private类型,所以Undergrad成员函数无法对它们进行赋值。进行赋值的唯一途径是将这些值传递给Student构造函数,它不是private类型。
扩展
在toString()
的两个派生类版本中处理派生类属性之前,都显式地调用了Student::toString()
,它用于处理(private类型的)基类属性。toString()
的每一个派生类版本都扩展了Student::toString()
的功能。
有必要再次强调的是,由于Student类的多数数据成员都是private类型的,需要一个非private类型的基类成员函数来使派生类能够访问基类中的private成员(如:toString())。
在main函数中调用finish函数时,理想中传入不同的类型 (不论是基类或派生类) ,会调用类型中独有的toString函数。而实际输出的结果却没有。那是因为:
编译器无法仅凭继承关系和基类指针来确定它正在操作何种对象。如果没有运行时检查,就无法保证运行时调用正确的函数。C++要求使用一个特殊的关键字来允许运行时通过指针和引用进行函数调用的绑定。这个关键字就是virtual
,它能使程序具有多态性。
以下为实例源码
#include <QTextStream>
#include "Student.h"
Student::Student(QString name, long id, QString major, int year)
:m_Name(name),m_Major(major),m_StudentId(id),m_Year(year){ }
Student::~Student()
{
qDebug("Bay Student");
}
QString Student::getClassName() const {
return "Student";
}
QString Student::toString() const {
QString retval;
QTextStream os(&retval); //使用流来添加多个字符串,更加可观。
os << "[" << getClassName() << "]"
<< "name:" << m_Name
<<", Id:" << m_StudentId
<< ",Year:" << m_Year
<< ",Majir:" << m_Major;
return retval;
}
---------------------------------------------------------------------
Undergrad::Undergrad(QString name, long id, QString major, int year, int sat)
:Student(name,id,major,year),m_SAT(sat) {}//基类对象被当成派生对象的子对象。
//类成员和基类都必须被初始化,初始化的顺序由它们在类定义中出现的顺序决定。
QString Undergrad::getClassName() const {
return "Undergrad";
}
QString Undergrad::toString() const
{
QString result;
QTextStream os(&result);
os << Student::toString()//调用基类版本
<<"\n [Sat: "
<< m_SAT//添加Undergrad 特有的项
<<"]";
return result;
}
---------------------------------------------------------------------
GradStudent::GradStudent(QString nm, long id, QString major, int yr, Support support)
:Student(nm,id,major,yr),m_Support(support){}
QString GradStudent::toString() const {
return QString("%1 %2 %3]")
.arg(Student::toString())
.arg("\n [Support: ")
.arg(supportStr(m_Support));
}
QString GradStudent::supportStr(Support sup){
switch (sup) {
case ra :
return "ra";
break;
case ta :
return "ra";
break;
case fellowship :
return "fellowship";
break;
case other :
return "other";
break;
default:
break;
}
return "";
}
void finish(Student* student) {
qDebug() << "\n The following "
<< student->getClassName()
<<" has applied for graduation. \n"
<< student->toString() << "\n";
}
int main()
{
Student s("Tom",12345,"Algorithm",2);
qDebug() << s.toString();
Undergrad un("Ben",23456,"Algorithm",2,5);
qDebug() << un.toString();
GradStudent gr("Ben",23456,"Algorithm",2,GradStudent::ra);
qDebug() << gr.toString();
finish(&s);
finish(&un);
finish(&gr);
}
具有多态性的派生
在基类的函数之前加上virtual
的关键字,那么在调用toString函数时,就会自己定义的toString函数,而不是调用基类的toString函数.
利用多态,解决了运行时方法的间接调用(通过指针或引用),这被称为动态绑定或者运行时绑定。针对方法的直接调用(不通过指针或引用)仍然是由编译器解析的,这被称为静态绑定或者编译时绑定。
注意:
- 构造函数或者析构函数中不存在virtual方法的this调用。
- 一般而言,如果类中包含一个或多个virtual函数,则也应该包含一个虚析构函数。这是因为当对多态集合进行操作时,通常是通过基类指针删除这些对象,这会导致对析构函数的简介调用。如果析构函数不为virtual类型,则编译时绑定将决定应该调用哪一个析构函数,从而可能导致派生对象的不完整析构。
对于基类和派生类之间的赋值:
如果是派生类为基类赋值的话,然后基类调用的会是自己的方法。具体原因不明,因为基类没有派生类中的方法或者在数值传递过程中发生了丢失,可能同将float转换为int一样。
如果是基类赋值与派生类则报错。
抽象基类的派生
抽象基类用于封装具体派生类的共同特性。尽管不能实例化抽象类,但当整理不断积累的庞大而复杂的生物世界的现象时,这种机制是非常实用和高效的。
具体类代表某一类特定的实体,它们是真实存在的东西,是可以被实例化的。较为普通的物种类别(纲、目、科、子科)都是在现实世界中无法实例化的抽象基类。人们使用这些概念有助于分类和组织具体类(物种)。
回到编程
乍看起来,定义一个没有具体实现的抽象类似乎有违常理,但类不仅仅是函数和数据的分组,更多是用来实现某种方式的组织和复用的有效工具。将事物分类,可以使人和计算机更加简单地管理世界。
当研究设计模式时,开发框架和类库时,经常是设计继承树,其中只有叶节点才能够被实例化,而所有的内部节点都是抽象化。
抽象基类是无法或者不适合实例化的类,编写此种类是,至少具有一个纯virtual函数,即没有被实现的virtual函数。
任何继承于抽象基类的派生类都需要重写基类中全部的纯虚函数,即进行实例化基类。
来点源码:
class Shape {
public:
virtual double area() = 0;
virtual QString getName() = 0;
virtual ~Shape() {}
};
class Rectangle : public Shape {
public :
Rectangle(double h, double w)
: m_Height(h),m_Width(w) {}
double area();
QString getName();
~Rectangle() {cout << "Bye Rectangle"<< endl;}
private:
double m_Height, m_Width;
};
double Rectangle::area()
{
return m_Height*m_Width;
}
QString Rectangle::getName()
{
return QString("rectangle");
}
class Square : public Rectangle {
public :
Square (double h)
:Rectangle(h,h) {}
double area();
QString getName();
};
double Square::area()
{
return Rectangle::area();
}
QString Square::getName()
{
return QString("square");
}
void showNameAndArea(Shape* pshp)
{
qDebug() << "Name:" <<pshp->getName()
<< "Area:" << pshp->area();
}
int main()
{
// Shape s();
Rectangle re(3,4);
Square sq(3);
cout <<re.area() << endl;
qDebug() << re.getName();
showNameAndArea(&re);
showNameAndArea(&sq);
}
继承设计
有时引入继承关系之后,起初会对问题有所帮助(例如:减少冗余代码),但到最后必须将其它类添加到继承层次中时会引起一些问题。预先的分析有助于解决问题,从而避免后续的麻烦。
通过上面的抽象Shape类来说,还有各种各样的图形需要Shape作为基类
- 比如说,一个菱形,那么菱形是否可以作为矩形的基类呢?
- 是否需要为全部的四边形构造一个基类?
- 是否应为所有的多边形构造一个基类呢?
继承设计,要设计好一个继承树,需要考虑的方面很多
重载、隐藏和重写
重载:当一个函数foo在同一个作用域内存在两个或多个不同签名的版本时,就称foo函数被重载
重写: 当基类中一个virtual函数在派生类中也存在,并且拥有相同的签名和返回类型时,就称派生类重写了基类中的函数。
隐藏:派生类中的成员函数,会隐藏基类中与之同名的全部函数。如果出现这种情况,则:
- 只有派生类同名函数可以被直接调用
- 类作用域解析运算符
::
可以用来显示的调用基类函数。
class Rectangle : public Shape {
public :
Rectangle(double h, double w)
: m_Height(h),m_Width(w) {}
double area();
QString getName();
void foo() {
qDebug("Rectangle_foo()");
}
void foo(int a){
qDebug("Rectangle_foo(int a)");
} //重载
~Rectangle() {
cout << "Bye Rectangle"<< endl;
}
private:
double m_Height, m_Width;
};
class Square : public Rectangle {
public :
Square (double h)
:Rectangle(h,h) {}
double area();
void foo(float f) {
qDebug("Square_foo(float f)");
} //隐藏,基类中的两个float函数都将无法调用
QString getName();
};
int main()
{
Rectangle re(3,4);
Square sq(3);
sq.foo(1.1);
sq.Rectangle::foo();//通过类名作用域运算符调用
}
构造函数,析构函数与复制赋值运算符
有三种特殊的成员函数不会被继承:
- 复制构造函数
- 析构函数
- 赋值运算符函数
编译器会为没有定义它们的类自动生成这三种函数。
构造函数
任何派生类的构造函数都必须在初始化表中明确指定调用哪一个基类的构造函数。
初始化过程按照下面的顺序进行:
- 首先基类初始化,按照它们在派生类中出现的顺序依次进行。
- 数据成员初始化,按照声明的顺序进行。
赋值运算符函数
如果类没有明确定义拷贝赋值运算符,那么编译器会自动为它产生一个public版本的赋值运算符函数。由于基类数据成员通常为private类型,所以派生类赋值运算符函数必须(为每一个基类)调用基类的赋值运算符函数,以对继承下来的数据成员进行逐一赋值。随后,才可以为派生类数据成员进行逐成员的赋值。
拷贝构造函数
如同拷贝赋值运算符一样,如果类没有定义拷贝构造函数,编译器也会自动为它产生一个public类型的拷贝构造函数。编译器产生的拷贝构造函数通过拷贝它的实参对象的数据成员,对基类对象的数据成员进行初始化。
析构函数
析构函数不会被继承。如同拷贝构造函数和拷贝赋值运算符一样,如果没有显式地为某个类定义析构函数,编译器就会为它定义一个析构函数。当销毁派生的对象时,会自动调用基类的析构函数。数据成员和基类部分的销毁过程将按照与初始化过程相反的顺序进行。
事例参考:
class Account {
public:
Account(unsigned acctNum, double balance, QString owner)
:m_AcctNum(acctNum),m_Balance(balance),m_Owner(owner) {}
virtual ~Account() {
qDebug() << "Closing Acct"
<< m_Owner;
}
virtual QString getName() const {return m_Owner;}
private:
unsigned m_AcctNum;
double m_Balance;
QString m_Owner;
};
class JointAccout : public Account {
public :
JointAccout(unsigned acctNum, double balance, QString owner,QString jowner)
:Account(acctNum,balance,owner),m_Jowner(jowner){}
JointAccout(const Account& acct, QString jowner)
:Account(acct),m_Jowner(jowner){}
~JointAccout() {
qDebug() << "Closing joint Acct "
<<"" << m_Jowner;
}
QString getName() const {
return QString("%1 and %2").arg(Account::getName()).arg(m_Jowner) ;
}
private:
QString m_Jowner;
};
class Bank {
public:
Bank& operator<< (Account* acct){
m_Accounts << acct;
return *this;
}
~Bank() {
qDeleteAll(m_Accounts);
m_Accounts.clear();
}
QString getAcctListing() const {
QString listing("\n");
foreach (Account* acc, m_Accounts) {
listing += QString("%1\n").arg(acc->getName());
}
return listing;
}
void display() const {
foreach (Account* acc, m_Accounts) {
qDebug() << acc->getName();
}
}
private:
QList<Account*> m_Accounts;
};
int main(){
QString listing;
{
Bank bk;
Account* a1 = new Account(1,424,"one");
JointAccout* a2 = new JointAccout(2,23,"two","Tom");
JointAccout* a3 = new JointAccout(*a1, "Jerry");
bk << a1;
bk << a2;
bk << a3;
JointAccout* a4 = new JointAccout(*a3);
listing = bk.getAcctListing();
bk.display();
}
}