【ONE·C++ || 继承】

news2024/10/7 10:14:16

总言

  主要介绍继承相关内容。

文章目录

  • 总言
  • 1、继承介绍
    • 1.1、继承是什么
    • 1.2、继承方式与访问限定符
    • 1.3、继承作用域
  • 2、基类和派生类对象赋值转换
    • 2.1、子类对象可以赋值给父类对象/指针/引用
    • 2.2、基类对象不能赋值给派生类对象
    • 2.3、基类的指针可以通过强制类型转换赋值给派生类的指针
  • 3、派生类的默认成员函数
    • 3.1、构造函数
    • 3.2、拷贝构造函数
    • 3.3、赋值运算符重载
    • 3.4、析构函数
    • 3.5、取地址
  • 4、继承与友元、静态成员
    • 4.1、继承和友元
    • 4.2、继承和静态成员
    • 4.3、一道例题
  • 5、菱形继承、菱形虚拟继承
    • 5.1、单继承和多继承介绍
    • 5.2、菱形继承和菱形虚拟继承

  
  

1、继承介绍

1.1、继承是什么

  1)、继承是什么

  继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类
  继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

  
  2)、继承在类中复用性体现
  此处我们以一个人物身份管理的场景演示:以下有三个类,Person、Student、Teacher。Person类中定义的人物基本信息可以在Student、Teacher类中用到,因此可在Student和Teacher类中将Person类继承下来。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
	string _name = "peter"; // 姓名
	int _age = 0;  // 年龄
};


class Student
{
protected:
	int _stuid; // 学号
};


class Teacher
{
protected:
	int _jobid; // 工号
};

  一个定义:在上述描述的继承关系中,Person是父类,也称作基类。Student是子类,也称作派生类。
  语法格式如下:派生类 : 继承方式 基类

class Student : public Person //Student类以public的方式继承了Person类
{
protected:
	int _stuid; // 学号
};


class Teacher : public Person //Teacher类以public的方式继承了Person类
{
protected:
	int _jobid; // 工号
};

  继承中复用性的体现,演示代码如下:

void test01()
{
	//针对这两个类实例化创建对象
	Student s1;
	Teacher t1;
	//二者都调用相同变量,赋值不同
	s1._name = "张龙";
	s1._age = 21;
	t1._name = "公孙策";
	t1._age = 46;
	//验证最终结果是否独立
	s1.Print();
	t1.Print();

}

在这里插入图片描述

  
  
  

1.2、继承方式与访问限定符

  1)、类的继承方式和类访问限定符介绍
  继承方式有三种:public继承、protected继承、private继承
  访问限定符也有三种:public访问、protected访问、private访问
  
  对上述二者进行排列组合,可得继承基类成员访问方式的变化如下:

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员 派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员 派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见 在派生类中不可见在派生类中不可见

  
  相关说明:
  1、 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员虽然被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它

  2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

  3、观察上表可知,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected > private

  4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public。不过,一般情况下建议显示的写出继承方式。

  5、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
  
  总结:
  1、protected、private:类外不能访问,类里可以访问;
    不可见:类外、类里都不能访问
  2、如果父类中有不想被子类继承的成员,可以定义为私有。
  3、如果父类中有成员,想给子类复用,但又不想在类外暴露直接访问,可以将其定义为保护。(保护的价值体现)
  4、实际常用如下:
在这里插入图片描述

  
  
  相关演示:
  代码演示一:基类成员公有,公有继承,可在子类中访问,也可在类外访问。

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
	string _name = "peter";
	int _age = 0;
};

class Student : public Person
{
public:
	void Set(const char* name, int age) 
	{
		_name = name;//子类中访问
		_age = age;
	}
protected:
	int _stuid;
};


class Teacher : public Person
{
public:
	void Set(const char* name, int age)
	{
		_name = name;//子类中访问
		_age = age;
	}
protected:
	int _jobid;
};


void test01()
{
	Student s1;
	Teacher t1;

	类外访问
	s1._name = "学生A";
	s1._age = 21;
	t1._name = "教师A";
	t1._age = 46;
	s1.Print();
	t1.Print();


	s1.Set("学生B",18);
	t1.Set("教师B",33);
	s1.Print();
	t1.Print();

}

