C++(继承)

继承是C++面向对象的核心,实现代码重用和层次化设计。它支持多态性,减少冗余代码,使系统易于扩展和维护,是软件架构的基础

1.继承的概念及定义

1.1继承的概念

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

下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课

cpp 复制代码
#include<iostream>
using namespace std;
class Student
{
	public :
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	} 
	// 学习
		void study()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};
class Teacher
{
	public :
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
	// ...
} 
// 授课
void teaching()
{
	//...
}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};
int main()
{
	return 0;
}

从上面的代码我们可以看出,两个类Student和Teacher中的成员有许多重复的代码部分,哪有没有可能让他们都能使用重复的的成员,但是又可以使用各自独立的成员呢(独立成员有:

  1. 如teacher是成员函数授课teaching和成员变量职称string_title。
  2. 而student是成员函数学习study和成员变量学号_stuid。

这里我们就可以使用继承完成我们想要的效果,将公共的成员都放到Person类中,Student

和teacher都继承Person,就可以复用这些成员,就可以不需要重复定义了,省去了很多麻烦

cpp 复制代码
#include<iostream>
using namespace std;
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()
{
	cout << "学生 study" << endl;
}
protected:
	int _stuid; // 学号
};
class Teacher : public Person
{
	public :
	// 授课
	void teaching()
	{
		cout << "老师 teaching" << endl;
	}
protected:
	string title; // 职称
};
int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	s.study();
	t.teaching();
	return 0;
}

由下图我们可以看出teacher和student既可以使用它们的公共成员,又可以使用它们的独特成员。

1.2继承定义

1.2.1定义格式

下⾯我们看到Person是基类,也称作**⽗类**。Student是派⽣类,也称作**⼦类** 。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)

大家放心食用,上面的图片是我在学习课件上截取下来的。

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

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

这里我们可以设置一个类内与类外的概念:

  • 类内是继承类的内部(比如B继承A,那么B就是A的继承类)。
  • 类外是继承类所实例化的对象。

1、在类内(通过改变继承修饰符,看继承类内部能否访问父类成员)

cpp 复制代码
#include<iostream>
using namespace std;
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
	public :
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name="李四"; // 姓名
private:
	int _age=18; // 年龄
};
////1、共有继承修饰符public:class Student : public Person
////2、保护继承修饰符public:class Student : protected Person
////3、私有继承修饰符public:class Student : private Person
class Student : protected Person
{
	public:
	//1、当继承关系是public的时候可以访问到父类public和protected里面的成员。
	//void func()
	//{
	//	Print();
	//	cout << _name << endl;
	//	//cout << _age << endl;//不能访问父类私有成员_age
	//}

	//	//1、当继承关系是protected的时候可以访问到父类public和protected里面的成员。
	//void func()
	//{
	//	Print();
	//	cout << _name << endl;
	//	cout << _age << endl;//不能访问父类私有成员_age
	//}

			//3、当继承关系是private的时候可以访问到父类public和protected里面的成员。
	void func()
	{
		Print();
		cout << _name << endl;
		//cout << _age << endl;//不能访问父类私有成员_age
	}
	protected :
	int _stunum; // 学号
};
int main()
{
	Student s;
	s.func();
	return 0;
}

由上面代码可知,无论我们如何改变继承修饰符,在继承类Student内部都可以访问父类public和protected成员,而不能访问父类private成员。

可以得出结论:在类内能不能访问父类看父类本身访问限定符(如public访问、protected访问、private访问)。

2、在类外(通过改变继承修饰符,看继承类实例化对象能否访问父类成员)

cpp 复制代码
#include<iostream>
using namespace std;
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
	public :
		void Print()
		{
			cout << _name << endl;
		}
protected:
	string _name="张三"; // 姓名
private:
	int _age; // 年龄
};
//1、共有继承修饰符public:class Student : public Person
//2、保护继承修饰符public:class Student : protected Person
//3、私有继承修饰符public:class Student : private Person
class Student : protected Person
{
public:
	void func()
	{
		cout << _name << endl;
	}
	protected :
	int _stunum; // 学号
};
int main()
{
	Student s;
	//1、当继承关系是public的时候s可以访问到父类Person的Print函数
	//s.func();
	//s.Print(); 可以访问到

	//2、当继承关系是protected的时候s不可以访问到父类Person的Print函数
	//s.func();
	//s.Print();  访问不到
	 

	//3、当继承关系是private的时候s不可以访问到父类Person的Print函数
	//s.func();
	//s.Print();  访问不到
	
	return 0;
}

