【C++】继承----下篇

文章目录


前言

各位好呀!今天呢我们接着讲继承的相关知识,之前给大家已经分享了继承一部分知识。那今天小编就来给继承收个尾吧。来看看继承的剩下的一部分。


一、实现一个不能继承的类

想要实现一个不能被继承的类的呢有两种方法:

  1. 方法一:父类的构造函数私有,子类的构造必须调用父类的构造函数,但是父类的构造函数私有化以后呢,在子类中是不可见也不可调用的。那么 子类就无法实例化出对象,这样就可以达到父类不能被继承(C++98的方法)。
  2. 方法二:C++11新增了一个关键字final,用final修饰父类,那么子类就无法继承父类了
cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
class Person//C++11
{
public:
protected:
	string _name; 
private:
	Person()//私有化构造函数
	{}
};
class student:public Person  
{
public:
private:
	string ID;
};
int main()
{
	student s;//这里会报错的,因为构造函数已经被私有化,子类是调不到父类的构造函数的
	//但是这里要注意的是:如果我们这里不定义,代码是不会报错的。
	return 0;
}

#include<iostream>
#include<string>
using namespace std;
class Person final//C++11
{
public:
	Person()//私有化构造函数 
	{} 
protected:
	string _name; 
private:
	
};
class student:public Person  //像这样用final修饰父类的话,父类也不能被子类继承 
{
public:
private:
	string ID;
};
int main()
{
	 student s;
	return 0;
}

二、友元与继承

注意: 友元关系不能被继承,也就是说,父类的友元不能访问子类的私有成员和保护成员。

解决方法在子类中加上友元就可以了。还有就是要注意一下需要前置声明一下子类

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
class student;  //前置声明
class Person
{
	friend void  Print(const Person& p, const student& s); 
	//编译器在遇到一个变量和函数的时候,都只会向上查找(提高查找的效率),
	// 所以这里的student就会向上查找,但是上面没有student,student在下面
	//还有就是student不能放在方面取,因为student要继承Person。这两者相互依赖。
	//为了解决这个问题呢我们会在上面加一个前置声明
public:
protected:
	string _name="帅哥"; 
};
class student:public Person 
{
	friend void  Print(const Person& p, const student& s); 
	//由于继承关系不能被继承下来,所以就访问不到student中_num成员变量
	//解决这个问只需要像这样,加一个友元就可以解决这个问题了。
public:
protected :
	string _num="123456";
};
void Print(const Person& p, const student& s)
{
	cout << p._name << endl;  
	cout << s._num << endl; 
}
int main()
{
	student v;
	Person  c;
	Print(c,v);    
	return 0;
}

三、继承与静态成员

cpp 复制代码
#include<iostream>
#include<string>
using namespace std; 
class Person
{
public:
	string _name; 
	static int n;       
}; 
int Person::n = 1; 
class student :public Person
{
public:
	string  _num;
};
int main()
{
	Person p;
	student s;
	//非静态成员变量的地址 
	cout << &p._name << endl;
	cout << &s._name << endl; 
	cout << endl; 
	//静态成员变量的地址
	cout << &p.n  << endl;
	cout << &s.n << endl; 
	return 0;
}

我们通过看到非静态成员_name地址是不一样,这说明了子类继承下来的成员在子类和父类中各有一份。但是静态成员是不是地址相同呀?这又说明父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

还有一个就是在共有的情况下,父类和子类指定作用域就可以访问静态成员。这里突破作用域就可以访问,因为它没有存在对象里面,而是存在静态区,它只是受到类域的限制而已。还有就可以把静态成员理解成全局的。

四、多继承以及菱形继承问题

1.继承模型:

继承模型呢分为三种:单继承,多继承和菱形继承

  1. 单继承一个子类只有一个直接父类时称这种继承关系为单继承
  2. 多继承一个子类有两个或以上直接父类时称这个继承关系为多继承
  3. 菱形继承菱形继承属于一个特殊的多继承

2.菱形继承的问题

菱形继承主要时两个问题,分别是 二义性和数据冗余

二义性

首先,什么是二义性?二义性就是在访问数据的时候发生歧义,不知道访问那个。

示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std; 
class A
{
public:
	string _name;
	int _age;
};
class B:public A 
{
public:
protected:
	string  _number;
};
class C :public A
{
public:
protected:
	string Gender;  
};
class D :public B, public C
{
public:
protected:
	string ID;
};
int main()
{
	D d;
	d._name;//存在二义性 
}