在这里插入图片描述

  
  
  代码演示二:基类成员公有,保护继承,可在子类中访问,不能在类外访问。
在这里插入图片描述
  
  
  

1.3、继承作用域

  1)、问题说明
  在C语言中,我们学习过局部域全局域,其限制的是变量的生命周期,在C++中,我们又引入了命名空间域、类域,其影响的是变量的访问。
  
  问题1: 我们知道同一个类域中不能同时定义名称相同的变量,那么对于父类、子类,是否能同时在其内部各自定义相同变量?
  回答: 可以。在继承体系中基类和派生类都有独立的作用域。
  
  问题2: 同名变量能在父子类中同时存在,若子类从父类继承到该同名变量,那么在子类中访问该变量时,我们获取到的是子类自己的变量,还是父类的变量?
  相关演示如下:

class Person
{
protected:
	string _name = "景行止";
	int _num = 40196;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " name:" << _name << endl;
		cout << " num:" << _num << endl;
	}
protected:
	int _num =35127; 
};

void test02()
{
	Student s1;
	s1.Print();
};

在这里插入图片描述

  由上述可知:子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
  
  问题3: 那么,上述情况下,如何访问到父类中的该同名成员呢?
  回答:在子类成员函数中,可以使用 基类::基类成员 显示访问
在这里插入图片描述

class Person
{
protected:
	string _name = "景行止";
	int _num = 40196;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " name:" << _name << endl;
		cout << " Person_num:" << Person::_num << endl;//父类默认隐藏,需要访问时要添加对应类域
		cout << " Student_num:" << _num << endl;
		cout << " Student_num:" << Student::_num << endl;

	}
protected:
	int _num =35127; 
};

void test02()
{
	Student s1;
	s1.Print();
};

  
  问题: 如下述代码,假如父类和子类中有共同的同名函数,其是否构成函数重载?

class A 
{
public: 
	void fun() 
	{
		cout << "func()" << endl;
	}

};

class B : public A 
{
public: 
	void fun(int i) 
	{
		cout << "func(int i)->" << i << endl;
	}

};

void test03()
{
	B b;
	b.fun(10);
};

  回答:B中的funA中的fun不是构成重载,因为不是在同一作用域,二者构成隐藏关系。成员函数的隐藏,只需要函数名相同就构成隐藏。
  其它:实际上,B里面有两个fun,一个是自己的,一个是从父类那继承下来的,我们直接访问到的是自己的,如果要访问父类的,可添加相应类域。注意这里类域的写法
在这里插入图片描述

class A 
{
public: 
	void fun() 
	{
		cout << "func()" << endl;
	}

};

class B : public A 
{
public: 
	void fun(int i) 
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}

};

void test03()
{
	B b;
	b.fun(10);
	b.A::fun();
};

  
  2)、小结
  1、在继承体系中基类和派生类都有独立的作用域。
  2、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。此时若想在子类中访问父类成员,可以使用 基类::基类成员 显示访问。
  3、成员函数的隐藏,只需要函数名相同就构成隐藏。
  4、在实际中在继承体系里面最好不要定义同名的成员。
  
  
  

2、基类和派生类对象赋值转换

  引入: 我们知道同类型变量可以相互赋值,同类型类也能够相互赋值。那么,父类和子类非同类型类,其能否相互赋值转换呢?
  

2.1、子类对象可以赋值给父类对象/指针/引用

  1)、概述
  派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割,寓意把派生类中父类那部分切来赋值过去。
  
  演示代码如下:

class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};

class Student : public Person
{
public:
	int _No;
};

void test04()
{
	Student sobj;

	//子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;
}

  Person pobj = sobj;子类对象被赋值给父类对象。实际上是子类中父类相关部分被赋值给父类。
在这里插入图片描述
  Person* pp = &sobj;子类对象的地址赋值给父类对象的指针。需要注意该指针能够指向的范围,仍旧是父类相关部分。
在这里插入图片描述

  Person& rp = sobj;子类对象赋值给父类的引用。同理,需要注意范围。从该行代码就能看出,子类赋值给父类,并非是强制类型转换、也不是隐式类型转换,因此这二者中间都会生成临时变量,临时变量具有常性,需要加const修饰。此处能够直接转换,说明这是父类子类赋值转换的一种语法支持。
