【C++】仅需一文速通继承

news2024/9/20 8:08:12

文章目录

    • 1.继承的概念及定义
      • 继承的概念
      • 继承的定义
        • 定义格式:
        • 继承关系和访问限定符
        • 继承基类成员访问方式的变化
    • 2.基类和派生类对象赋值转换
    • 3.继承中的作用域
    • 4.派生类的默认成员函数
      • 题目:设计出一个类A,让这个类不能被继承(继承了也没用)
    • 5.继承与友元
    • 6.继承与静态成员
    • 7.复杂的菱形继承及菱形虚拟继承
      • 虚继承
      • 虚继承的原理
    • 8.继承的总结和反思
    • 9.笔试面试题

面向对象的三大特性

  • 封装:数据和方法都封装到一起,通过访问限定符更好的管理数据和方法

  • 继承:类抽象级别的复用,公共数据和方法提取到父类,更好实现现实世界的关系

  • 多态 : 就是多种形态,具体点就是去完成某个行为,当不同的对象去完成同一件事情会产生出不同状态


1.继承的概念及定义

继承的概念

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


例如,下面的代码中Student类和Teacher类就继承了Person类,

//父类(基类)
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "张三"; //姓名
	int _age = 18;     //人的年龄
};
//子类
class Student : public Person
{
protected:
	int _stuid;   //学生的学号
};
//子类
class Teacher : public Person
{
protected:
	int _jobid;   //老师的工号
};

image-20220312221340588

继承后,父类Person的成员函数和成员变量,都会变成子类的一部分,也就是说,子类Student和Teacher复用了父类Person的成员,


继承的定义

class 派生类名字 : 继承方式 基类名字

定义格式:

下面我们看到Person是父类,也称作基类,Student是子类,也称作派生类,

image-20220311211225338

注意:在继承当中,父类也可以称为基类,子类是由基类派生而来的,所以子类又称为派生类,


继承关系和访问限定符

共有3种继承方式,访问限定符也有3种 所以可以组成9种不同的继承和访问限定方式

image-20220311211259918


继承基类成员访问方式的变化

基类(父类)的成员被不同访问限定符修饰,会以不同的继承方式继承到派生类(子类)当中后,该成员最终在派生类(子类)当中的访问方式将会发生变化,

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见
得出规律:认为三种访问限定符的权限大小为:public > protected > private ,取二者权限的较小值作为成员的权限

实际上面的表格我们进行一下总结会发现,基类的私有成员(private)在子类都是不可见,基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private

所以我们只需要记住:

  • 父类的私有成员在子类不可见
  • 继承关系和父类访问限定符取Min,就是派生类的成员关系

注意:保护和私有的成员在父类中没有区别,但是在子类中,private成员是不可见的,protected成员是可见的


1)基类private(私有)成员在派生类中无论以什么方式继承都是不可见的,

什么叫’不可见’ ->继承了但是在子类不能用

  • 这里的不可见是指基类的私有成员还是被继承到了派生类对象中,
  • 但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它,
  • 从内存上: 子类对象拥有这个成员 语法上:规定了我们不能去访问,但是我们可以通过父类的函数去访问!
//基类
class Person
{
private:
	string _name = "张三";//这里不是初始化,这里是给缺省值
};

//派生类
class Student :public Person
{
public:
	void Print()
	{
		//在派生类当中访问基类的private成员,报错!!!
		cout << _name << endl;
	}
protected:
	int _stdid;//学生学号
};

2)基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected,

  • 可以看出保护成员限定符是因继承才出现的,

基类中,如果想给别人访问,就定义成公有, 不想别人访问就定义成保护,父类保护的成员被继承了,在子类中能使用,但是父类私有的成员被继承下来,在子类中不能使用

在基类中,保护和私有是一样的,都是对外不能访问,但是在子类中的意义不一样

但是我们可以在子类通过基类的成员函数访问基类的私有成员

//基类
class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;//通过调用基类的成员函数,在派生类中访问基类的私有成员
	}
protected:
	string _name = "zhangsan";
private:
	int _age = 19;//私有成员
};

class Student : public Person
{
private:
	int _stuid;
};

int main()
{
	Student s;
	s.Print();
	return 0;
}

3)在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强,


4)使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式,