由上面我们可知,通过改变继承修饰符,继承类Student所实例化的对象s能否访问到父类public对象结果不一。

可以得出结论:在类外继承类所实例化的对象能不能访问到父类的public成员看的是继承修饰符(public继承、protected继承、private继承)。

总结:

  1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
  2. 在类内能不能访问父类看父类本身访问限定符(如public访问、protected访问、private访问)。
  3. 在类外继承类所实例化的对象能不能访问到父类的public成员看的是继承修饰符(public继承、protected继承、private继承)。

1.3继承类模板

cpp 复制代码
#include<iostream>
#include<vector>
using namespace std;
namespace bit
{
	template<class T>
	class stack : public std::vector<T>
	{
		public :
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: "push_back": 找不到标识符
           // 因为stack<int>实例化时,也实例化vector<int>了
				// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
				vector<T>::push_back(x);
			//push_back(x);这种是错误的,上面vector<T>::push_back(x)这种是正确的
		} 
		void pop()
		{
			vector<T>::pop_back();
		} 
		const T& top()
		{
			return vector<T>::back();
		} 
		bool empty()
		{
			return vector<T>::empty();
		}
	};
} 
int main()
{
	bit::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	} 
	return 0;
}

2.基类和派生类间的转换

  1. public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
	protected :
	string _name="张三"; // 姓名
	string _sex="男"; // 性别
	int _age=18; // 年龄
};
class Student : public Person
{
	public :
	int _No=123456; // 学号
};
int main()
{
	Student sobj;
	Person pobj = sobj;//这里我们将派生类对象sobj给基类对象pobj
	return 0;
}

2.基类对象不能赋值给派⽣类对象。

这里如果我们在VS上试试用将基类实例化对象赋值给派生类实例化对象看看会不会报错。

综上所述基类对象不能赋值给派⽣类对象,否则会出现编译错误。

3.基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)的dynamic_cast来进⾏识别后进⾏安全转换。

cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
	protected :
	string _name="张三"; // 姓名
	string _sex="男"; // 性别
	int _age=18; // 年龄
};
class Student : public Person
{
	public :
	int _No=123456; // 学号
};
int main()
{
	Student sobj;
	// 1.派⽣类对象可以赋值给基类的指针/引⽤
	Person* pp = &sobj;
	Person& rp = sobj;
	// 派⽣类对象可以赋值给基类的对象是通过调⽤基类的拷⻉构造完成的
	Person pobj = sobj;
	//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
	//sobj = pobj;
	return 0;
}

3.继承中的作用域

3.1隐藏规则

  1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。
  2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
	protected :
	string _name = "小李子"; // 姓名
	int _num = 111; // ⾝份证号
};
class Student : public Person
{
	public :
	void Print()
	{
		//由于派生类Student中的成员身份证号_num
		//与基类Person中的成员学号_num同名,因此这里C++自动隐藏了基类的同名_num
		//导致在派生类中只能访问到派生类类域自己的_num
		cout << " 身份证号:" << Person::_num << endl;//要想访问需要使用Person::_num ,就可以继续访问到基类的_num
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

4.派生类的默认构造函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这⼏个成员函数是如何⽣成的呢?

  1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
  2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
  3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。
  4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
  5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
  6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
  7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。

4.1 派生类的构造函数

4.1.1当我们基类有默认构造函数,可以不在派⽣类构造函数的初始化列表阶段显⽰调⽤。

当我们基类有默认构造函数,派生类属于基类成员那一部分,需要调用基类的构造函数。如派生类实例化对象s1中的Person就是调用基类的构造函数Person(const char* name = "peter")。

cpp 复制代码
#include<iostream>
using namespace std;
class Person
{
	public :
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	} 
protected:
	string _name; // 姓名
};
class Student : public Person
{
	public :
		//默认生成的构造函数的行为
		//1、内置类型->不确定
		//2、自定义类型->调用默认构造
		//3、继承父类成员看做一个整体对象,要求调用父类的默认构造
protected:
	int _num; //学号
	string _address="河南省";//地址
};
int main()
{
	Student s1;
	return 0;
}

4.1.2当我们基类没有默认构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。

cpp 复制代码
#include<iostream>
using namespace std;
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 num,const char* addrss)
			: Person(name)//C++这里必须这样使用,我们可以把父类当成一个整体对象。
			, _num(num)
			,_addrss(addrss)
		{
			cout << "Student()" << endl;
		}