在这里插入图片描述

  注意事项:这种赋值转换,前提条件是子类需要公有继承
在这里插入图片描述
  
  
  

2.2、基类对象不能赋值给派生类对象

  基类对象不能赋值给派生类对象。原因是父类缺少了子类中的一部分。

 sobj = pobj;
 sobj = (Student)pobj;//哪怕这里使用强制类型转换,也是不支持的。

在这里插入图片描述

  
  
  

2.3、基类的指针可以通过强制类型转换赋值给派生类的指针

  基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
  PS:相关问题后续讲解。
  
  
  

3、派生类的默认成员函数

  引入: 根据之前所学,类有六个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,默认成员函数对自定义类型和内置类型有其独立行为。由于子类继承父类中的成员,此处有一个问题,这些默认成员函数对父类成员的行为是什么样的?
  

3.1、构造函数

  前提回顾: 默认生成的构造函数,对内置类型不做处理,对自定义类型会去调用它自己的构造函数。
  
  派生类中构造函数说明: 子类中,编译器默认生成的构造函数,对自己的成员遵循以前的规则,对继承的父类成员,必须调用父类的默认构造函数初始化。
在这里插入图片描述
  相关代码如下:

class Person
{
public:
    Person(const char* name = "peter")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Person& operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;

        return *this;
    }

    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name;
};


class Student : public Person
{
public:

protected:
    int _num;
};

void test05()
{
    Student s;
}

  
  问题: 假如父类中,没有默认构造函数,这时候该怎么办?
  说明: 首先,可以明确的是,如果父类没有默认构造(比如我们自己写的非默认构造函数),而子类中也没有做相关处理,那么,其结果是编译不通过。
在这里插入图片描述

    Person(const char* name)
        : _name(name)
    {
        cout << "Person()" << endl;
    }

  
  解决方案:基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

  注意这里的调用方式:
  1、直接在子类中对父类成员进行初始化是错误的,因为子类是把整个父类当作一个整体进行初始化的。即子类处理子类自己,而子类中的父类成员,让父类自己处理。

class Student : public Person
{
public:
    Student(const char* name,int num)
        :_name(name)
        ,_num(num)
    {}

protected:
    int _num;
};

void test05()
{
    Student s("王朝"23);
}

  子类中如何处理父类整体?写法如下:使用匿名对象构造

class Person
{
public:
    Person(const char* name)//构造函数:非默认
        : _name(name)
    {
        cout << "Person()" << endl;
    }
    
    //……

protected:
    string _name;
};

class Student : public Person
{
public:
    Student(const char* name,int num)
        :Person(name)//子类初始化列表,对父类初始化的处理方式
        ,_num(num)
    {
		cout << "Student()" << endl;//验证代码
	}

protected:
    int _num;
};

void test05()
{
    Student s("王朝",23);
}

在这里插入图片描述

  
  

3.2、拷贝构造函数

  前提回顾: 默认生成的拷贝构造,对内置类型做浅拷贝,对自定义类型回去调用它自己的拷贝构造。(因此,若涉及深拷贝,则需要我们手动写)
  
  派生类中拷贝构造函数说明: 子类中,编译器默认生成的拷贝构造函数,对自己的成员遵循以前的规则,对继承的父类成员,须调用父类的拷贝构造完成父类的拷贝初始化
  
  假如涉及深拷贝,需要我们显示拷贝构造,在上述构造函数中我们知道,基类在派生类中是被当作整体看待的,因此拷贝构造初始化时,也要将其当作整体处理,但我们传入的参数并没有Person类,所以如何赋值就是一个问题。如何写?

    Student(const Student& s)
    :Person(s)
    ,_num(s._num)
{
    cout << "Student(const Student& s)" << endl;
}

  Person(s):这里就是基类和派生类赋值转换的运用体现。
在这里插入图片描述

  
  

3.3、赋值运算符重载

  前提回顾: 编译器默认生成的赋值运算符重载,其行为和拷贝构造类似,对内置类型不做浅拷贝,对自定义类型会调用它的赋值运算符重载。
  
  派生类中赋值运算符重载说明: 派生类的operator=必须要调用基类的operator=完成基类的复制。
  
  一个错误写法:

    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            operator=(s);
            _num = s._num;
        }
        return *this;
    }

  如下述,运行该代码时其结果显示栈溢出,这是因为operator=(s);,我们本意是想借助基类和派生类进行赋值转换,显示调用基类的operator=,这里编译器将其理解成调用student自身的operator=