//基类
class Person
{
public:
	string _name;
};

//派生类
class Student : Person //class默认的继承方式是private
{
protected:
	int _stuid;
};

继承下来的 基类成员_name的访问方式变为private,


//基类
class Person
{
public:
	string _name;
};

//派生类
struct Student : Person //struct默认的继承方式是public
{
protected:
	int _stuid;
};

继承下来的基类成员_name的访问方式为public


常见的使用:

父类成员:公有private和保护protected 子类继承方式:公有继承public


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

  • 派生类对象(子类对象) 可以赋值给 基类的对象 / 基类的指针 / 基类的引用,这里有个形象的说法叫切片或者切割,寓意把派生类中父类那部分切来赋值过去 (赋值兼容规则)

在这个过程中,会发生基类和派生类对象之间的赋值转换, (子可以赋给父 但是父不可以赋给子,强转也不行)

例子

//基类
class Person
{
protected:
	string _name; //姓名
	string _sex;  //性别
	int _age;     //年龄
};
//派生类
class Student : public Person
{
protected:
	int _stuid;   //学号
};
int main()
{
    Student s;
    Person p = s;     //派生类对象赋值给基类对象
    Person* ptr = &s; //派生类对象赋值给基类指针
    Person& ref = s;  //派生类对象赋值给基类引用
	//s = (Student)p;   //err	不存在用户定义的从 "Person" 到 "Student" 的适当转换	
}

//基类
class Person
{
public:
	string _name; //姓名
	string _sex;  //性别
	int _age;     //年龄
};
//派生类
class Student : public Person
{
protected:
	int _stuid;   //学号
};
int main()
{
	Person p;
	Student s;
	s._name = "Mango";
	s._sex = "female";
	s._age = 19;
	p = s;//子赋给父  切片,此处是深拷贝
}

image-20220313082831582


这里可以形象认为是切片:

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

image-20220312224256847

派生类对象赋值给基类指针

image-20220312224311217

父类指针指向子类对象, 但是父类指针ptr只能看(访问)继承的父类成员,即指向父类的那一部分

派生类对象赋值给基类引用

image-20220312224324167

ref成为父类那一部分的别名


注意:

  • 基类对象不能赋值给派生类对象

  • **基类的指针可以通过强制类型转换赋值给派生类的指针,但是必须是基类的指针是指向派生类对象时才是安全的,**这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换,(ps:这个我们有会讲,这里了解一下)

  • 子类赋值给赋给的父类对象/指针/引用,这里不存在类型转化,是语法天然支持的行为

    • 如果是类型转化,就不能引用了,因为类型转化中间会产生临时变量,临时变量具有常性,不能修改!

      int i = 1;
      double d = 2.2;
      //i = d;//err,类型转化中间会产生临时变量
      const int& ri = d;//临时变量具有常性,不能修改,
      

这种赋值只适用与公有继承,为什么?

原因:可能发生权限的转化:

例子:

class Person
{
protected:
	string _name;//姓名
	string _sex;///性别
public:
	int _age;//年龄
};
//私有继承父类
class Student :private Person
{
public:
	int _Num;//学号
};

父类成员是公有的和保护的,被子类私有继承下来了,访问权限变成私有的,当我们切割切片子类对象给父类对象/引用/.指针,从父类的角度去看,此时子类成员是保护和公有的!权限变大


父类对象不可以赋值给子类对象  但是指针和引用可以,需要强制类型转化

  • 但是可能会存在越界的风险

    image-20220430155537836

int main()
{
	/*
	Student s;
	Person p;
	//父类对象不可以赋值给子类对象,强制类型转化也不可以
	//s = p;
	//s = (Student)p;
	*/

	//但是指针和引用可以
	Person p;
	Student* pptr = (Student*)&p;
	Student& rref = (Student&)p;

	//但是存在越界的风险:
	//pptr->_Num = 1;//程序崩溃
	return 0;
}

如果父类的私有成员被子类继承下来,然后可以进行切片后的指针/引用吗? 如果可以,可以通过切片后的父类的指针/引用访问父类的私有成员吗?

父类的私有成员在子类不可见,但是切片仍然可以完成! 但是切片的父类的指针/引用无法访问父类的私有成员