问题分析

这里为什么会存在访问不明确呢?结合之前的知识,子类继承父类成员,那么子类和父类中是不是都分别有一份独立的成员。但是现在B和C这两个类都继承了A,然而D又继承B和C。也就是说A,B,C,D中分别都有一份_name,现在要访问父类中的成员_name,但是这里的D是继承了两个类,两个类中都有_name ,所以这里编译器就不知道该访问那个类里面的_name。这就是二义性。

解决方法:

怎样解决这个问题呢?其实很简单,只需要显示指定访问那个父类中_name就可以解决问题,但是不能解决数据冗余的问题

cpp 复制代码
#include<iostream>
#include<string>
using namespace std; 
class A
{
public:
	string _name;
	int _age;
};
class B:public A 
{
public:
protected:
	string  _number;
};
class C :public A
{
public:
protected:
	string Gender;  
};
class D :public B, public C
{
public:
protected:
	string ID;
};
int main()
{
	D d;
	d.B::_name="张三";//指定要访问的父类
	d.C::_name = "小张"; //指定要访问的父类
}

数据冗余:

数据冗余可以理解成数据重复造成空间的浪费

示例:

菱形继承还有个特别烦的点就是他会让空间变大,一个它的父类在被几个类继承时,那它就有几份。比如下列代码中,A在B,C中各有一份。

cpp 复制代码
#include<iostream>
#include<string>
using namespace std; 
class A
{
public:
	string _name; 
	int _age; 
};
class B:public A 
{
public:
protected:
	string  _number;
};
class C :public A
{
public:
protected: 
	string Gender;  
};
class D :public B, public C
{
public:
protected:
	string ID;
};
class F:public A,public B
{
public:

};
int main()
{
	D d;
	F f;
	cout << sizeof(d) << endl;//菱形继承大小
	cout << sizeof(f) << endl;//多继承的大小
}

可以看出两者的空间大小相差的将近一倍了。所以,一般不建议创建菱形继承,因为这样有太多的问题,有时候还把握不住,建议不使用。

3.虚拟继承解决数据冗余和二义性的原理

菱形继承一般不建议使用,但是如果非要使用,那该怎样解决二义性和数据冗余呢?这里就要引用一个新的关键字 virtual(虚拟继承)。

那这个关键字该怎么用呢?该加在哪里呢?先看示例:

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
	string _name;
	int _age;
};
class B :virtual public A
{
public:
protected:
	string  _number;
};
class C :virtual public A
{
public:
protected:
	string Gender;
};
class D :public B, public C
{
public:
protected:
	string ID;
};
int main()
{
	D d;
	d._name = "张三";
	return 0;
}

通过上面的代码可以看出:
virtual应该加在产生二义性和数据冗余继承的地方 ,现在A是不是产生了二义性和数据冗余 ,那virtual就加在B和C继承的哪里。这样就解决了二义性和数据冗余的问题,这点我们可以通过监视窗口可以看出。

从监视窗口展示的原因,这里虽然看起来是三份,但其实是一份。这样是不是就解决了数据冗余和二义性的问题啊。
**注意:**这里的virtual不能只加在B或者C,必须要同时加在B和C

大家看看这上图,图中的关系是不是菱形继承呢?其实 上图也时菱形继承哦,大家不要对形状太刻板了哦,认为菱形继承那他的形状就必须时菱形。形状不是菱形但是有二义性和数据冗余的产生那他就是菱形继承,

思考以及解决方法

那这里的virtual该加在哪里呢?BC?还是DC?其实正确是应该是加在BC,这里是不是A产生二义性和数据冗余,那就要加BC呀!那这里可以不可以在BCD都加上virtual呢?这里就好比一个人只需要两根拐棍,而你偏要给他三根是一样的性质。在D那里都没有产生二义性和数据冗余那就没必要加。

还有就是大家在写代码的时候遇到上图这种继承的时候都加上virtual,这种情况呢属于过度防范了。首先我们可以先看看B和C有没有被同一个类继承。如果没有,可以先不用加;如果有,那再加上virtual是不是也不迟啊?所以大家在写代码的时候不要过度的防范二义性和数据冗余。

4.虚拟继承的原理

