zoukankan      html  css  js  c++  java
  • C++继承和多态

    C++继承和多态

    继承和派生

    C++ 中的继承是类与类之间的关系,继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。

    派生(Derive)和继承是一个概念,

    被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼

    • 当你创建的新类与现有的类相似
    • 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承
    class Student: public People
    

    继承的一般语法为:

    class 派生类名:[继承方式] 基类名{
        派生类新增加的成员
    };
    

    继承方式: 包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。

    C++三种继承方式

    继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为 private(成员变量和成员函数默认也是 private)

    public、protected、private 指定继承方式

    1) public继承方式

    • 基类中所有 public 成员在派生类中为 public 属性;
    • 基类中所有 protected 成员在派生类中为 protected 属性;
    • 基类中所有 private 成员在派生类中不能使用。

    2) protected继承方式

    • 基类中的所有 public 成员在派生类中为 protected 属性;
    • 基类中的所有 protected 成员在派生类中为 protected 属性;
    • 基类中的所有 private 成员在派生类中不能使用。

    3) private继承方式

    • 基类中的所有 public 成员在派生类中均为 private 属性;
    • 基类中的所有 protected 成员在派生类中均为 private 属性;
    • 基类中的所有 private 成员在派生类中不能使用。
    • 继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
    • 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)
    • 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
    • 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

    类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

    在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问

    #include<iostream>
    using namespace std;
    //基类People
    class People{
    public:
        void setname(char *name);
        void setage(int age);
        void sethobby(char *hobby);
        char *gethobby();
    protected:
        char *m_name;
        int m_age;
    private:
        char *m_hobby;
    };
    void People::setname(char *name){ m_name = name; }
    void People::setage(int age){ m_age = age; }
    void People::sethobby(char *hobby){ m_hobby = hobby; }
    char *People::gethobby(){ return m_hobby; }
    //派生类Student
    class Student: public People{
    public:
        void setscore(float score);
    protected:
        float m_score;
    };
    void Student::setscore(float score){
        m_score=score;
    }
    //派生类Pupil
    class Pupil: public Student{
    public:
        void display();
        void setranking(int ranking);
    private:
        int m_ranking;
    };
    void Pupil::display(){
        cout<<m_name<<m_age<<m_score<<m_ranking<<gethobby()<<endl;//hobby
    }//派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问
    void Pupil::setranking(int ranking){
        m_ranking=ranking;
    }
    

    改变访问权限

    使用 using 关键字可以改变基类成员在派生类中的访问权限

    using 只能改变基类中 public 和 protected 成员的访问权限, private 成员不能改变,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

    #include<iostream>
    using namespace std;
    
    //基类People
    class People{
    public:
        void show();
    protected:
        char *m_name;
        int m_age;
    };
    void People::show(){
        cout<<m_name<<m_age<<endl;
    }
    
    class Student:public People{
    public:
        void learning();
    public:
        using People::m_name;
        using People::m_age;
        float m_score;
    private:
        using People::show;//public 改为private, 函数不加()
    };
    void Student::learning(){
        cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
    }
    //使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。
    

    C++继承时的名字遮蔽

    如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的

    基类成员函数和派生类成员函数不构成重载

    对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样

    C++基类和派生类的构造函数

    基类的成员函数可以被继承,类的构造函数不能被继承。

    对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,派生类中无法访问,

    解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数

    示例代码:

    #include<iostream>
    using namespace std;
    class People{
    protected:
        char *m_name;
        int m_age;
    public:
        People(char*, int);
    };
    People::People(char *name, int age): m_name(name), m_age(age){}
    //派生类Student
    class Student: public People{
    private:
        float m_score;
    public:
        Student(char *name, int age, float score);
        void display();
    };
    //People(name,age)就是调用基类的构造函数
    Student::Student(char *name, int age, float score):People(name,age),m_score(score){
        
    }
    //也可以将基类构造函数的调用放在参数初始化表后面:
    //Student::Student(char *name, int age, float score): m_score(score), //People(name, age){ }
    
    //但不能放在函数体内
    //Student::Student(char *name, int age, float score){
    //    People(name, age); 因为People构造函数没有被继承。
    //    m_score = score;
    //}
    
    void Student::display(){
        cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
    }
    

    构造函数的调用顺序

    A --> B --> C

    A类构造函数 --> B类构造函数 --> C类构造函数

    构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

    因为在调用B构造函数时,A已经调用了,C再调用就是重复调用,因此C++禁止在 C 中显式地调用 A 的构造函数

    基类构造函数调用规则

    定义派生类构造函数时最好指明基类构造函数;如果不指明(在派生类构造函数的初始化列表前未调用),就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。

    C++基类和派生类的析构函数

    析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉
    析构函数的执行顺序和构造函数的执行顺序刚好相反:

    • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
    • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

    C++多继承(多重继承)详解

    派生类都只有一个基类,称为单继承(Single Inheritance)

    一个派生类可以有两个或多个基类,称为多继承(Multiple Inheritance)

    class D: public A, private B, protected C{
        //类D新增加的成员
    }
    

    多继承下的构造函数:

    多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:

    D(形参列表): A(实参列表), B(实参列表), C(实参列表){///和声明派生类时基类出现的顺序相同
        //其他操作
    }
    

    命名冲突:

    当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::以显式地指明到底使用哪个类的成员,消除二义性

    #include <iostream>
    using namespace std;
    //基类
    class BaseA{
    public:
        BaseA(int a, int b);
        ~BaseA();
    public:
        void show();
    protected:
        int m_a;
        int m_b;
    };
    BaseA::BaseA(int a, int b): m_a(a), m_b(b){
        cout<<"BaseA constructor"<<endl;
    }
    BaseA::~BaseA(){
        cout<<"BaseA destructor"<<endl;
    }
    void BaseA::show(){
        cout<<"m_a = "<<m_a<<endl;
        cout<<"m_b = "<<m_b<<endl;
    }
    //基类
    class BaseB{
    public:
        BaseB(int c, int d);
        ~BaseB();
        void show();
    protected:
        int m_c;
        int m_d;
    };
    BaseB::BaseB(int c, int d): m_c(c), m_d(d){
        cout<<"BaseB constructor"<<endl;
    }
    BaseB::~BaseB(){
        cout<<"BaseB destructor"<<endl;
    }
    void BaseB::show(){
        cout<<"m_c = "<<m_c<<endl;
        cout<<"m_d = "<<m_d<<endl;
    }
    //派生类
    class Derived: public BaseA, public BaseB{
    public:
        Derived(int a, int b, int c, int d, int e);
        ~Derived();
    public:
        void display();
    private:
        int m_e;
    };
    Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
        cout<<"Derived constructor"<<endl;
    }
    Derived::~Derived(){
        cout<<"Derived destructor"<<endl;
    }
    void Derived::display(){
        BaseA::show();  //调用BaseA类的show()函数
        BaseB::show();  //调用BaseB类的show()函数
        cout<<"m_e = "<<m_e<<endl;
    }
    

    C++虚继承和虚基类详解

    多继承(Multiple Inheritance), 指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。

    //间接基类A
    class A{
    protected:
        int m_a;
    };
    //直接基类B
    class B: public A{
    protected:
        int m_b;
    };
    //直接基类C
    class C: public A{
    protected:
        int m_c;
    };
    //派生类D
    class D: public B, public C{
    public:
        void seta(int a){ m_a = a; }  //命名冲突
        void setb(int b){ m_b = b; }  //正确
        void setc(int c){ m_c = c; }  //正确
        void setd(int d){ m_d = d; }  //正确
    private:
        int m_d;
    };
    int main(){
        D d;
        return 0;
    }
    

    类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。

    void seta(int a){ B::m_a = a; }//使用 B 类的 m_a
    
    void seta(int a){ C::m_a = a; }//使用 C 类的
    

    虚继承(Virtual Inheritance)

    为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

    在继承方式前面加上 virtual 关键字就是虚继承

    class A{
    protected:
        int m_a;
    };
    class B: virtual public A{
    protected:
        int m_b;
    };
    class C: virtual public B{
    protected:
        int m_c;
    };
    class D: public B, public C{
    protected:
        int m_d;
    public:
        void set_a(int a){m_a=a};
        void set_b(int b){m_b=b};
        void set_c(int c){m_c=c};
        void set_d(int d){m_d=d};
    };
    

    virtual class

    虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

    必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。

    虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身

    虚基类成员的可见性

    该成员被两条或多条路径覆盖了,不能直接访问了,此时必须指明该成员属于哪个类。(::)

    以上图中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:

    • 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 B 的成员,此时不存在二义性。
    • 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高
    • 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。

    C++虚继承时的构造函数

    #include <iostream>
    using namespace std;
    class A{
    public:
        A(int a);
    protected:
        int m_a;
    };
    A::A(int a): m_a(a){ }
    //直接派生类B
    class B: virtual public A{
    public:
        B(int a, int b);
    public:
        void display();
    protected:
        int m_b;
    };
    //直接派生类C
    class C: virtual public A{
    public:
        C(int a, int c);
    public:
        void display();
    protected:
        int m_c;
    };
    C::C(int a, int c): A(a), m_c(c){ }
    void C::display(){
        cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
    }
    
    //间接派生类D
    class D:public B, public C{
    public:
        D(int a, int b, int c, int d);
    public:
        void display();
    private:
        int m_d;
    };
    D::D(int a, int b, int c, int d):A(a), B(90,b), C(100,c), m_d(d){ }
    void D::display(){
        cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
    }
    

    D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类

    虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参

    虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;

    C++将派生类赋值给基类(向上转型)

    类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)

    向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。

    将派生类对象赋值给基类对象

    #include <iostream>
    using namespace std;
    //基类
    class A{
    public:
        A(int a);
    public:
        void display();
    public:
        int m_a;
    };
    A::A(int a): m_a(a){ }
    void A::display(){
        cout<<"Class A: m_a="<<m_a<<endl;
    }
    //派生类
    class B: public A{
    public:
        B(int a, int b);
    public:
        void display();
    public:
        int m_b;
    };
    B::B(int a, int b): A(a), m_b(b){ }
    void B::display(){
        cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl;
    }
    int main(){
        A a(10);
        B b(66, 99);
        //赋值前
        a.display();
        b.display();
        cout<<"--------------"<<endl;
        //赋值后
        a = b; //派生类赋值给基类
        a.display();
        b.display();
        return 0;
    }
    

    赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。

    对象之间的赋值不会影响成员函数,也不会影响 this 指针。

    只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值

    将派生类指针赋值给基类指针

    对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。

    • 通过基类指针访问派生类的成员

      将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数

      编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。

    • 赋值后值不一致的情况
    • 将派生类引用赋值给基类引用

    引用变量在功能上等于一个指针常量,本质上是通过指针的方式实现的

    基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的

    int main(){
    	D d(4,40,400,4000);
        A &ra=d;
        B &rb=d;
        C &rc=d;
    }
    

    C++多态和虚函数

    当基类指针 p 指向派生类,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数

    为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在基类函数声明前面增加 virtual 关键字。

    虚函数使得基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员

    基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态

    多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。

    C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

    虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。

    不是根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数

    借助引用也可以实现多态

    People p("王志刚", 23);
    Teacher t("赵宏佳", 45, 8200);
    People &rt = t;
    rt.display();
    

    引用只能指代固定的对象,在多态性方面缺乏表现力

    对于具有复杂继承关系的大中型程序,多态可以增加其灵活性,让代码更具有表现力

    为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数

    当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。

    只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)

    构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。

    析构函数可以声明为虚函数,而且有时候必须要声明为虚函数

    下面是构成多态的条件:

    • 必须存在继承关系;
    • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
    • 存在基类的指针,通过该指针调用虚函数。

    成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

    纯虚函数

    纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

    包含纯虚函数的类称为抽象类(Abstract Class)。

    抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

    抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

    可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现,

    虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

    关于纯虚函数的几点说明

    1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
    2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

    C++ typeid运算符:获取类型信息

    typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;

    typeid 的使用非常灵活,它的操作数可以是普通变量、对象、内置类型(int、float等)、自定义类型(结构体和类),还可以是一个表达式

    类型比较 结果 类型比较 结果
    typeid(int) == typeid(int) true typeid(int) == typeid(char) false
    typeid(char*) == typeid(char) false typeid(str) == typeid(char*) true
    typeid(a) == typeid(int) true typeid(b) == typeid(int) true
    typeid(a) == typeid(a) true typeid(a) == typeid(b) true
    typeid(a) == typeid(f) false typeid(a/b) == typeid(int) true

    表达式typeid(*p1) == typeid(Base)typeid(p1) == typeid(Base*)的结果为 true 可以说明:即使将派生类指针 p2 赋值给基类指针 p1,p1 的类型仍然为 Base*。

    指针的类型声明后就不变了

    C++运算符重载基础教程

    重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的操作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。

    #include <iostream>
    using namespace std;
    class complex{
    public:
        complex();
        complex(double real, double imag);
    public:
        //声明运算符重载
        complex operator+(const complex &A) const;
        void display() const;
    private:
        double m_real;
        double m_imag;
    };
    
    complex complex::operator+(const complex &A) const{
        complex B;
        B.m_real=this.m_real+A.m_real;
        B.m_imag=this.m_imag+A.m_imag;
        return B;
    }
    
    

    运算符重载格式:

    返回值类型 operator 运算符名称 (形参表列){
        //TODO:
    }
    
    
    

    operator是关键字,专门用于定义重载运算符的函数。我们可以将operator 运算符名称这一部分看做函数名

    c3 = c1.operator+(c2);
    
    
  • 相关阅读:
    android动画坐标定义
    Android animation 动画背景图自动播放的实现
    松开手,你可以拥有更多
    Serializable继承和用途
    程序员必备的国外IT网站
    android 解析json数据格式
    免费的天气预报API谷歌,雅虎,中央气象台
    关于Lockfree Hash Table的一些链接资料
    字符串匹配算法之Brute force algorithm
    java 处理高精度计算
  • 原文地址:https://www.cnblogs.com/Zak-NoS/p/11673942.html
Copyright © 2011-2022 走看看