class Person
{
protected:
	string _name;//姓名
	string _sex;///性别
private:	//此时_age是父类的私有成员!!
	int _age;//年龄
};
//私有继承父类
class Student :public Person
{
public:
	int _Num;//学号
};
int main()
{
	Student s;
	Person p;
	p = s;
	Person* ptr = &s;
	Person& ref = s;
	ptr->_age = 1; //err,不可以访问!
}

3.继承中的作用域

在继承体系中基类和派生类都有独立的作用域

1)子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义,(在子类成员函数中,可以使用域限定符进行访问 基类::基类成员 显示访问)

例子:

//父类
class Person
{
protected:
	int _num = 111;
};
//子类
class Student : public Person
{
public:
	void fun()
	{
		cout << _num << endl;
	}
protected:
	int _num = 999;
};
int main()
{
	Student s;
	s.fun(); //999   访问的是子类的_num成员变量
	return 0;
}

如果我们就是想访问父类的_num成员:可以使用 基类::基类成员 显示访问)

void fun()
{
	cout << Person::_num << endl; //指定域访问父类的_num成员
}

注意 : 如果是成员函数的隐藏,只需要函数名相同就构成隐藏,参数可以相同,也可以不相同

//父类
class Person
{
public:
	void fun(int x)
	{
		cout << x << endl;
	}
};
//子类
class Student : public Person
{
public:
	void fun(double x)
	{
		cout << x << endl;
	}
};
int main()
{
	Student s;
	s.fun(3.14);       //直接调用子类当中的成员函数fun
    //s.fun(20);//被隐藏了,调不动
	s.Person::fun(20); //通过指定类作用域,指定调用父类当中的成员函数fun
	return 0;
}

父类中的fun函数和子类中的fun函数并不是构成函数重载,二者是隐藏


函数重载的要求:函数要在同一作用域

注意在实际中在继承体系里面最好不要定义同名的成员,


问:父子有同名变量时,当子给父赋值的时候,切割用的是继承的变量还是子自己的变量?

答: 不管有没有同名的,只会切割从父类继承的成员

image-20220313083329926


4.派生类的默认成员函数

6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

image-20220311211615146


派生类的重点的四个默认成员函数,如果我们不写,编译器会默认生成的会干些什么事情呢?如果我们要写,要做些什么事情呢?


a.我们不写,默认生成的派生类的构造和析构:

  • 父类继承下来的(调用父类默认构造和析构进行处理)
  • 自己的(内置类型和自定义类型成员) ->跟普通类一样处理

b.我们不写,默认生成的派生类的拷贝构造和赋值重载:

  • 父类继承下来的(调用父类默认拷贝构造和赋值重载)
  • 自己的(内置类型和自定义类型成员) ->跟普通类一样处理

总结:原则:继承下来的,调用父类的进行处理,自己的就按普通类的规则进行处理


什么时候必须我们自己写?

  • 父类没有默认构造,需要我们自己显示写构造
  • 如果子类有资源需要释放,就需要自己显示写析构
  • 如果子类存在浅拷贝,就需要自己实现拷贝构造和赋值解决浅拷贝问题

例子

基类

