【c++】面向对象三大特性之——继承(菱形继承详细讲解)

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

【c++】模板进阶,模板的分离编译重点讲解——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】面向对象三大特性之——继承


一、继承的概念及定义

继承的概念

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

  1. 继承的思想是属性相同,例如有两个类分别为学生和老师,他们都是人,如果单独进行设计都要设计姓名,年龄,身高等,那么为了类设计层次的复用,把这些相同的属性抽象为一个类名为人,那么学生和老师就可以继承人的属性,例如姓名,年龄,身高等,从而进行类设计层次的复用
  2. 并且继承后,人的成员(成员变量和成员函数)都会变成学生和老师成员的一部分
class person
{
public:
	void Print()
	{
		cout << _age << endl;
		cout << _name << endl;
	}

protected:
	int _age = 18;
	string _name = "xiaowang";
};

class student :public person
{

protected:
	int _stuid = 111111;
};

class teacher :public person
{

protected:
	int _teaid = 222222;
};

int main()
{
	student s;
	teacher t;

	s.Print();
	t.Print();

	return 0;
}

运行结果如下

  1. 体现了成员函数的复用

在这里插入图片描述
2. 调用监视窗口,可以体现成员变量的复用
在这里插入图片描述

继承定义

定义格式

person是父类,又称基类。student是子类,又称派生类

  1. 继承方式分为public继承,protected继承,private继承
  2. 继承使用方法:class 子类名 :继承方式 父类名
     //派生类  :继承方式 基类
class student :public person
{

protected:
	int _stuid = 111111;
};
继承方式和访问限定符

在这里插入图片描述在这里插入图片描述

继承基类访问方式的变化
基类成员\继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见
  1. 基类的private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象,但是语法上限制派生类对象无论是在类里面还是在类外面都不可以访问基类的私有成员。这个基类的private成员跟派生类中原有的private成员不同,派生类原有的private成员在类里面可以访问,在类外不可访问
  2. 由于基类的private成员在派生类中是不能访问的,所以如果基类的成员不想在派生类继承后在派生类的类外面直接访问,但可以在派生类的类里面进行访问,那么就将基类的成员定义为protected成员(protected成员在基类中的里面可以访问,在基类的外面不可以访问)。由此可见protected保护成员限定符是因为继承才出现的
  3. 总结一下表格,在基类中的private私有成员无论是以什么方式被派生类继承,其在派生类中都是不可见的,基类其它成员在派生类的访问方式是取后面两个的关键字权限小的那一个(成员在基类的访问限定符,继承方式),关键字权限大小关系:public>protected>private
  4. 如果不加继承方式,那么使用关键字class时,默认继承方式是private继承。使用关键字struct时,默认继承方式是public继承。不过在实际的使用中最好显示写出继承方式
  5. 在实际的使用场景中,使用最多的是public继承,而protected/private继承的使用场景非常少。同时我们同样不建议使用protected/private继承,因为protected/private继承进行继承下来的成员,只能在派生类的类里面进行使用,可维护性不强
派生类进行公有继承
class person
{
public:
	void Print()
	{
		cout << _age << endl;
		cout << _name << endl;
	}
protected:
	int _age = 18;
private:
	string _name = "xiaowang";
};

class student :public person
{
public:
	void fun()
	{
		_age = 20;
		Print();
	}
protected:
	int _stuid = 111111;
};

int main()
{
	student s;

	s.fun();
    s.Print();

	return 0;
}

运行结果如下

  1. 基类的public成员可以在派生类的里面和外面进行调用
  2. 基类的protected成员只能在派生类里面进行访问,在派生类外面不能进行访问
  3. 基类的private成员在派生类的里面和外面都不可以进行访问

在这里插入图片描述

派生类进行保护继承或私有继承
class person
{
public:
	void Print()
	{
		cout << _age << endl;
		cout << _name << endl;
	}
protected:
	int _age = 18;
private:
	string _name = "xiaowang";
};

//class student :private person
class student :protected person
{
public:
	void fun()
	{
		_age = 21;
		Print();
	}
protected:
	int _stuid = 111111;
};

int main()
{
	student s;

	s.fun();

	return 0;
}

运行结果如下

  1. 基类的public成员在派生类里面可以进行访问,在派生类外面不可以进行访问
  2. 基类的protected成员在派生类里面可以进行访问,在派生类外面不可以进行访问
  3. 基类的private成员在派生类的里面和外面都不可以进行访问

