栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++ Primer Plus (6th) Chap13 类继承 摘录

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++ Primer Plus (6th) Chap13 类继承 摘录

面向对象编程的主要目的之一是提供可重用的代码。开发新项目,尤其是当项目十分庞大时,重用经过测试的代码比重新编写代码要好很多。

必须考虑的细节越少,便越能专注于程序的整体策略。

类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。

C++提供了比修改代码更好的方法来扩展和修改类。这种方法叫做类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(称为基类)的特征,包括方法。

下面是可以通过继承完成的一些工作:

        可以在已有类的基础上添加功能;

        可以给类添加数据;

        可以修改类方法的行为;

继承机制只需要提供新特性,甚至不需要访问源代码就可以派生出类。而且可以在不公开实现的情况下将自己的类分发给其他人,同时允许它们在类中添加新特性。

13.1 一个简单的基类

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。

// tabtenn0.h -- a table-tennis base class
#ifndef TABTENN0_H_
#define TABTENN0_H_

#include
using std::string;
class TableTennisPlayer
{
private:
    string firname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer();
    void Name() const;
    bool HasTable() const {return hasTable;}
    void RestTable(bool v) {hasTable = v;}
};


#endif

// tabtenn0.cpp    -- simple base-class method
#include
#include"tabtenn0.h"

TableTennisPlay::TableTennisPlay(const string& fn, const string& ln, bool ht) : firstname(fn), lastname(ln), hastable(ht){}

void TableTennisPlay::Name() const
{
    std::cout << lastname << , << firstname;
}
13.1.1 派生一个类
class RatePlayer : public TableTennisPlayer
{
    ...
};

冒号指出RatePlayer类的基类是TableTennisPlayer类。public表示公有派生。使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有成员也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

派生类对象将具有以下特征:

        派生类对象存储了基类的数据成员(派生类继承了基类的实现);

        派生类对象可以使用基类的方法(派生类继承了基类的接口);

需要在继承特性中添加什么呢?

        派生类需要自己的构造函数;

        派生类可以根据需要添加额外的数据成员和成员函数;

// simple defrived class
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; }
};

构造函数必须给新成员(如果有的话)和继承的成员提供数据。

13.1.2 构造函数

派生类不能直接访问基类的私有成员,而必须通过基类方法访问。例如,RatedPlayer构造函数不能直接设置继承的成员(firstname, lastname和hasTable),而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数。

创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。

RatedPlayer(unsigned int r = 0; const string& fn = "none", 
            const string& ln = "none", bool ht = false) : TableTennisPlayer(fn, ln, ht)
{
    rating = r;
}

其中,:TableTennisPlayer(fn,ln,ht)是成员初始化列表。将参数列表中的实参值传递给基类构造函数。

必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认构造函数。

现在看第二个派生类构造函数,由于tp是基类引用,因此将调用基类的复制构造函数。

RatedPlayer(unsigned int r, const TableTennisPlayer& tp) :TableTennisPlayer(tp)
{
    rating = t;
}

// 如果愿意,也可以对派生类成员使用成员初始化列表语法。
RatedPlayer(unsigned int r, const TableTennisPlayer& tp) :TableTennisPlayer(tp), rating(r)
{}

有关派生类构造函数的要点如下:

        首先创建基类对象;

        派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;

        派生类构造函数应初始化派生类新增的数据成员;

释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

注意:创建派生类对象时,程序首先调用基类构造函数,然后在调用派生类构造函数。基类构造函数负责初始化继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化器列表语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。

除虚基类外,类只能将值传递回相邻的基类。

13.1.3 使用派生类

如果两个类是相关的,建议将其类声明放在一起。也建议将其定义放在一起。

13.1.4 派生类和基类之间的特殊关系

派生类和基类之间有一些特殊关系。其中之一是派生类对象可以使用基类方法,条件是方法不是私有的。另外一个是:基类指针或引用可以在不进行显式类型转换的情况下指向派生类对象。

// 派生类对象调用基类公有方法。
RatedPlayer rplayer1(1140, "MO", "Li", true);
rplayer1.Name();

// 基类指针或引用指向派生类
TableTennisPlayer& rt = rplayer1;
TableTennisPlayer* pt = &rplayer1;

// 只能调用基类方法
rt.Name();       // OK!
rt.Rating();     // Wrong!