class Person
{
public:
	//基类的构造函数
	Person(const char* name = "Mango")
		:_name(name)
	{
		cout << "Person(const char* name)" << endl;
	}
	//基类的拷贝构造函数
	Person(const Person& p)
		:_name(p._name)
	{
		cout<<"Person(const Person& p)"<<endl;
	}
	//基类的拷贝构造函数
	Person& operator=(const Person& p)
	{
		cout << "Person& opearator = (const Person & p)" << endl;
		if(this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	//基类的析构函数
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
};

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

//派生类的构造函数
Student(const char* name, int id)
    :Person(name)  //把父类当成一个整体,显示的初始化
        ,_id(id)
    {
        //调用父类构造函数初始化继承的父类的部分
        //再初始化派生类自己的成员
        cout << "Student()" << endl;
    }

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化

//派生类的拷贝构造
//s2(s1),用s1父类的部分初始化s2父类的部分!
Student(const Student& s)
    :Person(s)	//把子类传给父类的引用,是一种切片的行为
        ,_id(s._id)
    {
        //调用父类拷贝构造函数用于拷贝构造继承的父类的部分
        //再拷贝构造派生类自己的成员
        cout << "Student(const Student& s)" << endl;
    }

image-20220313084959911

派生类的operator=必须要调用基类的operator=完成基类的复制, 就近原则,子类的赋值重载函数把父类的隐藏了,所以是死循环不断调用自己,导致栈溢出. 所以需要指定作用域调用父类的赋值重载

Student& operator=(const Student& s)
{
    cout<<"Student& operator=(const Student& s)"<<endl;
    if(this != &s)
    {
        Person::operator=(s);//切片行为,调用父类的operator处理父类的那部分
        _id = s._id;
    }
    return *this;
}

image-20220313085543328


子类的析构函数会在被调用完成后自动调父类的析构函数清理父类的成员,因为这样才能保证先清理子类成员再清理父类成员的顺序,

  • 因为后面多态的原因,任何类的析构函数名都被统一处理为:destructor(),所以编译器会认为子类的析构函数和父类的析构函数构成隐藏关系
  • 为了保证析构时:保持先子再父的后进先出的顺序析构,子类析构函数完成后,会自动调用父类的析构函数

image-20220313090522354

初始化时:我们时先调用父类的函数初始化父类的那部分,然后再初始化子类的部分, 要符合栈的特征:后进先出

所以析构的时候:要先析构子类的,再析构父类的

//派生类的析构函数
~Student()
{
    cout << "~Student()" << endl;
}

总结:

//派生类
class Student : public Person
{
public:
	//派生类的构造函数
	Student(const char* name = "Mango", int id = 0)
		:Person(name)  //把父类当成一个整体,显示的初始化 调用基类的构造函数初始化基类的那一部分成员
		,_id(id)//初始化派生类的成员
	{
		//调用父类构造函数初始化继承的父类的部分
		//再初始化派生类自己的成员
		cout << "Student()" << endl;
	}
	//派生类的拷贝构造
	Student(const Student& s)
		:Person(s)	//把子类传给父类的引用,是一种切片的行为 调用基类的拷贝构造函数完成基类成员的拷贝构造
		,_id(s._id)//拷贝构造派生类的成员
	{
		//调用父类拷贝构造函数用于拷贝构造继承的父类的部分
		//再拷贝构造派生类自己的成员
		cout << "Student(const Student& s)" << endl;
	}
	//派生类的operator=赋值
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		//防止自己给自己赋值
		if (this != &s)
		{
			Person::operator=(s);//调用基类的operator=完成基类成员的赋值
			_id = s._id;//完成派生类成员的赋值
		}
		return *this;
	}
	//派生类的析构函数
	~Student()
	{
		cout << "~Student()" << endl;
        //派生类的析构函数调用完成后,会自动调用基类的析构函数
	}
private:
	int _id;
};

注意:

派生类对象初始化先调用基类构造再调派生类构造, (初始化:先父再子)

派生类对象析构清理先调用派生类析构再调基类的析构,(析构:先子再父)

  1. 派生类和基类的赋值运算符(operator=)重载函数因为函数名相同构成隐藏,因此在派生类当中调用基类的赋值运算符重载函数时,需要使用作用域限定符进行指定调用,
  2. 由于多态的某些原因,任何类的析构函数名都会被统一处理为destructor();,因此,派生类和基类的析构函数也会因为函数名相同构成隐藏,若是我们需要在某处调用基类的析构函数,那么就要使用作用域限定符进行指定调用,
  3. 在派生类的拷贝构造函数和operator=当中调用基类的拷贝构造函数和operator=的传参方式是一个切片行为,都是将派生类对象直接赋值给基类的引用,
  4. 基类的构造函数、拷贝构造函数、赋值运算符重载函数我们都可以在派生类当中自行进行调用,而基类的析构函数是当派生类的析构函数被调用后由编译器自动调用的,我们若是自行调用基类的构造函数就会导致基类被析构多次的问题,
  5. 创建派生类对象时是先创建的基类成员再创建的派生类成员,编译器为了保证析构时先析构派生类成员再析构基类成员的顺序析构,所以编译器会在派生类的析构函数被调用后自动调用基类的析构函数,

题目:设计出一个类A,让这个类不能被继承(继承了也没用)

想法:只需让A的默认构造函数私有即可, 因为派生类想创建对象,派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,而如果基类的构造函数无法访问,就不能创建对象,

class A
{
private:
	A()
	{}
};
class B :public A
{

};
int main()
{
	B b;//报错,如果不创建对象就不会报错
	return 0;
}

5.继承与友元

友元关系不能继承,也就是说基类友元可以访问基类的私有和保护成员,但是不能访问子类私有和保护成员

例如:你爸爸的朋友你不一定认识

image-20220313093937743

class Student;//声明
class Person
{
public:
	//声明Display函数是Person类的友元
	friend void Display(const Person& p, const Student& s);
private:
	string _name;
};

class Student: public Person
{
private:
	int _stuid;
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl; //可以访问
	cout << s._stuid << endl;//不可以访问
}

若想让Display函数也能够访问派生类Student的私有和保护成员,只能在派生类Student当中进行友元声明

class Student : public Person
{
public:
	//声明Display函数是Student类的友元
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuid; 
};

6.继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的静态成员,无论派生出多少个子类,都只有一个static成员实例 ,

例如:当基类Person中定义了一个静态成员变量count,派生类Student和Teacher都继承了基类Person的成员,但是再整个继承体系中,只有一个静态成员变量count

