何为继承
- 仔细想想继承在生活中无处不在,孩子可以通过父类继承父类拥有的财产以及事物。
- 在C++中,继承是面向对象的一种设计方法,是代码服用的最重要的手段,继承允许在保持原有类的基础上进行扩展,增加新功能。
- 在继承体系中,产生的新类叫做子类或者派生类,被继承的类叫做父类或者基类。继承体现了面向对象程序设计的层次结构,继承是类设计层次的复用。
我们可以看一个简单的继承:
class Base {
public:
void testFunc1() {
std::cout << "Base::testFunc1()" << std::endl;
}
public:
int _b;
};
class Derived : public Base {
};
int main()
{
std::cout << sizeof(Base) << std::endl;
std::cout << sizeof(Derived) << std::endl;
system("pause");
return 0;
}
通过打印结果可以看到,派生类中什么都没有,但是却还有四个字节,派生类中这四个字节的大小正是通过继承得来的。
继承定义格式
class Derived : public Base{
};
derived是派生类名称
public是继承方式,表示以那种方式继承
Base是基类名称
与类的访问限定符类似,类的继承也有三种方式,public,protected,private,那继承方式与类的访问限定符有什么关系?
三种继承方式与三种类访问限定符的关系
还是上面的例子,基类中变量是被public限定符修饰,以public继承,我们在派生类中直接访问基类成员
将基类中变量用private限定,修改代码
class Derived : public Base {
public:
void setNum() {
_b = 1;
}
void printNum() {
std::cout << _b << std::endl;
}
};
int main()
{
Derived d;
d.setNum();
d.printNum();
system("pause");
return 0;
}
程序报错,基类private成员无法访问
将基类中变量用protected限定,其他不变
程序正常运行。
通过其余验证我们可以得出结论
继承方式/类成员 | 基类public | 基类protected | 基类private |
---|---|---|---|
public继承 | 派生类public | 派生类protected | 派生类中不可见 |
protected继承 | 派生类protected | 派生类protected | 派生类中不可见 |
private继承 | 派生类private | 派生类private | 派生类中不可见 |
继承中派生类的对象模型
- 派生类继承了基类,那么基类中的数据是如何在派生类中存储的?
class Base {
public:
int _b1;
int _b2;
protected:
int _b3;
int _b4;
};
class Derived : public Base {
public:
int _d1;
int _d2;
};
int main()
{
Derived d;
std::cout << sizeof(d) << std::endl;
d._b1 = 1;
d._b2 = 2;
d._d1 = 3;
d._d2 = 4;
return 0;
}
由内存中变量的存储位置来看,派生类的对象模型是基类的成员变量在上,派生类的成员变量在下,中间无法访问的8个字节是继承自基类中的protected成员。
基类和派生类对象赋值转换
- 由上面的对象模型可以知道,派生类对象可以赋值给基类对象/基类指针/基类引用,原因就是派生类继承自基类,其中有基类的一部分,基类对象,指针引用可以直接拿来用,但是把基类对象赋值给派生类则不行
- 基类的指针可以通过强制类型转换赋值给派生类的指针,但必须是基类的指针指向派生类的对象。
继承中的作用域
- 在继承体系统,基类和派生类都有自己独立的作用域,如果在派生类中有与基类同名的成员,派生类将直接屏蔽父类成员,这种情况叫做隐藏
- 函数隐藏只需要函数同名就可以构成隐藏
class Base {
public:
void testFunc() {
std::cout << "Base::testFunc()" << std::endl;
}
int _a;
};
class Derived : public Base {
public:
void testFunc() {
std::cout << "Derived::testFunc()" << std::endl;
}
int _a;
};
int main()
{
Derived d;
d._a = 1;
d.Base::_a = 2;
d.testFunc();
d.Base::testFunc();
system("pause");
return 0;
}
直接成员名就会访问派生类中的:
加上作用域限定符之后可以访问基类中的成员:
派生类的默认成员函数
派生类的构造函数必须调用基类的构造函数来初始化属于基类的一部分成员,如果基类没有默认的构造函数可用,必须在派生类构造函数初始化列表阶段显式调用。
派生的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化
派生类的operator=必须调用基类的operator=完成基类的赋值
派生类的析构函数在调用完成之后自动调用基类的析构函数清理基类成员。
派生类初始化先调用基类构造在调用派生类构造
派生类对象析构先清理再调用基类析构
如何实现一个不能被继承的类?
- 要实现很简单,派生类需要调用基类拷贝构造函数,只要在基类中将构造函数私有化,提供一个静态方法即可。
class noInherit{
public:
static noInherit getInstance() {
return noInherit;
}
private:
noInherit() {}
};
在C++11中给出了新的关键字final,被final修饰的类不可以被继承。
多级继承
- 单继承:一个子类只有一个直接父类的继承关系称为单继承
- 多继承:一个子类有两个或者以上直接父类的继承关系称作多继承
之前看的都是单继承。那么多继承中派生类的对象模型又是怎么样的。
class Base1 {
public:
void testFunc() {
std::cout << "Base1::testFunc()" << std::endl;
}
int _b1;
};
class Base2 {
public:
void testFunc() {
std::cout << "Base2::testFunc()" << std::endl;
}
int _b2;
};
class Derived : public Base1, public Base2 {
public:
void testFunc() {
std::cout << "Derived::testFunc()" << std::endl;
}
int _d;
};
int main()
{
Derived d;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
system("pause");
return 0;
}
在派生类模型中是按基类继承的声明顺序来依次来存放基类的。
菱形继承
菱形继承带就是代代遗传,每一个派生类都继承了基类的数据以及基类的基类的数据,一层一层下去就会导致在最终派生类中的数据庞大到了一个无发想象的地步,更会导致数据的二义性。
class Base {
public:
void testFunc() {
std::cout << "Base::testFunc()" << std::endl;
}
int _b;
};
class Derived1 : public Base{
public:
void testFun() {
std::cout << "Derived1::testFunc()" << std::endl;
}
int _d1;
};
class Derived2 : public Base {
public:
void testFun() {
std::cout << "Derived2::testFunc()" << std::endl;
}
int _d2;
};
class Derived : public Derived1, public Derived2 {
public:
void testFun() {
std::cout << "Derived::testFunc()" << std::endl;
}
int _d;
};
int main()
{
Derived d;
std::cout << sizeof(d) << std::endl;
d._d1 = 1;
d._b = 2;
d._d2 = 3;
d._b = 4;
d._d = 5;
system("pause");
return 0;
}
如果直接访问,会直接报错对基类中成员的访问不明确。
对程序做出修改,对基类成员加上作用域限定符,程序运行
可以看到对于Base类中整形变量_b在Derived类中存了两份,分别是两个中间派生类继承而来。这样下去越来越多的继承会导致最高基类的数据在最终派生类中存在很多份,C++为了解决数据的二义性问题引入了虚拟继承。
虚拟继承
虚拟继承只需要在继承方式前加上virtual
关键字即可。
我们先来看一个单虚拟继承
class Base {
public:
int _b;
};
class Derived : virtual public Base{
};
int main()
{
Derived d;
std::cout << sizeof(d) << std::endl;
d._b = 1;
system("pause");
return 0;
}
按理说派生类中只有继承自基类的一个整形变量而已,基类的大小为4个字节,我们可以看看结果。
派生类的大小是8个字节,比之前猜测的4个字节多出了四个字节。
查看内存窗口,观察多出来的4个字节是什么。
查看反汇编发现在调用构造函数之前给对象d中压入了一个地址。
在给基类对象赋值时先将对象d中的数据取出来,然后将其加4将其中的内容赋给ecx,最后把变量1赋值给d[ecx],所以我们猜测多出来的四个字节的指针指向的地址存放着基类对象数据相对于派生类的相对偏移量
菱形虚拟继承
class Base {
public:
int _b1;
int _b2;
};
class Derived1 : virtual public Base {
public:
int _d1;
};
class Derived2 : virtual public Base {
public:
int _d2;
};
class Derived : public Derived1, public Derived2 {
public:
int _d;
};
int main()
{
Derived d;
std::cout << sizeof(d) << std::endl;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
d._d1 = 4;
d._d2 = 5;
d._b1 = 6;
d._b2 = 7;
system("pause");
return 0;
}
查看内存,在没有给变量赋值之前,有两个指针,是两个虚拟继承的派生类继承下来的。
给派生类中的变量赋值,再根据之前的猜测,得出结论。虚拟继承中多出来的4个字节存放一个指针,指向一张表,这个表就是虚基表,这个多出来的指针就是虚基表指针,虚基表中存放了基类在派生类中的偏移量,通过表中偏移量可以找到基类
所以我们可以写出菱形虚拟继承中派生类的对象模型