然而,基类指针或引用只能用于调用基类方法。因此,不能使用rt来调用派生类的Rating方法。

通常,C++要求引用和指针类型与赋给的类型匹配。但这一规则对继承来说是例外,然而,这种例外只能是单向的,不可以将基类对象的地址付给派生类的引用或指针。

// 不能将基类对象的地址赋给派生类引用或指针
TableTennisPlayer rplayer1;
RatedPlayer& rr = rplayer1;    // Wrong!
RatedPlayer* rr = &rplayer1;    // Wrong!

原因在于如果允许将基类对象赋给派生类引用,因为派生类拥有基类没有的成员。

如果基类引用和指针可以指向派生类。其中一个有趣的结果是基类引用定义的函数或指针参数可用于基类对象或派生类对象。例如,在下面的函数:

void Show(const TableTennisPlayer& rt)
{
    ...
}

形参rt是一个基类引用,他可以指向基类对象或者派生类对象。

另外,也可以将派生类对象赋给基类对象,程序将使用隐式重载赋值运算符。但只讲派生类中继承的基类数据赋值给基类对象。派生类对象自己独有的数据没有赋给基类对象。

13.2 继承:is-a关系

C++有3中继承方式:公有继承、保护继承和私有继承。公有继承是最常用的方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。

继承可以在基类的基础上添加属性,但不能删除基类的属性。

在C++中,可以使用公有继承来建立has-a,is-implemented-as a或者uses-a关系,然而,这样做通常会导致编程方面的问题。因此,还是坚持使用is-a关系吧。

13.3 多态公有继承

希望同一个方法在派生类和基类中的行为是不同的。换句话说,方法的行为应取决于调用该方法的对象。这种较复杂的行为称为多态--具有多种形态,即同一个方法的行为随上下文而异。有两种重要机制可用于实现多态公有继承:

        在派生类中重新定义基类方法;

        使用虚方法;

is-a关系通常是不可逆的。也就是说,水果不是香蕉。

13.3.1 开发Brass类和BrassPlus类
// Brass.h -- bank account classes
#ifndef BRASS_H_
#define BRASS_H_
#include

// Brass Account Class
class Brass
{
private:
    std::string fullName;
    long acctNuml;
    double balance;
public:
    Brass(const std::string& s = "Nullbody", long an = -1, double bal = 0.0);
    void Deposit(double amt);
    virtual void Withdraw(double amt);
    double Balance() const;
    virtual void ViewAcct() const;
    virtual ~Brass(){}
};


// Brass Plus Account Class
class BrassPlus : public Brass
{
private:
    double maxLoad;
    double rate;
    double owesBank;
public:
    BrassPlus(const std::string& s = "Nullbody", long an = -1, double bal = 0.0, 
              double ml = 500, double r = 0.1125);
    BrassPlus(const Brass& ba, double ml = 500, double r = 0.1125);
    virtual void ViewAcct() const;
    virtual void WithDraw(double amt);
    void ResetMax(double m) { maxLoad = m; }
    void ResetRate(double r) { rate = r; }
    void ResetOwes() { owesBank = 0; }
} ;


#endif

需要说明下面几点:

       1. BrassPlus类在Brass类的基础上添加了3个私有数据成员和3个公有成员函数;

        2. Brass类和BrassPlus类都声明了ViewAcct()和Withdraw()方法,但BrassPlus对象和Brass对象的这些方法的行为是不一样的。

        3. Brass类在声明ViewAcct()和Withdraw()时使用了新关键字virtual。这种方法称为虚方法(virtual emthod);

        4. Brass类还声明了一个虚析构函数,虽然该析构函数不执行任何操作。

第2点介绍了声明如何指出方法在派生类的行为的不同。

Brass dom{"Dom", 11234, 4521.2};
BrassPlus dot{"Tye", 12156, 76.6};
dom.ViewAcct();    // use Brass::ViewAcct()
dot.ViewAcct();    // use BrassPlus::ViewAcct()

第3点使用virtual。如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有关键字virtual,程序将根据引用或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。

Brass dom{"Dom", 11234, 4521.2};
BrassPlus dot{"Tye", 12156, 76.6};
Brass& b1_ref = dom;
Brass& b2_ref = dot;

