C++ Primer Plus -- 第13章 笔记

类继承

       派生类对象包含基类对象。使用公有继承,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

    13.1 需要在继承特性中添加的东西:

    (1)派生类需要自己的构造函数;(2)派生类可以根据需要添加额外的数据成员和成员函数。

     13.2 构造函数:访问权限的考虑

(1)派生类不能直接访问基类的私有成员,必须通过基类方法进行访问。 (2)派生类构造函数必须使用基类构造函数。(3)创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建。 C++使用成员初始化列表来完成这个工作。

 一般情况下,都是基类对象首先被创建,如果不调用基类构造函数,程序将使用默认的基类构造函数。

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

(1)基类对象首先被创建;

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

(3)派生类构造函数应初始化派生类新增的数据成员。

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

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

(1)基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不显式类型转换的情况下引用派生类对象。

(2)基类指针或引用只能用于调用基类方法。

 13.5 继承--is a 关系

        公有继承是最常用的一种方式,它建立一种is-a关系,即派生类对象也是基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。继承可以在基类的基础上添加属性,但不能删除基类的属性。

13.6 多态公有继承

        使用Virtual,在进行多态继承的时候,如果方法是通过引用或指针而不是对象调用的,它将确定使用那种方法。如果没有使用关键字Virtual,程序将根据引用类型或指针类型选择方法;如果使用了Virtual,程序将根据引用或指针指向的对象的类型来选择方法。

        一般情况下会在基类中将派生类会重新定义的方法声明为虚方法。方法在基类中被声明为虚拟的后,它在派生类中将自动成为虚方法。一般在派生类中也用Virtual来指出那些函数是虚函数。

        派生类调用基类方法:标准的技术是使用作用解析操作符来调用基类方法。

        如果派生类没有重新定义基类成员函数的话,可以不必使用作用域解析操作符。

beer13.7 虚拟析构函数

        如果析构函数不是虚拟的,则将只调用对应于指针类型的析构函数。如果析构函数是虚拟的,将调用相应对象类型的析构函数。

13.8 静态联编和动态联编

        定义:将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在编译过程中进行的联编被称为静态联编。由于虚函数,编译器必须生成能够在程序运行时选择正确的虚方法代码,这被称为动态联编。

        关于两种联编:效率,为使程序能够在运行阶段进行决策,必须采用一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。如果类不会用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。这些情况下,使用静态联编更合理,效率也更高。Strousstrup说,C++的指导原则之一,不要为不使用的特性付出代价;概念模型,在设计类时,可能包含一些不在派生类重新定义的成员函数。只需要将那些预期将被重新定义的方法声明为虚拟的。

13.9 虚函数的工作原理

        编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表。虚函数表中存储了为类对象进行声明的虚函数的地址。

        基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中。

使用虚函数时的成本:每个对象都将增大,增大量为存储地址的空间;对每个类,编译器都创建一个虚函数地址表;每个函数调用都要执行一步额外的操作,即到表中查找地址。

有关虚函数的注意事项:

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

(2)如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。

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

注意:构造函数不能是虚函数。析构函数应当是虚函数,除非类不用做基类。友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果派生类没有重新定义函数,将使用该函数的基类版本。

如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征表如何。

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

   1: class Dwelling
   2: {
   3: public:
   4:     virtual Dwelling & build(int n);
   5:     ...
   6: };
   7:  
   8: class Hovel : public Dwelling
   9: {
  10: public:
  11:     virtual Hovel & build(int n); // same function signature
  12: };

第二:如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如下例:

 
1: class Dwelling 
2: {   
3: public:  
4:     virtual void showperks(int a) const;   
5:     virtual void showperks(double x) const;   
6:     virtual void showperks() const;   

7: };

 

   9: class Hovel : public Dwelling
  10: {
  11: public:
  12:     // three redefined showperks()
  13:     virtual void showperks(int a) const;
  14:     virtual void showperks(double x) const;
  15:     virtual void showperks() const;
  16:     ...

17: };

13.10 访问控制:protected

private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。

因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。

警告:最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。

单设计模式:希望有且只有一个类的实例返回给调用程序时,就可以使用单元素模式。这种类的声明如下:

   1:  class TheOnlyInstance
   2:  {
   3:    public:
   4:          static TheOnlyInstance * GetTheOnlyInstance();
   5:      protected:
   6:          TheOnlyInstance(){}
   8:  };

