文章目录
前言
本人在阅读C++ primer plus(第六版)的过程中做的笔记,写这篇文章既是为了分享,也是为了以后查阅。以下是本篇文章正式内容。
一、派生
1.派生一个类
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);
};
冒号指出RatedPlayer类的基类是TableTennisPlayer,public表明TableTennisPlayer类是一个公有基类,这是一个公有派生。派生类对象包含基类对象,基类的公有成员成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但是只能通过基类的公有方法和保护方法访问。
派生类对象存储了基类的数据成员,派生类对象可以使用基类的方法。
2.构造函数
派生类不能直接访问基类的私有成员,必须通过基类方法进行访问,所以派生类构造函数必须使用基类构造函数。
创建派生类对象时程序将首先创建基类对象,也就是说基类对象应当在程序进入派生类构造函数之前被创建,为此,可以使用成员列表初始化语法:
RatedPlayer::RatedPlayer(unsigned int r, const string &fn,
const string &ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
其中,TableTennisPlayer(fn, ln, ht)是成员初始化列表,它是可执行的代码,调用TableTennisPlayer类的构造函数。
如果省略初始化成员列表,程序将使用默认构造函数,也就是说下面两种派生类的构造函数是等价的:
RatedPlayer::RatedPlayer(unsigned int r, const string &fn,
const string &ln, bool ht);
{
rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const string &fn,
const string &ln, bool ht) : TableTennisPlayer()
{
rating = r;
}
第二个构造函数:
RatedPlayer(unsigned int r, const TableTennisPlayer &tp)
: TableTennisPlayer(tp)
{
rating = r;
}
其中TableTennisPlayer(tp)将调用基类的复制构造函数,如果基类没有定义复制构造函数,编译器将自动生成一个。
释放对象的顺序与创建对象的顺序相反,即先调用派生类的析构函数再调用基类的析构函数。
3.基类和派生类的特殊关系
派生类对象可以调用基类的公有方法。基类指针(或引用)可以在不进行显示强制类型转换的情况下指向(或引用)派生类对象:
RatedPlayer rplayer1(1140, “Mallory”, “Duck”, true);
TableTennisPlayer *pt = &rplayer1;
TableTennisPlayer &rt = rplayer1;
pt->Name();
rt.Name();
但是基类的指针和引用只能调用基类方法,因为它们是通过基类声明的,它们的类型是基类类型。反过来,不能将基类对象和地址赋给派生类引用和指针:
TableTennisPlayer player(“Betsy”, “Bloop”, true);
RatedPlayer *pr = &player;
RatedPlayer &rr = player;
这样的话基类对象似乎可以调用派生类的方法,但这样是没有意义的,因为基类没有派生类新增的数据成员。
基类对象可以初始化为派生类对象:
RatedPlayer olaf1(1840, “Olaf”, “Loaf”, true);
TableTennisPlayer olaf2(olaf1);
匹配的构造函数原型如下:
TableTennisPlayer(const RatedPlayer &);
类中没有这样的构造函数,但有隐示复制构造函数:
TableTennisPlayer(const TableTennisPlayer &);
也就是说将基类对象初始化为派生类对象时将调用隐示复制构造函数。同样,也可以将派生类对象赋给基类对象:
RatedPlayer olaf1(1840, “Olaf”, “Loaf”, true);
TableTennisPlayer winner;
winner = olaf1;
这将调用隐示重载赋值运算符:
TableTennisPlayer & operator=(const TableTennisPlayer &);
二、多态公有继承
同一个方法在基类和派生类中的行为不同,这叫做多态继承。有两种机制可以实现多态公有继承:
♦在派生类中重新定义基类的方法;
♦使用虚方法。
有两个类,Brass类是基类,BrassPlus类是派生类。这两个类都声明了Withdraw()和ViewAcct()方法,但是Brass对象和BrassPlus对象的这些方法的行为是不同的。Brass类在声明Withdraw()和ViewAcct()方法的时候使用了关键字virtual,这些方法被称为虚方法。Brass类还声明了一个虚析构函数。
class Brass
{
……
public:
……
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
virtual ~Brass() {}
};
class BrassPlus : public Brass
{
……
public:
……
virtual void Withdraw(double amt);
virtual void ViewAcct() const;
};
第一,两个ViewAcct()原型表明有两个独立的方法定义,基类版本的限定名为Brass::ViewAcct(),派生类的限定名为BrassPlus::ViewAcct(),如果通过对象来调用,程序根据对象类型来确定使用哪个方法:
Brass dom(“Dominic Banker”, 11224, 4183.45);
BrassPlus dot(“Dorothy Banker”, 12118, 2592.00);
dom.ViewAcct(); //使用Brass::ViewAcct()
dot.ViewAcct(); //使用BrassPlus::ViewAcct()
第二,如果不是通过对象来调用,而是通过引用或指针的方式,将根据方法前面是否有关键字virtual来确定调用哪个方法。如果没有使用关键字virtual,程序将根据引用或指针的类型选择方法:
Brass dom(“Dominic Banker”, 11224, 4183.45);
BrassPlus dot(“Dorothy Banker”, 12118, 2592.00);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct(); //使用Brass::ViewAcct()
b2_ref.ViewAcct(); //使用Brass::ViewAcct()
如果使用了关键字virtual,程序将根据引用或指针指向的对象的类型选择方法:
Brass dom(“Dominic Banker”, 11224, 4183.45);
BrassPlus dot(“Dorothy Banker”, 12118, 2592.00);
Brass &b1_ref = dom;
Brass &b2_ref = dot;
b1_ref.ViewAcct(); //使用Brass::ViewAcct()
b2_ref.ViewAcct(); //使用BrassPlus::ViewAcct()
派生类构造函数的实现需要用成员初始化列表语法调用基类构造函数完成,然而非构造函数不能使用成员初始化列表,但是可以调用公有的基类方法来访问基类私有数据成员:
void BrassPlus::ViewAcct() const
{
……
Brass::ViewAcct();
cout << “Maximum loan: $” << maxloan << endl;
cout << “Owed to bank: $” << owesBank << endl;
cout << “Loan rate: ” << 100 * rete << endl;
}
在派生类中调用基类方法必须使用作用域解析运算符,否则程序不知道调用的是基类的方法还是派生类的方法。
注:设置cout格式(参考函数探幽一章)
typedef std::ios_base::fmtflags format; //存储格式信息
typedef std::streamsize precis; //存储小数点信息
format setFormat();
void restore(format f, precis p);
……
format setFormat()
{
return cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
}
void restore(format f, precis p)
{
cout.setf(f);
cout.precision(p);
}
……
int main()
{
//将原来的格式信息返回到initial保存然后修改格式
format initial = setFormat();
//将原来小数点信息(显示多少位)返回到prec保存然后修改为显示2位
precis prec = cout.precision(2);
……
//重置为原来的格式
restore(initial, prec);
}
1.演示虚方法的行为
假设要同时管理Brass和BrassPlus账户,可以用同一个数组来保存Brass和BrassPlus对象,方法是创建一个指向Brass的指针数组,因为Brass指针既可以指向Brass对象,也可以指向BrassPlus对象。
Brass *p_clients[4];
string temp;
long tempnum;
double tempbal;
char kind;
for (int i = 0; i < 4; i++)
{
cout << “名字:”;
getline(cin, temp);
cout << “账户:”;
cin >> tempnum;
cout << “余额:”;
cin >> tempbal;
cout << “输入1表示Brass账户,输入2表示BrassPlus账户:”;
while (cin >> kind && kind != ‘1’ && kind != ‘2’)
cout << “输入1或2:”;
if (kind == ‘1’)
p_clients[i] = new Brass(temp, tempnum, tempbal);
else
{
double tmax, trate;
cout << “输入透支额度:”;
cin >> tmax;
cout << “输入利率:”;
cin >> trate;
p_clients[i] = new Brass(temp, tempnum, tempbal, tmax, trate);
}
}
for (int i = 0; i < 4; i++)
{
p_clients[i]->ViewAcct();
}
for (int i = 0; i < 4; i++)
{
delete p_clients[i];
}
2.为何需要虚析构函数
在上述代码中,使用delete释放由new分配的对象说明了为什么基类需要一个虚析构函数(将基类的析构函数声明为虚的,派生类会自动生成自己的析构函数)。有了虚析构函数,可以自动调用指针所指向对象类型的析构函数。如果析构函数不是虚的,将只调用对应指针类型的析构函数,在上述代码中,这意味着只有Brass对象的析构函数被调用,尽管数组中有BrassPlus对象;如果析构函数是虚的,将调用指针所指向类型的析构函数,如果指针指向的是Brass对象,则调用Brass的析构函数,如果指向的是BrassPlus对象,将调用BrassPlus的析构函数,然后自动调用基类析构函数。
三、有关虚函数需要注意的事项
第一,如果重新定义继承的方法,应确保与原来的原型相同,但如果返回类型是基类的引用或指针,则可以修改为派生类的引用或指针。
class Dwelling
{
public:
virtual Dwelling & build(int n);
……
};
class Hovel : public Dwelling
{
public:
virtual Hovel &build(int n);
……
};
需要注意的是,这种特例只适用于返回类型,而不适用于参数。
第二,如果基类声明的函数被重载,则在派生类中需要重新定义所有的函数版本。
class Dwelling
{
public:
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;
……
};
class Hovel : public Dwelling
{
public:
virtual void showperks(int a) const;
virtual void showperks(double x) const;
virtual void showperks() const;
……
};
需要注意的是,如果只重新定义了一个版本,则其余的版本将会隐藏,派生类对象将无法使用它们。
四、访问控制:protected
protectd与private类似,在类外只能通过公有成员函数访问protected成员。protected与private的区别在基类和派生类之间体现,派生类可以直接访问基类的protected成员,但不能直接访问private成员。对于外部世界来说,保护成员的行为与私有成员相似;对于派生类来说,保护成员的行为与公有成员相似。
假如Brass类将balance声明为保护的:
class Brass
{
protected:
double balance;
……
}
BrassPlus类可以直接访问balance,而不用使用Brass方法:
void BrassPlus::Withdraw(double amt)
{
if (amt < 0)
cout << “withdraw amount must be positive;”
<< “ withdraw canceled.\n”;
else if (amt <= balance)
balance -= amt;
else if (amt <= balance + maxLoan - owesBank)
{
double advance = amt – balance;
owesBank += advance * (1.0 + rate);
cout << “Bank advance:$” << advance << endl;
cout << “Finance charge:$” << advance * rate << endl;
Deposit(advance);
balance -= amt;
}
else
cout << “Credit limit exceeded. Transaction canceled.\n”;
}
下图是继承体系中的访问控制说明符:
五、抽象基类
假设有两个类,它们有共同的特点,但不是一个类包含在另一个类中(它们也有相互区别的地方),从一个类派生出另一个类并不合适,而是应该抽象出这两个类的共同点作为基类,便可以使用基类指针数组同时管理这两个类。
下面是由Ellipse(椭圆)类和Circle(圆)类抽象出的BaseEllipse类:
class BaseEllipse
{
private:
double x;
double y;
……
public:
BaseEllipse(int x0 = 0, int y0 = 0) : x(x0), y(y0) {}
virtual ~BaseEllipse() {}
void Move(int nx, int ny) {x = nx; y = ny;}
virtual double Area() const = 0; //纯虚函数
}
类中没有实现Area()方法,因为它没有包含必要的数据成员。C++通过使用纯虚函数提供未实现的函数,但C++允许纯虚函数有定义。纯虚函数声明的结尾处为=0.当类中包含纯虚函数时,就不能创建该类的对象,因为包含纯虚函数的类是基类。要成为真正的抽象基类,就必须包含至少一个纯虚函数。原型中的=0使虚函数成为纯虚函数。例如,即使基类中的方法都像Move()一样具有定义,但还是需要将至少一个函数声明为虚的:
void Move(int nx, int ny) = 0;
然后可以在实现文件中进行函数定义:
void BaseEllipse::Move(int nx, int ny) {x = nx; y = ny;}
六、继承和动态内存分配
如果基类使用动态内存分配,并重新定义复制构造函数和重载赋值运算符,那么派生类该如何实现?
1.派生类不使用new
假设基类使用了动态内存分配:
class BaseDMA
{
private:
char *label;
int rating;
public:
BaseDMA(const char *l = “null”, int r = 0);
BaseDMA(const BaseDMA &rs);
virtual ~BaseDMA();
BaseDMA & operator=(const BaseDMA &rs)
……
}
当类声明中的数据成员需要使用new来动态分配内存时,则该类需要包含析构函数、复制构造函数和重载赋值运算符。
而派生类不使用new为数据成员动态分配内存:
class LacksDMA : public BaseDMA
{
private:
char color[40];
public:
……
}
那么派生类是否需要定义自己的析构函数、复制构造函数和重载赋值运算符呢?不需要。当派生类对象执行到需要使用析构函数、复制构造函数和赋值运算符函数时,对象的LacksDMA部分完全可以使用默认的方法,而对象的BaseDMA部分会自动调用BaseDMA的析构函数、复制构造函数和重载赋值运算符函数。
2.派生类使用new
假设派生类使用new:
class HasDMA : public BaseDMA
{
private:
char *style;
public:
……
}
这时需要定义析构函数、复制构造函数和重载赋值运算符。
基类的析构函数负责清除基类使用new来分配内存的数据成员的空间,派生类则需要定义自己的析构函数来清除新增的使用new分配内存的数据成员的空间:
BaseDMA::~BaseDMA()
{
delete[] label;
}
HasDMA::~HasDMA()
{
delete[] style;
}
基类的复制构造函数只能复制基类的数据成员,所以派生类需要定义自己的复制构造函数,而且需要调用基类的复制构造函数,这样基类的复制构造函数会处理基类部分的数据成员:
BaseDMA::BaseDMA(const BaseDMA &rs)
{
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}
HasDMA::HasDMA(const HasDMA &hs) : BaseDMA(hs)
{
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
}
这里通过成员列表初始化的方式调用基类的复制构造函数,虽然基类复制构造函数的参数类型是const BaseDMA &而不是const HasDMA &,但不要忘记基类的引用可以指向派生类。
基类的赋值运算符只能处理基类的数据成员,派生类数据成员使用了动态内存分配,也需要定义自己的赋值运算符:
BaseDMA & BaseDMA::operator=(const BaseDMA &rs)
{
if (this == &rs)
return *this;
delete[] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}
HasDMA & HasDMA::operator=(const HasDMA &hs)
{
if (this == &hs)
return *this;
BaseDMA::operator=(hs);
delete[] style;
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
调用基类的赋值运算符必须以下面这种形式:
BaseDMA::operator=(hs);
而不能这样:*this = hs;
因为这样会发生递归调用。
3.派生类如何使用基类的友元
基类中有这样一个友元:
friend ostream & operator<<(ostream &os, const BaseDMA &rs);
ostream & operator<<(ostream &os, const BaseDMA &rs)
{
os << “Label: ” << rs.label << endl;
os << “Rating: ” << rs.rating << endl;
return os;
}
在派生类中也重载了<<运算符,但是需要调用基类重载<<运算符函数:
friend ostream & operator<<(ostream &os, const HasDMA &hs)
ostream & operator<<(ostream &os, const HasDMA &hs)
{
os << (const BaseDMA &)hs;
os << “Style: ” << os.style << endl;
return os;
}
因为是基类的友元函数,所以派生类不能通过作用域解析运算符来访问,而是通过强制类型转换,以便匹配原型时能够选择正确的函数。
总结
以上就是本文的内容——本文记录了C++中的类继承,包括派生、多态公有继承、虚函数、访问控制说明符、抽象基类、继承和动态内存分配。