文章目录
一、继承的概念和定义
1.1 继承的概念:
- 实现代码复用,实现多态的必要条件
- 保持原有类特性的基础上进行扩展,增加新功能
- 呈现了面向对象设计的层次结构
继承之后:成员函数和成员变量都被继承了,sizeof(子类) = sizeof(父类) + 子类新增
1.2 继承权限
小结:
- 基类 private 修饰的成员在子类中是不可见的,不可见:尽管继承到了子类中,但是语法规定不能访问
- 基类中 protect 修饰的成员,在子类中可以访问,但是类外不能访问,保护成员限定符是因继承才出现的
- 这个继承权限是针对类外的访问,派生类中还是可以访问基类中public 和 protected 成员的
C++中 calss 和 struct 的区别:
1.默认访问权限
2.默认继承权限
3.定义一个模板 template< class T >
二、继承规则
2.1 继承中的对象模型
class A
{
public:
int a = 1;
};
class B : public A
{
public:
int b = 2;
};
对象模型(子类对象的内存分布):
赋值兼容规则:
- 基类对象可以用子类对象赋值
- 基类的指针或者引用,可以指向子类对象
- 子类指针可以指向,通过强转的基类对象,但使用时可能造成越界,因为指针是按子类解析的
2.2 继承中的作用域
前题:
- 在继承体系中基类和派生类的作用域是独立的
- 基类和派生中同名的成员,会发生同名隐藏,也就是基类的成员不会被访问到。注意:这里并不是函数重载,因为作用域都不同
- 可以通过 基类:基类成员 或者 子类对象.基类::基类成员 访问基类对象
2.3 派生类的默认成员函数
派生的构造,析构,拷贝构造函数,赋值重载的规则:
- 基类没有显示定义构造函数 或者说 重载函数是无参或全缺省的:则派生类也不用显示定义构造函数,但编译器会生成一份默认的构造
- 基类的构造带有参数(不是全缺省):则派生的构造必须显示定义出来,并且要在初始化列表的位置调用基类的构造函数
- 派生的拷贝构造:必须调用基类的拷贝构造
- 派生的赋值运算符重载:必须调用基类的赋值重载
- 派生的析构:在调用完派生的析构函数之后,编译器会自动调用基类的析构函数
class A
{
public:
int a_;
A(int a)
:a_(a)
{}
A(const A& temp)
:a_(temp.a_)
{}
A& operator=(const A& temp)
{
if (this != &temp)
{
a_ = temp.a_;
}
return *this;
}
~A()
{
cout << "A析构" << endl;
}
};
class B : public A
{
public:
int b_;
// 派生的构造必须显示定义,并且在初始化列表中调用基类的构造
B(int a, int b)
:A(a)
,b_(b)
{}
// 拷贝构造必须在初始化列表调用基类拷贝构造
B(const B& temp)
:A(temp)
,b_(temp.b_)
{}
// 赋值运算符重载
B& operator=(const B& temp)
{
if (this != &temp)
{
A::operator=(temp);
b_ = temp.b_;
}
return *this;
}
~B()
{
cout << "B析构" << endl;
}
};
其他性质:
- 友元特性不能被继承:基类的友元函数无法调用派生类的成员
- static 修饰的成员在基类和派生都表示同一地址,也就是说静态成员在整个继承体系中只有一份
三、多继承和菱形继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
3.1 多继承对象模型
class A
{
public:
int a_ = 1;
};
class B
{
public:
int b_ = 2;
};
class C :public A, public B
{
public:
int c_ = 3;
};
多继承规则:
- 多继承下,每个基类名前都要加权限,否则编译器默认给private 权限
- 继承的顺序决定了,不同基类的成员在派生中的位置,也就是对象模型中的位置
3.2 菱形继承
这样有两个问题:
- 数据冗余,基类A中的成员有两份
- 二义性,派生类C的对象调用 a 时会产生二义性,例
c.a_ = 10;
过不了编译
虚拟继承可以解决菱形继承的二义性和数据冗余的问题,但需要注意的是,虚拟继承不要在其他的地方使用
虚拟继承:
汇编中的显示:
这里是通过了 B1 和 B2 的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量,通过偏移量可以找到下面的A。
class A
{
public:
A()
:a_(1)
{}
int a_;
};
class B1 : virtual public A
{
public:
B1()
:b1_(2)
{}
int b1_;
};
class B2 : virtual public A
{
public:
B2()
:b2_(3)
{}
int b2_;
};
class C : public B1, public B2
{
public:
C()
:c_(1)
{}
int c_;
};
虚拟继承下的赋值方式:
- 先给基类成员赋值:根据虚基表中相对于子类对象起始位置的偏移量找到基类成员地址,并赋值
- 给B1类成员赋值,根据继承的顺序决定赋值顺序
- 给B2类成员赋值
- 最后给基类成员赋值
注意:编译器会给派生类C生成一份构造函数,如果用户定义出了构造函数,则会对构造函数进行改造,这样做的目的是:给前四个字节填充数据
四、继承和组合
- public 继承是一种 is-a 的关系,也就是每个派生对象都是基类对象
- 组合是一种 has-a 的关系,假设B组合了A,则每个对象B中都有一个对象A
继承被称为:白箱复用,也就是基类内部细节对子类可见,在一定程度上破坏了封装性,并且基类和派生的耦合度很高
对象组合被称为:黑箱复用,优先使用对象组合有助于类的封装,并且耦合度低,代码维护性好
面试题:
什么是菱形继承?菱形继承存在什么问题?
什么是菱形虚拟继承?如何解决数据冗余和二义性?
继承和组合的区别?什么时候用继承?什么时候用组合?