通过将TheOnlyInstance构造函数声明为protected,并省略公有构造函数,可以防止局部实例被创建:

   1: int main()
   2: {
   3:     TheOnlyInstance noCanDo; // not allowed.
   4: }

只有通过公有静态方法GetTheOnlyInstance来访问类。该方法被调用时,将返回类TheOnlyInstance的实例。

   1: TheOnlyInstance* TheOnlyInstance::GetTheOnlyInstance()
   2: {
   3:     static TheOnlyInstance objTheOnlyInstace;
   4:     return &objTheOnlyInstance;
   5: }

GetTheOnlyInstance方法仅在第一次被调用时,创建TheOnlyInstance类的一个实例。以这种方法构造的静态对象一直有效,直到程序终止,此时这种静态对象将自动被在、释放。要检索指向这个类的唯一一个实例的指针,只需要调用静态方法GetTheOnlyInstace,该方法返回但对象的地址:

   1: TheOnlyInstace* pTheOnlyInstace = TheOnlyInstance::GetTheOnlyInstace();

因为静态变量在函数调用结束后仍保存在内存中,所以以后再调用GetTheOnlyInstace时,将返回一个静态对象的地址。

13.11 抽象基类

C++使用纯虚函数提供未实现的函数。纯虚函数声明的结尾处=0,参见Area()方法:

   1: class BaseEllipse
   2: {
   3:     private:
   4:         double x;
   5:         double y;
   6:         ...
   7:     public:
   8:         BaseEllipse(double x0 = 0, double y0 = 0) : x(x0), y(y0){}
   9:         virtual ~BaseEllipse(){}
  10:         void Move(int nx, int ny){ x = nx; y = ny; }
  11:         virtual double Area() const = 0; // a pure virtual function.
  12: }

当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要称为真正的ABC,必须至少包含一个纯虚函数。原型中的=0使虚函数称为纯虚函数。C++甚至允许纯虚函数有定义。

http://it.dengchao.org/local-install-use-wordpress/wordpress/,看看如何安装wordpress。

13.12 继承和动态内存分配

第一种情况:派生类不使用new,基类使用new。

假设基类为下述使用了动态内存分配的类:

   1: class baseDMA
   2: {
   3:     private:
   4:         char * label;
   5:         int rating;
   6:  
   7:     public:
   8:         baseDMA(const char *l = null, int r = 0);
   9:         baseDMA(const baseDMA & rs);
  10:         virtual ~baseDMA();
  11:         baseDMA & operator=(const baseDMA & rs);
  12:         ...
  13: };

声明中包含了构造函数使用new时需要的特殊方法:析构函数、复制构造函数和重载复制操作符。

派生类:

   1: class lacksDMA: public baseDMA
   2: {
   3:     private:
   4:         char color[40];
   5:     public:
   6:     ...
   7: };

首先来看是否需要析构函数.如果没有定义析构函数,编译器将定义一个不执行任何操作的默认析构函数。实际上派生类的默认构造函数总是要进行一些操作:执行自身的代码后调用基类析构函数。派生类的默认复制构造函数使用显式基类复制构造函数来复制派生类对象的基类部分。对赋值来说也是如此。

第二种情况:派生类使用new,基类使用new。

   1: class hasDMA: public baseDMA
   2: {
   3:     private:
   4:         char * style;
   5:     public:
   6:         ...
   7: };

这种情况必须为派生类定义显式析构函数、复制构造函数和赋值操作符。派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作进行清理。因此,hasDMA析构函数必须释放指针style管理的内存,并依赖于baseDMA的析构函数来释放指针label管理的内存。

   1: baseDMA::~baseDMA()
   2: {
   3:     delete [] label;
   4: }
   1: hasDMA::~hasDMA()
   2: {
   3:     delete [] style;
   4: }

接下来是复制构造函数:baseDMA的复制构造函数遵循常规模式:

   1: baseDMA::baseDMA(const baseDMA & rs)
   2: {
   3:     label = new char[ std::strlen(rs.label) + 1 ];
   4:     std::strcpy( label, rs.label )    ;
   5:     rating = rs.rating;
   6: }

(突然想起来了,按值传递意味着创建原始变量的一个副本。编译器生成临时对象时,也将使用复制构造函数。初始化对象时,并不一定会使用赋值操作符。)

hasDMA复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据:

   1: hasDMA::hasDMA(const hasDMA & hs)
   2:     : baseDMA(hs)
   3: {
   4:     style = new char[ std::strlen(hs.style) + 1 ];
   5:     std::strcpy( style, hs.style );
   6: }