在这里插入图片描述

  解决方案如下:添加类域作为区分

    Student& operator=(const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            Person::operator=(s);
            _num = s._num;
        }
        return *this;
    }

在这里插入图片描述

  
  
  

3.4、析构函数

  前提回顾: 编译器默认的析构函数,对内置类型不做处理,对自定义类型会去调用它自己的析构函数。
  
  派生类中析构函数的说明:
  错误写法演示:

    ~Student()//写法一
    {
      	~Person();
        cout << "~Student()" << endl;
    }

    ~Student()//写法二
    {
       Person::~Person();
        cout << "~Student()" << endl;
    }

  结果如下:
在这里插入图片描述
  写法一报错说明:由于多态的需要,析构函数名字会统一处理成destructor(),导致子类的析构的函数跟父类析构函数构成隐藏。因此,如果要显示析构父类,则需要添上父类的类域。

  写法二存在的问题:可以看到父类析构次数比子类析构多。这是因为为了保证先析构子类,再析构父类,实际中不需要显示调用父类的析构函数,每个子类析构函数后面,会自动调用父类析构函数。(PS:这里多次析构没报错,是因为该例子本身什么也没做)
  
  总结: 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象的清理顺序:先清理派生类成员,再清理基类成员。
  
  
  
  

3.5、取地址

  默认生成的即可使用,一般不需要显示写。

    Student* operator&()
    {
        return this;
    }

  
  
  

4、继承与友元、静态成员

4.1、继承和友元

  前提回顾:若A类是B类的友元,则在A类中可以访问B类的非公有成员。但这种关系是单向的,不具有交换性。友元函数同理。
  
  问题:由于子类继承父类,若父类中使用friend声明友元类,则该类是否为子类的友元类?
  回答:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。

class Student;
class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; 
};

class Student : public Person
{
protected:
    int _stuNum;
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;//是否能访问Person保护/私有成员?
    cout << s._stuNum << endl;//是否能访问Student保护/私有成员?
}

void main()
{
    Person p;
    Student s;
    Display(p, s);
}

  
  延伸:假如上述例子中,Display想要访问子类私有/保护成员,该如何操作?
  回答:在子类中将该函数/该类设置为其友元函数/友元类。

class Student;
class Person
{
public:
    friend void Display(const Person& p, const Student& s);//父类中设置为友元函数
protected:
    string _name; 
};

class Student : public Person
{
public:
    friend void Display(const Person& p, const Student& s);//子类中设置为友元函数
protected:
    int _stuNum;
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}
void main()
{
    Person p;
    Student s;
    Display(p, s);
}

  
  
  
  

4.2、继承和静态成员

  
  说明:基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
  
  演示代码如下:

class Person
{
public:
   
//protected:
    string _name;
    static int _count;
};
 int Person::_count = 0;


class Student : public Person
{
public:
    
//protected:
    int _stuNum;
};

int main()
{
    Person p;
    Student s;
    s._name = "马汉";
    p._name = "王朝";
    cout << s._name << endl;

    p._count++;
    cout << p._count << " " << s._count << endl;
    s._count++;
    cout << p._count << " " << s._count << endl;

    return 0;
}

在这里插入图片描述
  
  
  

4.3、一道例题

  1)、例题一:

  提问:如何定义一个不能被继承的类? 演示例子如下:

class A //父类
{
public:
	A(){}
	//……

protected:
	int _a;
};

class B : public A  //子类
{
	//……
};

  
  方案一(C++98):将父类的构造函数私有化。这时候,由于继承关系,父类构造在子类中不可见,因此子类对象实例化时,无法调用构造函数。

class A 
{
//public:
private://方案一:构造函数设置为私有,那么根据继承方式,子类中为不可见
	A(){}
	//……

protected:
	int _a;
};

class B : public A
{
	//……
};

int main()
{
	B b;
	return 0;
}

  
  方案二(C++11):final关键字。上述方案有一个缺点,在对象实例化时才得以体现,等同于间接起到效果,而该方案即使我们不实例化对象,也能达到题目要求。