在这里插入图片描述

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

  1. 派生类对象可以赋给基类的对象/基类的指针/基类的引用。这里实际上是赋值兼容,发生了切片,也叫做切割。可以把派生类对象理解为两部分,基类的那一部分和派生类自己的那一部分。切片或切割也就是将派生类对象中的基类的那一部分切割出来进行赋值

  2. 我们已知的当类型不兼容的赋值方式可以进行强制类型转换或隐式类型转换,但是这里是一个特例由于发生了赋值兼容转换,所以切割或切片不产生消耗,即不会产生临时变量
    在这里插入图片描述

  3. 基类对象不可以赋值给派生类对象

  4. 基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。因为基类的指针或引用是有可能指向派生类的父类的那一部分。所以这种方式必须是基类的指针或引用指向派生类对象的时候才最安全。关于这种方式小编不进行代码演示,在后文的多态文章中,小编会进行补充讲解
    在这里插入图片描述

class person
{
public:
	int _age = 18;
	string _name = "xiaowang";
};

class student :public person
{
protected:
	int _stuid = 111111;
};

int main()
{
	person p;
	
	student s;
	s._age = 20;
	s._name = "zhangshan";

	p = s;
	person* ptr = &s;
	person& rp = s;

	return 0;
}

运行结果如下

  1. 赋值前的情况

在这里插入图片描述
2. 赋值成功,赋值后的情况

在这里插入图片描述

三、继承中的作用域

  1. 在继承体系中的基类和派生类都有其独立的作用域
  2. 当派生类和基类中有同名成员时,派生类中的同名成员将屏蔽基类中的同名成员,直接对当前派生类的同名成员进行访问,这种情况叫做隐藏,也叫做重定义(在派生类的成员函数中,可以使用 基类::同名成员 的形式直接访问基类的同名成员)
  3. 注意,如果是派生类和基类中的成员函数构成隐藏的话,只要函数名相同就构成隐藏
  4. 但是注意,在实际的继承体系中,最好不要定义同名成员
class person
{
public:
	void fun()
	{
		cout << "基类:" << _age << endl;
	}

protected:
	int _age = 18;
	string _name = "xiaowang";
};

class student :public person
{
public:
	void fun()
	{
		person::fun();
		cout << "派生类:" << _age << endl << endl;

		_age = 200;

		person::fun();
		cout << "派生类:" << _age << endl << endl;

		person::_age = 180;

		person::fun();
		cout << "派生类:" << _age << endl;
	}

protected:
	int _age = 20;
};

int main()
{
	student s;
	s.fun();

	return 0;
}

测试结果如下
在这里插入图片描述

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

6个默认成员函数,当我们不写的时候,编译器会默认帮我们生成这六个成员函数,那么在派生类中,这几个成员函数是如何生成的呢?
在这里插入图片描述

  1. 派生类的构造函数在走初始化列表时会优先自动调用基类的默认构造函数完成基类那一部分成员的初始化,如果基类没有默认成员函数,那么在派生类的构造函数中必须显示调用基类的构造函数
class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name,int id)
		:_id(id)
	{
		cout << "Student()" << endl;
	}

protected:
	int _id;
};

int main()
{
	Student s("zhangshan", 111111);

	return 0;
}

运行结果如下

  1. 基类有默认构造函数的情况下,定义派生类对象,调用派生类的构造函数时,编译器走派生类的初始化列表前会优先调用基类的默认构造函数完成基类那一部分成员的初始化,调用完基类的默认构造函数后再对自身的成员变量进行初始化

在这里插入图片描述

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 id)
		:Person(name)
		,_id(id)
	{
		cout << "Student()" << endl;
	}

protected:
	int _id;
};

int main()
{
	Student s("zhangshan", 111111);

	return 0;
}

运行结果如下

  1. 当基类没有默认构造函数的时候,定义派生类对象,调用派生类的构造函数,编译器在走初始化列表时,编译器只能够去调用基类的默认构造函数,当基类没有默认构造函数的时候,这时候需要我们显示在初始化列表像定义匿名对象一样,传入参数进行显示调用基类的构造函数完成基类那部分成员的初始化,显示调用完基类的构造函数后再对自身的成员变量进行初始化

在这里插入图片描述

  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;
	}
protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name,int id)
		:Person(name)
		,_id(id)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)//实际上是将派生类对象赋值给基类对象的引用,发生了赋值兼容转换,即切片和切割
		,_id(s._id)
	{
		cout << "Student(const Student& s)" << endl;
	}

protected:
	int _id;
};

int main()
{
	Student s("zhangshan", 111111);

	Student s1(s);

	return 0;
}

运行结果如下
在这里插入图片描述
在这里插入图片描述

  1. 派生类的operator=必须要显示调用基类的operator=完成对基类的赋值
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;
	}

protected:
	string _name;
};