值得我们注意的是,成员初始化列表将一个hasDMA引用传递给baseDMA的构造函数。因为复制构造函数baseDMA有一个baseDMA引用参数,而基类引用可以指向派生类型。

接下来是赋值操作符。baseDMA遵循常规模式:

   1: baseDMA& baseDMA::operator=(const baseDMA & rs)
   2: {
   3:     if(this == &rs)
   4:         return *this;
   5:     delete [] label;
   6:     label = new char[ std::strlen(rs.label) + 1 ];
   7:     std::strcpy(label, rs.label);
   8:     rating = rs.rating;
   9:     return *this;
  10: }

由于hasDMA也使用动态内存分配,所以它也需要一个显式赋值操作符。作为hasDMA的方法,它只能访问hasDMA的数据。然而,派生类的显式赋值操作符必须负责所有继承的baseDMA基类对象的赋值,可以通过显式调用基类赋值操作符来完成这项工作:

   1: hasDMA & hasDMA::operator= (const hasDMA & hs)
   2: {
   3:     if(this == &hs)
   4:         return *this;
   5:     baseDMA::operator=(hs);
   6:     delete [] style;
   7:     style = new char[ std::strlen(hs.style) + 1 ];
   8:     std::strcpy( style, hs.style );
   9:     return *this;
  10: }

总之,当基类和派生类都采取动态内存分配时,派生类的析构函数、复制构造函数以及赋值操作符都必须使用相应的基类方法来处理基类元素。

派生类使用基类的友元:因为友元不是成员函数,所以不能使用作用域解析操作符来指出要使用那个函数。这个可以通过强制类型转换,以便匹配原型时能够选择正确的函数。

基类的友元函数:

   1: std::ostream & operator << (std::ostream & os, const baseDMA & rs)
   2: {
   3:     os << "Label: " << rs.label << std::endl;
   4:     os << "Rating: " << rs.rating << std::endl;
   5:     return os;
   6: }

派生类使用友元函数:

   1: std::ostream & operator << (std::ostream & os, const hasDMA & hs)
   2: {
   3:     os << (const baseDMA &) hs;
   4:     os << "Style: " << hs.style << endl;
   5:     return os;
   6: }

13.13 类设计回顾
1) 编译器生成的成员函数

1. 默认构造函数

        默认构造函数要么没有参数,要么所有的参数都有默认值。如果没有定义任何构造函数,编译器将定义默认构造函数,让你能够创建对象。自动生成的默认构造函数的另一项功能是,调用基类的默认构造函数以及调用本身是对象的成员所属类的默认构造函数。另外,如果派生类构造函数的成员初始化列表没有显式的调用基类构造函数,则编译器将使用基类的默认构造函数来构造派生类对象的基类部分。在这种情况下,如果基类没有构造函数,将导致编译阶段错误。如果定义了某种构造函数,编译器不会定义默认的构造函数。在这种情况下,如果需要默认构造函数,则必须自己提供。提供构造函数的动机之一是确保对象总能被正确地初始化。另外,如果类包含指针成员,则必须初始化这些成员。因此,最好提供一个显式默认构造函数,将所有的类数据成员都初始化为合理的值。

2.复制构造函数

复制构造函数接受其所属类的对象作为参数。在下述情况下,将使用复制构造函数:将新的对象初始化为一个同类对象。按值将对象传递给函数。函数按值返回对象。编译器生成临时对象。

3.赋值操作符

默认的赋值操作符用于同类对象之间的赋值。不要将赋值与初始化混淆了。

4.构造函数
       构造函数不同于其他类方法,因为它创建新的对象,而其他类方法只是被现有的对象调用。

5.析构函数

一定要定义显式析构函数来释放类构造函数使用new分配的所有内存,并完成类对象所需的任何特殊的清理工作。

6.转换

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

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

通常,编写使用对象作为参数的函数时,应按引用而不是按值来传递对象。这是为了提高效率。按值传递对象涉及生成临时拷贝,即调用复制构造函数,然后调用析构函数。另一原因是,在继承使用虚函数时,被定义为接受基类引用参数的函数可以接受派生类。

8.返回对象和返回引用

返回对象涉及到生成返回对象的临时拷贝,返回引用可以节省时间和内存。如果函数返回在函数中创建的临时对象,则不要使用引用。如果函数返回的是通过引用或指针传递给它的对象,则应按引用返回对象。

9.使用const

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值