【C++修仙录03】进阶篇:继承

嗨~大家好,这里是春栀怡铃声的博客~

"做你害怕的事,然后发现,不过如此~"

目录

继承定义

​编辑

定义格式

​编辑

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

继承类模板

基类和派生类间的转换

继承中的作用域

隐藏规则

派生类的默认成员函数

4个常见默认成员函数

实现一个不能被继承的类

继承与友元

继承与静态成员

多继承及其菱形继承问题

继承模型

虚继承

继承和组合


继承定义

继承是面向对象程序设计使代码可以复用的手段,他允许我们

首先我们为什么要有继承?

来看这段代码

cpp 复制代码
class Student
{
public:
	void identfiy()
	{
		cout << "void identfiy()" << endl;
		cout << _name << endl;
	}
	void study()
	{

	}
protected:
	string _name;
	string _addrss;
	string _stuid; //学号

};
class Teacher
{
public:
	void identfiy()
	{
		cout << "void identfiy()" << endl;
		cout << _name << endl;
	}
protected:
	string _name;
	string _addrss;
	string _title; //称职

};

老师和学生这2个类,只有学号和称职的不同,类里面的其他都一致。

单独写出2个类似乎还可以升级为另一种写法

cpp 复制代码
class Teacher
{
public:
	void identfiy()
	{
		cout << "void identfiy()" << endl;
		cout << _name << endl;
	}
protected:
	string _name;
	string _addrss;
	string _title;

};
class Student :public Teacher
{
public:

protected:
	string _stuid;

};

而且升级前后,结果保持不变

这种升级后的写法就叫做继承。

定义格式

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

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

我们先看最后一行

在基类也就是父类的私有成员(private),无论是什么继承方式,在派生类中通通不可见

请看代码验证

在父类中,_age 是他的私有成员,子类继承了他的私有成员但是不能在子类中使用,类外面同样也不能用。

这个私有成员(_age)只能在父类中访问

我们把目光转到第二行

在这之前我们回顾一下protected 作用

protected:限制访问限定符不想在类外面使用,又想在子类中使用

例子

如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

基类的protected 成员 与 继承方式,哪个权限小,就依照哪个执行 第一行同样如此

继承类模板

cpp 复制代码
namespace xuan
{
	template<class T>
	class Stack:public vector<T>
	{
	public:
		void pop()
		{
			vector<T>::pop_back();
		}
		void push(const T&x)
		{
			vector<T>::push_back(x);
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}

	};
}
int main()
{
	xuan::Stack<int> s;
	s.push(5);
	s.push(6);
	while (!s.empty())
	{
		cout << s.top() << endl;
		s.pop();
	}
	return 0;
}

基类是类模板时,需要指定⼀下类域,否则编译报错: "push_back": 找不到标识符

因为stack实例化时,也实例化vector了 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到

基类和派生类间的转换

public继承的派生类对象可以赋值给 基类的指针/基类的引用。 这里有个形象的说法叫切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

• 基类对象不能赋值给派生类对象。

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

继承中的作用域

隐藏规则

1.在继承 体系中基类和派生类都有独立的作用域。

  1. 派生类和基类 中有同名成员 ,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏

(在派生类成员函数中,可以使用基类::基类成员 显示访问)

  1. 需要注意的是如果是成员函数的隐藏 ,只需要函数名相同就构成隐藏

  2. 注意在实际中在继承体系里面最好不要定义同名的成员。

同名变量

cpp 复制代码
class Person
{
public:

protected:
	string _addrss;
	int _age = 18;
	string _name;
};
class Student :public Person
{
public:
	void Printf()
	{
		cout << "void Printf()" << endl;
		cout << _age << endl;
	}
protected:
	int _age = 999;
};
int main()
{
	Student s;
	s.Printf();
	return 0;
}

子类和父类都有 _age 同名变量 构成隐藏,隐藏了父类的_age

同名函数

cpp 复制代码
class A
{
public:
 void fun()
 {
 cout << "func()" << endl;
 }
};
class B : public A
{
public:
 void fun(int i)
 {
 cout << "func(int i)" <<i<<endl;
 }
};
int main()
{
 B b;
 b.fun(10);
 b.fun(); 
 
 return 0;
};

只要父类和子类的函数名相同,就构成隐藏。

