C++中的继承

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++中的继承相关的知识,希望对你有一定的参考价值。

目录

 

一.继承的概念及定义

 1.1 继承的概念

 1.2继承定义

1.2.1 继承定义的格式

 1.2.2 继承方式

二. 基类和派生类对象的赋值转化

三. 继承中的作用域 

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

五.继承和友元

 六.继承与静态成员

七.复杂的菱形继承和菱形虚拟继承

7.1菱形继承的问题

7.2 解决办法

7.3 虚拟继承实现原理

八.继承与组合

8.1 什么是组合

8.2 继承和组合的区别

 九:总结


一.继承的概念及定义

发现问题:当我们编写一个类时,发现这个类与类外一个类的成员变量和成员方法相似,并且具有一定的包含关系时,我们编写的这两个类会有很多相似的地方。

比如:

 1.1 继承的概念

        继承机制是面向对象程序设计使代码可以复用的重要手段。它允许程序员保持原有类特性的基础上进行扩展,增加功能,这样产生的类称为派生类。

        继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承使类设计层次的复用。

        简单来说就是:继承就是一个类复用了另外一个类的成员函数和成员变量。就好像在这个类里编写了另外一个类的成员。

来一个例子简单了解一下:

 1.2继承定义

1.2.1 继承定义的格式

用上面的例子:

 1.2.2 继承方式

对应关系为:

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

总结:

  • 基类的私有成员在派生类中不可以见,基类的其它成员在派生类中的访问方式是:继承方式和基类该成员访问限定符范围那个小,就是什么访问方式,等价于min(继承方式,基类该成员访问限定符)。范围从大到小:public > protected > private
  • 基类的private成员,无论以什么继承方式继承,在派生类里是不可以访问的。但是,派生类还是继承了基类,只是在语法上限制了派生类在类里或者类外对基类私有成员的访问。
  • 基类protected成员,通过public或者protected继承方式继承的派生类,该成员变成了派生类的protected成员,只能在类里访问,不能在类外访问。但是基类的private成员,派生类不可见,在继承中体现了两者的区别。可以看出保护成员限定符因继承才出现的
  • 使用关键字class定义的类的默认继承方式是private,使用关键字struct定义的类的默认继承方式是public,但是,最后显示写出继承方式。
  • 在实际运用中一般使用public继承,很少用protected和private继承。因为protected和private继承下来的成员只能在派生类中使用,实用性不强。

演示和说明:

二. 基类和派生类对象的赋值转化

  • 派生类对象可以赋值给基类对象/基类指针/基类引用。这里有一个形象的说法是切片或者切割,意思是将派生类父类中的那部分切来赋值过去。

图示为:

  • 基类对象不能赋值给派生类对象。因为基类对象中可能没有派生类中的成员,所以赋值不过去。
  • 但是当一个基类指针指向派生类时,可以通过强制类型转化赋值给派生类指针。
class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	int _No; // 学号
};


void Test()
{
	Student s;
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person p1 = s;
	Person* pp = &s;
	Person& rp = s;

	//2.基类对象不能赋值给派生类对象,会报错
	s = p1;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
    //前提,指向基类指针指向派生类
	pp = &s;
	Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;
	//基类指针指向基类的话
	pp = &p1;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题,
	//由于基类中没有_NO成员,访问会越界
	ps2->_No = 10;

}

三. 继承中的作用域 

  • 在继承体系中基类和派生类具有独立的作用域
  • 派生类和基类具有同名成员(成员变量和成员函数)时,派生类将屏蔽对父类同名成员的直接访问,这种情况叫隐藏(重定义)除非指定作用域:基类名 : : 基类成员 (显示访问)

  • 如果时成员函数的隐藏,只需要基类和派生类函数名相同即可。与参数,返回值无关。

注意:在实际的继承体系同最好不要定义同名成员。 

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

