<C++>深度学习继承

目录

一、继承概念

二、继承的语法

2.1继承关系和访问限定符

2.2派生类继承基类后的成员权限

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

四、继承中的作用域

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

六、继承与友元

七、继承与静态成员        

八、复杂的菱形继承及菱形虚拟继承

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

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

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

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

九、继承的总结

十、继承的一些面试题

 1. 什么是菱形继承?菱形继承的问题是什么?

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

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


一、继承概念

继承是面向对象程序设计使代码复用的重要手段,继承中有父类(基类)和子类(派生类);

继承中派生类可以使用父类的成员函数和成员变量;

class A // 基类、父类
{
public:
	int _a;
};

class B : public A // 派生类、子类
{
private:
	int _b;
};

二、继承的语法

下面我们看到的Student是派生类(子类),Person是基类(父类)

<C++>深度学习继承_第1张图片

2.1继承关系和访问限定符

继承关系和访问限定符是类似的;

<C++>深度学习继承_第2张图片

2.2派生类继承基类后的成员权限

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

 虽然有这么多的继承方法组合,但是我们最常使用的只有public继承、public成员、protected成员组合的方式继承;

私有成员的意义?

不想被子类继承的成员,可以设计成私有;

 保护成员的的意义?

基类中想给子类复用,但是又不想直接暴露访问成员,就该定义成保护;

注意:

struct默认继承/访问限定符 : public

class默认继承/访问限定符 :private

总结:

  1.  基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3.  实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  5.  在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强

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

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
class Person
{
public:
	Person(string name = "zs", int age = 0)
		:_name(name)
		,_age(age)
	{
		
	}

	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
	}

public:
	string _name;
	int _age;
};

class Student : public Person
{
public:
	Student(int No = 213310)
		:_No(No)
	{
		
	}

	void Print()
	{
		cout << _name << endl;
		cout << _age << endl;
		cout << _No << endl;
	}
private:
	int _No; //编号
};

int main()
{
	Person p1;
	Student s1;

	s1.Print();

	p1._name = "张三";//修改基类数值
	p1._age = 18;

	p1 = s1; // 将子类赋值给父类
	p1.Print();

    //结果两次打印的结果不变

	return 0;
}

<C++>深度学习继承_第3张图片

 上面的运行结果子类已将将父类重新赋值;

四、继承中的作用域

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

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

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

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

class A
{
public:
	void Print() { cout << "class A" << endl; }
public:
	int _a = 1;
};

class B : public A
{
public:
	void Print(){	cout << "class B" << endl;	}
public:
	int _b = 2;
};

class C : public A
{
public:
	void Print() { A::Print(); }
public:
	int _c = 2;
};

int main()
{
	B b;
	b.Print(); // 调用b中重定义的Print()

	C c;
	c.Print(); // 通过c显示调用A中的Print()

	return 0;
}

<C++>深度学习继承_第4张图片

 

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

<C++>深度学习继承_第5张图片

上面的图我们可以从下面的几点去理解: 

1.派生类的默认构造函数会调用基类的构造函数,如果基类没有构造函数,那么必须在派生类的的初始化列表阶段显示调用;

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

3.派生类的operator=必须要调用基类的operator=完成基类的复制;

4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。

5. 派生类对象初始化先调用基类构造再调派生类构造;

6. 派生类对象析构清理先调用派生类析构再调基类的析构;

7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系;

六、继承与友元

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

七、继承与静态成员        

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

八、复杂的菱形继承及菱形虚拟继承

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

class A
{
public:
	void Print() { cout << "class A" << endl; }
public:
	int _a = 1;
};

class B : public A // 只继承了A类
{
public:
	void Print(){	cout << "class B" << endl;	}
public:
	int _b = 2;
};

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

class A
{
public:
	void Print() { cout << "class A" << endl; }
public:
	int _a = 1;
};

class B
{
public:
	void Print() { cout << "class B" << endl; }
public:
	int _b = 2;
};

class C : public A, public B // 继承了A类和B类
{
public:
	void Print() { A::Print(); }
public:
	int _c = 2;
};

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

<C++>深度学习继承_第6张图片

 


class A
{
public:
	void Print() { cout << "class A" << endl; }
public:
	int _a = 1;
};

class B : public A
{
public:
	void Print() { cout << "class B" << endl; }
public:
	int _b = 2;
};

class C : public A
{
public:
	void Print() { A::Print(); }
public:
	int _c = 3;
};
class D : public B, public C
{
public:
	void Print() { A::Print(); }
public:
	int _d = 4;
};

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

<C++>深度学习继承_第7张图片

 

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用。虚继承即在继承方式前加virtual关键字

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

使用了virtual虚拟继承后,派生类会将基类的成员放到一个公共的区域,所有的派生类共享这个区域,会产生一个指针指向一个虚表,虚表中存放各个派生类相对于公共区域的偏移量,当派生类需要访问基类的成员时就需要处理一下偏移量,找到基类的成员,再进行访问;

<C++>深度学习继承_第8张图片

 

九、继承的总结

  • 一定不要设计出多继承
  • 多继承可以说是C++的缺陷之一
  • 继承和组合

public继承是一种is-a的关系,也就是说每一个派生类对象都是一个基类;

组合是一种has-a的关系,假设B组合了A,每一个对象中都有一个A对象;

  • 优先使用对象组合,而不是继承
  • 继承允许你根据基类的实现来定义派生类的实现
  • 继承一定程度破坏了类的封装,基类的改变对派生类的影响很大
  • 对象组合和基类间的依赖关系很强,耦合度高
  • 对象组合是类继承之外的另外一种复用选择
  • 实际尽量多去使用组合

十、继承的一些面试题

 1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承:多继承的一种特殊,派生类A和派生类B继承了一个基类,同时派生类C又多继承了A和B就构成了菱形继承

解决方法:使用virtual虚拟继承

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

菱形虚拟继承:在继承方式前面加vritual关键字

原理:加了virtual后,派生类将基类继承的成员放在一个公共的区域,同时多创建了一个指针指向一个虚表,虚表中存放该派生类相对于基类成员的偏移量,在派生类访问基类时再进行偏移量的处理

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

继承:is-a也就是说每一个派生类对象都是一个基类;

组合:has-a假设B组合了A,每一个对象中都有一个A对象;

你可能感兴趣的:(C++,c++,开发语言)