b1_ref.ViewAcct();    // use Brass::ViewAcct()
b2_ref.ViewAcct();    // use BrassPlus::ViewAcct()

经常在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚方法后,它在派生类中自动成为虚方法。然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法。

虚方法非虚方法
指针/引用右值左值
非指针/非引用左值左值

第4点,基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。

// brass.cpp -- banl account class method
#include 
#include "brass.h"
using std::cout;
using std::cin;
using std::endl;

// formatting stuff
typedef std::ios_base::fmtflags format;
typedef std::streamsize precis;
format setForm();
void restore(format f, precis p);

// Brass method
Brass:Brass(const string& s, long an, double bal)
{
    fullName = s;
    acctNum = an;
    balance = bal;
}

void Brass::Deposit(double amt)
{
    if (amt < 0)
        cout << "Wrong!n";
    else
        balance += amt;
}

void Brass::Withdraw(double)
{
    // set up ###.## format
    format initialState = setFornat();
    precis prec = cout,precision(2);

    if (amt < 0)
        cout << "Wrong!";
    else if (amt <= balance)
        balance -= amt;
    else
        cout << "Wrong!";
    restore(initialState, prec);
}

double Brass::Balance() const
{    
    return balance;
}

void Brass::ViewAcct() const
{
    // set up ###.## format
    format initialState = setFornat();
    precis prec = cout,precision(2);
    cout << "Client: " << fullName << endl;
    cout << "Account Number: " << acctNum << endl;
    cout << "Balance: " << balance << endl;
    restore(initialState, prec);
}

// BrassPlus method
BrassPlus::BrassPlus(const string& s, long an, double bal,
                     double ml, double r) : Brass(s, an, bal)
{
    maxLoad = ml;
    owesBank = 0.0;
    rate = r;
}

// refine how ViewAcct() works
void BrassPlus::VewAcct() const
{
    // set up ###.## format
    format initialState = setFornat();
    precis prec = cout,precision(2);
    Brass::ViewAcct();
    cout << "MaxLoad: " << maxLoad << endl;
    cout << "Owe to bank: " << owesBank << endl;
    cout << "Load rating: " << rate << endl;
    cout,precision(3);
    restore(initialState, prec);
}

void BrassPlus::Withdraw(double amt)
{
    // set up ###.## format
    format initialState = setFornat();
    precis prec = cout,precision(2); 

    double bal = Balance();
    if (amt <= bal)
        Brass::Withdraw(amt);
    else if (amt <= bal + maxLoad - owesBank)
    {
        double advance = amt - bal;
        owesBank += advance * (1.0 + rate);
        cout << "Bank advance: $" << advance << endl;
        cout << "Finance charge: $" << rate * advance << endl;
        Deposit(advance);
        Brass::Withdraw(amt);
    }
    else
        cout << "Credit limit exceeded. Transcation cancel!n"
  
    restore(initialState, prec);
}

format setFormat(format f, precis p)
{
    cout.setf(f, std::ios_base::floatfield);
    cout.precision(p);
}

记住,派生类并不能直接访问基类的私有数据,而必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法声明。

派生类构造函数在初始化基类私有数据时,采用的成员初始化列表语法。将参数中的基类信息传递给基类构造函数,然后使用构造函数体初始化BrassPlus类新增的数据项。

非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。BrassPlus::ViewAcct()显示BrassPlus数据成员,并调用基类方法Brass::ViewAcct()来显示基类数据成员。在派生类中,标准技术是使用作用域解析运算符来调用基类方法。如果没使用作用域运算解析符,编译器认为是派生类对象的成员函数,这将创建一个不会终止的递归函数。

不能创建一个数组来同时存储基类和派生类对象,但能创建一个指向基类的指针数组。这样,每个元素的类型都相同,但由于使用的是的公有继承模型,因此Brass指针既可以指向Brass类,也能指向BrassPlus类。

13.4 静态联编和动态联编

将源代码中的函数调用解析为执行特定的函数代码块被称为函数名联编(bind)。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C++编译器可以在编译过程完成这种联编。在编译过程进行联编被称为静态联编(staic binding),又称早期联编(early binding)。然而,虚函数使这项工作变得更困难。因为编译器不知道用户将选择哪种类型的对象。所以,编译器必须生成能够在程序运行时选择正确的虚函数的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。

13.4.1 指针和引用类型的兼容性