  • 我们可以通过该静态成员变量count的数值,得知创建的对象个数

由于通过派生类/基类创建一个对象,都需要调用基类的构造函数/拷贝构造函数初始化基类的部分成员,所以我们可以在基类的构造函数/拷贝构造函数中累加_count的值,就可以得知创建了多少个对象

//基类
class Person
{
public:
	//基类的构造函数
	Person()
	{
		++_count;
	}
	//基类的拷贝构造函数
	Person(const Person& p)
	{
		++_count;
	}
public:
	static int _count;//静态成员变量
protected:
	string _name;
};
int Person::_count = 0;//静态成员变量在类外初始化

//派生类
class Student :public Person
{
private:
	int _stuid;
};

//派生类
class Teacher :public Person
{
private:
	int _teaid;
};
void func(Student s)
{}
//可以通过_count的大小得知创建的对象的个数
int main()
{
	Student s1;
	Student s2 = s1;
	Teacher t1;
	Person p;
    func(s1);//传值传参也是一次拷贝构造
    //_count成员是同一个 整个继承体系里面只有一个这样的静态成员
	cout << Person::_count << endl;//5
	cout << Student::_count << endl;//5
	return 0;
}

验证:整个继承体系里面只有一个这样的静态成员

//三者的地址相同
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Teacher::_count << endl;

7.复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

image-20220311211657847

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,用,分隔

image-20220311211707047

多继承的坑: 菱形继承:菱形继承是多继承的一种特殊情况,

image-20220311211721737

有多继承就可能导致菱形继承,会导致问题:

  • 数据冗余
  • 二义性

例子

//基类
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; 
};

int main()
{
	Assistant a;
	a._name = "peter"; //导致二义性:无法明确知道要访问哪一个_name
	return 0;
}
image-20220313095735105

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份,


解决办法1:显示指定作用域去访问

int main()
{
	Assistant a;
    //指定哪个父类的成员
	a.Teacher::_name = "Mango";
	a.Student::_name = "Lemon";
	return 0;
}

image-20220313100015787

虽然这样可以解决二义性的问题,但是不能解决数据冗余的问题


虚继承

C++不能容忍数据冗余和二义性,提出了新的解决方案, 虚继承:新的关键字:virtual

虚拟继承可以解决菱形继承的二义性和数据冗余的问题,

如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题,需要注意的是,虚拟继承不要在其他地方去使用,

image-20220313100550236


//基类
class Person
{
public:
	string _name;
};
//虚继承
class Student : virtual public Person
{
protected:
	int _num;
};
//虚继承
class Teacher : virtual public Person
{
protected:
	int _id;
};
//菱形继承
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};
int main()
{
	Assistant a;
	//虚继承之后,可以解决数据冗余和二义性的问题
	a._name = "Mango";
	return 0;
}

image-20220313100756430

此时我们可以直接访问_name成员,并且都是同一个结果,解决了二义性的问题,

我们打印Assistant的Student父类和Teacher父类的_name成员的地址时,显示的也是同一个地址,解决了数据冗余的问题,

//二者地址相同,解决了数据冗余的问题
cout << &a.Student::_name << endl; 
cout << &a.Teacher::_name << endl;