class A final
{
public:
	A(){}
	//……

protected:
	int _a;
};

class B : public A //error
{
	//……
};

在这里插入图片描述
  
  
  

5、菱形继承、菱形虚拟继承

5.1、单继承和多继承介绍

  1)、单继承和多继承
  单继承:一个子类只有一个直接父类时称这个继承关系为单继承
  多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述
  
  
  2)、一道例题
  下面说法正确的是( )

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}
A:p1 == p2 == p3  
B:p1 < p2 < p3 
C:p1 == p3 != p2 
D:p1 != p2 != p3
E:编译报错
F:运行报错

  
  演示结果:可以看到答案选C。
在这里插入图片描述
  分析如下:
  1、public Base1, public Base2,多继承,先继承的先定义,故在栈区,Base1地址小于Base2。
  2、Base1* p1 = &d; Base2* p2 = &d;二者均为父类,故此处存在切片行为,需要注意父类指针的指向范围。
在这里插入图片描述
  
  
  

5.2、菱形继承和菱形虚拟继承

  1)、菱形继承概念介绍
  菱形继承是多继承的一种特殊情况。
在这里插入图片描述
  

  演示例子:

class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

  
  
  2)、存在的问题
  菱形继承存在数据冗余和二义性的问题。
  1、以上述1)中示例代码为例,对于数据冗余,在Assistant的对象中Person成员会有两份。
在这里插入图片描述

  2、对于二义性,指Person类在Sutdent、Teacher中都得到继承,随后又同时被Assistant继承,因此若在Assistant中调用Person成员,性无法明确知道访问的是哪一个。

void Test()
{
	//此处调用_name存在二义性
	Assistant a;
	a._name = "王朝";
}

在这里插入图片描述
  
  
  3)、解决方案
  1、指定类域访问:显示指定访问父类的成员,可以解决二义性问题,但数据冗余无法得到解决。

void test07()
{
	Assistant a;
	a.Student::_name = "王朝";
	a.Teacher::_name = "马汉";
	cout << a.Student::_name << endl;
	cout << a.Teacher::_name << endl;
}

在这里插入图片描述

  
  
  2、使用关键字virtual:菱形虚拟继承。需要注意该关键字添加位置。

class Person
{
public:
	string _name; // 姓名
};

class Student : virtual public Person//virtual关键字
{
protected:
	int _num; //学号
};

class Teacher : virtual public Person//virtual关键字
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};


void test07()
{
	Assistant a;
	a.Student::_name = "王朝";
	a.Teacher::_name = "马汉";
	a._name = "张龙";
}

  
  PS:在监视窗口我们看到的是多份,而实际上它们都指向一份。即菱形虚拟继承不仅能够解决二义性,还能够解决数据冗余问题

在这里插入图片描述

  
  
  4)、菱形虚拟继承的原理(对象存储模型)
  
  未使用虚拟继承,情况如下:
在这里插入图片描述

class A
{
public:
	int _a;
};

class B : public A
//class B : virtual public A
{
public:
	int _b;
};

class C : public A
//class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

void test08()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	//d._a = 0;

	d._b = 3;
	d._c = 4;
	d._d = 5;

}

  
  使用虚拟继承,情况如下:
在这里插入图片描述
  B和C中存储有两个指针,二者指向一张表,这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量,通过偏移量可以找到virtual继承下来的A。

class A
{
public:
	int _a;
};

//class B : public A
class B : virtual public A
{
public:
	int _b;
};

//class C : public A
class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

void test08()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._a = 0;

	d._b = 3;
	d._c = 4;
	d._d = 5;

}

  
  在上述菱形虚拟继承下,我们来探讨一下B/C类的大小:

class A
{
public:
	int _a;
};

//class B : public A
class B : virtual public A
{
public:
	int _b;
};

//class C : public A
class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

void test09()
{
	B b;
	b._a = 1;
	b._b = 2;
	cout << sizeof(b) << endl;

}

int main()
{
	test09();
	return 0;
}

  分析如下:
在这里插入图片描述
  
  问题:为什么B类中也要保存A的偏移量?
  可以考虑以下情形:如下,我们有一个函数,其参数为B类指针,但根据切片相关知识,B类只能指向其对应类的首地址,那么如何找到A呢?此处就需要借助偏移量。

