深入挖掘C++中的特性之一 — 继承

发布于:2024-10-09 ⋅ 阅读:(39) ⋅ 点赞:(0)

目录

1.继承的概念

2.举个继承的例子

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

1.父类成员的访问限定符对在子类中访问父类成员的影响

2.父类成员的访问限定符+子类的继承方式对在两个类外访问子类中父类成员的影响

4.继承类模版(注意事项)

5.父类与子类间的转换

6.继承中的作用域(主讲隐藏)

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

1.子类中的构造函数

2.子类中的拷贝构造函数

3.子类中的赋值运算符重载

4.子类中的析构函数

8.不能被继承的类

9.继承与友元

10.继承与静态成员

11.多继承及其菱形继承问题

0.简单介绍

1.单继承模型

2.多继承模型

3.菱形继承模型

4.二义性例子

5.虚继承

12.继承和组合


1.继承的概念

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

2.举个继承的例子

下面我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。

class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
	int _age = 18; // 年龄 
};


class Student : public Person
{
public:
	// 学习 
	void study()
	{
		// ...
	}
protected:
	int _stuid; // 学号 
};


class Teacher : public Person
{
public:
	// 授课 
	void teaching()
	{
		//...
	}
protected:
	string title; // 职称 
};

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

1.父类成员的访问限定符对在子类中访问父类成员的影响

1.父类成员public、protected修饰:子类中可以访问父类的成员变量。

2.父类成员private修饰:子类中不可以访问父类的成员变量。

2.父类成员的访问限定符+子类的继承方式对在两个类外访问子类中父类成员的影响

1.private继承:在两个类外访问不了。

2.除了private继承以外的其它继承方式:

父类成员在子类中的访问方式==min(成员在父类的访问限定符,继承方式),其中public>

protected>private。

4.继承类模版(注意事项)

namespace yx
{


	//用vector容器实现stack容器适配器
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域, 
			// 否则编译报错:error C3861: “push_back”: 找不到标识符 
			
			// 因为stack<int>实例化时,也实例化vector<int>了 
			
			// 但是模版是按需实例化,虽实例化vector<int>了,
			// 但vector<int>中的push_back等成员函数未实例化,所以找不到 
			vector<T>::push_back(x);

			//push_back(x);需要指定类域实例化模版参数
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};


}

5.父类与子类间的转换

class Person
{
protected:
	string _name; // 姓名 
	string _sex; // 性别 
	int _age; // 年龄 
};


class Student : public Person
{
public:
	int _No; // 学号 
};

1.public继承的派生类对象、指针、引用可以赋值给基类的对象、指针、引用;(切片)(基类指针或引用指向的是派生类中切出来的基类那部分)

2.基类对象不能赋值给派生类对象;

3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。

Student sobj;
// 1.派⽣类对象、指针、引用可以赋值给基类的对象、指针、引⽤ 
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错 
sobj = pobj;
//3.可以这么写
Student sobj;
Person* pp = &sobj;
Person& rp = sobj;
Student* sp = (Student*)pp;
Student& rs = (Student&)rp;

6.继承中的作用域(主讲隐藏)

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆 
class Person
{
protected:
	string _name = "⼩轩"; // 姓名 
	int _num = 111; // ⾝份证号 
};


class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " ⾝份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号 
};


int main()
{
	Student s1;
	s1.Print();

	return 0;
}

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

在讨论子类的默认成员函数时,我们只需要按照之前看默认成员函数的方式+将父类中的那些成员看作是子类中的一个对象来看就可以。

1.子类中的构造函数

class Person
{
public:
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	
protected:
	string _name; // 姓名
};



class Student : public Person
{
public:
	// 子类中默认生成的构造函数的行为
	// 1、内置类型->不确定
	// 2、自定义类型->调用默认构造
	// 3、继承的父类成员看做一个整体对象,要求调用父类的默认构造

    //自己写的子类构造
	Student(const char* name, int num, const char* addrss)
		:Person(name)//必须显式调用父类的构造函数
		, _num(num)
		, _addrss(addrss)
	{}

protected:
	int _num = 1; //学号
	string _addrss = "武汉市洪山区";
};

2.子类中的拷贝构造函数

class Person
{
public:

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	
protected:
	string _name; // 姓名
};


class Student : public Person
{
public:
	
	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现

	Student(const Student& s)//注意:这里写成了初始化列表的方式,
//但请不要混淆(这里并不是初始化的意思,
//但如果写成了初始化列表的方式,这里没有写Person(s)的话,将会调用Person类的默认构造函数)

		:Person(s)//必须显式调用父类的拷贝构造函数
		, _num(s._num)
		, _addrss(s._addrss)
	{
		// 深拷贝
	}

	
protected:
	int _num = 1; //学号
	string _addrss = "武汉市洪山区";
};

3.子类中的赋值运算符重载

class Person
{
public:
	

	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赋值重载默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			// (注意)规定:父类和子类的operator=构成隐藏关系,故这里需要指定类域。
			Person::operator=(s);//这里要显式调用父类的operator=()函数
            //这里的s变量传给父类的函数,用到了刚才讲的父类与子类间的转换的知识

			_num = s._num;
			_addrss = s._addrss;
		}

		return *this;
	}

	
protected:
	int _num = 1; //学号
	string _addrss = "武汉市洪山区";
};

4.子类中的析构函数

class Person
{
public:
	
	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name; // 姓名
};


class Student : public Person
{
public:
	

	// 严格说Student析构默认生成的就够用了
	// 如果有需要显示释放的资源,才需要自己实现

	// 子类和父类的析构函数都会被特殊处理成destructor() ,从而导致同名
    // 至于为什么要这么处理,详情请看多态章节
	~Student()
	{
		_addrss.~string();

		// (注意)规定:子类的析构和父类析构函数也构成隐藏关系
		// 规定:子类中不需要显示调用父类的析构,子类析构函数之后,会自动调用父类析构
		// 这样保证析构顺序,先子后父,显示调用取决于实现的人,不能保证先子后父
		// 先子后父
		//Person::~Person();//指定类域(因为隐藏)
		
	}
protected:
	int _num = 1; //学号
	string _addrss = "国庆快乐! 祖国万岁!";
};

8.不能被继承的类

C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。

// C++11的⽅法 
class Base final
{
public:
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};

9.继承与友元

友元关系不能继承到子类。

10.继承与静态成员

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

11.多继承及其菱形继承问题

0.简单介绍

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承

多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在下图的Assistant的对象中Person成员会有两份。⽀持多继承就可能会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

1.单继承模型

2.多继承模型

3.菱形继承模型

4.二义性例子

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()
{
	// 编译报错:error C2385: 对“_name”的访问不明确 
	Assistant a;
	//a._name = "peter";会报错


	// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决 
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

5.虚继承

class Person
{
public:
	string _name; // 姓名 
};


// 使⽤虚继承Person类 
class Student : virtual public Person
{
protected:
	int _num; //学号 
};


// 使⽤虚继承Person类 
class Teacher : virtual public Person
{
protected:
	int _id; // 职⼯编号 
};


// 教授助理 
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程 
};


int main()
{
	// 使⽤虚继承,可以解决数据冗余和⼆义性 
	Assistant a;
	a._name = "peter";
	return 0;
}

举个例子:

注意:谁有数据冗余和二义性,就在继承它的时候加上virtual,使用虚继承,可以解决数据冗余和⼆义性

由上图可以看出A有数据冗余和二义性,所以就在继承A的位置B和C加上virtual

12.继承和组合