【C++】继承----下篇
文章目录
- 前言
- 一、实现一个不能继承的类
- 二、友元与继承
- 三、继承与静态成员
- 四、多继承以及菱形继承问题
- 1.继承模型:
- 2.菱形继承的问题
- 3.虚拟继承解决数据冗余和二义性的原理
- 4.虚拟继承的原理
- 五、继承的总结和反思
- 1.继承和组合
- 总结
前言
各位好呀!今天呢我们接着讲继承的相关知识,之前给大家已经分享了继承一部分知识。那今天小编就来给继承收个尾吧。来看看继承的剩下的一部分。
一、实现一个不能继承的类
想要实现一个不能被继承的类的呢有两种方法:
- 方法一:父类的构造函数私有,子类的构造必须调用父类的构造函数,但是父类的构造函数私有化以后呢,在子类中是不可见也不可调用的。那么 子类就无法实例化出对象,这样就可以达到父类不能被继承(C++98的方法)。
- 方法二:C++11新增了一个关键字final,用final修饰父类,那么子类就无法继承父类了。
#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;
}
二、友元与继承
注意:友元关系不能被继承,也就是说,父类的友元不能访问子类的私有成员和保护成员。
解决方法:在子类中加上友元就可以了。还有就是要注意一下需要前置声明一下子类
#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;
}
三、继承与静态成员
#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.继承模型:
继承模型呢分为三种:单继承,多继承和菱形继承
- 单继承:一个子类只有一个直接父类时称这种继承关系为单继承
- 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 菱形继承:菱形继承属于一个特殊的多继承,
2.菱形继承的问题
菱形继承主要时两个问题,分别是 二义性和数据冗余
二义性:
首先,什么是二义性?二义性就是在访问数据的时候发生歧义,不知道访问那个。
示例:
#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就可以解决问题,但是不能解决数据冗余的问题
#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中各有一份。
#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(虚拟继承)。
那这个关键字该怎么用呢?该加在哪里呢?先看示例:
#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.虚拟继承的原理
#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++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
1.继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系,。假设B组合了A,每个B对象中都有一个A对象。
示例:我们以实现一个简单的栈来演示:
//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;
}
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
总结
到这里继承的相关知识小编就基本分享完了咯,如果有什么疑问欢迎大家讨论。那今天就到这里吧。