class Student :public Person
{
public:
	Student(const char* name,int id)
		:Person(name)
		,_id(id)
	{
		cout << "Student()" << endl;
	}

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

	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator=(s);//这里必须使用基类::operator=的方式进行调用
			_id = s._id;		 //如果直接调用operaotr=会产生一直去调用派生类的
		}						 //operator=引发无穷递归调用,导致栈溢出

		return *this;
	}

protected:
	int _id;
};

int main()
{
	Student s("zhangshan", 111111);

	Student s1(s);

	Student s2("lisi", 222222);

	s1 = s2;

	return 0;
}

运行结果如下
在这里插入图片描述
在这里插入图片描述

  1. 派生类的析构函数会在被调用完成之后自动调用基类的析构函数完成对基类成员的清理。因为这样才能保证派生类先清理派生类的自己成员的那一部分,再清理派生类中的基类成员的那一部分的顺序。同时再派生类清理自己成员的那一部分的时候,有可能访问了基类成员,所以在对派类清理自己成员的时候不可以显示调用基类的析构函数完成派生类中的基类那一部分的成员的清理
  2. 派生类的析构函数会在被调用完成之后自动调用基类的析构函数这种方式有效的保证了在栈上先定义后析构,后定义,先析构的顺序,在定义派生类成员的时候,先调用派生类中基类那一部分进行初始化,在去完成对派生类自己那一部分的初始化,之后生命周期结束,派生类自己那一部分由于是后定义的,所以先调用派生类的析构函数进行析构,派生类中的基类那一部分由于是先定义的,所以后析构,派生类那一部分析构完成,编译器自动调用基类的析构函数完成派生类中基类那一部分成员的清理
  3. 由于后续的一些场景,析构函数需要构成重写,重写的条件之一是函数名相同(小编后续文章会进行讲解),所以那么编译器会对析构函数的函数名进行重写,那么会统一修饰为destructor(),所以父类析构函数不加virtual的情况下,子类的析构函数和父类析构函数构成隐藏
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:
	Student(const char* name,int id)
		:Person(name)
		,_id(id)
	{
		cout << "Student()" << endl;
	}

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

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

		return *this;
	}

	~Student()
	{
		cout << "~Student()" << endl;
	}

protected:
	int _id;
};

int main()
{
	Student s("zhangshan", 111111);

	Student s1(s);

	Student s2("lisi", 222222);

	s1 = s2;

	return 0;
}

运行结果如下
在这里插入图片描述

五、继承与友元

  1. 友元关系不能继承,也就是说基类的友元函数不能访问的基类的派生类的保护和私有成员
  2. 如果基类的友元函数想要访问基类的派生类的私有和保护成员,应该声明为派生类的友元函数
class Person
{
	friend void Print(const Person& p);
public:

protected:
	int _age = 18;
private:
	string _name = "xiaowang";
};

void Print(const Person& p)
{
	cout << p._age << endl;
	cout << p._name << endl;
}

class Student :public Person
{
	friend void Print(const Person& p);
public:

protected:
	int _stuid = 111111;
};

int main()
{
	Student s;
	Person p;

	Print(p);
	Print(s);

	return 0;
}

运行结果如下
在这里插入图片描述

六、继承与静态成员

  1. 基类定义了static静态成员,那么在整个继承体系里就只有一个这样的成员。无论派生出多少个子类都只有一个static静态成员
  2. 那么我们可以根据这个特性以及派生类在进行构造的时候会去优先调用基类的默认构造函数完成派生类中基类那部分成员的初始化,在基类定义静态成员变量,那么同时在基类的默认构造函数使用static静态成员变量用于统计基类进行构造的总次数即统计出了基类及其派生类的总个数
class Person
{
public:
	Person()
	{
		_count++;
	}

	static int _count;
};

int Person::_count = 0;

class Student :public Person
{
public:
	Student()
	{}
};

int main()
{
	Person p;

	Student s1;
	Student s2;
	Student s3;

	cout << Person::_count << endl;

	return 0;
}

运行结果如下

  1. 定义了1个基类,3个派生类,基类及其对应的派生类共计4个,结果正确

在这里插入图片描述

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

单继承

  1. 单继承:一个子类只有一个直接父类时,这个继承关系称为单继承
    在这里插入图片描述
class A
{
public:
	int _a;
};

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

class C :public B
{
public:
	int _c;
};


int main()
{
	C c;
	c._a = 1;
	c._b = 2;
	c._c = 3;

	return 0;
}

运行结果如下,对象模型如下
在这里插入图片描述

