继承
1、继承基础
概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,在保持原有类特性的基础上进行扩展,增加功能,产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
格式
class Student : public/protected/private Person {};
继承关系 不同的继承方式,基类的访问权限在派生类的访问权限遵循如下规则:
访问的方式 = Min{成员在基类中的访问限定符,继承方式},遵循 public > protected > private
基础总结:
-
基类的 private 成员总在派生类中不可见-- private成员被继承到了派生类对象中,但是语法限制派生类对象在类外和类内都不能访问
-
基类的 protected 成员被继承到派生类对象中,但是只能在派生类的类内进行访问,不能在类外访问
-
class 默认继承方式为 private,struct 默认继承方式为 public
-
建议使用 public 继承
2、基类对象和派生类对象赋值转换(赋值兼容规则)---
单继承对象模型:
-
切片(切割):派生类对象可以赋值给基类的对象/ 引用/ 指针---内存截断(基类对象可以代替派生类对象、基类指针可以指向派生类对象、基类引用可以引用派生类对象)
-
基类对象不能赋值给派生类对象
-
基类指针可以通过强制类型转换赋值给派生类的指针(使用内存层次),能通过编译,但是通过派生类指针访问修改派生类的数据成员时,运行时报错(内存越界)
3、继承中的作用域
-
继承体系中基类和派生类都有自己独立的作用域
-
同名隐藏(重定义):派生类成员会屏蔽基类同名成员的访问,可以使用 基类 :: 基类成员进行显示访问基类的成员
-
成员函数只要同名就构成隐藏
-
不建议在继承体系中使用同名成员
4、派生类的默认成员函数
-
派生类的构造函数会调用基类的构造,对派生类中所继承的基类成员进行初始化。如果基类没有默认构造函数,就必须在派生类构造函数中的初始化列表阶段显式调用
-
派生类的拷贝构造函数会调用基类的拷贝构造函数,对派生类中所继承的基类成员完成拷贝初始化
-
派生类的 operate=() 会调用基类的 operate=() 完成派生类中所继承的基类成员复制
-
派生类的析构函数会在调用结束后自动调用基类的析构函数,对派生类中所继承的基类成员进行清理工作
-
派生类对象初始化先调用基类的构造再调用派生类的构造
-
派生类对象析构清理资源先调用派生类的析构在调用基类的析构
不能被继承的类:
// 1. 将基类的构造函数设置为 private 属性,派生类中不可见
class Base {
private:
Base ()
{}
};
class Derived : public Base {
}; // 报错,无法调用基类的私有构造函数完成派生类中基类部分的成员初始化
// 2. c++11提供 final 关键字,表示该类不能被继承
class Base final
{};
5、继承与友元
友元关系不能继承,基类友元不能访问派生类的私有和保护成员
class Base {
public:
friend ostream& operator<< (ostream& _cout, const Base& b);
friend void TestFunc();
};
class Derived : public Base {
private:
int _d;
};
// 将基类的 << 输出运算符重载设置为友元函数
ostream& operator<<(ostream& _cout, const Base& b) {
_cout << b._b << endl;
return _cout;
}
void TestFunc() {
Derived d(10, 20);
d._d;
}
int main() {
Base b(3);
cout << b;
Derived d(10, 20);
cout << d << endl; // 仍然可以打印,因为基类引用可以引用派生类对象,结果是打印出来派生类中属于基类的一部分数据
return 0;
}
6、继承与静态成员
基类的静态成员在整个继承体系中都只有一个实例
class Base {
public:
Base(const int b = 0)
: _b(b)
{
++_count;
cout << "Base:: Base" << endl;
}
Base(const Base& b)
: _b(b._b)
{
++_count;
}
~Base()
{
--_count;
cout << "Base:: ~Base" << endl;
}
private:
int _b;
public:
static int _count;
};
int Base::_count = 0;
class Derived :public Base {
private:
int _d;
};
class Derived1 :public Base {
private:
int _d1;
};
class Derived2 :public Base {
private:
int _d2;
};
int main() {
Base b;
Derived::_count = 0;
Derived1::_count = 10;
Derived2::_count = 20;
cout << &b._count << endl;
cout << &Derived::_count << endl;
cout << &Derived1::_count << endl;
cout << &Derived2::_count << endl;
// 结果相同,整个继承体系中只有唯一的一份 _count 静态成员
return 0;
}
7、几种不同继承方式的对象模型
单继承:派生类只有一个基类的继承关系
多继承:派生类具有多个基类的继承关系
多继承体系下,派生类的对象模型与多继承的先后次序有关
class B1 {
public:
int _b1;
};
class B2 {
public:
int _b2;
};
class D: public B1, public B2 {
public:
int _d;
};
int main() {
D d;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
// 对象模型中继承部分的次序由继承方式给出
return 0;
}
菱形继承---钻石继承
菱形继承的二义性问题: 基类 Base 中的成员,在 C1 和 C2 中各有一份,而 Deriverd 同时继承了 C1 和 C2,因此,Deriverd 中含有两份 Base 中的成员,使用 Deriverd 对象直接访问 Base 中的成员,将产生二义性问题
class Base {
public:
int _b;
void TestFunc() {
cout << "测试菱形继承的二义性问题" << this << endl;
}
};
class C1 : public Base {
public:
int _c1;
};
class C2 : public Base {
public:
int _c2;
};
class Deriverd : public C1, public C2 {
public:
int _d;
};
int main() {
Deriverd d;
//d._b = 1; //对“_b”的访问不明确
d.C1::_b = 1;
d._c1 = 2;
d.C2::_b = 3;
d._c2 = 4;
d._d = 10;
cout << sizeof(Deriverd) << endl;
d.C1::TestFunc(); // 对“TestFunc”的访问不明确
d.C2::TestFunc(); // 对“TestFunc”的访问不明确
// 两个 Func 打印的 this 指针结果不同,表示 Base 的每个函数都被继承了一份
return 0;
}
普通单继承的对象模型:
Base::_b + Deriverd::_d
class Base {
public:
int _b;
};
class Deriverd :public Base {
public:
int _d;
};
int main() {
Deriverd d;
d._b = 1;
d._d = 2;
cout << sizeof(Deriverd) << endl;
return 0;
}
虚拟继承的对象模型:
虚继承体系下,前四个字节位虚基表指针---偏移量表格指针,最后面是继承的部分
偏移量表格:
前四个字节:相对于自己的偏移量
后四个字节:相对于基类部分的偏移量
派生类对象的虚拟继承部分的成员访问方式:
1.取对象前 4 个字节的内容(虚基表指针)
2.通过虚基表指针从偏移量表格中获取基类成员起始位置相对于派生类对象起始位置的偏移量
3.通过偏移量在派生类对象中找到基类的成员并访问/修改
派生类对象本身的成员访问方式:
直接进行访问/修改
class Base {
public:
int _b;
};
class Deriverd : virtual public Base {
public:
int _d;
};
int main() {
Deriverd d;
d._b = 1;
d._d = 2;
cout << sizeof(Deriverd) << endl; // 12个字节,比普通的单继承多了4个字节
return 0;
}
对比虚拟继承与普通单继承:
1.派生类对象的大小不同:虚拟继承的派生类对象比普通单继承的对象多四个字节---虚基表指针
2.对象模型不同:普通单继承的对象模型为 Base::_b + Deriverd::_d,虚拟继承的对象模型为 虚基表指针 + Deriverd::_d + Base::_b
3.普通单继承的派生类对象不会生成默认构造函数,虚拟继承的派生类对象会生成默认的构造函数,并且多余空间(即对象成员的偏移量表格指针)在默认构造中进行初始化
菱形虚拟继承---解决菱形继承的二义性问题
菱形虚拟继承的对象模型:
C1::虚基表指针 + C1::_c1 + C2::虚基表指针 + C2::_c2 + Deriverd::_d + Base::_b
class Base {
public:
int _b;
void TestFunc() {
cout << "使用菱形虚拟继承解决菱形继承的二义性问题" << this << endl;
}
};
class C1 : virtual public Base {
public:
int _c1;
};
class C2 : virtual public Base {
public:
int _c2;
};
class Deriverd : public C1, public C2 {
public:
int _d;
};
int main() {
Deriverd d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 10;
cout << sizeof(Deriverd) << endl; // 24字节
d.TestFunc();
d.TestFunc();
// 两个 Func 结果相同
return 0;
}
// 可以通过 C1 或 C2 对象引用派生类 Deriverd 对象,从而改变派生类对象所继承到的 Base 的成员 _b
C1 c1; c1._d = 5;
C2 c2; c2._d = 30;