明显,我们看这段代码

b.func();

这里想要调用父类的 func 函数,可是父类的func 函数已经被隐藏了,直接调用会报错

派生类的默认成员函数

4个常见默认成员函数

Student 没有写构造

内置类型不知道不确定,自定义类型调用默认构造,继承父类的成员会调用父类的默认构造,所以name是peter

子类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在子类构造函数的初始化列表阶段显式调用。

默认构造

不允许构造父类成员

cpp 复制代码
class Person
{
public:
	Person(const char* name="Peter")
		:_name(name)
	{ }
	~Person()
	{
		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)
		:_name(name)  //错误写法
		,_num(num)
	{

	}
protected:
	int _num=1; // 学号
	string _addrss="西安";
};
int main()
{
	Student s1("lisi",8);
	return 0;
}

不允许在子类中直接构造父类成员 如图所示:

显示调用

cpp 复制代码
class Student:public Person
{
public:
	Student(const char *name,int num)
		:Person(name)
		,_num(num)
	{

	}

拷贝构造

cpp 复制代码
class Person
{
public:
	Person(const char* name="Peter")
		:_name(name)
	{ }
	~Person()
	{
		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)
	{ }
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
	{ }
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _num; // 学号
};
int main()
{
	Student s1("lisi",9);
	Student s2 = s1;
	return 0;
}

拷贝构造中,继承的父类成员也要调用父类的拷贝构造

cpp 复制代码
Student(const Student& s)
		:Person(s)
		,_num(s._num)
	{ }

至于子类调用父类构造 为什么传参 s ? 请看父类的拷贝构造和子类的拷贝构造

cpp 复制代码
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

p 是 父类成员的别名, s也是 子类继承父类成员的别名,相当于子类的一部分切割给了父类。

所以传s

operator=重载

子类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是子类的 operator=隐藏了基类的operator=,所以显式调用基类的operator=,需要指定基类作用域

cpp 复制代码
class Person
{
public:
	Person(const char* name="Peter")
		:_name(name)
	{ }
	~Person()
	{
		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)
	{ }
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
	{ }
	~Student()
	{
		cout << "~Student()" << endl;
	}
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (&s != this)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num; // 学号
};
int main()
{
	Student s1("lisi",9);
	Student s3("wangwu", 8);
	s1 = s3;
	return 0;
}

注意不能出现(s1=s1) 自己赋值给自己的情况,用if语句避免

原理:如果s指向的和this 指向不同,说明并不是自己给自己赋值

这里为什么显示Person?

因为父类的operator=函数和 子类的operator= 函数构成隐藏,隐藏了父类的函数,调用不了父类的operator= ,必须显示调用才可以调到

cpp 复制代码
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (&s != this)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}

父类

cpp 复制代码
Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}

析构函数

子类的析构函数会在被调用完成后自动调用父类的析构函数清理基类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。

请看这个代码执行结果

cpp 复制代码
class Person
{
public:
	Person(const char* name="Peter")
		:_name(name)
	{ }
	~Person()
	{
		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)
	{ }
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
	{ }
	~Student()
	{
		cout << "~Student()" << endl;
	}
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (&s != this)
		{
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num; // 学号
};
int main()
{
	Student s1("lisi",9);
	Student s2 = s1;
	Student s3("wangwu", 8);
	s2 = s3;
	return 0;
}

在 int 函数中,有3个子类对象,看最后如何析构

析构s1时,析构后自动调用父类的析构函数,

析构s2,析构后自动调用父类的析构函数

析构s3,析构后自动调用父类的析构函数

为什么是这样的呢?

因为 子类对象初始化 先调用父类构造 再调子类构造。也就是说继承的父类成员先构造,先构造后析构,自动调用父类析构函数 为了保证 子类对象先进行析构清理

