继承 订阅
按照法律或遵照遗嘱接受死者的财产、职务、头衔、地位等。 展开全文
按照法律或遵照遗嘱接受死者的财产、职务、头衔、地位等。
信息
词    目
继承
拼    音
jì chéng
基本解释
依法承受等
词    性
动词
用    法
谓语等
组    词
~权,~人等
继承基本信息
【词目】继承【拼音】jì chéng【基本解释】泛指把前人的作风、文化、知识等接受过来。
收起全文
精华内容
下载资源
问答
  • 继承
    万次阅读 多人点赞
    2021-03-02 14:58:30

    目录

    前言

    继承

    继承的优点

    重写和隐藏父类方法

    重写父类中的方法

    隐藏父类中的方法

    方法重写和隐藏后的修饰符

    子类访问父类私有成员

    使用super关键字

    使用super调用父类中重写的方法、访问父类中被隐藏的字段

    使用super调用父类的无参数构造方法/有参数构造方法

    最后


    前言

    继承是面向对象语法的三大特征之一。继承可以降低代码编写的冗余度,提高编程的效率。通过继承,子类获得了父类的成员变量和方法。一个子类如何继承父类的字段和方法,如何修改从父类继承过来的子类的方法呢。今天我们开始学习有关Java继承的知识。

    继承

    继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

    继承的作用:通过继承可以快速创建新的类,实现代码的重用,提高程序的可维护性,节省大量创建新类的时间,提高开发效率和开发质量。

    在 Java 中通过 extends 关键字可以申明一个类是从另外一个类继承而来的,一般形式如下:

    class 父类{
        ...       //成员变量、成员方法
    }
    class 子类 extends 父类{
        ...       //类体
    }

    例如:

    class teacher{             //声明一个teacher类为父类
    	String name;      	   //定义父类的成员变量name、age   
    	int age;
    	void show(){           //定义父类成员方法,将成员变量输出
    		System.out.println(name); 	  
    		System.out.println(age); 
    	}
    }
    class Student extends teacher {     //声明一个Student类为子类并继承父类
    }
    public class myfirst {
    	public static void main(String[] args) {
    	System.out.println("学生");
    	Student student=new Student();     //声明一个Student类的实例对象student
    	student.name="Tom";                //子类调用父类的成员变量name并赋值
    	student.age=19;                    //子类调用父类的成员变量age并赋值
    	student.show();                    //子类调用父类的成员方法show
    	}
    }

    运行结果为:

    学生
    Tom
    19

    注意:

    • 子类不能选择性继承父类;
    • Java不支持多重继承,但一个类可以实现多个接口,从而克服单继承的缺点;
    • 构造方法不会被子类继承,但可以从子类中调用父类的构造方法。

    继承的优点

    • 继承过来的字段和方法,可以像任何其他字段和方法一样被直接使用;
    • 在子类中可以声明一个与父类中同名的新字段或静态方法,从而“隐藏”父类中的字段或方法;
    • 可以在子类中声明一个在父类中没有的新字段和方法;
    • 可以在子类中编写一个父类当中具有相同名的新实例方法,这称为“方法重写”或“方法覆盖”;
    • 可以在子类中编写一个调用父类构造方法的子类构造方法,既可以隐式地实现,也可以通过使用关键字super来实现。

    重写和隐藏父类方法

    子类继承了父类中的所有成员及方法,但在某种情况下,子类中该方法所表示的行为与其父类中该方法所表示的行为不完全相同,例如,在父类语言中定义了说话这个方法,而在子类中说话的方法是不同的:外国人说英文,中国人说中文,这时我们就需要重写或隐藏父类的该方法。

    重写父类中的方法

    当一个子类中一个实例方法具有与其父类中的一个实例方法相同的签名(指名称、参数个数和类型)和返回值时,称子类中的方法“重写”了父类的方法。例如:

    class A{
    	public void sayHello() {                      //输出英文欢迎
    		System.out.println("Hello,Welcome to Java!!!");
    	}
    	public void sayBye() {
    		System.out.println("GoodBye,everyone");
    	}
    }
    class B extends A {           
        public void sayHello() {                      //输出中文欢迎  
        	System.out.println("大家好,欢迎学习Java!!!");
        }
    }
    public class myfirst {
    	public static void main(String[] args) {
    	B b=new B();                                //创建子类B的一个实例对象,使用默认构造方法
    	b.sayHello();                               //调用子类中重写的方法
    	b.sayBye();                                 //调用父类中的方法
    	}
    }

    运行结果为:

    大家好,欢迎学习Java!!!
    GoodBye,everyone

    注意:重写的方法具有与其所重写的方法相同的名称、参数数量、类型和返回值。

    隐藏父类中的方法

    如果一个子类定义了一个静态类方法,而这个类方法与其父类的一个类方法具有相同的签名(指名称、参数格式和类型)和返回值,则称在子类中的这个类方法“隐藏”了父类中的该类方法。

    • 当调用被重写的方法时,调用的版本是子类的方法;
    • 当调用被隐藏的方法时,调用的版本取决于是从父类中调用还是从子类中调用。
    class A{
    	public static void sayHello() {             //静态类方法
    		System.out.println("大家好,这是A的静态类方法");
    	}
    	public void sayHello2() {                   //实例方法
    		System.out.println("大家好,这是A中的实例方法");
    	}
    }
    class B extends A {    
        public static void sayHello() {             //静态类方法
        	System.out.println("大家好,这是B的静态类方法");
        }
        public void sayHello2() {                   //实例方法
        	System.out.println("大家好,这是B的实例方法");
        }
    }
    public class myfirst {
    	public static void main(String[] args) {
    	    B b=new B();                           //创建B类的实例对象b
    	    A a=b;                                 //隐式对象类型转换
    	    A.sayHello();                          //调用A类的静态类方法
    	    a.sayHello();                          //调用a对象的静态类方法
    	    B.sayHello();                          //调用B类的静态方法
    	    a.sayHello2();                         //调用a对象的实例方法
    	    b.sayHello2();                         //调用b对象的的实例方法
    	    A a2=new A();                          //创建A类的实例对象a2
    	    a2.sayHello2();                        //调用a2对象的实现方法
    	}
    }

    运行结果为:

    大家好,这是A的静态类方法
    大家好,这是A的静态类方法
    大家好,这是B的静态类方法
    大家好,这是B的实例方法
    大家好,这是B的实例方法
    大家好,这是A中的实例方法

    可以看出,得到调用的隐藏方法是父类中的方法,而得到调用的重写方法是子类中的方法。

    方法重写和隐藏后的修饰符

    在子类中被重写的方法,其访问权限允许大于但不允许小于被其重写的方法,例如:父类中一个受保护的实例方法(protected)在子类中可以是公共的(public)的,但不可以是私有的(private)。如果一个方法在父类中是static方法,那么在子类也必须是static方法;如果一个方法在父类中是实例方法,那么在子类中也必须是实例方法。

    子类访问父类私有成员

    子类继承其父类的所有public和protected成员,但不能继承其父类的private成员。那么如何在子类中访问到父类中的字段呢,我们可以在父类中提供用来访问其私有字段的public或protected方法,子类使用这些方法来访问相应的字段。例如:

    class A{                     //父类A
    	private int value=10;    //声明一个私有变量value并赋值为10
    	public int getvalue() {  //声明一个公有成员方法getvalue,返回value
    		return value;
    	}
    }
    class B extends A{           //A的子类B
    }
    public class myfirst {    
    	public static void main(String[] args) {
    	  B b=new B();           //创建子类B的一个实例对象
    	  System.out.println("子类通过父类提供的公共接口访问A中的私有字段value:"+b.getvalue());
    	}
    }

    运行结果为:

    子类通过父类提供的公共接口访问A中的私有字段value:10

    使用super关键字

    使用super调用父类中重写的方法、访问父类中被隐藏的字段

    子类重写了父类中的某一个方法,隐藏父类中的字段,假如想在子类中访问到父类中被重写的方法和隐藏父类的字段,可以在子类中通过使用关键字super来调用父类中被重写的方法和访问父类中被隐藏的字段。例如:

    package first;
    class A{
        public String name="张飞";         //添加成员变量
    	public void say() {                //添加成员方法say
    		System.out.println("我是父类A成员方法say");
    	}
    }
    class B extends A{
        public String name="关羽";         //与父类中同名的字段,隐藏父类
    	public void say(){                 //重写方法say
    		super.say();                   //使用super关键字调用父类中的方法
    		System.out.println("我是子类B成员方法say");
            System.out.println("父类的name名字:"+super.name); //使用super关键字访问父类中的变量
    	}
    }
    public class myfirst {
    	public static void main(String[] args) {
    	  B b=new B();                     //创建子类的一个实例对象
    	  b.say();                         //调用子类中重写的方法
    	  System.out.println("子类的name名字:"+b.name);   //调用子类中的name
    	}
    }

    运行结果为:

    我是父类A成员方法say
    我是子类B成员方法say
    父类的name名字:张飞
    子类的name名字:关羽

    使用super调用父类的无参数构造方法/有参数构造方法

    子类不继承其父类的构造方法。

    • 当使用无参数的super()时,父类的无参数构造方法就会被调用;
    • 当使用带有参数的super()方法时,父类的有参数构造方法就会被调用。

    例如:

    class SuperClass {              //创建父类SuperClass
    	  private int n;            //声明一个私有变量n
    	  SuperClass(){             //父类无参数构造方法
    	    System.out.println("这是父类SuperClass无参数构造方法");
    	  }
    	  SuperClass(int n) {       //父类有参数构造方法
    	    System.out.println("这是父类SuperClass有参数构造方法");
    	    this.n = n;
    	  }
    	}
    	class SubClass extends SuperClass{     // SubClass类继承SuperClass类
    	  private int n;                       //声明一个私有变量n
    	  SubClass(){                          // 自动调用父类的无参数构造器
    	    System.out.println("这是子类无参数构造方法");
    	  }  
    	  
    	  public SubClass(int n){              //子类有参数构造方法
    	    super(300);                        //调用父类中带有参数的构造器
    	    System.out.println("这是子类有参数构造方法"+n);
    	    this.n = n;
    	  }
    	}
    public class myfirst {
    	public static void main(String[] args) {
    		    SubClass sc1 = new SubClass();      //创建子类SubClass实例对象,调用其无参数构造方法
    		    SubClass sc2 = new SubClass(100);   //创建子类SubClass实例对象,调用其有参数构造方法
    	}
    }
    

    运行结果为:

    这是父类SuperClass无参数构造方法
    这是子类无参数构造方法
    这是父类SuperClass有参数构造方法
    这是子类有参数构造方法100

    注意

    • 如果要初始化父类中的字段,可以在子类的构造方法中通过关键字super调用父类的构造方法;
    • 对父类的构造放的调用必须放在子类构造方法的第一行;
    • 如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器;
    • 如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表;
    • 子类是不继承父类的构造器(构造方法或者构造函数)的,它只是调用(隐式或显式)。

    最后

    好了,有关Java继承的知识讲到这里了,谢谢观看!!!

    我们下篇文章再见!!!

    成功不是将来才有的,而是从决定去做的那一刻起,持续累积而成。

    更多相关内容
  • C++ 继承详解

    千次阅读 2022-02-21 20:04:49
    C++ 继承继承语法继承方式改变访问权限名字遮蔽继承时的对象模型无变量遮蔽有变量遮蔽 继承语法 继承的一般语法为: class 派生类名:[继承方式] 基类名{ 派生类新增加的成员 }; 继承方式 继承方式包括 public...

    继承语法

    继承的一般语法为:

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

    继承方式

    继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。不同的继承方式会影响基类成员在派生类中的访问权限。

    (1)public继承方式

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

    (2)protected继承方式

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

    (3)private继承方式

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

    通过上面的分析可以发现:

    1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限将保持不变。也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
    2. 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
    3. 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
    4. 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

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

    private继承特点

    如果是private继承,则不会自动将派生类类型转换为基类类型(不会自动转换,但是可以手动显式进行转换),不能隐式转换。private继承就是一种纯粹的实现技术,意味着子类继承了父类,纯粹是看中了父类里面的某些函数实现罢了,这个新的类将不会与父类指针有关系(接口都变private了)。

    改变访问权限

    使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

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

    using 关键字使用示例:

    #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;
    }
    //派生类Student
    class Student : public People {
    public:
        void learning();
    public:
        using People::m_name;  //将protected改为public
        using People::m_age;  //将protected改为public
        float m_score;
    private:
        using People::show;  //将public改为private
    };
    void Student::learning() {
        cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
    }
    int main() {
        Student stu;
        stu.m_name = "小明";
        stu.m_age = 16;
        stu.m_score = 99.5f;
        stu.show();  //compile error
        stu.learning();
        return 0;
    }
    

    代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。

    不过,我们使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

    名字遮蔽

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

    下面是一个成员函数的名字遮蔽的例子:

    #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;
    }
    //派生类Student
    class Student: public People{
    public:
        Student(char *name, int age, float score);
    public:
        void show();  //遮蔽基类的show()
    private:
        float m_score;
    };
    Student::Student(char *name, int age, float score){
        m_name = name;
        m_age = age;
        m_score = score;
    }
    void Student::show(){
        cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
    }
    int main(){
        Student stu("小明", 16, 90.5);
        //使用的是派生类新增的成员函数,而不是从基类继承的
        stu.show();
        //使用的是从基类继承来的成员函数
        stu.People::show();
        return 0;
    }
    

    运行结果:

    小明的年龄是16,成绩是90.5
    嗨,大家好,我叫小明,今年16

    本例中,基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽。第 37 行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。

    但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符,如第 39 行代码所示。

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

    下面的例子很好的说明了这一点:

    #include<iostream>
    using namespace std;
    //基类Base
    class Base{
    public:
        void func();
        void func(int);
    };
    void Base::func(){ cout<<"Base::func()"<<endl; }
    void Base::func(int a){ cout<<"Base::func(int)"<<endl; }
    //派生类Derived
    class Derived: public Base{
    public:
        void func(char *);
        void func(bool);
    };
    void Derived::func(char *str){ cout<<"Derived::func(char *)"<<endl; }
    void Derived::func(bool is){ cout<<"Derived::func(bool)"<<endl; }
    int main(){
        Derived d;
        d.func("c.biancheng.net");
        d.func(true);
        d.func();  //compile error
        d.func(10);  //compile error
        d.Base::func();
        d.Base::func(100);
        return 0;
    }
    

    本例中,Base 类的func()、func(int)和 Derived 类的func(char *)、func(bool)四个成员函数的名字相同,参数列表不同,它们看似构成了重载,能够通过对象 d 访问所有的函数,实则不然,Derive 类的 func 遮蔽了 Base 类的 func,导致第 26、27 行代码没有匹配的函数,所以调用失败。

    如果说有重载关系,那么也是 Base 类的两个 func 构成重载,而 Derive 类的两个 func 构成另外的重载。

    继承时的对象模型

    无变量遮蔽

    有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,所有成员函数仍在另外一个区域——代码区,由所有对象共享。请看下面的代码:

    #include <cstdio>
    using namespace std;
     
    class A{
    protected:
        char a;
        int b;
    public:
        A(char a, int b): a(a), b(b){}
        void display(){
            printf("a=%c, b=%d\n", a, b);
        }
    };
     
    class B: public A{
    private:
        int c;
    public:
        B(char a, int b, int c): A(a,b), c(c){ }
        void display(){
            printf("a=%c, b=%d, c=%d\n", a, b, c);
        }
    };
     
    int main(){
        A obj_a('@', 10);
        B obj_b('@', 23, 95);
        return 0;
    }
    

    obj_a 是基类对象,obj_b 是派生类对象。假设obj_a 的起始地址为 0X1000,那么它的内存分布如下图所示:
    在这里插入图片描述

    虽然变量 a 仅占用一个字节的内存,但由于内存对齐的需要,编译器会添加 3 个无用的字节(图中灰色部分),保证地址是 4 的倍数。后面的讲解中将忽略内存对齐,假设 a 的长度为4个字节。

    假设 obj_b 的起始地址为 0X1100,那么它的内存分布如下图所示:
    在这里插入图片描述

    可以发现,基类的成员变量排在前面,派生类的排在后面。

    下面再由 B 类派生出一个 C 类:

    class C: public B{
    private:
        int d;
    public:
        C(char a, int b, int c, int d): B(a,b,c), d(d){ }
    };
     
    C obj_c('@', 45, 1009, 39);
    

    假设 obj_c 的起始地址为 0X1200,那么它的内存分布如下图所示:
    在这里插入图片描述

    成员变量按照派生的层级依次排列,新增成员变量始终在最后。

    有变量遮蔽

    更改上面的C类:

    class C: public B{
    private:
        int b;  //遮蔽A类的变量
        int c;  //遮蔽B类的变量
        int d;  //新增变量
    public:
        C(char a, int b, int c, int d): B(a,b,c), b(b), c(c), d(d){ }
        void display(){
            printf("A::a=%c, A::b=%d, B::c=%d\n", a, A::b, B::c);
            printf("C::b=%d, C::c=%d, C::d=%d\n", b, c, d);
        }
    };
     
    C obj_c('@', 23, 95, 2000);
    

    假设 obj_c 的起始地址为 0X1300,那么它的内存分布如下图所示:
    在这里插入图片描述

    当基类A、B的成员变量被遮蔽,仍然会留在派生类对象 obj_c 的内存中,C 类新增的成员变量始终排在基类A、B的后面。

    总结:派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。

    final关键字

    C++中,final关键字用于修饰类时,有以下作用:

    1. 禁止继承:将类标记为final,意味着无法继承。
    2. 禁止重写方法:当方法被标记为final时,在子类中无法重写该方法。
    class test final{
    };
    
    class test
    {
      public:
        virtual void func() final;
    };
    
    展开全文
  • 一文读懂C++虚继承的内存模型

    千次阅读 多人点赞 2021-06-30 21:04:01
    一文读懂C++虚继承的内存模型1、前言2、多继承存在的问题3、虚继承简介4、虚继承在标准库中的使用5、虚继承下派生类的内存布局解析6、总结 1、前言 C++虚继承的内存模型是一个经典的问题,其具体实现依赖于编译器,...

    1、前言

    C++虚继承的内存模型是一个经典的问题,其具体实现依赖于编译器,可能会出现较大差异,但原理和最终的目的是大体相同的。本文将对g++中虚继承的内存模型进行详细解析。

    2、多继承存在的问题

    C++的多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。从概念上来讲这是非常简单的,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个,比如典型的是菱形继承,如图2-1所示:

    在这里插入图片描述

    图2-1 菱形继承


    在图2-1中,类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自A–>B–>D这条路径,另一份来自A–>C–>D这条路径。
    在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的,因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类A有一个成员变量a,那么在类D中直接访问a就会产生歧义,编译器不知道它究竟来自A–>B–>D这条路径,还是来自A–>C–>D这条路径。下面是菱形继承的代码实现:

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: public A
    {
    public:
        long b;
    };
    
    
    class C: public A
    {
    public:
        long c;
    };
    
    class D: public B, public C
    {
    public:
        void seta(long v) { a = v; } // 命名冲突
        void setb(long v) { b = v; } // 正确
        void setc(long v) { c = v; } // 正确
        void setd(long v) { d = v; } // 正确
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
    }
    

    这段代码就是图2-1所示的菱形继承的具体实现,可以看到在类Dseta()方法中,代码试图直接访问间接基类的成员变量a,结果发生了错误,因为类B和类C中都有成员变量a(都是从类A继承的),编译器不知道选用哪一个,所以产生了歧义。

    为了消除歧义,我们可以在使用a时指明它具体来自哪个类,代码如下:

    void seta(long v) { B::a = v; }
    /* 或 */
    void seta(long v) { C::a = v; }
    

    使用GDB查看变量d的内存布局,如图2-2所示:

    在这里插入图片描述

    图2-2 变量d的GDB调试结果


    于是我们可以画出变量d的内存布局,如图2-3所示:

    在这里插入图片描述

    图2-3 变量d的内存布局

    3、虚继承简介

    为了解决多继承时命名冲突和冗余数据的问题,C++提出了虚继承这个概念,虚继承可以使得在派生类中只保留一份间接基类的成员。使用方式就是在继承方式前面加上virtual关键字修饰,示例代码如下(基于前面的例子修改):

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: virtual public A
    {
    public:
        long b;
    };
    
    
    class C: virtual public A
    {
    public:
        long c;
    };
    
    class D: public B, public C
    {
    public:
        void seta(long v) { a = v; } // 现在不会冲突了
        void setb(long v) { b = v; } // 正确
        void setc(long v) { c = v; } // 正确
        void setd(long v) { d = v; } // 正确
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
    }
    

    可以看到这段代码使用虚继承重新实现了前面提到的那个菱形继承,这样在派生类D中就只保留了一份间接基类A的成员变量a了,后续再直接访问a就不会出现歧义了。虚继承的目的是让某个类做出声明,承诺愿意共享它的基类,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的类A就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。本例的继承关系如图3-1所示:

    在这里插入图片描述

    图3-1 虚继承下菱形继承


    从这个新的继承体系中我们可以发现虚继承的一个特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在图3-1中,我们是当定义类D时才出现了对虚派生的需求,但是如果类B和类C不是从类A虚派生得到的,那么类D还是会保留间接基类A的两份成员,示例代码如下:

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: public A
    {
    public:
        long b;
    };
    
    
    class C: public A
    {
    public:
        long c;
    };
    
    class D: virtual public B, virtual public C
    {
    public:
        void seta(long v) { a = v; } // 错误,不能等到定义类D时再来做虚继承的工作
        void setb(long v) { b = v; } // 正确
        void setc(long v) { c = v; } // 正确
        void setd(long v) { d = v; } // 正确
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
    }
    

    换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。

    4、虚继承在标准库中的使用

    C++标准库中的iostream就是一个虚继承的典型案例。iostream是从istreamostream直接继承而来的,而istreamostream又都继承自一个名为ios的类,这个就是一个典型的菱形继承。此时istreamostream必须采用虚继承,否则将导致iostream中保留两份ios的成员。

    iostream相关的源代码如下(从gcc-2.95.3版本中摘录出来的,内容有所省略):

    struct _ios_fields
    { // The data members of an ios.
        streambuf *_strbuf;
        ostream* _tie;
        int _width;
        __fmtflags _flags;
        _IO_wchar_t _fill;
        __iostate _state;
        __iostate _exceptions;
        int _precision;
    
        void *_arrays; /* Support for ios::iword and ios::pword. */
    };
    
    class ios : public _ios_fields
    {...};
    
    class istream : virtual public ios
    {...};
    
    class ostream : virtual public ios
    {...};
    
    class iostream : public istream, public ostream
    {
    public:
        iostream() { }
        iostream(streambuf* sb, ostream*tied=NULL);
    };
    

    5、虚继承下派生类的内存布局解析

    g++中是没有所谓的虚基类表的(据说vs是有单独一个虚基类表的),只有一个虚表,由于平时用的比较多的是虚函数,所以一般情况下都直接管它叫做虚函数表,在g++编译环境下这种叫法其实是不严谨的。测试程序如下:

    #include <iostream>
    #include <stdint.h>
    
    class A
    {
    public:
        long a;
    };
    
    class B: virtual public A
    {
    public:
        long b;
    };
    
    class C: virtual public A
    {
    public:
        long c;
    };
    
    class D: public B, public C
    {
    public:
        void seta(long v) { a = v; }
        void setb(long v) { b = v; }
        void setc(long v) { c = v; }
        void setd(long v) { d = v; }
    
    private:
        long d;
    };
    
    int main(int argc, char* argv[])
    {
        D d;
        d.seta(1);
        d.setb(2);
        d.setc(3);
        d.setd(4);
    }
    

    D在当前编译器(GCC 4.8.5)下的内存布局如图5-1所示:

    在这里插入图片描述

    图5-1 类D的内存布局


    从图5-1中可以看出这个表和之前这篇文章《一文读懂C++虚函数的内存模型》讲的虚函数表是差不多的,就多了一个vbase_offset而已。因为这里的类设计比较简单,没有把虚函数加进来,有虚函数的话_vptr.B或者_vptr.C下面的内存空间存储的就是指向对应虚函数的指针了(以下只讲_vptr.B的相关内容,_vptr.C同理就不赘述了)。

    这里可以看到_vptr.B指向的是虚函数的起始地址(因为这里没有虚函数,所以下面紧接着就是_vptr.C的内容),而不是与它相关联的全部信息的起始地址,事实上从图5-1中可以看出_vptr.B - 3 ~ _vptr.B这个范围内的数据都是类B虚表的内容(不知道编译器为什么这么设计,这里也进行揣测了),这三个特殊的内存地址存储的内容解析如下:

    1. _vptr.B - 1:这里存储的是typeinfo for D,里面的内容其实也是一个指针,指向的是类D的运行时信息,这些玩意都是为了支持RTTI的。RTTI的相关内容以后会讲,这里就先不多分析了。
    2. _vptr.B - 2:这里存储的是offset_to_top,这个表示的是当前的虚表指针距离类开头的距离,可以看到对于_vptr.B来说这个值就是0,因为_vptr.B就存在于类D的起始位置,而对于_vptr.C来说这个值是-16,大家可以算一下_vptr.C与类D的起始位置确实是差两个地址也就是16个字节(64位系统),至于为什么是负数,这是因为堆内存是向下增长的,越往下地址数值越大。

    offset_to_top深度解析:在多继承中,由于不同基类的起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this指针的偏移量也不相同。由于实际类型在编译时是未知的,这要求偏移量必须能够在运行时获取。实体offset_to_top表示的就是实际类型起始地址到当前这个形式类型起始地址的偏移量。在向上动态转换到实际类型时(即基类转派生类),让this指针加上这个偏移量即可得到实际类型的地址。需要注意的是,由于一个类型即可以被单继承,也可以被多继承,因此即使只有单继承,实体offset_to_top也会存在于每一个多态类型之中。
    (这里要注意一点就是offset_to_top只存在于多态类型中,所以我们可以看到在第二小节那个例子中,根本就没有什么所谓的虚表之类的东西,它也就不支持RTTI,最简单的大家可以使用dynamic_cast去试试,会报错说该类型不具备多态性质的。那么问题来了,怎样才能以最简短的方式让它具备多态的性质呢?很简单,定义一个析构函数,用virtual修饰即可)

    1. _vptr.B - 3:这里存储的是vbase_offset,这个表示的是当前虚表指针与其对应的虚基类的距离。从图中可以看出对于_vptr.B来说这个值是40,算一下刚好是_vptr.Ba的差距,_vptr.C同理。

    vbase_offset深度解析:以测试程序为例,对于类型为B的引用,在编译时,无法确定它的虚基类A它在内存中的偏移量。因此,需要在虚表中额外再提供一个实体,表明运行时它的基类所在的位置,这个实体称为vbase_offset,位于offset_to_top上方。

    接下来我们通过GDB来验证一下前面讲的内容,先打印出变量d的内存信息,如图5-2所示:

    在这里插入图片描述

    图5-2 变量d的内存信息


    从图5-2中可以看到变量d的内容与前面分析的差不多,接下来我们来看一下这两个虚表的内容,如图5-3所示:

    在这里插入图片描述

    图5-3 虚表内存信息


    从图5-3中可以看出前面的内存图是正确的,接下来就再看一下变量d自身的内存布局,如图5-4所示:

    在这里插入图片描述

    图5-4 变量d的内存布局


    图5-4显示出的结果和前面图5-1的完全一致,到这里调试就结束了,由调试结果可以知道图5-1的内存模型是正确的。

    这里要补充一点,就是对于虚继承下的类D,和第二节那个没有虚继承的相比,基类A的位置被移动到了类D的最末尾,不过不用担心,运行时可以靠vbase_offset找到它。

    6、总结

    本文先是对虚继承的概念以及使用场景进行了说明,然后通过一个内存模型图向大家展示了g++下虚继承的内存形态,最后使用GDB查看实际的内存情况来验证内存模型图的正确性。本文为了更直观地展示虚继承的内存模型,示例设计得很简单,类的设计中只有一个成员变量而没有成员函数、虚函数等其它内容。本文与前文《一文读懂C++虚函数的内存模型》相当于抛砖引玉,为下文作铺垫,在下一篇文章中我将对一些稍微复杂一点的情景进行分析,看看完整形态的虚表究竟是什么样的。

    最后,如果大家觉得本文写得好的话麻烦点赞收藏关注一下谢谢,也可以关注该专栏,以后会有更多优质文章输出的。

    展开全文
  • C++虚继承详解

    千次阅读 2022-04-21 15:57:14
    C++虚继承详解 看侯捷老师的C++内存模型时,讲到了虚继承。虚继承算是C++特有的知识了,特此记录下。 什么是虚继承 由于C++支持多继承,可能会出现菱形继承,代码如下: #include <iostream> // std::cout ...

    C++虚继承详解

    看侯捷老师的C++内存模型时,讲到了虚继承。虚继承算是C++特有的知识了,特此记录下。

    什么是虚继承

    由于C++支持多继承,可能会出现菱形继承,代码如下:
    在这里插入图片描述

    #include <iostream> // std::cout std::endl
    
    class base
    {
    public:
        long long par=0;
        void show(int par) const noexcept;
    };
    
    void base::show(int par) const noexcept
    {
        par=par;
        std::cout << "par:" << par << std::endl;
    }
    
    class derived1 : public base{};
    
    class derived2 : public base{};
    
    class final_derived : public derived1, public derived2
    {
    public:
        long long fpar=3;
    };
    
    int main(void)
    {
        final_derived object;
        // 两个show()函数,编译器不知道调用哪个
        object.show();  
        return 0;
    }
    

    首先需要明白,编译器寻找普通成员函数时,和this指针有关系:

    this指针是一个指针常量,其地址不能改变,指向当前对象,做为成员函数的第一个默认缺省参数,由编译器管理。

    this指针的两个作用:
    其一,类似于模板函数的类型推导确定对象所属类型,所以不同的类调用同名函数,是不会出现问题的,并确定函数操作的数据块大小;其二,它的值就是对象object的地址;

    因此,通过this指针,当存在多个同名函数时,编译可以根据对象推导参数类型,找到这个类对应的函数,并操作对应空间的数据。

    接下里,剖析下多继承中菱形继承问题。

    多继承–成员函数方面

    继承关系中的成员函数

    由于函数会占用内存中代码区的资源,所以如果子类不用修改父类中的某一个成员函数,那直接用父类的这个函数就好了:

    #include <iostream> 
    using namespace std;
    
    class base
    {
    public:
        long long par=0;
        void show(int par) const noexcept;
    };
    
    void base::show(int par) const noexcept
    {
        par=par;
        std::cout << "par:" << par << std::endl;
    }
    
    class derived1 : public base{
    public:
        void show(int par){
            cout<<"show of derived1"<<endl;
        }
    };
    
    class derived2 : public base{};
    
    class final_derived :  public derived2,public derived1
    {
    public:
        long long fpar=3;
    };
    
    // 成员函数地址读取
    template<typename dst_type,typename src_type>
    dst_type pointer_cast(src_type src)
    {
        // 巧妙地转换:由于static_cast不能转换两个毫不相关的变量,利用void* 进行转换
        return *static_cast<dst_type*>(static_cast<void*>(&src));
    }
    
    int main(void)
    {
        base* p1 = pointer_cast<base*>(&base::show);
        derived1* p2 = pointer_cast<derived1*>(&derived1::show);
        derived2* p3 = pointer_cast<derived2*>(&derived2::show);
        cout<<p1<<endl<<p2<<endl<<p3<<endl;
        return 0;
    }
    

    三个成员函数的地址为:0x401550 0x4159c0 0x401550

    可以看出:由于derived2的show函数就是用的base父类的show函数,而没有新建show函数;说的直白点,就是derived1的成员函数show实际变成了show(derived* const this,int par),而derived2的成员函数show还是show(base* const this,int par)。

    多继承中成员函数问题

    接下来,并在main函数加入如下代码:

    final_derived object;
    object.show(1);
    

    发现编译器直接报错:show函数目标不明确

    一开始我是这么以为的:

    这是由于final_derived没有重写show函数,所以会调用父类的show函数。然而,final_derived类调用show函数时,既能匹配show(derived* const this,int par),也能匹配show(base* const this,int par),编译器不明确到底调用哪个。

    然而,实事并不是这样,我把derived1中的show函数删除了,也就是只剩一个show(base* const this,int par)了,但仍然出现show函数目标不明确的报错。所以,实事就是编译器在进行语法分析时,发现final_derived有两个相同的show函数,直接就报错了。

    解决方法

    为避免调用函数时,语法问题造成调用失败,有三种方法可以解决:

    一、final_derived重写show函数:

    class final_derived :  public derived2,public derived1
    {
    public:
        long long fpar=3;
        void show(int par){
            derived1::show(par);
        }
    };
    

    但这样有个缺点,本来final_derived就是用的derived1的方法,且未作任何修改,按C++的设计思想,直接用父类derived1类的show方法就可以了,不应该用额外的内存。

    二、调用的时候,指定具体的类,给this指针传更精确的类型:

    int main(void)
    {
        final_derived object;
        object.derived1::show(1);
        object.derived2::show(2);
        return 0;
    }
    

    这样就是写代码会很麻烦,别人还得知道你是怎么继承的。

    三、虚继承

    也就是在derived1类和derived2类继承base时,添加virtual关键字:

    #include <iostream> 
    using namespace std;
    
    class base
    {
    public:
        long long par=0;
        void show(int par) const noexcept;
    };
    
    void base::show(int par) const noexcept
    {
        par=par;
        std::cout << "par:" << par << std::endl;
    }
    
    class derived1 :  virtual public base{
    public:
        void show(int par){
            std::cout << "show of derived1"<< std::endl;
        }
    };
    
    class derived2 :  virtual public base{};
    
    class final_derived :  public derived2,public derived1
    {
    public:
        long long fpar=3;
    };
    
    int main(void)
    {
        final_derived object;
        object.show(1);
        return 0;
    }
    

    这里输出的是show of derived1。
    如果删除derived1的show函数,输出为par=1,也就是调用base的show函数

    虚继承实现原理

    derived1 和 derived1 虚继承 base,会新建一个虚基类表,存储虚基类相对直接继承类的偏移量,并把指向虚基类表的虚基类指针存入类中。

    这样,final_derived在调用show时,过程如下:

    • 首先找自己类中有没有show函数;如果没有,找父类。
    • 父类中如果能找到,就用父类的show函数。(注意:如果derived1和derived2都重写了show函数,object.show(1)仍然报目标不明确的错误)
    • 找derived1和derived2和虚基类表,如果发现show函数在两个类中的虚基类表中都存在,就直接调用base的show函数。

    可以看出,虚继承并不能保证object.show(1)的合法调用,最好不要用多继承,就把虚继承这种机制当作语法糖吧。

    多继承–成员变量方面

    继承中类成员变量的分布

    删除上述代码的show函数,专注于成员变量par上,观察object对象中的成员变量分布,以及它的大小。

    #include <iostream> 
    using namespace std;
    
    class base
    {
    public:
        long long par=0;
        void show(int par) const noexcept;
    };
    
    void base::show(int par) const noexcept
    {
        par=par;
        std::cout << "par:" << par << std::endl;
    }
    
    class derived1 :  public base{};
    
    class derived2 :  public base{};
    
    class final_derived :  public derived2,public derived1
    {
    public:
        long long fpar=3;
    };
    
    int main(void)
    {
        final_derived object;
        cout<<sizeof(object)<<endl;
        return 0;
    }
    

    其大小为24bytes(64位机器下),成员变量分布为:
    在这里插入图片描述
    依次是从derived2类继承的par,从derived1类继承的par,以及自身的fpra。由于final_derived是先继承的 derived2 后继承 derived1,因此从derived2继承来的par也分布在前端。

    那么现在问题来了:其一:这个类中有两个par变量,要怎么访问呢?其二:从上述代码来看,final_derived直接用base的par就可以了,用两个par变量不是浪费空间吗?

    解决办法

    如果只解决第一个问题,可以通过指明具体类的方法:

    cout<<object.derived1::par<<endl;
    

    但如果还要解决第二个问题,仍然得借助虚继承,为了更方便说明,我加了两个参数par2和par3:

    #include <iostream> 
    using namespace std;
    
    class base
    {
    public:
        long long par=0;
        long long par2=1;
        long long par3=2;
        void show(int par) const noexcept;
    };
    
    void base::show(int par) const noexcept
    {
        par=par;
        std::cout << "par:" << par << std::endl;
    }
    
    class derived1 :  virtual public base{};
    
    class derived2 :  virtual public base{};
    
    class final_derived :  public derived2,public derived1
    {
    public:
        long long fpar=3;
    };
    
    int main(void)
    {
        final_derived object;
        cout<<sizeof(object)<<endl;
        return 0;
    }
    
    

    输出结果为48bytes,内存分布从上到下依次为:derived2的虚基类指针,derived1的虚基类指针,final_derived自身的fpar,base的三个成员变量(单继承中,父类的成员变量是放前面的)。

    访问成员变量类似于成员函数的调用,先看类本身是否存在这个变量,然后去父类中找,最后找父类的虚继承表。

    题外话: 虽然我一直觉得组合比继承好,但这里用组合好像没啥办法省内存,算是继承的一个优点吧,但代价就是代码写起来很麻烦。

    展开全文
  • Go 继承

    千次阅读 2021-06-14 23:38:52
    面向对象编程的三大特性:封装、继承、多态。可见继承是面向对象程序设计中一个重要的概念。Go 作为面向对象的编程语言,自然也支持继承。 比较特殊的是 Go 实现继承的方式与其他传统 OOP 语言所有不同,不像 C++ 有...
  • Python编程基础19:封装、继承与多态

    万次阅读 多人点赞 2022-01-26 12:02:19
    掌握单继承和多继承,会重写和调用父类方法 理解多态的使用 一、类的封装 (一)为什么需要封装 Python是面向对象的编程语言,因为面向对象中所有的实体都是以对象为基本单位的,以宏观世界的实体来映射到计算机...
  • Java继承

    千次阅读 2021-02-13 02:17:13
    Java继承1.什么是继承呢?类似人类中的继承(继承上一辈、上上辈的东西),在Java中就是将一个已知的类进行拓展,让一个新的类来继承这个已知的类,这样就能使这个新的类具有已知类的属性和行为。新的这个类被称为子类...
  • C++ 多重继承

    千次阅读 热门讨论 2021-05-13 04:17:10
    C++ 多重继承. 多重继承是什么, 多重继承的优缺点以及注意事项.
  • java中的继承详解

    万次阅读 多人点赞 2020-12-19 08:43:00
    继承的描述在现实生活中,继承一般指的是子女继承父辈的财产。在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种关系体系。例如猫和狗都属于动物,程序中便可以描述为猫...
  • C++ 继承 详解

    千次阅读 多人点赞 2021-05-09 16:34:16
    1.1、概念1.2、定义1.2.1、继承关系和访问限定符1.2.2、继承基类成员访问方式的变化注意:二、基类和派生类对象赋值转换三、继承中的作用域四、派生类的默认成员函数五、继承与友元六、继承与静态成员七、菱形继承八...
  • C++ 继承(2): 多重继承, 多继承, 虚继承(virtual)

    千次阅读 多人点赞 2019-03-20 22:59:49
    C++远征之继承篇 视频教程 笔记 方便自己查阅和复习,温故而知新。 接着C++ 继承(1): 继承方式(public, protected, private), 继承中的特殊关系(隐藏 , is-a) 继续做笔记 目录 4 多继承与多重继承 4.1 多重...
  • C++的三种继承方式详解

    千次阅读 2022-04-22 19:12:36
    文章目录@[toc]C++的三种继承方式详解以及区别前言一、public继承二、protected继承三、private继承四、三者区别五、总结后话 C++的三种继承方式详解以及区别 前言 我发现有时候概念性的东西,理解起来还是很难的,...
  • JS 常见的 6 种继承方式

    万次阅读 多人点赞 2021-09-15 21:51:56
    继承可以使得子类别具有父类的各种方法和属性。先思考几个问题: JS 的继承到底有多少种实现方式呢? ES6 的 extends 关键字是用哪种继承方式实现的呢? 一、JS 实现继承的几种方式 第一种:原型链...
  • java关于多继承和单继承

    千次阅读 2021-09-11 15:47:55
    面试官说:java怎么实现多继承? 答:java不允许,不允许,不允许多继承。 面试官:我就是要实现多继承? 答:好吧,行。 首先要区分一下三个概念: 单继承 多级继承继承 1.单继承 java两个类之间可以有继承关系...
  • python 继承(史上最详细版本)

    万次阅读 多人点赞 2021-12-06 22:38:57
    继承 继承简介 继承是一种创建新类的方式,新建的类可称为子类或派生类,父类可称为基类或超类 python支持多继承,新建的类可以支持一个或多个父类 '''单继承和多继承简单定义''' class Parent1: pass class ...
  • js继承 - class的继承

    千次阅读 2021-02-01 17:01:00
    和之前讲的js的继承方式不同,class的继承是es6里面才有的。在讲class的继承之前,需要先了解一下class: class的基本语法 class,也就是我们常说的类, // class class Person { constructor(name, age) { this....
  • C++继承详解

    千次阅读 多人点赞 2020-05-08 00:55:09
    class 派生类名:[继承方式] 基类名{ 派生类新增加的成员 }; 继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为...
  • C++中的继承

    万次阅读 多人点赞 2019-06-02 22:41:24
    1.为什么要使用继承 继承性是一个非常自然的概念,现实世界中的许多事物都是具有继承性的。人们一般用层次分类的方法来描述他们的关系。例如,下图就是一个简单的汽车分类图: 在这个分类树中建立了一个层次结构,...
  • idea 查看类继承关系的快捷键

    千次阅读 2021-01-12 18:28:49
    并且类之间单继承和多继承频繁使用, 这个继承,不仅仅是一级的继承关系,包括好几层的继承.父类的父类的父类. 直到最后->很难清楚一个类的实现关系, 迷失类的作用. 这里我们可以通过查看类的继承关系, 明白类的作用 ...
  • java继承和多态

    千次阅读 多人点赞 2021-10-08 21:38:20
    继承和多态 文章目录继承和多态继承多态 继承 多态
  • C++普通继承、多继承、虚继承内存空间排布分析一、普通继承二、多继承三、菱形继承四、虚继承1.部分虚继承2.完全虚继承总结 一、普通继承 代码 class A { public: virtual void virtualfunA(){ } virtual void ...
  • C++继承与派生

    千次阅读 2022-04-04 14:45:37
    1.继承与派生:保持已有类的特征构造新类的过程为继承,在已有类的基础上新增特性而产生新类的过程称为派生 2.继承目的:实现代码重用;派生目的:实现源程序的改造 3.声明:class 派生类:继承方式 基类 4.派生类:...
  • 【Java语法】包 继承 多态 抽象类 接口

    千次阅读 多人点赞 2022-03-20 14:17:40
    一、包 包 (package) 是组织类的一种方式. 使用包的主要目的是保证类的唯一性 将类将放到包中 基本规则 ...二、继承 继承 对共性的抽取,使用extends关键字进行处理,可以对代码重复使用 语
  • 当面试官问到JavaScript中常用的继承方式时,你是否能说出如下几种继承方式呢? 原型继承、组合继承、寄生组合继承、extends。如果不能,那这篇文章教你入门,了解四种常见的继承方式以及它们的优缺点。
  • Java中的封装,继承和多态(详解)

    千次阅读 多人点赞 2021-05-21 22:07:08
    目录封装继承 封装 所谓的封装就是把类的属性和方法使用private修饰,不允许类的调用者直接访问,我们定义如下一个类,可以看到所有的成员变量和成员方法都使用private修饰了,我们现在来使用一下这个类。 当我们...
  • C++多继承(多重继承)详解

    千次阅读 2020-12-28 22:40:55
    在前面的例子中,派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。多继承容易让代码逻辑复杂、思路混乱,一直备受...
  • java关于继承的好处

    千次阅读 2021-02-27 15:58:14
    继承的概念继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的...
  • Python中的多重继承

    千次阅读 2022-01-07 11:45:35
    分享一个大牛的人工智能...在Python中一个类能继承自不止一个父类,这叫做Python的多重继承(Multiple Inheritance)。 语法: class SubclassName(BaseClass1, BaseClass2, BaseClass3, ...): pass 菱形继承
  • Qt5类继承关系图

    千次下载 热门讨论 2014-01-29 12:21:51
    Qt5类继承关系图,对于理清Qt5框架还有有点帮助的。pdf文档只有一面,缩放比例放小一点。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,892,495
精华内容 1,156,998
关键字:

继承

友情链接: 程序.zip