(1)基类
C++中,可以由一个类派生出另一个类,原始的类叫做基类,派生出的类叫做派生类。
webtown 俱乐部决定跟踪乒乓球会的会员。 于是设计一个简单的TableTennisPlayer类作为基类。
class TableTennisPlayer //类名
{
private:
string firstname; //名字都是字符串形式
string lastname;
bool hasTable; //会员判断标志
public:
TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false); //默认构造函数
TableTennisPlayer(const char* fn, const char* ln, bool ht); //普通构造函数
void Name()const;
bool HasTable()const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
下面是函数的实现细节
#include"tabtenn0.h"
#include
#include
//#pragma warning(disable:4996)
#pragma warning(disable:4996)
TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht):firstname(fn),lastname(ln),hasTable(ht){} //参数是const string的构造函数
void TableTennisPlayer::Name()const
{
std::cout << this->lastname.c_str() << ", " << this->firstname.c_str();
}
下面是主函数
#include"tabtenn0.h"
#include
int main(void)
{
using std::cout;
TableTennisPlayer player1("Chuck", "Blizzard", true); //这句话直接调用复制构造函数
TableTennisPlayer player2("Tare", "Boomdea", false);
player1.Name();
if (player1.HasTable())
cout << ":has a table\n";
else
cout << ": hasn't a table\n";
player2.Name();
if (player2.HasTable())
cout << ":has a table\n";
else
cout << ": hasn't a table\n";
return 0;
}
注意到该程序实例化对象时将C风格字符串作为参数:
TableTennisPlayer player(“Chuck”, “Blizzard” , true);
TableTennisPlayer player(“Tara”, “Boomdea” , false);
但构造函数的形参类型被声明为const string &。这导致类型,但是string类有一个将const char *作为参数的构造函数,使用C风格字符串的时候,将自动调用这个构造函数。也就是说,无论参数是const string&还是const char *都有相应的构造函数。
再一个,成员初始化列表直接调用string的复制构造函数。如果不使用初始化列表,那么将会先调用string的默认构造函数,再调用string的赋值运算符。
(2)派生类
webtnow俱乐部的一些成员曾经参加过当地的乒乓球竞标赛,需要这样一个类,它能包括成员在比赛中的得分。由此从TableTennisPlayer类中派生出一个类。我们叫他RatedPlayer类。
声明RatedPlayer类从TableTennisPlayer类派生来:
class RatedPlayer:public TableTennisPlayer{
private:
unsigned int rating; //派生类新增的成员
public:
RatedPlayer(unsigned int r=0,const string &fn="none",const string &ln="none", bool ht=false); //派生类自己的构造函数,很明显仍然需要对基类成员赋值
RatedPlayer(unsigned int r,const TableTennisPlayer &tp); //派生类的构造函数
unsigned int Rating()const {return rating;}
void ResetRating(unsinged int r) {rating=r;}
};
派生类不能直接访问基类的私有成员,需要通过基类的方法进行访问。
举个例子:儿子可以进爸爸的房间,但是不能动爸爸的保险柜,只有爸爸才能开保险柜。但是儿子可以通过爸爸去开保险柜。所以实际上儿子是不能开保险柜的,根本上还是爸爸开的。即使结果都是开了保险柜。
派生类构造函数必须使用基类构造函数,创建派生类对象的时候,首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。那么就需要使用成员初始化列表语法来完成这个工作。
RatedPlayer::RatedPlayer(unsigned int r,const string &fn,const string &ln, bool ht):TableTennisPlayer(ln,fn,ht)
{
rating=r;
}
如果没有显示使用基类构造函数,将会调用默认基类构造函数,像下面这样:
RatedPlayer::RatedPlayer(unsigned int r,const string &fn,const string &ln, bool ht):TableTennisPlayer()
{
rating=r;
}
除此之外,还可以这样使用构造函数
RatedPlayer::RatedPlayer(unsigned int r,const TableTennisPlayer &tp):TableTennisPlayer(tp)
{
Rating=r;
}
由于tp是引用,所以将调用基类的复制构造函数。
有关派生类构造函数的要点如下:
要使用派生类,程序必须要能够访问基类声明。
由于两个类相关联,所以将他们的类声明放在一起
#ifndef TABTENN0_H_
#define TABTENN0_H_
#include
using std::string; //使用名称空间std里面声明的string
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string& fn = "none", const string& ln = "none", bool ht = false); //默认构造函数
TableTennisPlayer(const char* fn, const char* ln, bool ht);
void Name()const;
bool HasTable()const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
class RatedPlayer :public TableTennisPlayer
{
private:
unsigned int rating;
public:
RatedPlayer(unsigned int r = 0, const string& fn = "none", const string& ln = "none", bool ht = false); //默认构造函数
RatedPlayer(unsigned int r, const TableTennisPlayer& tp);
unsigned int Rating()const { return rating; }
void ResetRating(unsigned int r) { rating = r; }
};
#endif // ! TABTENN)_H_
具体实现方法:
#include"标头.h"
#include
#include
//#pragma warning(disable:4996)
#pragma warning(disable:4996)
TableTennisPlayer::TableTennisPlayer(const string &fn,const string &ln,bool ht):firstname(fn),lastname(ln),hasTable(ht){} //参数是const string的构造函数
TableTennisPlayer::TableTennisPlayer(const char* fn, const char* ln, bool ht):hasTable(ht) //参数是const char*的构造函数
{
firstname = fn;
lastname = ln;
}
void TableTennisPlayer::Name()const
{
std::cout << this->lastname.c_str() << ", " << this->firstname.c_str();
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp) :TableTennisPlayer(tp), rating(r)
{
}
RatedPlayer::RatedPlayer(unsigned int r, const string& fn, const string& ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
注意这两个类对象如何使用Table类的Name()和Hastable()方法的。
#include"标头.h"
#include
int main(void)
{
using std::cout;
using std::endl;
TableTennisPlayer player1("Chuck", "Blizzard", true); //这句话直接调用复制构造函数
//TableTennisPlayer player2("Tare", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true); //直接调用复制构造函数
rplayer1.Name();
if (rplayer1.HasTable())
cout << ":has a table\n";
else
cout << ":hasn't a table\n";
player1.Name();
if (player1.HasTable())
cout << ":has a table\n";
else
cout << ":hasn't a table\n";
rplayer1.Name();
cout << "; Rating: " << rplayer1.Rating() << endl;
RatedPlayer rplayer2(1212, player1);
cout << "Name :";
rplayer2.Name();
cout << "; Rating: " << rplayer2.Rating() << endl;
return 0;
}
引用兼容性规则
这个规则能够让你将基类对象初始化为派生类对象。
C++有三种继承方式:公有继承,保护继承,私有继承。
公有继承是最常用的方式,他建立一种is-a关系。即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。
注意,is-a关系是不可逆的,比如,苹果是水果,但是水果不是苹果。
如果希望基类和派生类的同名函数实现不同的功能,就需要知道多态的概念。------即一个方法有多种实现。
有两种重要的机制可用于实现多态公有继承:
如果要使同名方法的实现不同,可以这样:
在基类定义的方法,在派生类又再次定义,这叫虚方法。方法在基类被声明为虚方法后,在派生类将自动成为虚方法。然而,在派生类声明中使用关键字virtual也不失为一种好方法。 注意,基类声明了一个虚析构函数,这样做是为了确保释放对象的时候,按照正确的顺序调用析构函数。
非构造函数不能使用成员初始化列表语法,但是派生类方法可以调用公有的基类方法。
虚析构函数可以确保正确的析构函数序列被调用。如果析构函数不是虚的,则将只调用对应于指针类型的析构函数(析构函数是公有成员,会被派生类继承)
首先知道什么是联编:
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在编译过程中进行联编被称为静态联编,又称为早期联编。在程序运行时联编叫做动态联编,也叫晚期联编。
在C中,这很简单,因为C中的函数名是唯一的。
但是C++不一样,因为C++可以重载函数,这使这项工作复杂一点。
指向基类的引用或指针可以引用派生类对象,而不必进行显示类型转换。将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显示类型转换。
将基类指针或者引用转换为派生类指针或者引用被称为向下强制转换。但是这两个转换有区别,如果不使用显示转换类型,则向下强制转换是不允许的。
隐式向上强制类型转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚函数来满足这种需求.
BrassPlus ophelia;
brass *bp;
bp=&ophelia;
bp->ViewAcct();
如果基类Brass(派生类是BrassPlus)没有将ViewAcct()声明为虚的,则bp->ViewAcct()将根据指针类型(Brass*)调用Brass::ViewAcct()。由于指针类型在编译的时候就是已知的,因此编译器在编译的时候就可以完成联编,将ViewAcct()转换为Brass::ViewAcct()。编译器对非虚方法使用静态联编。
相反,如果将基类成员函数ViewAcct()声明为虚的,则bp->ViewAcct()根据对象类型调用BrassPlus::ViewAcct()。很明显,只有在运行程序时才能确定对象的类型,所以编译器生成的代码在程序执行时,根据对象类型将ViewAcct()关联到Brass::ViewAcct()或BrassPlus::ViewAcct()。总之,编译器对虚方法使用动态联编。
编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这个数组叫虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。例如:基类对象包含一个指针,该指针指向基类中所有虚函数的地址表(这说明这个表概念上是一个连续的空间,一个指针指向这个空间的首地址)。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。如果派生类没有重新定义虚函数,则虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则将该函数的地址添加到表中。无论类中包含的虚函数是一个还是10个,都只需要在对象中添加一个指针,只是表的大小不一样而已。
使用虚函数时,在内存和执行速度方面有一定的成本
假设创建如下所示代码:
class Dwelling
{
public:
virtual void showperks(int a)const;
...
}
class Hovel:public Dwelling
{
public:
virtual void showperks()const;
...
}
如果这样使用,会出现问题:
Hovel trump;
trump.showperks() ; //可以
trump.showperks(2); //不可以
为什么会这样呢?
因为实际上trump对象的虚函数表将基类的同名函数隐藏了(该函数不在函数表中),各玩各的。重新定义继承的方法不是重载。如果重新定义派生类的函数,将覆盖所有基类同名的函数。
所以,为了不出现错误,我们应该规范使用:
class Dewlling
{
public:
virtual Dewlling &bulit(int n);
…
};
class Hovel:public Dewlling
{
public:
virtual Hovel&bulit(int n);
…
}
3.如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本。
class Dewlling
{
public:
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks()const;
}
class Hovel:public Dewlling
{
public:
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks()const;
}
如果只重新定义一个版本,则其它两个被隐藏,派生类对象将无法使用他们。注意,如果不需要修改,则新定义可以只调用基类版本:
void Hovel::showperks()const{Dewlling ::showperks();}
关键字protected与private 相似,在类外只能用公有类成员来访问protected中的成员。但是两者还是有区别的,只是这区别只有来基类派生的类中才会表现出来。