文章目录
前言
各位好呀!今天呢我们接着讲继承的相关知识,之前给大家已经分享了继承一部分知识。那今天小编就来给继承收个尾吧。来看看继承的剩下的一部分。
一、实现一个不能继承的类
想要实现一个不能被继承的类的呢有两种方法:
- 方法一:父类的构造函数私有,子类的构造必须调用父类的构造函数,但是父类的构造函数私有化以后呢,在子类中是不可见也不可调用的。那么 子类就无法实例化出对象,这样就可以达到父类不能被继承(C++98的方法)。
- 方法二: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.继承模型:
继承模型呢分为三种:单继承,多继承和菱形继承
- 单继承 :一个子类只有一个直接父类时称这种继承关系为单继承
- 多继承 :一个子类有两个或以上直接父类时称这个继承关系为多继承
- 菱形继承 :菱形继承属于一个特殊的多继承,

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也因该显示调用。
总结:不要轻易使用菱形继承和写出菱形继承的代码,多继承可以用
五、继承的总结和反思
-
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
-
多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
1.继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种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;
}
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
总结
到这里继承的相关知识小编就基本分享完了咯,如果有什么疑问欢迎大家讨论。那今天就到这里吧。