在C++中,动态联编与通过指针和引用调用方法相关,从某种程度说,这是由继承控制的。公有继承建立is-a关系的一种方法是如何处理指向对象的指针和引用。

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),这使公有继承不需要进行显式类型转换。向上强制是可以传递的,也就是说,如果从BrassPlus对象派生出BrassPlusPlus类,则Brass指针或引用可以引用Brass对象,BrassPlus对象,BrassPlusPlus对象。

相反的过程,将基类指针或引用转换为派生类指针或引用--称为向下强制转换(downcasting)。如果不使用显式类型转换,则向下强制转换是不允许的。

隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此,需要动态联编。C++使用虚成员函数来满足这种要求。

13.4.2 虚成员函数和动态联编

总之,编译器对非虚函数使用静态联编,对虚函数使用动态联编。

为何不摒弃静态联编呢?原因有二:效率和模型。

首先来看效率,为使程序在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用所指向的对象类型,这增加了额外的处理开销。例如,如果类不作为基类,则不会需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。由于静态联编效率高,因此被设置为C++的默认选择。

Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或处理时间)。

在设计类时,可能包含一些不在派生类重新定义的成员函数。不该将函数设置为虚函数。有两方面好处:首先效率更高,其次,指出不需要重新定义该函数。

编译器处理虚函数的方法是,给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table, vtbl)。虚函数表存储了为类对象进行声明的虚函数的地址。例如,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。如果派生类没有重新定义虚函数,该虚函数表将保存原始版本的地址。

调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。

总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:

        每个对象都将增大,增大量为存储地址的空间;

        对于每个类,编译器都将创建一个虚函数地址表(数组);

        对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

13.4.3 有关虚函数的注意事项

在基类方法的声明中使用关键字virtual可使该方法在基类以及所有派生类中是虚的。(包括派生类的派生类)。

如果使用指向对象的引用或指针来调用虚函数,程序将使用为对象类型定义的方法,而不使用为引用或指针类型的方法。这称为动态联编或晚期联编。这种行为非常重要,因此这样基类指针或引用可以指向派生类对象。

如果定义的类将被用作基类,则应将那些需要在派生类中重新定义的类方法声明为虚的。

1. 构造函数

构造函数不能为虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以讲类构造函数声明为虚的没什么意义。

2. 析构函数

析构函数应当是虚函数,除非类不用做基类。因为如果使用静态联编,指向派生类对象的基类引用的析构函数只会删除派生类对象中基类部分指向的内存,但不会释放派生类中新的类成员指向的内存。

通常给基类提供一个虚析构函数,即使它不需要析构函数。

3. 友元

友元不能是虚函数,因为友元不是类成员。只有类成员才能使虚函数。

4. 没有重新定义

如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

5.重新定义将隐藏方法

class Dwelling
{
public:
    virtual void show(int a) const;
...
};

class Hovel : public Dwelling
{
public:
    virtual void show() const; // 重新定义了show函数,

};

重新定义不会生成函数的两个版本,而是隐藏了基类版本。总是,重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名的基类方法。

这引出两条经验规则:

        第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特征被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化。

        第二, 如果基类声明被重载了,而应在派生类中重新定义所有的基类版本。如果只定义了一种版本,则另外两个版本都将被隐藏,派生类对象无法使用它们。

class T1
{
public:
    virtual void funn1();
    virtual void funn1(int a);
    virtual void funn1(int a, double b);
...
};

class T2 : public T1
{
public:
    virtual void funn1();
    virtual void funn1(int a);
    virtual void funn1(int a, double b);
...

}
13.5 访问控制 :protected

本书已经使用public和private来控制对类成员的访问。还存在另一个访问控制,这种类别用关键字protected表示。其特性为在类外只能用公有类成员来访问protected部分中的类成员。派生类的成员能够直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

class Brass
{
protected:
    double balance;
};

BrassPlus类可以直接访问balance,而不需要Brass方法。

但是,最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。然而,对于成员函数来说,保护控制很有用,它让派生类能够访问公众不能使用的内部函数。

13.6 抽象基类(Abstract base class)

当两个类有很多共同点时,可以考虑将这些特性放到一个ABC中。然后从ABC中派生出这两个类。

C++提供使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处等于0。