我们知道在类中有6个默认成员函数,即使我们不写,编译器会帮我们自动生成。

 我们主要考虑前四个默认成员函数。

  • 派生类和基类默认成员函数时独立的,他们各自调用各自的成员函数。
  • 派生类的构造函数必须去调用基类的构造函数去初始化基类的那一部分成员。如果基类没有默认构造函数(无参,全缺省或者编译器生成),必须在派生类构造函数中显示调用基类的构造函数。

隐式调用基类的构造函数

 显示调用基类构造函数

 总结:

派生类中的基类成员部分只能调用基类的成员函数。

不显示调用,基类构造函数时默认构造函数

显示调用,基类构造函数是默认构造函数或者不是默认构造函数。如果不是默认构造函数,必须显示调用。

  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成属于基类成员的拷贝构造。

  • 派生类的赋值操作符重载函数(operator=)必须调用基类的赋值操作符重载函数(operator=)赋值属于基类的成员。

注意:在派生类里调用基类的赋值重载函数要加类作用域限定符。 

  • 派生类的析构函数有点不同,在调用后会自动调用基类的析构函数,清理基类成员。

  • 派生类先调用基类的构造,再调用派生类的构造。
  • 派生类对象析构,先调用派生类的析构,再调用基类的析构。

原因:因为是成员变量在栈里开辟空间,基类成员先构造, 派生类成员后构造。析构时,按照栈先入后出性质,所以,派生类成员先析构,基类成员后析构。

 总结:

默认成员函数基类成员部分掉基类的默认成员函数,派生类部分掉派生类的默认成员函数。

最好是都显示调用基类默认成员函数。

赋值操作符要加类的作用域,因为隐藏。

派生类先调用基类的构造,再调用派生类的构造。

派生类对象析构,先调用派生类的析构,再调用基类的析构。

析构不用显示调用,会自己调用。

五.继承和友元

基类友元的关系,派生类不能继承下来。也就是说基类的友元不能访问派生类的私有和保护成员。

class Student;//声明
class Person
{
public:
	friend void Print(Person& p, Student& s);//友元
protected:
	string _name = "tom";
};

class Student :public Person
{
public:
    //friend void Print(Person& p, Student& s);解决
protected:
	int _age = 10;
};

void Print(Person& p, Student& s){
	cout << p._name << endl;
	//cout << s._age << endl;//报错,友元不能继承,不能访问派生类的保护成员
}

int main()
{
	Person p;
	Student s;
	Print(p, s);
	system("pause");
	return 0;
	
}

 六.继承与静态成员

 基类定义了一个静态成员,在继承体系中,只有这一个成员。也就是说,无论派生了多少派生类,操作的这个静态成员都是一个成员。

七.复杂的菱形继承和菱形虚拟继承

单继承:一个派生类只有一个字节的父类

多继承:一个子类有两个或者两个以上的的基类

 菱形继承:多继承的特殊情况,有一个共同的基类。

 更复杂一点可以是这样:

 7.1菱形继承的问题

 上面说明了在类Student和Teacher里都各自有一份基类Person的成员,然后又全部继承到了派生类Assistant中,导致Assistant中有了两份Person的成员int _id。

解决:

 上面说明了了菱形继承的两个问题:

  1. 菱形继承会导致数据冗余,导致数据多了。
  2. 菱形继承导致数据具有二义性,不知道访问的是哪个成员。需要指明类域。

7.2 解决办法

虚拟继承,是用关键字vritual修饰中间类。

 7.3 虚拟继承实现原理

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;
	cout << sizeof(d) << endl;//输出20
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	system("pause");
	return 0;
}

 当不带virtual关键字时:查看内存

 这里也表现了数据冗余的问题,有两份_a。

带virtual关键字

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;
	cout << sizeof(d) << endl;//输出24
	d.B::_a = 1;
	d.C::_a = 1;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	system("pause");
	return 0;

 保存偏移量的表为虚基表,共同的基类A叫虚基类。

抽象一点:

