• C++学习随笔之十:类的继承


    1.基类与派生类:

    从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。基本语法是:class SubClassName:public BaseClassName{};public表示公有派生,当然也有私有派生和受保护派生(private和protected)。派生类对象包含基类对象。使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将称为派生类的一部分,但只能通过基类的公有和保护方法访问。

    派生类不能直接访问基类的私有成员,而必须通过基类的的类方法进行访问。

    派生类构造函数:

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

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

    RatePlayer::RatePlayer(int r,cosnt char *fn,const  char *ln):TableTennisPlayer(fn,ln)

    {

    rating = r;

    }

    基类对象必须首先被创建,如果不调用基类构造函数,则程序使用默认的基类构造函数。一般情况,除非要使用默认构造函数,否则应该显式调用基类的构造函数。

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

    注意:创建派生类对象时,程序首先调用基类的构造函数,然后再调用派生类构造函数。基类构造函数复制初始化继承来的数据成员;派生类构造函数主要用于初始化新增的数据成员。派生类构造函数总是调用一个基类构造函数。派生类对象过期时,程序首先调用派生类析构函数,然后再调用基类析构函数。

    2.派生类和基类直接的特殊关系

    派生类和基类之间存在着一些微妙的关系,主要有:

    (1)派生类对象可以使用基类的方法,但是方法不是私有的

    (2)基类指针可以在不进行显示类型转换的情况下指向派生类对象;引用也是,基类引用可以在不进行显示类型转换的情况下引用派生类对象;不过基类指针和引用只能用于调用基类方法,不能调用派生类的方法(不考虑虚函数)

    (3)不能将基类对象和地址赋给派生类引用和指针。

    (4)引用兼容性也可以将基类对象初始化为派生类对象

    (5)也可以将派生类对象赋给基类对象

    3.继承————is-a关系

    虽然在C++中,完全可以使用公有继承来建立has-a,is-imlimented-as-a或uses-a关系模型,但是为了避免编程方面的问题,建议坚持使用is-a关系。

    4.多态公有继承

    当一个方法在基类和派生类中的行为不同时,重新修改基类中的那个方法,使之具有多态。一般有两种机制用于实现多态公有继承:

    (1)在派生类中重新定义基类的方法

    (2)使用虚函数(虚方法)

    第一种就不说了,说说第二种吧,虚函数,关键字是virtual,在声明中需要virtual关键字,在定义实现中不用。

    至于为什么要使用虚函数呢?

    如果方法是通过引用或指针而不是对象调用的,如果没有采用虚函数方法,则程序根据引用类型或指针类型选择方法,如果采用虚函数了,则程序根据引用或者指针指向的对象的类型来选择方法。

    例如:有个 Brass类,SubBrass是Brass的派生类,都有一个叫Show()的方法,

    Brass tom ;

    SubBrass jack;

    Brass &pt1 = tom;

    Brass &pt2 = jack;

    pt1.Show();

    pt2.Show();

    如果show()不是虚函数的话,上面最后两行分别是

    pt1.Show(); //调用Brass::Show();

    pt2.Show();//调用Brass::Show();

    如果show()是虚函数的话,上面最后两行分别是

    pt1.Show(); //调用Brass::Show();

    pt2.Show();//调用SubBrass::Show();

    虚拟析构函数:是为了确保释放对象的时候按照正确的顺序调用洗过后函数。

    注意:通常如果在派生类中重新定义基类的方法,一般都应将基类方法声明为徐您的,这样程序将根据引用或指针指向的对象来选择方法。为基类声明一个虚拟析构函数也是一种惯例。

    静态联编和动态联编:

    联编:将源代码中函数调用解释为执行特定的函数代码成为函数名联编。C/C++在编译过程中进行的联编称之为静态联编,又称为早期联编;在程序运行过程中联编称之为动态联编,也称为晚期联编。编译器对非虚拟方法使用静态联编,对虚拟方法使用动态联编。

    注意:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则设置为非虚方法。

    虚函数的工作机制:

    C++规定了虚函数的行为,但将实现方法留给了编译器作者。不需要知道实现方法就可以使用虚函数,但是了解虚函数的工作原理有助于更好的理解虚函数:

    通常,编译器处理虚函数的方法是:给每个类对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类

    将包含一个指向对立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,则该vtbl将保存

    函数原始版本的地址。如果派生类定义了 新的虚函数,则该函数的地址也将被添加到vtbl中。但是注意,无论类中包含多少个虚函数,都只需要在对象中添加1个地址成员表,只是表的大小不一样。

    有关虚函数的注意事项:

    (1)在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类(包括从派生类的派生出来的派生类)中是虚拟的,因为方法在基类中被声明为虚拟的后,它在派生类中自动默认为虚拟的。

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

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

    (4)构造函数不能是虚函数,因为派生类不能继承构造函数,所以没有什么意义

    (5)虚构函数应当是虚函数,除非类不做基类。通常应该给基类提供一个虚析构函数,即使它不需要析构函数。

    (6)友元函数不能是虚函数,因为友元不是类成员,而只有类成员才能是虚函数。如果由于这个原因而导致了设计问题 ,可以通过友元函数使用虚函数来解决。

    (7)如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生类链中,则将使用最新的虚函数方法办法,如果基类是隐藏的除外。

    (8)重新定义 隐藏方法:

    class BaseClass

    {

    public:

    virtual void Show(int a)const;

    ...

    }

    class SubClass : public BaseClass

    {

    public:

    virtual void Show()const;

    ...

    }

    有如下代码

    SubClass sc;

    sc.Show();//这个是有效的,调用的是SubClass的方法

    sc.Show(5);//这个是无效的,因为SubClass 类中的方法已经隐藏了基类BassClass的Show()方法。

    这就出现了两条经验规则:

    第一,如果重新定义继承的方法,应确保与原来的原型完全一致,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性称为

    返回类型协变(covariance of return type),因为返回类型随类类型的变化而变化:

    lass BaseClass

    {

    public:

    virtual BaseClass & Show(int a)const;

    ...

    }

    class SubClass : public BaseClass

    {

    public:

    virtual SubClass & Show()const;

    ...

    }

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

    class BaseClass

    {

    public:

    virtual void Show(int a)const;

    virtual void Show(double x)const;

    virtual void Show()const;

    ...

    }

    class SubClass : public BaseClass

    {

    public:

    virtual void Show(int a)const;

    virtual void Show(double x)const;

    virtual void Show()const;

    ...

    }

    这是因为如果只重新定义一个版本,其他另外两个版本将被隐藏,派生类对象无法使用它们。还有就是,如果不需要修改,则新定义可只调用基类版本。

    5.Protected访问控制:

    protected和private 相似,在类外只能用公有类成员访问protected部分中的类成员。protected与private的区别只有在派生类中能体现出来,派生类的成员可以直接访问基类的保护成员,但不能访问基类的私有成员,因此,对于外部,保护成员的行为与private相似,对于派生类来说,保护成员与public相似。

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

    6.单设计模式:

    希望有且只有一个类的实列返回给调用程序时,就可以使用单元素模式(singleton pattern):

      class  TheOnlyInstance

    {

    public:

    static TheOnlyInstance * GetTheOnlyInstance(); //工厂方法,用来获得实例

    protected:

    TheOnlyInstance(){}

    };

    通过 把构造函数声明为protected,并去掉公有构造。防止局部实例被创建。

    TheOnlyInstance the; // not allowed

    只能通过公有静态方法GetTheOnlyInstance 来访问类。

    通过 把构造函数声明为protected,并去掉公有构造。防止局部实例被创建。

    TheOnlyInstance *  TheOnlyInstance : GetTheOnlyInstance()

    {

    static TheOnlyInstance obj;

    return &obj;

    }

    检索指向这个类的指针,只需要调用GetTheOnlyInstance()

    TheOnlyInstance* pTheOnlyInstance = TheOnlyInstance: GetTheOnlyInstance();

    单设计模式C++代码实现:

    #include <iostream>   

    using namespace std;   

    //单例类的C++实现   

    class Singleton   

    {   

    private:   

    Singleton();//注意:构造方法私有   

    virtual ~Singleton();   

    static Singleton* instance;//惟一实例   

    int var;//成员变量(用于测试)   

    public:   

    static Singleton* GetInstance();//工厂方法(用来获得实例)   

    int getVar();//获得var的值   

    void setVar(int);//设置var的值   

    };   

    //构造方法实现   

    Singleton::Singleton()   

    {   

    this->var = 20;   

    cout<<"Singleton Constructor"<<endl;   

    }   

    Singleton::~Singleton()   

    {   

         delete instance;   

    }   

    //初始化静态成员   

    Singleton* Singleton::instance=new Singleton();   

    Singleton* Singleton::GetInstance()   

    {   

    return instance;   

    }   

    //seter && getter函数   

    int Singleton::getVar()   

    {   

    return this->var;   

    }   

    void Singleton::setVar(int var)   

    {   

    this->var = var;   

    }   

    //main   

    int main(int argc, char* argv[])   

    {   

    Singleton *ton1 = Singleton::GetInstance();   

    Singleton *ton2 = Singleton::GetInstance();   

    cout<<"ton1 var = "<<ton1->getVar()<<endl;   

    ton1->setVar(150);

    cout<<"ton2 var = "<<ton2->getVar()<<endl;

    return 0;   

    }

    输出如下:

    Singleton Constructor

       ton1 var = 20

       ton2 var = 150

    在输出结果中,构造方法只调用了一次,ton1与ton2是指向同一个对象的。

      

    7.抽象基类和纯虚函数:

    ★抽象类:一个类可以抽象出不同的对象来表达一个抽象的概念和通用的接口,这个类不能实例化(创造)对象。

    ★纯虚函数(pure virtual):在本类里不能有实现(描述功能),实现需要在子类中实现。

    例:

     virtual typeT function_name(parameter_list)=0;

     

     virtual void draw()=0; //画,纯虚函数;

     virtual void rotate(double)=0; //旋转,纯虚函数;

    ★抽象类(abstract class):如果一个类包含纯虚函数,那么这个类就叫抽象类。

    ★一个抽象类只能用作基类,不能能用作派生,不能实例化(创建)对象。一个类要是包含至少一个纯虚函数,则这个类是抽象类。一个抽象类的子类可以添加更多的数据成员和成员函数。

    ★抽象类的子类可以还是抽象类,可以添加更多的成员函数和成员方法,直到可以产生对象为止。

    ★由于抽象类不能构造对象,因此它的构造函数不能被单独调用。它的构造函数只能在子类的成员初始化列表里面调用。

    ★抽象类不一定有析构函数,如果有必须是虚析构函数。

    ★★★一个函数不能有抽象类对象的值参数<参数不能传值>,这个函数不能有抽象类对象的值返回。然而可以有抽象类类型的指针和引用可以作为参数,同样抽象类的指针和引用可以作为函数的返回值类型。因为他们可以指向或者引用抽象类的子类对象。

    抽象基类接口规则:抽象基类要求具体派生类覆盖其纯虚函数,迫使派生类遵循抽象基类所设置的规则。

    8.继承和动态内存分配:

    (1)派生不使用new

    先调用派生析构函数对派生新成员进行处理,再调用基类的析构函数。(通常需要在基类中把析构函数定义为虚拟,因为

    在调用基类引用或指针的时候,析构函数就只能处理基类对象的,派生类将无法处理,所以定义为虚拟后,会安要求

    顺序,正确处理)

    如派生类没用new则,在派生类中,默认析构函数,默认复值构造函数,默认赋值函数都符合要求,也即是说不需要定义显示析构函数,复制构造函数和赋值操作符

    (2)派生类使用new

    如果派生类使用new,则必须为派生类定义显示析构函数,复制构造函数和赋值操作符。

    派生类的友元函数:通常需要在其中嵌套调用基类的对应友元函数,完成对基类成员的操作

    9.静态成员的继承:

    在一个类中还可以定义静态成员,但静态成员是所有对象公有的。静态成员分为静态数据成员和静态成员函数。

    1.静态数据成员

    在类中定义静态数据成员的方法就是在该成员的前面加上关键字static.

    定义静态数据成员的语句格式如下:

    class 类名

    {

    ……

    static 类型说明符 成员名;

    ……

    };

    静态数据成员是类的所有对象共享的成员。静态数据成员所占的空间不会随着对象的产生而分配,也不会随着对象的消失而回收。对静态数据成员的操作和类中一般数据成员的操作是不一样的,定义为私有的静态数据成员不能被外界所访问。静态数据成员可由任意访问权限许可的函数所访问。

    由于静态数据成员是类的所有对象共享的,而不从属于任何一个具体对象,所以必须对类的静态数据成员进行初始化,但对它的初始化不能在类的构造函数中进行,其初始化语句应当写在程序的全局区域中,并且必须指明其数据类型与所属的类名,其初始化格式如下:

    类型 类名::变量名=值;

    对于在类的public部分说明的静态数据成员,在类的外部可以不使用成员函数而直接访问,但在使用时必须用类名指明所属的类,其访问格式为:

    类名::静态数据成员名

    对于在类的非public部分说明的静态数据成员,则只能由类的成员函数访问,其访问方法与访问类中普通数据成员的访问方法完全一样,但在类的外部不能访问。

    2.静态成员函数

    静态成员函数的定义与一般成员函数的定义相同,只是在其前面冠以static关键字,其定义格式如下:

    class 类名

    {

    static 类型 函数名(形参)

    {   函数体   }

    };

    说明:

    (1)类的静态成员函数只能访问类的静态数据成员,而不能访问类中的普通函数成员(非静态数据成员),因为普通数据成员只有类的对象存在时才有意义。

    (2)静态成员函数与类相联系,而不与类的对象相联系,所以,在类的外部调用类中的公有静态成员函数,必须在其左面加上“类名::”,而不是通过对象名调用公有静态成员函数。在类的外部不能调用类中的私有静态成员函数。

    10.类总结:

    学到现在了,对类继承有了比较深的了解了,现在就到目前为止作个总结吧。

    (1)派生类从基类那里继承了什么?又不能从基类那里继承到什么呢?

    基类的公有成员成为派生类的公有成员,基类的保护成员成为了派生类的保护成员,基类的私有成员被继承,但是不能直接访问。不能继承的有构造函数、析构函数、赋值操作符和友元。

    (2)构造函数:

    构造函数不同于其他函数,因为它要创建对象,要么没有参数,要么所有参数都要有默认值。

    (3)析构函数:

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

    (4)赋值操作符:

    是不能被继承的,如果类构造函数使用new来初始化指针,则需要提供一个显示赋值操作符,因为对于派生对象的基类部分,C++将使用基类的操作符,所以不需要为派生类重新定义赋值操作符,除非它添加了需要特别留意的数据成员。不过,如果派生类使用new,则必须提供显示赋值操作符。必须给类的每个成员提供赋值操作符,不仅仅是新成员。

    将派生类对象赋给基类对象会如何呢?答案是 可以,但只涉及到基类的成员。那反过来呢,将基类对象赋给派生类对象呢?答案是也许。如果派生类包含了这样的构造函数,即对将基类对象转换为派生类对象进行了定义,则可以将基类对象赋给派生类对象。如果派生类定义了用于将基类对象赋给派生类对象的赋值操作符,则也可以这样做。如果上述连个条件都不满足,则不能将基类对象赋给派生类对象,除非使用显示强制类型转换。

    (5)友元:

    由于友元非类成员,故不能继承。但是,如果希望派生类的友元函数能够使用基类的友元函数,可以这样做,即通过强制类型转换将派生类引用或坐或站转换为基类引用或指针,然后使用转换后的指针或引用来调用基类的友元函数:

    ostream & operator<<(ostream & os ,const SubClassName & sc)

    {

    os <<(const BaseClassName &)sc;//转换派生类的引用为基类的引用

    ...

    return os;

    }

    当然也可以用操作符dynamic_cast<>来进行强制类型转换。

    (6)虚方法:

    在设计基类时,要注意是否要将类方法声明为虚拟的。如果希望派生类能够重新定义方法,则应在基类中将方法声明为虚拟的。如果不希望的话,则不必。

    (7)使用基类方法的有关说明:

     ● 派生类对象自动使用类继承的基类方法,如果派生类没有重新定义

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

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

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

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

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

    (8)类成员函数属性总结表:

       成员函数属性

     

  • 相关阅读:
    Android SDK Manager下载和更新失败的解决方法!!!
    java反射详解
    Maven--->学习心得--->maven project的标准目录结构
    Maven--->学习心得--->maven 的生命周期(LifeCycle)
    Maven--->学习心得--->maven的配置文件pom.xml
    硬盘管理
    JavaScript------>调试JavaScript代码------>使用 浏览器 中的 “开发者工具” 来调试
    java框架---->spring框架---->使用实例
    step5--->往工程中添加Spring框架---->修改maven的配置文件pom.xml,向工程中添加spring框架的某些模块
    JavaScript------第一课
  • 原文地址:https://www.cnblogs.com/JczmDeveloper/p/2964823.html
Copyright © 2020-2023  润新知