C++ Primer 笔记六

第十三章 类继承

13.1公有派生

基类的公有成员将成为派生类的公有成员,基类的私有成员也将成为派生类的一部分,但是只能通过基类的公有和保护方法访问。

派生类对象特征:派生类对象存储了基类的数据成员(派生类继承了基类的实现)。派生类对象可以使用基类的方法(派生类继承了基类的接口)。派生类需要自己的构造函数。可以添加额外的数据成员和成员函数。

访问权限:派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。具体地说,派生类的构造函数必须使用基类的构造函数。创建派生类对象时,程序首先创建基类对象。可以理解为,将派生类对象初始化时,调用嵌套了基类构造函数的派生类构造函数。

RatedPlayer:: RatedPlayer(unsigned int r,const char* fn,const char* ln,bool ht):TableTennisPlayer(fn,ln,ht){ratubg =r;}

如果省略成员初始化列表RatedPlayer:: RatedPlayer(unsigned int r,const char* fn,const char* ln,bool ht){….}

又基类对象必须首先被创建,所以程序将使用默认的基类构造函数。

等效于:RatedPlayer:: RatedPlayer(unsigned int r,const char* fn,const char* ln,bool ht): TableTennisPlayer() {….}

除非要使用默认构造函数,否则应显示调用正确的基类构造函数。

13.2有关派生类构造函数的要点

基类对象首先被创建;派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;派生类构造函数应初始化派生类新增数据成员;释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。

派生类和基类的关系:派生类可以使用基类的非私有方法;基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式转换时引用派生类对象,不过基类指针或引用只能调用基类方法。即派生类对象或地址可以赋值给基类引用或指针,但不可以将基类对象和地址赋给派生类引用和指针。此匹配是单向的。

13.3继承——is-a关系

三种继承方式:公有继承、保护继承、私有继承

公有继承:最常见的集成方式,它建立一种is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行任何操作,也可以对派生类对象执行。这种关系称为is-a-kind-of,比如,苹果是一种水果。

13.4多态公有继承

如果希望同一个方法在基类和派生类中的行为是不同的,换句话说,方法的行为取决于调用该方法的对象,这种较复杂的行为称为多态——具有多种形态,就是指同一个方法的行为将随上下文而异。

两种重要的机制可以实现多态公有继承:在派生类中重新定义基类的方法;使用虚方法。

 

class Brass

{

private:

enum {MAX = 35};

char fullName[MAX];

    long acctNum;

    double balance;

public:

    Brass(const char *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() {}

};

class BrassPlus : public Brass

{

private:

    double maxLoan, rate ,owesBank;

public:

    BrassPlus(const char *s = "Nullbody", long an = -1,

            double bal = 0.0, double ml = 500,

            double r = 0.10);

    BrassPlus(const Brass & ba, double ml = 500, double r = 0.1);

    virtual void ViewAcct()const;

    virtual void Withdraw(double amt);

    void ResetMax(double m) { maxLoan = m; }

    void ResetRate(double r) { rate = r; };

    void ResetOwes() { owesBank = 0; }

};

 

Brass类在声明Withdraw() 

Brass Piggy("Porcelot Pigg", 381299, 4000.00);

BrassPlus Hoggy("Horatio Hogg", 382288, 3000.00);

Brass &b1_ref = Piggy;

Brass &b2_ref = Hoggy;

如果ViewAcct()不是虚拟的,则:

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

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

如果ViewAcct()是虚拟的,则:

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

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

虚函数的这种行为十分方便,因此,经常在基类中将派生类会重新定义的方法声明为虚方法,方法在基类中声明为虚拟的以后,它在派生类中将自动成为虚方法。

 

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

派生类的构造函数采用成员初始化列表的句法,将基类信息传递给基类构造函数。非构造函数不能使用成员初始化列表,但派生类可以调用共有的基类方法。示例代码如下:

 

BrassPlus::BrassPlus(const Brass & ba, double ml, double r)