虚继承的原理

C++编译器是如何通过虚继承解决数据冗余和二义性的呢?

1.通过监视窗口已经看不到真实的存在,因为监视窗口被编译器处理过

2.建议使用内存窗口来进行查看

虽然虚继承补了菱形继承的坑,但是也付出了很大的代价, 1.对象模型更复杂了,学习理解成本很高 2.有一定的效率影响


虚拟继承解决数据冗余和二义性的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型,

image-20220313101142212

未使用虚继承时的情况:

class A
{
public:
    int _a;
};
class B : public A
{
public:
    int _b;
};
class C : public A
{
public:
    int _c;
};
class D : public B, public C
{
public:
    int _d;
};
int main()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    return 0;
}

未使用虚继承

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余

image-20220311212146687

image-20220313102612176

先继承B,再继承C 我们也可也看出, 先继承的在前面,后继承的在后面, 这里就可以看出为什么菱形继承导致了数据冗余和二义性,根本原因就是D类对象当中含有两个_a成员,


使用虚拟继承后

image-20220313102534200

class A
{
public:
	int _a;
};
class B : virtual public A
{
public:
	int _b;
};
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

image-20220313105402108

其中D类对象当中的_a成员被放到了最后,而在原来存放两个_a成员的位置变成了两个指针,这两个指针叫虚基表指针,它们分别指向一个虚基表,

虚基表中包含两个数据,第一个数据是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类对象位置距离公共虚基类的偏移量,也就是说,这两个指针经过一系列的计算,最终都可以找到成员_a

注意:虚基表中存的是相对地址

image-20220313105551978


下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?

这里是通过了B和C的两个指针,指向的一张表,这两个指针叫虚基表指针,这两个表叫虚基表,虚基表中存的偏移量,通过偏移量可以找到下面的A,

image-20220311212159114


下面是上面的Person关系菱形虚拟继承的原理解释:

image-20220311212216501


若是将D类对象赋值给B类对象,在这个切片过程中,就需要通过虚基表中的第二个数据找到公共虚基类A的成员,得到切片后该B类对象在内存中仍然保持这种分布情况

子类对象赋值给父类对象 ->称为切片

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	B b = d;//切片行为
	return 0;
}

image-20220313110545549

_a对象仍然存储在该B类对象的最后


只要有公共的祖先类就是菱形继承, 例如:

image-20220430163058152


8.继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现,有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,所以不要设计出菱形继承,否则在 复杂度及性能上都有问题,出问题难以分析,并且会有一的效率影响

  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java,

  3. 继承和组合

    class A
    {
    public:
    	void func(){}
    protected:
    	int _a;
    };
    //B继承了A,可以复用A
    class B : public A
    {
    protected:
    	int _b;
    };
    //C组合A,也可以复用A
    class C
    {
    private:
    	int _c;
    	A _a;
    };
    
    • 继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象,组合是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象,

      • 例如: 水果和芒果就是 is-a的关系,它们之间适合使用继承
      • 例如:车和轮胎之间就是has-a的关系,它们之间则适合使用组合,
    • 若是两个类之间既可以看作is-a的关系,又可以看作has-a的关系,优先使用对象组合,而不是类继承

    原因:

    • 比如一起出去旅行: 自由团:关联度,耦合度低 群体团: 关联度,耦合度高

    软件设计类之间关系或者模块间的关系强调: 高内聚,低耦合 ,方便维护即:类里面的成员之间关联度很高,类和类之间关联度很低

    image-20220313163459026

    • 继承允许你根据基类的实现来定义派生类的实现,这种通过生成派生类的复用通常被称为白箱复用,术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 ,继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响,派生类和基类间的依赖关系很强,耦合度高,
    • 对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得,对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用,因为对象的内部细节是不可见的,对象只以“黑箱”的形式出现, 组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于你保持每个类被封装,
    • 实际尽量多去用组合,组合的耦合度低,代码维护性好,不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承,类之间的关系可以用继承,可以用组合,就用组合,

    完全符合is-a,就用继承, 完全符合has-a 就用组合,都可以,优先使用组合


9.笔试面试题

  • 什么是菱形继承?菱形继承的问题是什么?

菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,我们称这种继承为菱形继承, 菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题

  • 什么是菱形虚拟继承?如何解决数据冗余和二义性的