cpp 复制代码
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
	A(const char*name,int age=18)
		:_name(name)
		,_age(age) 
	{}
	string _name;
	int _age;
};
class B :virtual public A
{
public:
	B(const char*name,const char*number="1234567899")
		:A(name)
		,_number(number) 
	{}
protected:
	string  _number;
};
class C :virtual public A
{
public:
	C(const char* name, const char* gender="男")
		:A(name)
		, Gender(gender) 
	{}
protected:
	string Gender;
};
class D :public B, public C 
{
public:
	D(const char*name,const char*id="1263457")
		:B(name) 
		,C(name) 
		//,A(naem)//这里必须显示调用,不然会报错,
		//还有就是这里初始化怎么多name,那到底以谁的为准呢?
		,ID(id) 
	{}
protected:
	string ID;
};
int main()
{

	D d("张三"); 
	return 0;
}

这里从我们之前学的知识来看这里代码的逻辑应该时没有 问题的呀。调用子类的构造函数先调用父类的默认构造函数嘛。但是这里说class A 不存在默认构造。那是怎么回事呢?这不得不就要看看虚拟继承的原理了。

相比于普通的多继承,虚拟继承呢是要把class A拿出来放在最底下的一个类中。他就不像普通多继承那样class A分别存在class B 和class C中。因为他要解决二义性和数据冗余。与此同时,这里还要引入一个虚基表和虚基表指针,复杂的很,所以小编这里就没有展示出来。小编这里主要是像让大家看看这两种继承的有什么不同。回到上面的问题:由于这里A不在B和C里面了,而是一个单独的父类,所以A也因该显示调用。

总结:不要轻易使用菱形继承和写出菱形继承的代码,多继承可以用

五、继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

1.继承和组合

  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  2. 组合是一种has-a的关系,。假设B组合了A,每个B对象中都有一个A对象。
    示例:我们以实现一个简单的栈来演示:
cpp 复制代码
//stack和cin构成has-a的关系
#include<iostream>
#include<string>
#include<stdbool.h> 
using namespace std;
class stack
{
public :
	void push(const int& x)
	{
		cin.push_back(x);
	}
	void pop()
	{
		cin.pop_back();
	}
	const int& top()const 
	{
		return cin.back();
	}
	bool empty()
	{
		return cin.empty();
	}
private:  
	vector<int >  cin; 
};
int main()
{
	stack s;
	s.push(1);
	s.push(2);
	s.push(3);
	while (!s.empty())
	{
		cout << s.top() << " ";
		s.pop();
	}

	return 0;
}

//stack和vector是is-a关系
#include<iostream>
#include<stdbool.h> 
#include<vector>
using namespace std;
class stack:public std::vector<int>  
{
public:
	void push(const int& x)
	{
		vector<int> ::push_back(x);
	}
	void pop()
	{
		vector<int> ::pop_back();
	}
	const int& top()const
	{
		return vector<int> ::back();
	}
	bool empty()
	{
		return vector<int> ::empty(); 
	}
};
int main()
{
	stack s; 
	s.push(1); 
	s.push(2);
	s.push(3); 
	while (!s.empty()) 
	{
		cout << s.top() << " "; 
		s.pop();
	}
	return 0;
}
  1. 优先使用对象组合,而不是类继承 。
  2. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  3. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
  4. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

总结

到这里继承的相关知识小编就基本分享完了咯,如果有什么疑问欢迎大家讨论。那今天就到这里吧。

相关推荐
一只叫煤球的猫4 分钟前
@Async的六大常见坑,今天给你盘明白
java·spring boot·后端
亚马逊云开发者22 分钟前
智能化 Graviton 迁移:Amazon Q CLI 加速应用架构现代化
java·人工智能
风象南30 分钟前
Spring Boot 的 3 种二级缓存落地方式
java·spring boot·后端
移动开发者1号1 小时前
解析401 Token过期自动刷新机制:Kotlin全栈实现指南
android·kotlin
移动开发者1号1 小时前
网络缓存策略与DiskLruCache解析
android·kotlin
皮皮林55113 小时前
使用 Java + WebSocket 实现简单实时双人协同 pk 答题
java·websocket
一只柠檬新14 小时前
Web和Android的渐变角度区别
android
志旭14 小时前
从0到 1实现BufferQueue GraphicBuffer fence HWC surfaceflinger
android
_一条咸鱼_14 小时前
Android Runtime堆内存架构设计(47)
android·面试·android jetpack
码小凡14 小时前
优雅!用了这两款插件,我成了整个公司代码写得最规范的码农
java·后端