protected:
	int  _num; //学号
	string _addrss="河南省信阳市";//地址
};
int main()
{
	Student s1("李四",1111,"信阳市");
	return 0;
}

那如果当我们基类没有默认构造函数,也没有在派⽣类构造函数的初始化列表阶段显⽰调⽤会出现什么呢?

下面的代码结果是我们将上面代码的基础上,注释掉初始化列表中

Person(name)后的结果:

可以看出当我们基类没有默认构造函数,也没有在派⽣类构造函数的初始化列表阶段显⽰调⽤会出现报错。

因此得出结论:派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。

4.2派生类的拷贝构造函数

派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

cpp 复制代码
#include<iostream>
using namespace std;
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 num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	} 
	//严格来说这里Student拷贝默认生成的就够用了
	//如果有需要深拷贝资源,才需要自己实现
	//下面是我们写出来的一个样例:
	Student(const Student& s)
		: Person(s)
		//Person(s)这里涉及到了基类和派生类间的转换:
		//public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。
		// 这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	} 
protected:
	int _num; //学号
};
int main()
{
	Student s1("jack", 18);
	Student s2(s1);
	return 0;
}

4.3派生类的operaor=

派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的

operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域。

cpp 复制代码
#include<iostream>
using namespace std;
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 num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	} 
	//严格来说这里Student拷贝默认生成的就够用了
	//如果有需要深拷贝资源,才需要自己实现
	//下面只是我们写出来的一个样例:
	Student(const Student& s)
		: Person(s)
		//Person(s)这里涉及到了基类和派生类间的转换:
		//public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。
		// 这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	} 

	//严格来说这里Student赋值重载默认生成的就够用了
	//如果有需要深拷贝资源,才需要自己实现
	//下面只是我们写出来的一个样例:
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			// 派生类与基类存在同名成员operator=构成隐藏,所以需要显⽰调⽤
			Person::operator =(s);
			_num = s._num;
		} 
		return* this;
	} 
protected:
	int _num; //学号
};
int main()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
	return 0;
}

4.4析构函数

因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。因为编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。

cpp 复制代码
//#include<iostream>
//using namespace std;
//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 num,const char* addrss)
//			//: Person(name)//C++这里必须这样使用,我们可以把父类当成一个整体对象。
//			: _num(num)
//			,_addrss(addrss)
//		{
//			cout << "Student()" << endl;
//		}
//protected:
//	int  _num; //学号
//	string _addrss="河南省信阳市";//地址
//};
//int main()
//{
//	Student s1("李四",1111,"信阳市");
//	return 0;
//}

#include<iostream>
using namespace std;
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()会被编译器自动成destructor()
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};
class Student : public Person
{
	public :
	Student(const char* name, int num)
		: Person(name)
		, _num(num)
	{
		cout << "Student()" << endl;
	} 
	//严格来说这里Student拷贝默认生成的就够用了
	//如果有需要深拷贝资源,才需要自己实现
	//下面只是我们写出来的一个样例:
	Student(const Student& s)
		: Person(s)
		//Person(s)这里涉及到了基类和派生类间的转换:
		//public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。
		// 这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分
		, _num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	} 

	//严格来说这里Student赋值重载默认生成的就够用了
	//如果有需要深拷贝资源,才需要自己实现
	//下面只是我们写出来的一个样例:
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			// 派生类与基类存在同名成员operator=构成隐藏,所以需要显⽰调⽤
			Person::operator =(s);
			_num = s._num;
		} 
		return* this;
	} 

	//严格来说这里Student析构函数默认生成的就够用了
	//如果有需要深拷贝资源,才需要自己实现
	// 下面只是我们写出来的一个样例:
	//~Student()会被编译器自动成destructor()
	~Student()
	{
	//因为子类的析构函数~Student()和父类的析构函数~Person()都被
	//编译器特殊处理成destructor(),导致子类和父类的析构析构函数构成隐藏。
		Person::~Person();
		cout << "~Student()" << endl;
	}
