1.继承关系(is a关系)
1.1概述
简单来说:就是父亲的东西,都会被儿子继承下来。
注意:构造函数,析构函数,拷贝构造函数,拷贝赋值函数不能被子类继承
1.2.继承关系的用意和目的是什么?
老子的特有属性都被儿子继承了下来,所以儿子类中是不是就可以不用再写父类中的属性与方法了。这样就提高了代码的复用性。
同时儿子类中我们还可以添加一些儿子类独有的新特性。这样就提高了代码的拓展性。
所以说继承关系的用意是:代码复用性与高拓展性。就如同这个缤纷复杂的世界,我们可以找到很多的现实的例子。
使用继承关系,需要同学们看待这个世界的事物要上升一个维度,要具有一些抽象的思维。
首先要把这一类的事件进行抽象出来一些共有的属性与特征,把它们定义为父类。然后,再用这个父类,对具有不同特点的子类进行派生。这样就可以派生出各种不同的子类。子类不仅拥有父类的共有的特性,与具备子类独用的特性。这样的代码的复用性与拓展性就会非常灵
2继承关系的分类
2.1单继承的方式:
class + 子类 : 继承方式 + 父类
{
//单继承的方式
};
2.2继承方式:
继承方式有三种:
public:公有继承
protected:受保护继承
private:私有继承
当继承方式与类中访问权限的结合时,类内属性到子类之中的访问权限的改变如图所示:
2.3简单的例子
#include <iostream>
using namespace std;
class Car{
private:
int weight;
public:
Car()
{
cout<<"Car的构造"<<endl;
}
~Car()
{
cout<<"Car的析构"<<endl;
}
void run()
{
cout<<"Car正在行驶过程中"<<endl;
}
int setweight(int weight)
{
this->weight=weight;
return this->weight;
}
};
class Bwm:public Car
{
private:
string logo;
public:
Bwm(string logo,int weigth)
{
this->logo=logo;
this->setweight(weigth);
cout<<"宝马的构造"<<endl;
}
~Bwm()
{
cout<<"宝马的析构"<<endl;
}
};
int main()
{
return 0;
}
2.4单继承关系的内存布局:
子类在定义对象时,先创建子类的空间,然后构造顺序是:先调用父类的构造对父类中的属性完成初始化,然后再调用子类的构造完成对子类属性的初始化。当子类对象被销毁时,析构的顺序是:首先调用子类的析构,然后再调用父类的析构,最后资源就被回收
2.5当使用继承时,如果父类中没有默认构造,需要在子类的初始化列表指定编译器所应该调用父类构造。
#include <iostream>
using namespace std;
class Car{
private:
int weight;
public:
Car(int weigth)
{
this->weight=weight;
cout<<"Car的构造"<<endl;
}
~Car()
{
cout<<"Car的析构"<<endl;
}
void run()
{
cout<<"Car正在行驶过程中"<<endl;
}
int setweight(int weight)
{
this->weight=weight;
return weight;
}
};
class Bwm:public Car
{
private:
string logo;
public:
Bwm(string logo,int weigth):Car(1)
{
this->logo=logo;
this->setweight(weigth);
cout<<"宝马的构造"<<endl;
}
~Bwm()
{
cout<<"宝马的析构"<<endl;
}
};
int main()
{
return 0;
}
2.6当父类中有与子类中的属性或方法同名时,父类中的同名属性或方法,将被自动隐藏在父类的类域之中。
#include <iostream>
using namespace std;
class A{
public:
int a=10;
};
class B:public A
{
public:
int a=20;
};
int main()
{
B b;
cout<<b.a<<endl;
cout<<b.A::a<<endl;
return 0;
}
结果图:
2.7C++中继承关系下的内存布局与类型兼容规则:
is a关系是一种特殊的has a关系:也和包含关系一样,起始地址是一样的,可以通过父类访问到子类。
2.7.1证明起始地址是一样的
#include <iostream>
using namespace std;
class A{
public:
int one=10;
A()
{
cout<<"父类的起始地址"<<this<<endl;
}
};
class B:public A
{
public:
int two=20;
B()
{
cout<<"子类的起始地址"<<this<<endl;
}
};
int main()
{
B b;
return 0;
}
结果图:
2.7.3证明是包含关系的代码(可以通过父类的直接访问到子类的数值)
#include <iostream>
using namespace std;
class A{
public:
int one=10;
};
class B:public A
{
public:
int two=20;
};
int main()
{
A* a=new B;
cout<<a->one<<endl;
cout<<(a+1)->one<<endl;
cout<<static_cast<B*>(a)->two<<endl;
return 0;
}
结果图:
所以在单继承情况下,父类指针与子类指针保持一致,父类指针可以天然且安全指向父类对象。
这就是单继承情况下的类型兼容规则,反之则不可以。
3多继承及棱形继承的相关问题及解决方案
3.1多继承的语法:
class + 子类 : 继承方式 + 父类1,继承方式 + 父类2,继承方式 + 父类3,...
{
//多继承的方式
};
3.2多继承的实例
3.2.1当我们这样写的时候多继承就会出现二义性,如图:
3.2.2而且如果像这样我们只想要power和e的时候我们会继承很多我们不需要的东西,造成代码膨胀的问题
using namespace std;
class Phone{
public:
int power;
int a;
int b;
};
class Competer
{
public:
int c;
int d;
int f;
};
class Notebook:public Phone,public Competer
{
Notebook(int power)
{
this->power=power;
}
};
int main()
{
return 0;
}
3.3解决方案:
一般在使用多继承时,使用多继承多个抽象类,而且实体体。这样就可以避免以上的问题。如果,一定要继承多个实体时,在访问属性或方法时,一定要加上具体的父类的类域,这样也可以避免同名属性或方的二义性的问题,如下。
#include <iostream>
using namespace std;
class Phone{
public:
int power;
};
class Competer
{
public:
int power;
};
class Notebook:public Phone,public Competer
{
Notebook(int power)
{
this->Competer::power=power;
}
};
int main()
{
return 0;
}
4棱形继承及相关问题及解决方案:
4.1棱形继承图:
4.2菱形的缺点
4.2.1如下代码所示,我们用代码来说明问题,总结在结果图处。
#include <iostream>
using namespace std;
class A{
public:
int a;
};
class B:public A
{
public:
};
class C:public A
{
public:
};
class D:public B,public C
{
public:
};
int main()
{
D d;
cout<<sizeof (d)<<endl;
return 0;
}
结果图:
1.我们在代码中只定义了一个代码为int a的内存,内存大小为4,而到了最远的D时,内存大小为了8,这就导致不管父类有多少内存,最远的那个类型接到的内存大小永远是父类的两倍,导致了最远处的类被多次构造。
这两个和多次继承一样。
2.同名属性与方法的二义性的问题。
3.代码膨胀的问题。
4.3解决方法
4.3.1首先我们说一下内部机制
当使用virtual修饰继承权后,继承类中,编译器就会默默安插了一根虚指针,这个虚指针。这两个直接继承类中各有一根虚基表指针,指向一张共有的虚基表。这张虚基表中存在偏移量,通过偏移量就可以找到共有的那个属性。也就是说B 与 C 是共享了一分虚基类。所以A只需要构造一分,B与C就可以虚基表中的偏移找到A中的属性。
4.3.2代码说明
当我们没有用virtual修饰的时候,内存大小在4.2.1中的代码中有说过,也就是最后D的内存大小为8,当我们加上virtual的时候我们再来看看,代码如下
#include <iostream>
using namespace std;
class A{
public:
int a;
};
class B:virtual public A
{
public:
};
class C:virtual public A
{
public:
};
class D:public B,public C
{
public:
};
int main()
{
cout<<sizeof (B)<<endl;
cout<<sizeof (C)<<endl;
cout<<sizeof (D)<<endl;
return 0;
}
结果图:
由结果图来说,我们可以看到B和C的内存大小为16,他们当中各有一个虚指针,大小为8,还有一个共同使用的int a,内存大小为4,因为涉及结构体补齐的问题,所以大小为16,所以D就是继承了两个虚指针,以及B和C共同使用的int a 所以结构体大小为24。
注意:结构体补齐的问题。
4.3.3总结:
在实际工作中,如果使用继承与使用包含关系都可以解决,首选包含关系。
如果单继承与多继承都可以解决,首选单继承。
如果不可避免要要使用多继承,则要多继承多个接口类。
如果不可避免会发发生棱形继承,则要使用虚继承