Class base1
{
public:
    virtual void func1() = 0; // 纯虚函数
...
};

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只能用作基类。要称为ABC,必须至少包含一个纯虚函数。原型中的=0使虚函数成为纯虚函数。纯虚函数可以不提供定义。

总之,在原型中使用=0指出类是一个抽象基类,在类中可以不定义该函数。

纯虚类的派生类,也可以被称为具体类(concrete),这表示可以创建这些类型的对象。

总之,ABC描述的是至少使用一个纯虚函数的接口,从ABC派生出来的类将根据派生类的具体特征,使用常规虚函数来实现这种接口。

对于独立函数而言,这些函数与客户定义的同名函数发生冲突。解决办法有多种,一种方法是将这些函数声明为静态,这样他们归文件本身及继任文件所有。另一种方法是将这些函数以及结构放在一个独立的名称空间中。在一个方法是将这些结构和函数放在类的定义的保护部分,这使得他们对于基类和派生类可用,同时向外隐藏了他们。

13.6.2 ABC理念

ABC方法更具有系统性,规范性。设计ABC之前,首先应开发一个模型----指出编程问题所需的类以及它们之间的相互关系。一种学院派思想认为,如果要设计类继承层次,则只能将那些不会用作基类的类设计为具体的类。这种方法设计更清晰,复杂程度更低。

可以将ABC看做一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数----迫使派生类遵循ABC设置的接口准则。使用ABC使得组件设计人员能够制定“接口约定”,这样确保从ABC派生的所有组件都至少支持ABC制定的功能。

13.7 继承和动态内存分配
// base Class
class baseDMA
{
private:
    char* label;
    int rating;

public:
    baseDMA(const char* = "NULL", int r = 0); // 默认构造函数
    baseDMA(const baseDMA& rs);               // 复制构造函数
    virtual ~baseDMA()                        // 虚析构函数
    baseDMA& operator =(const baseDMA& rs);   // 赋值运算符重载
};
1.3.7.1 第一种情况:派生类不使用new

若派生类中并没有使用new,也未包含其他一些不常用的、需要特殊处理设计特性,则不需要为派生类定义显式析构函数、复制构造函数和赋值运算符。

首先,来看是否需要析构函数。如果没有定义析构函数,编译器将定义一个不执行任何操作的默认析构函数。实际上,派生类的默认析构函数总是要进行一些操作:执行自身的代码后调用基类析构函数。若派生类成员不需要执行任何特殊操作、所以默认析构函数是合适的。

13.7.2 第二种情况:派生类使用new

声明中包含了构造函数使用new时需要的特殊方法:析构函数、复制构造函数和重载赋值运算符。则必须为派生类显式定义析构函数、复制构造函数和重载赋值运算符。

class hasDMA : public baseDMA
{
private:
    char* style;
public:
...
};

派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。

// 析构函数
hasDMA::~hasDMA()
{
    delete [] style;
}

派生类的复制构造函数只能访问派生类的数据,因此它必须调用基类复制构造函数来处理共享的基类数据。基类的复制构造函数的参数为基类引用,所以可以传递派生类对象给基类的复制构造函数。

// 复制构造函数
hasDMA::hasDMA(const hasDMA& rs) : baseDMA(rs)
{
    style = new char[std::stylen(rs.style) + 1];
    strcpy(style, rs.style);
}

派生类赋值运算符重载,由于它只是派生类方法,无法直接访问基类数据,所以需要显示调用基类赋值运算符来完成这项工作。

// 赋值运算符重载
hasDMA& hasDMA::operator =(const hasDMA& rs)
{
    if (*this == rs)
        return *this;
    baseDMA::operator =(rs);            // 基类成员的赋值交给基类赋值运算符
    delete [] style;
    style = new char[std::strlen(rs.style) + 1];
    std::strcpy(style, rs.style);
    return *this;
}

总之,当基类和派生类都采用动态内存分配时,派生类的析构函数、复制函数、赋值运算符都必须使用相应的基类方法来处理基类元素。对于析构函数,这是自动完成的;对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的;对于赋值运算符,这是通过使用作用域解析运算符显示地调用基类的赋值运算符来完成的。

13.7.3 派生类如何使用友元
class hasDMA
{
...
public:
    firend std::ostream& operator <<(std::ostream& os, const hasDMA& rs);
...
};