image-20220313111128023

菱形虚拟继承是指在菱形继承的腰部使用虚拟继承(virtual)的继承方式,菱形虚拟继承对于D类对象当中重复的A类成员只存储一份,然后采用虚基表指针和虚基表使得D类对象当中继承的B类和C类可以找到自己继承的A类成员,从而解决了数据冗余和二义性的问题

  • 继承和组合的区别?什么时候用继承?什么时候用组合?

继承是一种is-a的关系,而组合是一种has-a的关系,如果两个类之间是is-a的关系,使用继承;如果两个类之间是has-a的关系,则使用组合;如果两个类之间的关系既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合

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

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

相关文章

VK Cup 2017 - Round 1 A - Bear and Friendship Condition(并查集维护大小 + dfs 遍历图统计边数)

题目大意&#xff1a; 给你一些n个点m条边&#xff0c;如果三个点&#xff08;a,b,c&#xff09;是合法的&#xff0c;当且仅当 a-b,b-c,c-a都有一条边&#xff0c;问你这个图是否合法&#xff0c;如果有一个或两个点视为合法 思路 考虑什么图才是个合法图&#xff1a;除了点…

Spring 更简单的读取和存储对象

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 前面介绍了通过配置文件的方式来存储 Bean 对象&#xff0c;那么有没有更简单的方式去存储 Bean 对象&#xff1f; 有以下 2 种方…

【论文】LearningDepth from Single Monocular Images

2005 NIPS 文章目录 特征提取卷积核的使用Multiscale 多尺度提取特征特征的相对深度 模型结论特征提取数据集导致的error 文章使用了Markov 随机场(Markov Random Fields, MRF) 从单图像上直接估计出图像的深度信息。 与RGBD输入数据不同的是&#xff0c;文章中采用了YCbCr数据…

知识点总结-DAY1

1. 请解释OSI模型中每一层的作用 应用层&#xff1a;为用户提供服务&#xff0c;处理应用程序之间交换的数据。 表示层&#xff1a;处理数据在网络上的表示形式&#xff0c;如加密和解密、压缩和解压缩等。 会话层&#xff1a;建立、维护和终止两个节点之间的会话&#xff0c…

安全防御 --- IPSec理论

IPSec 1、概述&#xff1a; 是IETF&#xff08;Internet Engineering Task Force&#xff09;制定的一组开放的网络安全协议&#xff0c;在IP层通过数据来源认证、数据加密、数据完整性和抗重放功能来保证通信双方Internet上传输数据的安全性。 IPSec安全服务 机密性完整性…

雨季时,骑行经过泥泞路段该怎么办?

泥泞路段骑行是一项需要技巧和勇气的挑战。在泥泞路段骑行&#xff0c;骑友又叫玩泥巴&#xff0c;不仅需要良好的车技和身体素质&#xff0c;还需要有足够的经验和判断力&#xff0c;以应对各种突发情况。下面&#xff0c;将从多个角度介绍泥泞路段骑行的挑战和技巧&#xff0…

宏观经济笔记--社会消费品零售总额

我们讨论了GDP的三个分项&#xff1a;投资、消费、净出口。投资我们前面已经介绍了&#xff0c;消费这一个分项我们还一直没有讨论。消费最重要的数据是每个月月中统计局公布的社会消费品零售总额。 一般的论调中&#xff0c;认为消费是三个GDP驱动项中最健康的一项&#xff0…

2023-5-4-Lua语言学习

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

openQA----基于openQA新增指定版本的openSUSE的iso镜像进行测试

【原文链接】openQA----基于openQA新增指定版本的openSUSE的iso镜像进行测试 &#xff08;1&#xff09;执行如下命令下载openSUSE的测试脚本&#xff0c;它会从openSUSE的测试脚本github地址 /usr/share/openqa/script/fetchneedles&#xff08;2&#xff09;然后执行如下命…

在 SourceTree 中使用 rebase (win10)

原始状态 创建两个分支 dev1 dev2, 并且推送到远端 切换到dev1 做一些修改并提交dev1-1&#xff0c;注意不要推送到到远端 切换到master分支&#xff0c;拉取最新的代码 切换到dev1 分支&#xff0c;进行变基操作&#xff0c;右击master分支 推送dev1分支到远端 切换到master分…

