继承
--前人栽树,后人乘凉
继承
(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手
段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样
产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现
了由简单到复杂的认知过程。简单来说,继承就是在获取“父辈”东西的基础上,有选择的增添自己的东西。
比如手机功能的更迭:
继承的格式
继承的方式:
总结:
- 基类private成员在派生类中是不能被访问的,如果基类成员不想在
类外直接被访问,但需要在派生类中能访问,就定义为protected。
可以看出保护成员限定符是因继承才出现的 - public继承是一个接口继承,保持is-a原则,每个父类可用的成员对
子类也可用,因为每个子类对象也都是一个父类对象 - protected/private继承是一个实现继承,基类的部分成员并非完全
成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下
不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继
承。私有继承意味着is-implemented-in-terms-of(是根据……实
现的)。通常比组合(composition)更低级,但当一个派生类需要访
问基类保护成员或需要重定义基类的虚函数时它就是合理的 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保
护成员,基类的私有成员存在但是在子类中不可见(不能访问)使用关键字class时默认的继承方式是private,使用struct时默认的
,不过最好显示的写出继承方式
继承方式是public- 在实际运用中一般使用都是public继承,极少场景下才会使用
protetced/private继承
派生类默认成员函数
继承体系下,派生类中如果没有显示定义这六个成员函数,编译器则会合
成这六个默认的成员函数
【派生类对象的构造与析构】
继承的分类
这里只把<有无重写>放到虚拟继承不够准确,因为重写的要求如下:
1. 基类中被重写的函数必须为虚函数(先知道,就是在函数前面加虚拟virtual关键字)
2. 基类和派生类中的虚函数的原型的原型必须保持一致(返回值类型,函数名(参数列表)),即完全一样。那么普通继承里,自然也可以有重写。
class father
{
public:
virtual void func1()
{
cout<<"father::func1()"<<endl;
}
private:
int _a;
};
class child:public father
{
public:
//子类重写了父类的func1函数
virtual void func1()
{
cout<<"child::func1()"<<endl;
}
private:
int _b;
};
所以①②两个分类之间的继承方式,可以组合,比如有一下组合方式:
普通单继承无重写、普通单继承有重写、虚拟单继承有重写、虚拟单继承无重写、
普通多继承无重写、普通多继承有重写、虚拟多继承有重写、虚拟多继承无重写
先来说下普通继承吧:
class father
{
public:
father()
{
_a=10;
}
void func1()
{
cout<<"father::func1()"<<endl;
}
private:
int _a;
};
class child:public father
{
public:
child()
{
_b=20;
}
void func2()
{
cout<<"child::func2()"<<endl;
}
private:
int _b;
};
void test()
{
father a;
child b;
cout<<"sizeof(father):"<<sizeof(father)<<endl;
cout<<"sizeof(child):"<<sizeof(child)<<endl;
}
int main()
{
test();
return 0;
}
运行结果:
可见子类继承了父类的成员,再加上自己的成员变量,一共8字节。
而如果将成员函数改成虚函数:
class father
{
public:
father()
{
_a=10;
}
virtual void func1()
{
cout<<"father::func1()"<<endl;
}
private:
int _a;
};
class child:public father
{
public:
child()
{
_b=20;
}
virtual void func2()
{
cout<<"child::func2()"<<endl;
}
private:
int _b;
};
void test()
{
father a;
child b;
cout<<"sizeof(father):"<<sizeof(father)<<endl;
cout<<"sizeof(child):"<<sizeof(child)<<endl;
}
int main()
{
test();
return 0;
}
运行结果:
父类和子类都多了4个字节,查看内存:
接下来,介绍一位关键人物:“菱形继承”
它包含了单继承和多继承
class B
{
public:
int _b;
};
class C1:public B
{
public:
int _c1;
};
class C2 :public B
{
public:
int _c2;
};
class D : public C1, public C2
{
public:
int _d;
};
void test1()
{
D d;
d._b=1; //<--------
d._c1=3;
d._c2=4;
d._d=5;
}
int main()
{
test1();
return 0;
}
这时候会报错:
类D继承自类C1和类C2,而C1类和C2类都继承自类B,则类D中会两次继承B。而直接对_b赋值,编译器会不知道是给从C1中继承的_b赋值,还是给从C2中继承的_b赋值。
所以,为了解决这个问题,有如下做法
void test1()
{
D d;
d.C1::_b=1; //在变量前面加上作用域限定
d.C2::_b=2; //明确访问的变量
d._c1=3;
d._c2=4;
d._d=5;
}
而更关键的是,为了节省空间,同时解决上面继承中的二义性的问题,我们可以将C1、C2对B的继承定义为虚拟继承,而B就成了虚拟基类。
关于虚拟继承,我参考的是这篇博客:【C++】简单介绍虚拟继承
虚拟继承与普通继承的区别:
1.书写形式上:虚拟继承要加虚拟关键字virtual
2.对象模型区别:虚拟继承要多四个字节空间,多的四个字节为偏移量表格的地址
如下,“菱形虚拟继承”:
class B
{
public:
virtual void Funtest1()
{
cout << "B::Funtest1()" << endl;
}
virtual void Funtest2()
{
cout << "B::Funtest2()" << endl;
}
public:
int _b;
};
class C1:virtual public B
{
public:
virtual void Funtest1()
{
cout << "B::Funtest1()" << endl;
}
virtual void Funtest3()
{
cout << "C1::Funtest3()" << endl;
}
public:
int _c1;
};
class C2 :virtual public B
{
public:
virtual void Funtest2()
{
cout << "B::Funtest2()" << endl;
}
virtual void Funtest4()
{
cout << "C2::Funtest4()" << endl;
}
public:
int _c2;
};
class D : public C1, public C2
{
virtual void Funtest8()
{}
virtual void Funtest7()
{}
public:
int _d;
};
插叙:
关于虚表的生成:
- 基类虚表的生成:
将类中的虚函数按照在类中的声明的先后次序添加到虚函数表中 - <单继承–派生类 >
如果派生类重写了基类的某个虚函数,使用派生类自己的虚函数,替换派生类虚表中相同偏移量位置
基类虚函数 ,然后再按照派生类特有的虚函数的声明次序,将其增加到虚函数表的最后(有些绕,仔细理解下)。 - 特别注意:<多继承> 将派生类新增加的虚函数放置到第一张虚表的最后
这样会方便调用(见下菱形虚拟继承的对象模型)
虚函数的调用原理:
- 从对象的前4个字节取虚表的地址
- 传递this指针
- 从虚表中取对应的虚函数 :虚表地址 + 偏移量
- 执行当前虚函数
正题:
菱形虚拟继承的内存分析:
菱形虚拟继承的对象模型:
可以看到,最后只继承了一次基类B,对成员变量_b的访问也明确了。