std::ostream& operator <<(std::operator& os, const hasDMA& rs)
{
    os << (const baseDMA&)rs;
    os << "Style: " << hs.style << std::endl; // 友元函数得通过对象才能访问对象的私有成员
    return os;
}

作为hasDMA类的友元,该<<能访问style成员。然而,存在一个问题:该函数如不是baseDMA类的成员,那它如何访问baseDMA的成员呢? 答案是使用baseDMA类的友元函数operator<<。但这引出一个问题,因为友元函数不是类成员,无法使用作用域解析符来指出要使用那个函数。这个问题的解决方法是使用强制类型转换,以便匹配原型时能够选择正确的函数。

13.8 类设计回顾 13.8.1 编译器生成的成员函数

1. 默认构造函数

        默认构造函数要么没有参数,要么所有参数都有默认值。如果没有定义任何构造函数,编译器将生成默认构造函数,让您能够创建对象。自动生成的默认构造函数的另一项功能是,调用基类的默认函数以及调用本身是对象的成员所属类的默认构造函数。

        另外,如果派生类构造函数的成员初始化列表中没有显示调用基类构造函数,则编译器将使用基类的构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译器错误。

        如果定义了某种构造函数,编译器将不会定义默认构造函数,在这种情况下,如果需要默认构造函数,则需要自己提供。

        提供构造函数的动机之一是确保对象总能被正确的初始化。如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数。

2. 复制构造函数

        复制构造函数接受所属类的对象作为参数。

        在下述情况下,将调用复制构造函数:

                将新对象初始化为一个同类对象;

                按值将对象传递给函数;

                函数按值返回对象;

                编译器生成临时对象;

        如果程序没有定义复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。

        当构造函数使用new时,成员初始化不合适了,需要使用深复制。

3. 赋值运算符

默认赋值运算符用于处理同类对象之间的赋值。

如果语句创建新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值。

如果需要显式定义复制构造函数,则基于同样的原因,应显式定义赋值运算符。

编译器不会生成一种类型赋给另一种类型的赋值运算符。将一种类型赋值给另一种类型的方法之一是显式定义赋值运算符。另一种是使用转换函数。

13.8.2 其他的类方法

定义类时,还需要注意其他几点。

1. 构造函数

        构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有对象调用。这是构造函数不能被继承的原因之一。继承意味着派生类对象可以使用基类的方法,然而,构造函数在完成其工作之前,对象并不存在。

2. 析构函数

        一定要定义显式析构函数来释放类构造函数使用new所分配的所有内存,并完成类对象所需要的的任何特殊的清理工作。对于基类,即使它不需要析构函数,也应提供一个虚析构函数。

3. 转换

        使用仅一个参数(其余均为默认参数)就可以调用构造函数定义了从参数类型到类类型的转换。在带有一个参数。在带一个参数的构造函数原型中使用explicit将禁止隐式转换,但仍允许显式转换。

hasDMA(int a, double r = 0...); //可用作转换的构造函数;

        要将类对象转换为其他类型,需定义转换函数。转换函数可以是没有参数列表的类成员函数,也可以是返回类型被声明为目标类型的类成员函数。

hasDMA::operator double(){...}    // converts hasDMA to double

4. 按值传递对象和传递引用

        按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。复制大型对象比传递引用花费的时间多得多。如果函数不修改对象,应将参数声明为const引用。

5. 返回对象和返回引用

        按引用传递对象的另一个原因是,有些成员函数直接返回对象,二另一些返回引用。有时犯法必须返回对象,但如果可以不返回对象,则应返回引用。

        其次,返回引用的原因在于,返回对象涉及生成返回对象的临时副本,这是调用函数的程序可以使用的副本。因此,返回对象的时间包括调用复制构造函数来生成副本所需的时间和调用析构函数删除副本所需的时间。返回引用可节省时间和内存。

        但是,函数不能返回在函数中创建的临时对象的引用,但函数结束时,临时对象将消失,因此这种引用将是非法的。

6. 使用const

        使用const可以确保方法不改变参数。不修改调用它的对象。

13.8.3 公有继承的考虑因素

1. is-a关系

        要遵循is-a关系。如果派生类不是一种特殊的基类,则不要使用公有派生。

        在某些情况下,最好的方法是创建包含纯虚函数的抽象数据类,并从它派生出其他的类。

        请记住,表示is-a关系的方式之一是,无需进行显式类型转换,基类指针或引用就可以指向派生类对象。