protected:
	int _num; //学号
};
int main()
{
	Student s1("jack", 18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
	return 0;
}

这里我们看出明明只是三个Student类型的实例化对象,为什么会调用六次父类析构函数呢?

正常来说一个派生类实例化对象只会调用一次派生类析构函数和一次基类析构函数。可以看出每一个实例化对象都多调用了一次父类析构函数,这样很明显是错误的,不安全的。

原来:派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序

上面我们将派生类析构函数~Student中的调用基类派生类析构函数代码注释掉以后,输出结果正确。可以看出我们并不需要额外再调用一次基类的析构函数,派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。

同时我们注意到:派⽣类对象析构清理是先调⽤派⽣类析构再调基类的析构。

但是派生类对象初始化中恰恰相反:派⽣类对象初始化是先调⽤基类构造再调派⽣类构造。

5. 实现一个不能被继承的类

实现一个不能被继承的类:

⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。所以这种方法不推荐,捡了芝麻丢了西瓜属于是。

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

cpp 复制代码
#include<iostream>
using namespace std;
class Base final
{
	public :
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
private:
	// C++98的⽅法
	/*Base()
	{}*/
};
class Derive : public Base
{
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

上面我们用的是方法二加了final关键字后的代码。结果如下:

所以被final修改后的基类,继承类不能从基类中继承。

6. 继承与友元

cpp 复制代码
class Student;
class Person
{
	public :
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};
class Student : public Person
{
	protected :
	int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;
} 
int main()
{
	Person p;
	Student s;
	// 编译报错:error C2248: "Student::_stuNum": ⽆法访问 protected 成员
	// 解决⽅案:Display也变成Student 的友元即可
	Display(p, s);
	return 0;
}

Display虽然是基类Person的友元函数,这层关系不能被派生类Studengt继承下来。相当于爸爸的熟人不是你的熟人。如果你也想使用Display,只需要把Display加入自己派生类当中即可使用。

7. 继承与静态成员

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

cpp 复制代码
class Person
{
	public :
	string _name;
	static int _count;
};
int Person::_count = 0;
class Student : public Person
{
	protected :
	int _stuNum;
};
int main()
{
	Person p;
	Student s;
	// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
	// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
	cout << &p._name << endl;//000000FB2419FB68
	cout << &s._name << endl;//000000FB2419FBA8

	// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
	// 说明派⽣类和基类共⽤同⼀份静态成员
	cout << &p._count << endl;//00007FF65F5E1444
	cout << &s._count << endl;//00007FF65F5E1444

	// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
	cout << Person::_count << endl;//0
	cout << Student::_count << endl;//0
	return 0;
}

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

继承模型

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

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

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

9.结言

C++的继承到这里就结束了,如有错误欢迎指正。拜拜喽~

相关推荐
lly2024061 小时前
XQuery 选择和过滤
开发语言
I_LPL1 小时前
day22 代码随想录算法训练营 回溯专题1
算法·回溯算法·求职面试·组合问题
金融RPA机器人丨实在智能1 小时前
2026动态规划新风向:实在智能Agent如何以自适应逻辑重构企业效率?
算法·ai·重构·动态规划
测试工程师成长之路2 小时前
Serenity BDD 框架:Java + Selenium 全面指南(2026 最新)
java·开发语言·selenium
czxyvX2 小时前
017-AVL树(C++实现)
开发语言·数据结构·c++
你真是饿了2 小时前
1.C++入门基础
开发语言·c++
天天进步20152 小时前
Python全栈项目:实时数据处理平台
开发语言·python
Tipriest_2 小时前
Python中is关键字详细说明,比较的是地址还是值
开发语言·python
sheji34162 小时前
【开题答辩全过程】以 基于Python的餐饮统计系统的设计和实 现为例,包含答辩的问题和答案
开发语言·python