这样解决了数据二义性的问题。但是反而空间增加了,所实现了增加了两个指针,但是这种情况只是放在这是增加了,在其它情况,它可能减少了空间。

如果A的成员变量是一个数值:

class A
{
public:
	int _a[10000];
};
//class B :virtual public A
class B
{
public:
	int _b;
};
//class C :virtual public A
class C 
{
public:
	int _c;
};
class D :public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	cout << sizeof(d) << endl;//输出24

	system("pause");
	return 0;
}

 不加virtual输出:

加virtual:

总结虚继承的原理:

B继承A,实际A的成员保存在最后面。B类对象中,还保存了一个指针,指针指向一个虚基表。虚基表里保存的是指针位置到实际保存A成员中第一个成员位置的偏移量。 

 C继承A,实际A的成员保存在最后面。C类对象中,还保存了一个指针,指针指向一个虚基表。虚基表里保存的是指针位置到实际保存A成员中第一个成员位置的偏移量。

 D继承B和C,首先会继承B和C属于自己的成员,还会继承B和C的指针(但是指针变量内容不一样,因为虚基表不一样)。两指针指向的是实际保存各自指针位置到A成员的第一个成员的位置的偏移量。

共同基类A的成员保存在最下面。

注意共同基类的内容被继承下来了,也占据对象的空间。只是保存在最后,并且是一份(解决数据二义性和冗余问题)。

八.继承与组合

8.1 什么是组合

 8.2 继承和组合的区别

  • public继承是一种is-a(是它)的关系,每个派生类都是基类。比如,学生是人。
  • 组合是一种has-a(有它)的关系。比如:车有轮胎。
  • 继承是通过派生类来复用基类,这种复用称为"白箱复用",白箱是通过可视性来说的。在继承方式中,基类的内部细节对派生类来说是是可见的。继承一定程度上破坏了基类的封装性,基类的改变,会对派生类有很大的影响,基类和派生类之间依赖关系强,增加了类与类之间的耦合度。
  • 组合是类继外的另外一种复用方式,在一个类中定义另外一个类的对象,通过这个类使用这个类的成员,这要求被组合对象要有良好的定义接口,这种复用方式为"黑盒复用"。因为对象的内部细节是不可见的,除了共有成员。组合类之间没有很强的依赖关系,耦合度低。
  • 实际组合和继承都可以用,优先使用组合。
//继承
// Car和BMW Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};

class BMW : public Car{
public:
void Drive() {cout << "好开-操控" << endl;}
};

class Benz : public Car{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};

//组合
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};

class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};

 九:总结

  • 什么是菱形继承?

多继承的特殊情况,派生类有多个基类,派生类的多个基类最后会有共同的基类。

  • 菱形继承的问题?

造成数据的二义性和冗余。

  • 什么是菱形虚拟继承?

派生类的基类用关键字virtual修饰。,共同的基类不修饰。

  • 如何解决数据二义性和冗余?

使用偏移量。中间的基类,里保存指针,指向的空间内容为,中间类保存指针位置,到共同基类成员位置的偏移量。共同基类成员只有一份。

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

继承复用一定是派生类复用基类的一种方法,基类的内部细节对子类可见,一定程度上破坏了基类的封装性,增加了基类和派生类之间的耦合度。

组合是通过在一个类里定义另外一个类对象,来实现类的复用。对象的内部细节对于类来说不可见,组合类之间没有很强的依赖关系,耦合度低。

当只能用继承时,用继承,只能用组合时,用组合,组合和继承都可以用,优先使用组合。

以上是关于C++中的继承的主要内容,如果未能解决你的问题,请参考以下文章

C++中的继承

这些 C++ 代码片段有啥作用?

调用非虚拟基方法时,C++ 中的虚拟继承是不是有任何惩罚/成本?

有趣的 C++ 代码片段,有啥解释吗? [复制]

以下代码片段 C++ 的说明

C++ 代码片段执行