void func(B* ptr)
{
	cout << ptr->_a << endl;
}

在这里插入图片描述

  
  
  5)、其它菱形继承例子演示

  例子一:

在这里插入图片描述

  
  例子二:实际中标准输入输出流里就存在菱形继承。

在这里插入图片描述
  
  
  
  
  其它:继承和组合说明。
  
  
  
  
  
  
  
  
  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/455066.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

flask学习-实践02

项目实战 入门文当(2条消息) python flask框架详解_flask python_尘世风的博客-CSDN博客(2条消息) python flask框架详解_flask python_尘世风的博客-CSDN博客 入门项目 抄作业了&#xff01;6 大 Flask 开源实战项目推荐_小詹学 Python的博客-CSDN博客 (66 条消息) GitHub 上有…

DataStructure--Tree

文章摘录链接 1.树基本概念 计算机数据结构中的树就是对显示中的树的一种抽象&#xff08;倒置现实中的树&#xff09;。 1.1 树 有层次关系N&#xff08;N≥0&#xff09;个节点的有限集合空树&#xff1a; N0 非空树&#xff1a; 有且只有一个根节点1.2 节点 根节点 分…

MongoDB【MongoRepository MongoTemplate】实现增删改查

目录 1&#xff1a;文章评论 1.1&#xff1a;需求分析 1.2&#xff1a;表结构分析 1.3&#xff1a;技术选型 1.3.1&#xff1a;mongodb-driver 1.3.2&#xff1a;SpringDataMongoDB 1.4&#xff1a;文章微服务模块搭建 1.5&#xff1a;文章评论实体类的编写 1.6&#x…

【计算机网络】为什么 TCP 每次建立连接时,初始化序列号都要不一样呢?

【计算机网络】为什么 TCP 每次建立连接时&#xff0c;初始化序列号都要不一样呢&#xff1f; 为什么 TCP 每次建立连接时&#xff0c;初始化序列号都要不一样呢&#xff1f; 主要原因是为了防止历史报文被下一个相同四元组的连接接收。 TCP 四次挥手中的 TIME_WAIT 状态不是会…

现代操作系统和 TCP/IP(第二篇)

接着 现代操作系统和 TCP/IP 继续。 现代分时系统的时间片轮转机制让人们可以 “同时使用计算机”&#xff0c;从而滋生了 “同时使用网络” 的需求&#xff0c;现代分时系统是分组交换网的原动力。 从来没有超过一个人同时使用同一部电话&#xff0c;因此独占线路的电路交换…

2023蓝桥杯省模拟赛附近最小

2023蓝桥杯省模拟赛附近最小 这个题算是一个经典的数据结构入门题了&#xff0c;写了几个解法水一篇文章 map维护 时间复杂度nlgn&#xff0c;但是常数比较大&#xff0c;所以只能过90%数据 #include <iostream> #include<vector> #include<map> #include…

卖房子真是稳赚不赔

上面是一段很长的语音&#xff0c;对话是用的我们河池的桂柳话&#xff0c;不过桂柳话和普通话有很多相识之处&#xff0c;理解起来并不困难。 大概的意思是 A公司要给员工一批福利房&#xff0c;然后就让开发商来竞标&#xff0c;竞标的时候开发商就会说明清楚到时候给员工的房…

关于stl容器的迭代器失效问题

场景 在项目中使用stl容器的时候&#xff0c;多线程环境下出错&#xff0c;调试很久发现问题是使用容器的时候由于容器扩容导致的线程不安全&#xff0c;还有扩容导致的迭代器失效问题&#xff0c;于是就想着把迭代器失效的问题总结一下。 场景重现1   我在项目开发中使用ve…

【AI帮我写代码,上班摸鱼不是梦】如何调教ChatGPT,让它帮我写程序。

最近发现磁盘空间严重不足&#xff0c;都弹窗提示我了&#xff1a; 想想看到底哪个文件夹占的空间比较大&#xff0c;好做针对性的删除和清理。奈何Windows系统没有查看文件夹大小的工具&#xff0c;只能鼠标放在某个文件夹上&#xff0c;等提示&#xff1a; AI时代都来临了&am…

AttributeError: ‘ChatGLMConfig‘ object has no attribute ‘quantization_bit‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

STM32 产生随机数方式