多继承

  1. 多继承:一个子类有两个或以上直接父类时,称这种继承关系为多继承
  2. 多继承的使用方法是在子类的位置对多个父类使用逗号,进行间隔,其余方式public形式不变,进行继承
  3. 以下图为例,根据class C :public A, public B语句中的继承对象A, B出现先后的顺序在内存中依次放置对象A,对象B,多继承中先继承对象A的放在对象C存储的内存空间的开头,对象A之后是放置的对象B
    在这里插入图片描述
class A
{
public:
	int _a;
};

class B
{
public:
	int _b;
};

class C :public A, public B
{
public:
	int _c;
};

int main()
{
	C c;
	c._a = 1;
	c._b = 2;
	c._c = 3;

	return 0;
}

运行结果如下,对象模型如下
在这里插入图片描述

菱形继承

  1. 菱形继承是多继承的一种特殊情况,由于继承关系上类似菱形,所以就称这种特殊的多继承为菱形继承
    在这里插入图片描述
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;

	return 0;
}

数据模型如下

  1. 那么在对象d中就会有两个_a的成员变量这就存在了数据冗余问题,如果要进行调用_a,那么编译器就不知道要调用哪一个_a,这就存在了二义性问题

在这里插入图片描述
2. 由于存在两个_a,存在二义性问题,进行调用_a编译器不知道会调用哪一个_a,那么编译进行语法检查的时候编译器会直接进行报错

在这里插入图片描述

缓解办法
  1. 缓解办法,采用显示指定访问哪个父类成员可以缓解二义性问题,但是针对数据冗余的问题无法解决
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;

	return 0;
}

运行结果如下

  1. 这样可以访问到两个_a,但是这种的显示指定父类的成员调用方式还需要写父类::这种形式,同时针对数据冗余还无法解决,对于d对象仅需要一个_a即可,不需要两个_a,同时我们想要直接使用d._a去访问_a,那么该如何去做呢?

在这里插入图片描述

解决办法
  1. 虚拟继承可以解决菱形继承的数据冗余和二义性问题。以上述的对象模型为例,在B和C继承A的时候采用虚拟继承,即可解决问题。虚拟继承是特定的解决菱形继承的方式,在其它场景不要进行使用
  2. 这种虚拟继承的使用方式是在菱形继承中进行多继承的前父类的位置(一个子类有两个即以上的父类,这多个父类)使用virtual关键字修饰,在在B和C继承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;
	d.B::_a = 1;
	d.C::_a = 2;
	d._a = 3;

	return 0;
}

对象模型如下

  1. 这样d对象中就只有一个_a,父类B和C中的_a以及d中的_a都是一个_a

在这里插入图片描述
2. 显示使用父类指定访问成员访问修改的是共同的_a

在这里插入图片描述
3. 显示使用父类指定访问成员访问修改的是共同的_a

在这里插入图片描述
4. 使用d._a的方式访问到的是同一个_a,即在d对象中就存放一个_a,这样就巧妙的解决了数据冗余和二义性问题

在这里插入图片描述

菱形虚拟继承的底层

小编使用上文代码示例的菱形继承的对象模型和菱形虚拟继承的对象进行比对讲解

在这里插入图片描述

继承和组合

public继承是一种is-a的关系,也就是说每个派生类的对象都是一个基类对象
组合是一种has-a的关系,也就是说有两个对象A和B,B组合了A,也就是说每个B对象中都有一个A对象

class A
{};

class B:public A//B和A是继承关系,B继承了A
{};

class C//A和C是组合关系,C组合了A
{
private:
	A _a;
};

在实际应用中优先使用类和类之间的组合而不是类继承

  1. 继承允许你根据基类的实现去定义基类的实现。通过这种复用基类方式生成派生类的方式通常称为白箱复用。在这种继承方式中,基类的内部实现细节对派生类可见。继承的方式一定程度上破坏了基类的封装性,在派生类中可以访问使用基类的公有和保护成员,那么基类的改变,对派生类的影响就很大。基类和派生类之间的依赖关系很高,耦合度高。
  2. 组合同样也是一种复用方式。组合要求被组合的对象具有良好定义的接口。这种复用方式被称为黑盒复用。假设B组合了A,那么B仅可以访问A类的公有成员,那么A类的改变,对B的影响不是很大,B对A的依赖性不是很强,所以耦合度低

所以根据上面的结论,继承的耦合度高,组合的耦合度低,由于在软件设计层次追求的是高内聚,即类内的关联很强,低耦合,即类外的关联性很弱

  1. 所以当这个类的实现可以使用继承去实现又可以使用组合去实现的时候,优先使用组合
  2. 当这个类比较适合使用继承的去实现时候就使用继承,当比较适合使用组合去实现的时候就使用组合
  3. 那么同时要实现多态的时候就必须使用继承。

总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!

评论 36
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值