C++:继承的概念和用法

文章目录

  • 继承的定义
    • 基类和派生类的对象赋值转换
    • 继承中的作用域
    • 派生类的默认成员函数
    • 几个继承小点
    • 继承理论的演示
  • 菱形继承和菱形虚拟继承
  • 虚拟继承

本篇主要总结的内容有

  1. 什么是继承
  2. 继承的一些概念
  3. 菱形继承和虚拟继承
  4. 继承的优缺点对比

继承的定义

继承是代码复用的一种重要手段,它允许程序员在一些原有的基础上进行拓展,由此增加新的类;继承体现了面向对象程序设计的层次结构,体现了由简单到复杂的设计过程

代码复用不仅仅有函数复用,在之前的学习中对于代码复用的认知都是停留在函数进行调用从而进行代码复用,而继承就是类设计层次上的复用

下面代码就是一个基础的继承实例

#include 
using namespace std;

class Person
{
public:
	Person(int age = 1, int num = 1)
		:_age(age)
		, _num(num)
	{}
	void Print()
	{
		cout << _age << " " << _num << endl;
	}
protected:
	int _age;
	int _num;
};

class Teacher :public Person
{
protected:
	int _tele;
};

int main()
{
	Person p;
	Teacher t;
	p.Print();
	t.Print();
	return 0;
}

从中就印证了前面对于继承的概念,继承后的父类的Person的成员,都会变成子类中的一部分,体现出了代码的复用

继承的定义方式其实就是前面的写法:

C++:继承的概念和用法_第1张图片

  • 继承方式主要有public继承,protected继承和private继承
  • 访问限定符有public访问,protected访问和private访问

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

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 在派生类中public成员 派生类的protected成员 派生类的private成员
基类的protected成员 在派生类中protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected,可以看出保护成员限定符是因继承才出现的

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

这在继承中是一个比较重要的概念,简单来说就是,派生类对象可以赋值给基类的对象指针引用等,原因是在赋值的过程中,可以把对象进行一定的切割,变成基类的成员,再进行赋值即可

// 派生类赋值给基类
int main()
{
	Person p;
	Teacher t;
	p = t;
	return 0;
}

上面演示的就是将派生类赋值给基类,这是行得通的,但是将基类赋值给派生类的操作是不被允许的,因为基类成员并不能包括派生类成员:

// error
// 基类赋值给派生类
int main()
{
	Person p;
	Teacher t;
	t = p;
	return 0;
}

继承中的作用域

  • 在继承中,基类和派生类都有自己独立的作用域
  • 子类和父类中有同名成员的时候,子类成员将屏蔽父类对同名成员的直接访问,这样被称之为隐藏,也叫做重定义
  • 对于成员函数的隐藏,只要函数名相同就是隐藏

举例来解释:

#include 
using namespace std;

class Person
{
public:
	Person(int age = 1, int num = 1)
		:_age(age)
		, _num(num)
	{}
	void Print()
	{
		cout << "Person::Print" << _age << " " << _num << endl;
	}
protected:
	int _age;
	int _num;
};

class Teacher :public Person
{
public:
	Teacher(int age = 10, int tele = 20)
		:_age(age)
		, _tele(tele)
	{}
	void Print()
	{
		cout << "Teacher::Print" << _age << " " << _tele << endl;
	}
protected:
	int _age;
	int _tele;
};

int main()
{
	Person p;
	Teacher t;
	p.Print();
	t.Print();
	return 0;
}

派生类的默认成员函数

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

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  5. 派生类对象初始化先调用基类构造再调派生类构造
  6. 派生类对象析构清理先调用派生类析构再调基类的析构

关于此部分的具体实例化表示在后演示

几个继承小点

1. 继承和友元关系:

继承是不会继承友元函数的,基类的友元不能访问子类的私有和保护成员

2. 继承和静态成员的关系:

基类中假如定义了一个静态成员,那么在整个继承体系中只有一个这样的成员,不管有多少个派生的子类,都只有一个static成员的实例

继承理论的演示

#include 
using namespace std;

class Person
{
public:
	// 继承是不会继承友元函数的,基类的友元不能访问子类的私有和保护成员
	Person(const string& name = "Tom")
		:_name("Tom")
	{
		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 (&p != this)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
	friend void Display(const Person& p)
	{
		cout << "friend void Display(const Person& p)" << " " << p._name << endl;
	}
protected:
	// 基类中假如定义了一个静态成员,那么在整个继承体系中只有一个这样的成员
	// 不管有多少个派生的子类,都只有一个static成员的实例
	static int _static;
	string _name;
};

int Person::_static = 10;

class Student :public Person
{
public:
	// 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员
	// 如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
	Student(const string& name = "Tom", int num = 123)
		:Person(name)
		, _num(num)
	{}
	// 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
	Student(const Student& s)
		:Person(s)
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	// 派生类的operator=必须要调用基类的operator=完成基类的复制
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (&s != this)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
	// 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员
	// 因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num;
};

菱形继承和菱形虚拟继承

菱形继承指的是一种多继承,一个子类有两个或以上直接父类时称这个继承关系为多继承

C++:继承的概念和用法_第2张图片
会造成什么问题?

这样会造成的问题有,Student类和Teacher类在创建的过程中,都会继承Person类的内容,也就是说,Student类和Teacher类当中都会有一份Person类的数据,当Assistant类继承了数据后,在Assistant类中会存在两份相同的Person类对象的内容,一份来源于Student类,一份来源于Teacher

这样会带来两个问题,首先是数据冗余,实际上在Assistant类中只需要一份Person类的数据即可,但是这里有两份,带来的是数据冗余,其次是二义性问题,当在Assistant类中调用Person类的内容数据是,会带来不知道是调用哪一个类中继承下来的Person类的问题,这是菱形继承带来的两个问题

如何解决?

对于解决二义性的问题,可以通过指定访问哪一个父类来解决二义性问题,但是数据的冗余是无法被彻底解决的,因此就要引入虚拟继承的概念

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 _addr;
};

void Test()
{
	Assistant a;
	a._name = "Tom";
}

虚拟继承

现在定义下面这些类:

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;
}

通过内存窗口进行观察:

C++:继承的概念和用法_第3张图片
从中看出,数据冗余的情况是存在的,类B中有A的内容,类C中也有A的内容,因此就造成了数据冗余的情况出现,针对这种情况,C++也做出了一定的调整,研发出了一个虚基表的内容,简单来说,就是菱形虚拟继承的内存对象成员模型,就是在D对象中将A放到了对象组成的最下面,这个A同时属于BC,而BC中存储了两个指针,这个指针指向一个表,这两个表就是虚基表,这个指针就是虚基表指针,虚基表中存储的是偏移量,通过偏移量就可以找到A的位置进而进行访问:

C++:继承的概念和用法_第4张图片
用下图来进行菱形虚拟继承的原理解释

C++:继承的概念和用法_第5张图片
经过这样的解决也算是解决了问题,但是菱形继承带来的问题很多,在实际生产应用中最好不适用它来解题

你可能感兴趣的:(C++,知识总结,c++)