如何快速获取已发表学术论文的期刊封面及目录(caj格式下载和caj转pdf)

目录 1 下载caj格式的封面和目录 2 CAJ格式的封面和目录转PDF格式 在进行职称评审或成果申报时&#xff0c;一般要求提交你发表的成果所在的期刊的当期封面和目录。本文就手把手带带你制作一个期刊目录。 重要提示&#xff1a;下载期刊封面和目录需要你有知网账号&#xff0…

iOS 17预计开放侧载,游戏安全对抗将迎来新高度

近日&#xff0c;据彭博社报道&#xff0c;iOS 17预计开放“侧载”机制。所谓的“侧载”是指&#xff1a;iPhone用户下载APP时&#xff0c;可选择不在APP store中下载&#xff0c;可在相关APP官网或者第三方应用市场进行下载。 众所周知&#xff0c;APP闭源生态是苹果公司最核…

C++ 基础二

C 核心编程 1 内存分区模型 内存分区模型 代码区&#xff1a;存放函数的二级制代码&#xff0c;由操作系统进行管理的 全局区&#xff1a;存放全局变量和静态变量以及常量 栈区&#xff1a;由编译器自动分配释放&#xff0c;存放函数的参数值&#xff0c;局部变量等 堆区&…

5.1 矩阵的特征值和特征向量

学习步骤&#xff1a; 学习特征值和特征向量的定义和性质&#xff0c;我会采取以下方法&#xff1a; 1. 学习线性代数基础知识&#xff1a;特征值和特征向量是线性代数中的重要概念&#xff0c;需要先掌握线性代数的基础知识&#xff0c;例如向量、矩阵、行列式、逆矩阵、转置…

Bark:基于转换器的文本到音频模型

Bark是由Suno创建的一个基于转换器的文本到音频模型。Bark可以生成高度逼真的多语言语音以及其他音频&#xff0c;包括音乐、背景噪音和简单的音效。该模型还可以产生非语言交流&#xff0c;如大笑、叹息和哭泣。为了支持研究社区&#xff0c;我们正在提供对预先训练的模型检查…

生物信息学中---数据集不平衡的处理方法

1.NearMiss&#xff1a; NearMiss 是 Mani 等人根据数据分布特征&#xff0c;基于 KNN 算法提出的欠采样方案&#xff0c; 对多数类样本利用随机欠采样来达到数据平衡。 根据不同数据采样的距离&#xff0c;可以分为三类&#xff1a; NearMiss-1、 NearMiss-2 和 NearMiss-3。…

什么是数据库分片?

什么是数据库分片&#xff1f; 数据库分片是指将一个大型数据库拆分成多个小型数据库&#xff0c;每个小型数据库称为一个分片。通过这种方式&#xff0c;可以将数据库的负载分散到多个服务器上&#xff0c;从而提高数据库的性能和可伸缩性。 为什么需要数据库分片&#xff1f…

从一到无穷大 #7 Database-as-a-Service租户隔离挑战与解决措施

文章目录 引言计算侧多租户隔离2DFQSQLVMRetro 其他隔离方法其他 引言 在云环境中租户之间的资源共享对于运营商的成本效益来说非常重要&#xff0c;但是一个主要问题是租户之间的资源隔离&#xff0c;这通常与Qos息息相关&#xff0c;从多租户的角度讲&#xff0c;安全性/性能…

ChatGPT 不好用?那你看下这份 Prompt 工程指南

作为大型语言模型接口&#xff0c;ChatGPT 生成的响应令人刮目相看&#xff0c;然而&#xff0c;解锁其真正威力的关键还是在于提示工程。 在本文中&#xff0c;我们将揭示制作提示的专家级技巧&#xff0c;以生成更准确、更有意义的响应。无论你使用 ChatGPT 是为了服务客户、…

用格林童话教你1分钟清晰JS加密

在许多格林童话中&#xff0c;我们可以看到许多隐藏的玄机和谜题&#xff0c;就像JavaScript代码一样。为了保护您的代码安全&#xff0c;我们可以使用JavaScript混淆加密技术来隐藏代码中的逻辑和关键信息。在本文中&#xff0c;我们将以“灰姑娘”为例&#xff0c;介绍如何使…