2. 什么不能被继承

        构造函数不能被继承。先构造基类,在构造派生类。

        在继承链中,每个类都可以使用成员初始化列表将信息传递给相邻的基类。

        析构函数也是不能被继承的。先析构派生类,在析构基类。通常对于基类,其析构函数应设置为虚函数。

        赋值运算符不能被继承的。赋值运算符的特征表随类而异,这是因为它包含一个类型为其所属类的形参。

3. 赋值运算符

        如果编译器发现程序将一个对象赋给同一个类的另一对象,它将自动为这个类提供一个赋值运算符。这个运算符的默认或隐式版本将采用逐成员赋值。然而,如果这个对象是派生类,编译器将使用基类赋值运算符处理派生类中基类部分的赋值。

        如果类构造函数使用new,则需要显式定义复制构造函数、析构函数、赋值运算符。

4. 私有成员和保护成员

        对于派生类而言,保护成员类似于公有成员;但对外部来说,保护成员与私有成员类似。派生类能直接访问基类的保护成员,但只能通过基类的成员函数来访问私有成员。因此,将基类成员设置为私有的可以提高安全性,而将他们设置为保护成员则可以简化代码的编写工作。

5. 虚方法

        设计基类时,必须确定是否将类方法声明为虚的。如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚的,这样讲启动动态联编。如果不需要重新定义,则不必将其声明为虚。虽然不能禁止他人重新定义方法,但表达了这样的意思。

6. 析构函数

        程序将首先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅是调用基类的析构函数。

7. 友元函数

        由于友元函数并非类成员,因此不能继承。然而,您可以希望派生类的友元函数能够使用基类的友元函数。为此,可以通过强制类型转换,派生类指针或引用转换为基类指针或引用时,然后使用转换后的指针或引用来调用基类的友元函数。

8. 有关使用基类方法的说明

以公有方式派生的类的对象可以通过多种方式来使用基类的方法。

        派生类对象自动使用继承而来的基类方法;

        派生类的构造函数自动调用基类的构造函数;

        派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表指定其他的构造数;

        派生类构造函数显式地调用成员初始化列表中指定的基类构造函数;

        派生类方法可以使用作用域解析符来调用公有的和受保护的基类方法;

        派生类的友元函数可以通过强制类型转换,派生类指针或引用转换为基类指针或引用时,然后使用转换后的指针或引用来调用基类的友元函数。

13.8.4 类函数小结
函数能否继承成员还是友元默认能否生成默认为虚函数是否可以有返回类型
构造函数成员
析构函数成员
=成员
&任意
转换函数成员
()成员
[]成员
->成员
op=任意
new静态成员void*
delete静态成员void*
其他运算符任意
其他成员成员
友元友元
13.9 总结

继承通过使用已有的类(基类)定义新的类(派生类),使得能够根据需要修改程序代码。

公有继承建立is-a关系,这意味着派生类对象也应该是某种基类对象。

作为is-a模型的一部分,派生类继承基类的数据成员和大部分方法,但不能继承构造函数、析构函数、赋值运算符。

派生类能直接访问基类的公有成员和保护成员,并能通过基类的公有方法和保护方法访问基类的私有成员。

可以在派生类中新增数据成员和方法,还可以将派生类用作基类,来做进一步开发。

每个派生类都必须有自己的构造函数。

程序创建派生类对象时,将首先调用基类的构造函数,然后调用派生类的构造函数。

程序删除派生类对象时,将首先调用派生类的析构函数,然后调用基类的析构函数。

如果要将类作为基类,则可以将成员声明为保护的,而不是私有的,这样,派生类将可以直接访问这些成员。然而,使用私有成员通常可以减少出现编程问题的可能性。

如果希望派生类重新定义基类的方法,这可以使用关键字virtual将它声明为虚的。可以通过基类指针或引用指向的对象类型来调用方法。

可以考虑定义一个ABC:只定义接口,而不涉及实现。ABC类必须至少包含一个纯虚函数,可以在声明中的分号前面加上=0来声明纯虚函数。

不一定非得定义纯虚方法。对于包含纯虚函数的类,不能用它来创建对象。纯虚方法用于定义派生类的通用接口。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/396081.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号