           : Brass(ba) // uses implicit copy constructor

{

    maxLoan = ml;

    owesBank = 0.0;

    rate = r;

}

void BrassPlus::ViewAcct() const

{

    Brass::ViewAcct();   // display base portion

    cout << "Maximum loan: $" << maxLoan << endl;

    cout << "Owed to bank: $" << owesBank << endl;

cout << "Loan Rate: " << 100 * rate << "%\n";

}

 

为何需要虚拟析构函数:对于前一个示例,两个Brass的引用一个指向Brass对象,一个指向BrassPlus对象,如果析构函数不是虚拟的,则将只会调用Brass的析构函数,即使指针指向的是一个BrassPlus对象。如果析构函数是虚拟的,将调用相应对象类型的析构函数。使用虚拟析构函数能保证正确的析构函数序列被调用。

友元不能是虚函数,因为友元不是类成员,只有成员才能使虚函数。但是可以通过让友元函数使用虚拟成员函数来解决。

虚函数注意事项:

重新定义继承的方法,应确保与原来的函数类型完全相同,因为重新定义继承的方法并不是重载,也不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针,这种特性被成为返回类型协变,因为允许返回类型随类类型的变化而变化。

如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。不然另外的重载版本将被隐藏,派生类对象无法使用它们。

13.5访问控制:protected

关键字protected与private相似,在类外只能用公有类成员访问protected部分中的成员,protected与private的区别只有在基类派生的类中才会表现出来,派生类的成员可以直接访问基类的保护成员protected,但不能直接访问类的私有成员private。对外部世界来说,保护成员和私有成员相似,对派生类来说保护成员和公有成员相似。

13.6抽象基类

纯虚函数virtual void ViewAcct() const = 0;  纯虚函数声明的结尾处为 =0

当类声明中包含纯虚函数时,则不能创建该类的对象,只能用作基类。

ABC函数:真正的ABC必须包含一个纯虚函数。纯虚函数可以没有定义,但C++甚至允许纯虚函数有定义,但需要将这个类声明为抽象的(至少有一个纯虚函数)。可以将原型声明为虚拟的void Move(int nx,ny)=0;,仍可以在实现文件中提供方法的定义。派生类的函数可以调用它。

ABC理念:在设计ABC之前,首先应开发一个模型——指出变成问题所需要的类,以及它们之间的关系,基类不能被创建对象。 可以将ABC看作是一种必须实施的借口,ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC所设置的接口规则。这种模型在基于组件的模式中很常见,在这种情况下,使得组件设计人员能够制定“接口约定”这样确保了从ABC派生的所有组件都至少支持ABC指定的功能。

13.7继承和动态内存分配

如果基类使用动态内存分配,并重新定义赋值和复制构造函数:

1.派生类不使用new那么派生类可以使用默认析构函数,默认赋值函数,默认复制构造函数。因为,派生类都会自动调用基类的函数来完成派生类对象中的基类部分的操作,派生类对象成员的操作由默认函数来操作是合适的。

2.派生类使用了new这种情况下,必须为派生类定义显式的析构函数、复制构造函数和赋值函数。派生类的析构函数、赋值操作符、复制构造函数都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来完成的;对于析构函数,这是自动完成的。对于构造函数,这是通过在初始化成员列表中调用基类的复制构造函数来完成的(如果不这样,将自动调用基类的默认构造函数),不需要在函数里对基类的对象赋值。对于赋值操作符,这是通过使用作用域解析操作符显式地调用基类的赋值操作符来完成的,必须给类的每个成员提供赋值操作符,不仅仅是新成员。

 

baseDMA & baseDMA::operator=(const baseDMA & hs)

{

If(this ==&hs) return *this;

delete [] label;

label = new char[std::strlen(hs.label)+1];

std::strcpy(lable,hs.label);

rating = rs.rating;

return *this;

hasDMA & hasDMA::operator=(const hasDMA & hs)

{

If(this ==&hs) return *this;

baseDMA::operator=(hs);

style = new char[std::strlen(hs.style)+1];

std::strcpy(style,hs.style);

return *this;

}

 

程序中,baseDMA::operator=(hs); //相当于使用基类的=操作符,把hs调用基类的=操作符执行:*this=hs;将基类的成员按照基类的赋值操作符赋给了派生类对象

使用动态内存分配和友元的继承范例

hasDMA类(派生类)的友元 friend std::ostream & operator<<(std::ostream & os,const hasDMA & rs);

baseDMA类(基类)的友元  friend std::ostream & operator<<(std::ostream & os,const baseDMA & rs);

 

std::ostream & operator<<(std::ostream & os,const baseDMA & rs)

{

os<<”Label: ”<<rs.label<<endl;

os<<”Rating: ”<<rs.rating<<endl;

return os;

}

std::ostream & operator<<(std::ostream & os,const hasDMA & rs)

{

os<<(const baseDMA &)hs;

os<<”Style; ”<<hs.style<<endl;

return os; 

}

 

os<<(const baseDMA &)hs因为友元不是成员函数,因此不能使用作用于解析操作符来指出要使用哪个函数,所以使用强制类型转换,以便匹配原型时能够选择正确的函数。

13.8类设计回顾

编译器生成的成员函数:

1.默认构造函数

默认构造函数要么没有参数,要么所有的参数都有默认值,如果没有定义任何构造函数,编译器将定义默认构造函数,让你能够创建对象。

自动生成的默认构造函数的另一项功能是,调用基类的默认构造函以及调用本身是对象成员所属类的默认构造函数。

如果派生类的构造函数的成员初始化列表没有显式地调用基类的构造函数,则编译器将调用基类的默认构造函数来构造派生类对象的基类部分,如果基类没有默认构造函数,将导致编译阶段错误。

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

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

2.复制构造函数

复制构造函数接受其所属类的对象作为参数,如:Class_name(const  Class_name & );

使用复制构造函数的场合:将新的对象初始化为一个同一类对象;按值将对象传递给函数;函数按值返回对象;编译器生成临时对象。

如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。

在某些情况下,默认复制构造函数的成员初始化是不合适的,如,使用new初始化的成员指针通常要求执行深复制,或者类可能包含需要修改的静态变量,上述情况,需要自己定义复制构造函数。

3.赋值操作符

默认的赋值操作符用于处理同类对象之间的赋值。不要将赋值和初始化混淆了。如果语句创建了新的对象,则使用初始化;如果语句修改已有对象的值,则是赋值。

如果需要显式定义复制构造函数,则基于相同的原因,也需要显式定义赋值操作符。

其他的类方法

1.构造函数

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

2.析构函数

析构函数不能被继承。释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。如果基类有默认析构函数,编译器将为派生类生成默认析构函数。通常,对于基类,其析构函数应设置为虚拟的。

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

3.转换

使用一个参数就可以调用的构造函数定义了从参数类型到类类型的转换。例如,下述star类的构造函数原型:

Star(const char *); //char *转换为Star Star(const Spectral & , int members=1); // Spectral转化成Star

将可转换的类型传递给以类为参数的函数时,将调用转换构造函数。

Star north; north=”polaris”;第二条语句将调用Star::operator=(const Star *)函数,使用Star::Star(const char*)生成一个Star对象,该对象将被用作上述复制操作符函数的参数,这里假设没有定义将char*赋给Star的赋值操作符。

在带一个参数的构造函数原型中使用explicit将禁止进行隐式转换,但仍允许显式转换:

 

Class Star

{…

public: 

explicit Star(const char*);

…};

 

Star north;

Star =”polaris”; //not allowed

Star =Star(“polaris”); //allowed

 

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

通常,编写使用对象作为参数的函数时,应按引用而不是按值传递对象。这样做的原因之一是为了提高效率。按值传递对象涉及到生成临时拷贝,即调用复制构造函数,然后调用析构函数。调用这些函数需要时间,复制大型对象比传递引用花费的时间要多得多。如果函数不修改对象,应将参数声明为const引用。

按引用传递对象的另外一个原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类

5.返回对象和返回引用

一些类方法返回对象,有些成员函数直接返回对象,而另一些则返回引用,有时必须返回对象,但如果可以步返回对象,则应返回引用,而不是返回对象。

返回对象涉及到生成返回对象的临时拷贝,这是调用函数的程序可以使用的拷贝。因此,返回对象的时间成本包括调用复制构造函数来声称拷贝所需的时间和调用析构函数删除拷贝所需的时间。返回引用可节省时间和内存,直接返回对象与按值传递对象相似:它们都生成临时拷贝。同样返回引用与按引用传递对象相似:调用和被调用的函数对同一个对象进行操作。

不过,并不总是可以返回引用,函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的,在这种情况下,应返回对象,以生成一个调用程序可以使用的拷贝。

通用规则是,如果函数返回在函数中创建的临时对象,则不要使用引用。如,加法后的返回值。

如果函数返回的是通过引用或指针传递给他的对象,则应按引用返回对象。

6.使用const

可以使用它来确保方法不修改参数:Star::Star(const char *s){…}//函数不能改变s

可以使用const来确保方法不修改调用他的对象:void Star::show() const {…}这里const表示const Star *this,而this表示调用的对象,表示不能使用show()函数修改对象。

通常,可以将返回引用的函数放在赋值语句的左侧,这实际上意味着可以将值赋给引用的对象,但可以使用const来确保引用或指针返回值不能用于修改对象中的数据:

const Stock & Stock::topval(const Stock & s) const

{

if(s.total_val>total_val) return s;

else return *this;

}

该方法返回thiss的引用,因为都被声明为const,所以函数不能对它们进行修改,这意味着返回的引用也必须被声明为const

注意,如果函数将参数声明为指向const的引用或指针,则不能将该参数传递给另一个函数,除非后者也确保了参数不会被修改。

公有继承的考虑因素

3.赋值操作符

赋值操作符是不能继承的,原因很简单。派生类集成的方法的特征标与基类完全相同,但赋值操作符的特征标应该随着类的改变而改变的,这是因为他包含一个类型为其所属类的形参。

如果编译器发现程序将一个对象赋给同一个类的另一个对象,它将自动为这个类提供赋值操作符。不过如果对象属于派生类,编译器将使用基类赋值操作符来处理派生对象中基类部分的赋值。如果显式地为基类提供了赋值操作符,将使用该操作符,与此相似,如果成员是另一个类的对象,则对于该成员,将使用其所属类的赋值操作符。

第十四章 C++中的代码重用

has-a关系

has-a代表的是对象和它的成员的从属关系。如果你确定两件对象之间是has-a的关系,那么应该使用聚合;比如电脑是由显示器、CPU、硬盘等组成的,那么应该把显示器、CPU、硬盘这些类聚合成电脑类,而不是从电脑类继承。

14.1包含对象成员的类

 

//包含

class Student

private:

    typedef std::valarray<double> ArrayDb;

    std::string name;       // contained object

ArrayDb scores;         // contained object

….. }

//私有继承

class Student : private std::string, private std::valarray<double>

{   

…..因为两个基类已经提供了所需的所有数据成员,所以私有继承提供了两个无名称的子对象成员。所以不需要私有数据了。

}

 

14.2私有继承

使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员,这意味着基类方法将不会成为派生对象的公有接口的一部分,但可以在派生类的成员函数中使用它们。

派生类不继承基类的接口,使用私有继承,类将继承实现。包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用属于子对象来表示通过继承或包含添加对象。

因此私有继承提供的特性与包含相同:获得实现,但不获得接口。

class student : private std::string,private std::valarray<double>{….}

包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。

1.初始化基类组件

隐式地继承组件而不是成员对象将影响代码的编写,因为再也不能使用namescores来描述对象了。例如

包含将使用这样的构造函数:Student(const char *str, const double *pd, int n):name(str),scores(pd,n) { }

私有继承的构造函数使用类名来标识:

Student(const char *str, const double *pd, int n):std::string(str),std::valarray<double>(pd,n) { }

2.访问基类的方法

使用私有继承时,只能在派生类的方法中,通过作用于解析操作符来调用基类的方法。

 

//包含

double Student::Average() const

{

    if (scores.size() > 0)

        return score.sum()/scores.size();  

    else

        return 0;

}

//私有继承

double Student::Average() const

{

    if (ArrayDb::size() > 0)

        return ArrayDb::sum()/ArrayDb::size();  

    else

        return 0;

}

 

总之,使用包含时将使用对象名来调用方法,而使用私有继承时,将使用类名和作用于解析操作符来调用方法。

3.访问基类对象

如果需要使用基类对象本身,那么派生类的代码如何访问内部的基类的未命名对象呢?使用强制类型转换。

由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。如下返回一个引用,该引用指向用于调用该方法的student对象中的继承而来的string对象。

const string & Student::Name() const { return (const string &) *this; }

4.访问基类的友元函数

用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类,不过可以通过显示地转换为基类来调用正确的函数。

ostream & operator<<(ostream & os, const Student & stu)

{

    os << "Scores for " << (const string &) stu  << ":\n";

    return os;

}

6.使用包含还是私有继承?

大多数C++程序员倾向于使用包含,易于理解,不容易出错。

私有继承提供的特性比包含多,继承能使用基类的保护成员,包含不能使用类数据成员的保护成员

派生类可以重新定义虚函数,但包含类不能。使用私有继承,从新定义的函数将只能在类中使用,不是公有的。

通常,应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

7.保护继承

class Student : private std::string, private std::valarray<double>  {….}

保护继承是私有继承的变体,使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。使用私有继承时,第三代类不能使用基类的接口,因为基类的共有方法在派生类中变成了私有方法。使用保护继承,第三代类可以在类中使用基类的接口,因为基类的公有方法在派生类中变成了保护方法,因此第三代类可以使用它们。

特征

公有继承

保护继承

私有继承

公有成员变成

派生类的公有成员

派生类的保护成员

派生类的私有成员

保护成员变成

派生类的保护成员

派生类的保护成员

派生类的私有成员

私有成员变成

只能通过基类接口访问

只能通过基类接口访问

只能通过基类接口访问

能否隐式向上转换

能(只能在类内)

不能

8.使用using重新定义访问权限

使用保护派生或私有派生,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法;外一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间一样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。

 

//包含和私有继承、保护继承都可以使用

double Student::sum() const

{

return valarray<double>::sum();

或 return score.sum();

}

这样便能调用Student.sum();

//不适合包含,只适合继承。using声明只是用成员名,没有圆括号、函数特征标和返回类型。

class Student : private std::string, private std::valarray<double>

{…..

public:

using std::valarray<double>::min;

using std::valarray<double>::max;

……}

 

14.3多重继承(MI

MI可能会给程序员带来许多新问题:从两个不同的基类继承同名方法;从两个或更多相关基类那里继承同一个类的多个实例。

如果Worker派生出Singer和Waiter,且SingerWaiter共同派生出了SingingWaiterSingerWaiter都继承了Worker,这样SingingWaiter ed; Worker *pw = &ed;这样,ed中包含两个Worker对象,有两个地址可以选择,所以必须使用类型转换来指定对象:Worker *pw = (Waiter *)&ed; 或 Worker *pw = (Singer *)&ed;

1.虚基类vitual

虚基类实得从多个类(它们的基类相同)派生出的对象只继承一个基类对象,例如,通过在类声明中使用关键字virtual,可以是Worker被用作SingerWaiter的虚基类(virtualpublic的次序无关紧要)

class Waiter : virtual public Worker{….}     class Singer: public virtual Worker{….}

class SingingWaiter : public Singer, public Waiter{….}

现在SingingWaiter对象只包含Worker对象的一个拷贝。从本质上来说,继承的SingerWaiter对象共享一个Worker对象,而不是各自引入自己的Worker对象拷贝。因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。

2.新的构造函数规则

使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即使基类构造函数,但这些构造函数可能需要将信息传递给其基类

SingingWaiter(const Worker &wk,int p=0,int v=Singer::other):Waiter(wk,p),Singer(wk,v) { }

存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter、Singer)将wk传递给Worker对象。为了避免这种冲突,C++在基类是虚拟的时,禁止信息通过中间类自动传递给基类。不过编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker的默认构造函数。

如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该为:

SingingWaiter(const Worker &wk,int p=0,int v=Singer::other):Worker(wk),Waiter(wk,p),Singer(wk,v) { }

上述代码将显式地调用构造函数Worker(const Worker &)请注意,对于虚基类,这种用法是合法的,必须这样做;对于非虚基类,则是非法的。

如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。

关于基类们的同名方法

比如没有在singingWaiter中重新定义show方法,又,每个直接祖先里都有一个show方法,多重继承可能导致函数调用的二义性。可以使用作用域解析操作符来确定调用它:singingWaiter sw; sw.Singer::show();更好的方法是在singingWaiter中重新定义show方法;

在重新定义的时候,任何一个基类组件都不能被忽略void singingWaiter::show(){ Singer::show(); Waiter::show(); }

这时候递增方式将不适合,可以使用模块化方式,即提供一个只显示Worker组件的方法和一个只显示Waiter组件或Singer组件(而不是WaiterSinger组件)的方法,然后在singingWaiter::show()方法中将组件组合起来

 

void Worker::Data() const

{

    cout << "Name: " << fullname << endl;

    cout << "Employee ID: " << id << endl;

}

void Waiter::Data() const

{

    cout << "Panache rating: " << panache << endl;

}

void Singer::Data() const

{

    cout << "Vocal range: " << pv[voice] << endl;

}

void SingingWaiter::Data() const

{

    Singer::Data();

    Waiter::Data();

void SingingWaiter::Show() const

{

    cout << "Category: singing Waiter\n";

    Worker::Data();

    Data();

}

 

下面介绍其他一些有关MI的问题

1.混合使用虚基类和非虚基类

当类通过多条虚拟途径和非虚拟途径继承某个特定的基类时,该类将包含一个表示所有的虚拟途径的基类子对象和分别表示各条非虚拟途径的多个基类子对象。

2.虚基类和支配

派生类中的名称优先于直接或间接基类中的同名名称。

如果某个名称优先于其他所有名称,则使用它时,即使不使用限定符(作用域解析操作符),也不会导致二义性。

MI小结

首先复习一下不使用虚基类的MI,这种形式的MI不会引入新的规则,不过如果一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用类限定符来区分它们。否则,编译器将指出二义性

如果一个类通过多种途径继承了一个非虚基类,则该类从每种途径分配继承非虚基类的一个实例。在某些情况下,这可能正是希望的,但是通常,多个基类实例都是问题。

当派生类使用关键字virtual来指示派生时,基类就成为虚基类。主要变化为:从虚基类的一个或多个实例派生而来的类将只继承了一个基类对象。为了实现这种特性,必须满足其他要求:

有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的;

通过优先选择解决名称二义性。

正如看到的。MI会增加编程的复杂程度,不过,这种复杂性主要是由于派生类通过多条途径继承同一个基类引起的,避免这种情况后,唯一需要注意的是,在必要时对继承的名称进行限定。

14.4 类模板

 

template <class Type>

class Stack

{

private:

    enum {MAX = 10};   

    Type items[MAX];  

    int top;       

public:

    Stack();

    bool isempty();

    bool isfull();

    bool push(const Type & item); 

bool pop(Type & item); 

};

template <class Type>

Stack<Type>::Stack()

{

    top = 0;

}

template <class Type>

bool Stack<Type>::isempty()

{

    return top == 0;

}

 

这里使用class并不意味着Type是一个类,而只是表Type是一个通用的类型说明符,在使用模版时,将使用实际的类型替换他,可以使用不太容易混淆的关键字typename代替class

如果编译器实现了新的export关键字,则可以将模版方法定义放在独立的文件中,条件是每个模版声明都以export开头:export template <class Type> class Stack {….},将方法定义放在源代码文件中,包含头文件,保证声明可用。

使用模版类:

仅在程序包含模版并不能生成模版类,必须请求实例化。为此,不要声明一个类型为模版类的对象,实例化时使用所需的具体类型替换通用类型名。

Stack<int> si; Stack<string> ss;

14.5总结

公有继承能够建立is-a关系,这样派生类可以重用基类的代码。私有继承和保护继承也是的能够重用基类的代码,但是建立的是has-a的关系。使用私有继承时,基类的公有成员和保护成员将成为派生类的私有成员;使用保护继承时,基类的公有成员和保护成员将成为派生类的保护成员。无论是私有继承还是保护继承,基类的公有接口都将成为派生类的内部接口。这有时候被称为集成实现,但并不集成接口,因为派生类对象不能显式地使用基类的接口,因此不能将派生对象看作是一种基类的对象。由于这个原因,私有继承和保护继承在不进行显示类型转换的情况下,基类指针或引用将不能指向派生类对象。

还可以通过开发包含对象成员的类来重用类代码,这种方法被成为包含、层次化或组合,它建立的也是has-a关系。与私有继承和保护继承相比,包含更容易实现和使用,所以通常优先采用这种方式。不过,私有继承和保护继承与包含有一些不同的功能。例如,继承允许派生类访问基类的保护成员;还允许派生类重定义从基类那里继承的虚函数。因为包含不是继承,所以通过包含来重用类代码时,不能使用这些功能。另一方面,如果需要使用某个类的几个对象,则使用包含更合适。

多重继承(MI)使得能够在类设计中重用多个类的代码。私有MI或保护MI简历has-a关系,而公有MI建立is-a的关系。MI会带来一些问题,即多次定义同一个名称,继承多个基类对象,可以使用类限定符来解决名称二义性问题,使用虚基类来避免继承多个基类对象的问题,但使用虚基类后,就需要为编写构造函数初始化列表以及解决二义性问题引入新的规则。

类模版实得能够创建通用的类设计,其中类型(通常是成员类型)有类型参数表示。可以用typename代替class类定义(实例化)在声明类对象并指定特定类型是生成。

class Ic<short> sic;//隐式实例化,类名为Ic<short>,Ic<short>成为模版具体化

template class Ic<int>;//显示实例化,编译器将声明称一个int具体化——Ic<int>虽然尚未请求这个类的对象

template<> class Ic<char*>//显示具体化—覆盖模版定义的具体类声明,从新声明。类型参数为char*将使用专用定义。

类模版可以被部分具体化:template<class T> Pals<T, T, 10>{…}n的值为10时,建立了一个具体化。

模版类可用作其他类、结构和模版的成员。

所有这些机制的目的都是为了让程序员能够重用经过测试的代码,而不用手工复制它们。这样可以简化编程工作,提供程序的可靠性。

第十五章 友元、异常和其他

15.1友元

类并非只能拥有友元函数,也可以将类作为友元,在这种情况下,友元类的所有方法都可以访问原始类的私有成员和保护成员。另外,也可以做更严格的限制,只将特定的成员函数指定为另一个类的友元。哪些函数、成员函数或类作为友元,只能是由类定义。而不能在外部加强友情。

比如电视机TV类,和遥控器Remote类,既不是is-a的关系,也不是has-a的关系,事实上,遥控器可以改变电视机状态,则表明应将Remote类作为TV类的一个友元。

 

class Tv

{

public:

friend class Remote; //友元类

….

class Tv

{

public: 

//友元函数

friend void Remote::set_chan(Tv & t,int c); ….

 

友元成员函数

共同的友元:函数需要访问两个类的私有数据,它可以是一个类的成员,同时是另一个类的友元,不过又是将函数作为两个类的友元更合理。

15.2嵌套类

C++中,可以将类声明放在另一个类中,在另一个类中声明的类被称为嵌套类。它通过新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公共部分才能在包含类的外面使用嵌套类,而且必须使用作用域解析操作符。

对类进行嵌套与包含并不同,包含意味着将类对象作为另一个类的成员,而对类进行嵌套不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效

嵌套类和访问权限

有两种访问权限适合嵌套类。首先,嵌套类的生命位置决定了嵌套类的作用域,即他决定了程序的哪些部分可以创建这种类对象。其次,和其他类一样,嵌套类的公有部分、保护部分和私有部分控制了外部对类成员的访问,在哪些地方可以使用嵌套类以及如何使用嵌套类,取决于作用域和访问控制。

1.作用域:如果嵌套类在另一个类的私有部分声明。只有那个包含类知道它。如果在保护部分声明,包含类和它所派生的类知道它。如果在共有部分声明,外部也知道它,可以使用类限定符来访问。

2.访问控制

类可见后,其决定作用的将是访问控制,包含类对嵌套类访问权的控制规则与对常规类相同。就是包含类对象只能显式地访问嵌套类对象的公有成员。

15.3异常

调用abort()

返回错误码

异常机制:

C++异常是对程序运行过程中发生的异常情况的一中相应。异常提供了将控制权从程序的一个部分传递到另一个部分的途径。对异常处理有三个组成部分:引发异常;捕获有处理程序的异常;使用try块。

throw语句实际上是跳转,即命令程序跳到另一条语句,throw关键字表示引发异常,紧随其后的值(例如字符串或对象)指出了异常的特征。

程序使用异常处理程序来捕获异常,处理程序以关键字catch开头。随后是位于括号中的类型声明,它指出了异常处理程序要响应的一场类型,然后是用一个花括号括起来的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应跳到这个位置执行,异常处理程序也被称为catch块。

try块表示其中特定的异常可能被激活的代码块,它后面跟一个或者几个catch块,try块是由关键字指示的,关键字try的后面是一个由花括号括起的代码块,表明需要注意这些代码引发的异常。

 

try {                   // start of try block

z = hmean(x,y);

}                       // end of try block

catch (const char * s) { // start of exception handler

std::cout << s << std::endl;

std::cout << "Enter a new pair of numbers: ";

continue;

}

double hmean(double a, double b)

{

    if (a == -b)

        throw "bad hmean() arguments: a = -b not allowed";

    return 2.0 * a * b / (a + b); 

}

 

将对象用作异常类型

exception类:C++可以把它用作其他异常类的基类,代码可以引发exception异常,也可以将exception类用做基类

1.stdexcept异常类

2.bad_alloc异常和new

异常、类和继承

15.4 RTTI

RTTI是运行阶段类型识别的简称,旨在为程序在运行阶段确定对象的类型提供一种标准方式。

RTTI工作原理

C++3个支持RTTI的元素:

1.dynamic_cast操作符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该操作符返回0—空操作符。

class Grand{//has virtual methods..}

class Superb : public Grand{…}

class Magnificent : public Superb{…}

dynamic_cast句法:Superb *ps = dynamic_cast<Superb *>(pg)

ps是否能被安全地转换为Superb *,如果可以,操作符将返回对象地址,否则返回一个空指针。

一般如果指向的对象(*pt)的类型为Type或者是从Type直接或简介派生而来的类型,

则表达式:dynamic_cast<Type*>(pt)将指针pt转换为Type类型的指针;否则,结果为0,即空指针。

typeid操作符返回一个指出对象的类型的值。

type_info结构存储了有关特定类型的信息。

只能将RTTI用于包含虚函数的类层次结构,原因在于只有于对这种类层次结构,才应该将派生对象的地址赋给基类指针。

posted @ 2014-08-27 19:07  tt_tt--->  阅读(104)  评论(0编辑  收藏  举报