STM32 产生随机数方式 C语言的stdlib.h库里的srand(unsigned seed)和rand(void)函数&#xff0c;可以配合产生伪随机数。其中srand(seed)产生算法种子&#xff0c;再由rand()通过算法产生随机数&#xff0c;产生的随机数在宏定义RAND_MAX范围内。如果seed不变&#xff0c;则产…

测试从未如此简单:接口自动化测试Python脚本实现

目录 摘要 步骤1&#xff1a;安装依赖项 步骤2&#xff1a;编写测试脚本 步骤3&#xff1a;运行测试 结论 摘要 自动化测试是现代软件开发过程中的重要环节。在许多情况下&#xff0c;特别是在web应用程序和移动应用程序中&#xff0c;接口自动化测试是其基础。下面就来介绍一…

嵌入式就业怎么样?

嵌入式就业怎么样? 现在的IT行业,嵌入式是大热门&#xff0c;下面也要来给大家介绍下学习嵌入式之后的发展以及就业怎么样。 首先是好找工作。嵌入式人才目前是处于供不应求的状态中&#xff0c;据权威统计机构统计在所有软件开发类人才的需求中&#xff0c;对嵌入式工程师的…

详解树与二叉树的概念,结构,及实现(上篇)

目录 一&#xff0c; 树 1.2 树的相关概念 1.3 树的表示 1.4 树在实际中的运用&#xff08;表示文件系统的目录树结构&#xff09; 二&#xff0c; 二叉树 2.1二叉树概念 三&#xff0c;特殊的二叉树 1. 满二叉树 2. 完全二叉树 3. 1 二叉树的性质 3. 2 二叉树的存储…

北邮22信通:二叉树层序遍历的两种方法首发模板类交互

北邮22信通一枚~ 跟随课程进度每周更新数据结构与算法的代码和文章 持续关注作者 解锁更多邮苑信通专属代码~ 获取更多文章 请访问专栏~ 北邮22信通_青山如墨雨如画的博客-CSDN博客 目录 一.总纲 二.用队列存储 2.1用模板类实现队列 2.1.1核心思路&#xff1a; …

FL Studio电音编曲软件V21中文完整版 安装下载教程

目前水果软件最版本是FL Studio 21&#xff0c;它让你的计算机就像是全功能的录音室&#xff0c;大混音盘&#xff0c;非常先进的制作工具&#xff0c;让你的音乐突破想象力的限制。喜欢音乐制作的小伙伴千万不要错过这个功能强大&#xff0c;安装便捷的音乐软件哦&#xff01;…

PTA L2-045 堆宝塔 (25 分)

堆宝塔游戏是让小朋友根据抓到的彩虹圈的直径大小&#xff0c;按照从大到小的顺序堆起宝塔。但彩虹圈不一定是按照直径的大小顺序抓到的。聪明宝宝采取的策略如下&#xff1a; 首先准备两根柱子&#xff0c;一根 A 柱串宝塔&#xff0c;一根 B 柱用于临时叠放。把第 1 块彩虹圈…

掌握ChatGPT:全面指南和GPT-3.5与GPT-Plus的对比分析

在人工智能领域&#xff0c;最近的一大重磅炸弹是OpenAI发布了GPT-4架构下的ChatGPT。这款先进的自然语言处理模型已经引起了很多关注&#xff0c;让我们来深入了解怎么使用这个强大的工具&#xff0c;以及比较GPT-3.5与GPT-Plus的差异。 什么是ChatGPT&#xff1f; ChatGPT是…

JavaScript经典教程(三)-- JavaScript -- 基础数据类型详解

183&#xff1a;基础数据类型详解 1、复习 1、全局变量&#xff1a; 依托于window环境下的的变量。 在window大环境下&#xff08;即最外层&#xff09;申明的变量a&#xff0c;即为全局变量。 2、window&#xff1a; window下的变量为全局变量。 window可省略。 3、作用域…

021 - C++ 构造函数

我们继续学习 C 的面向对象编程&#xff0c;本期主要是讲其中的 构造函数。 什么是构造函数呢&#xff1f; 构造函数基本上是一种特殊类型的方法&#xff0c;它在每次实例化对象时运行。 我们直接来看一个例子吧。 例子时间 我们将要通过创建一个 Entity 类来深入了解这个…