  1. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。

实现一个不能被继承的类

方法一:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

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

继承与友元

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

根据代码分析,第一个红框表明存在子类Student ,不过子类的定义在Person下面,为了防止在第二个框位置的友元函数出现错误,

结论是友元函数并不能继承,上图也成功表示了父类的友元函数不能访问子类自己的成员

如果一定要访问,将友元函数在子类再次声明一次就好

继承与静态成员

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

我们可以看到父类中有一个静态成员,一个正常成员变量,

子类继承了父类的成员,我们可以通过分别打印父类子类的成员的地址,就可以知道子类继承的成员变量是否是同一个

我们看到子类、父类的静态成员变量的地址相同,而其他成员变量地址不同

证实了父类中定义的静态成员变量,在整个继承体系中只有这一个

多继承及其菱形继承问题

继承模型

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

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

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

单继承

菱形继承

数据冗余:Student 类和 Teacher 类都有Person 中的值,而Assiant 类继承 Teacher 和 Student

数据肯定会有重合的,造成多份数据。

二义性:

在菱形继承中,最后的子类 Assiant 想要访问 他的2个父类共同的有的成员,必须明确指明访问哪个,不然会造成二义性!

像这样就可以避免二义性

但是数据冗余并没有解决

虚继承

使用虚继承,可以解决数据冗余和二义性

虚继承是在哪里会造成数据冗余的地方加 virtual

cpp 复制代码
class Person
{
	
public:
	int _age;
};

class Student:virtual public Person 
{
public:
	int _id=5;
};
class Teacher : virtual public Person
{
public:
	int _count=6;
};
class Assiant :public Student, public Teacher
{
public:
};

多继承一定会出现菱形继承,菱形继承最好不使用

继承和组合

public继承是⼀种is-a的关系。也就是说每个派生类对象都是⼀个基类对象。

• 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

• 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。

继承⼀定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依

赖关系很强,耦合度高。

• 对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用

因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关

系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

• 优先使用组合,而不是继承。

实际尽量多去用组合,组合的耦合度低,代码维护性好。

不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,

另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

继承实现stack

cpp 复制代码
template<class T>
class Stack :public vector<T>
{
public:
	void pop()
	{
		vector<T>::pop_back();
	}
	void push(const T&x)
	{
		vector<T>::push_back(x);
	}
	T& top()
	{
		return vector<T>::back();
	}
	bool empty()
	{
		return vector<T>::empty();
	}
	size_t size()
	{
		vector<T>::size();
	}
};
int main()
{
	Stack<int> s;
	s.push(8);
	s.push(1);
	s.push(6);
	s.push(5);


	while (!s.empty())
	{
		cout << s.top() << endl;
		s.pop();
	}
	return 0;
}

组合实现

cpp 复制代码
namespace xuan
{
	template<class T,class Compare=vector<T>>
	class Stack
	{
	public:
		        void pop()
				{
					_con.pop_back();
				}
				void push(const T&x)
				{
					_con.push_back(x);
				}
				T& top()
				{
					return _con.back();
				}
				bool empty()
				{
					return _con.empty();
				}
				size_t size()
				{
					_con.size();
				}
	private:
		Compare _con;
	};
}
int main()
{
	xuan::Stack<int> s;
	s.push(8);
	s.push(1);
	s.push(6);
	s.push(5);


	while (!s.empty())
	{
		cout << s.top() << endl;
		s.pop();
	}
	return 0;
}

感谢花时间阅读这篇内容!

如果觉得有价值,欢迎点赞支持、收藏备用,或分享给同行。你的认可,是我持续输出高质量内容的最大动力。

我们下期再见喽!!!

相关推荐
Shadow(⊙o⊙)1 小时前
C++进阶知识3.0
linux·服务器·开发语言·c++
.千余1 小时前
【C++】C++ map 与 multimap 完全指南:键值对容器详解
开发语言·c++·笔记·学习·其他
Frank学习路上1 小时前
【C++】面试:内存管理
c++·面试
牢姐与蒯1 小时前
c++数据结构之c++11(三)
开发语言·c++
Irissgwe1 小时前
数据结构-二叉树
数据结构·c++·二叉树·c·
凡人叶枫10 小时前
Effective C++ 条款30:透彻了解 inlining 的里里外外
linux·开发语言·c++·嵌入式开发·effective c++
noipp10 小时前
推荐题目:洛谷 P10907 [蓝桥杯 2024 国 B] 蚂蚁开会
c语言·c++·算法·编程·洛谷
学逆向的10 小时前
C++纯虚函数
开发语言·c++·网络安全
凡人叶枫12 小时前
Effective C++ 条款22:将成员变量声明为 private
linux·开发语言·c++