什么是继承
继承机制是使代码可以复用的手段、它在保持原有类特性的基础上进行拓展、这样产生的新类叫派生类。
继承的主要目的是为了代码复用和实现层次化的类结构。
当一个类继承自另一个类时,子类自动获得了父类的属性和方法。子类可以:
- 使用从基类继承来的属性和方法,不需要重复编写相同的代码。
- 覆盖或重写基类中的方法,以提供特定于子类的行为。
- 扩展基类的功能,增加新的属性和方法。
继承方式
C++中的继承方式主要有三种:公有继承(public),保护继承(protected)和私有继承(private)。它们定义了基类成员在派生类中的访问权限。
- 公有继承(Public Inheritance):这是最常用的继承方式。通过公有继承,基类的公有成员和保护成员在派生类中保持其原有的访问级别,而基类的私有成员不能直接被派生类访问,但可以通过基类提供的公有或保护成员函数访问。公有继承表示“是一个”关系,即派生类是一种特殊的基类。
- 保护继承(Protected Inheritance):通过保护继承,基类的公有成员和保护成员在派生类中都变为保护成员。这意味着这些成员在派生类中不能像公有成员那样自由访问,但可以被进一步派生的类访问。私有成员仍然保持私有。保护继承不常用,它模糊了“是一个”和“拥有一个”的关系。
- 私有继承(Private Inheritance):通过私有继承,基类的公有和保护成员在派生类中都变为私有成员。这意味着这些成员只能被派生类自己访问,而不能被派生类的对象或进一步派生的类访问。私有继承表示“拥有一个”的关系,即派生类拥有一个(私有的)基类。
默认继承
在C++中,如果在定义派生类时没有显式指定继承方式(即没有使用public,protected或private关键字),那么:
- 对于类(class),C++默认采取私有继承(private)。
- 对于结构体(struct),C++默认采取公有继承(public)。
继承后的成员权限
-
公有继承(Public):
- 基类的公有成员成为派生类的公有成员。
- 基类的保护成员成为派生类的保护成员。
- 基类的私有成员不能被派生类直接访问。
-
保护继承(Protected):
- 基类的公有成员和保护成员都变为派生类的保护成员。
- 基类的私有成员不能被派生类直接访问。
-
私有继承(Private):
- 基类的公有成员和保护成员都变为派生类的私有成员。
- 基类的私有成员不能被派生类直接访问。
子类到父类对象之间的赋值兼容转换
在C++中,子类到父类对象之间的赋值兼容转换是指子类对象可以被赋值给父类的引用或指针,这是基于C++的多态性特征。这种转换遵循以下规则:
1.通过指针或引用的赋值:子类的对象可以被赋值给指向父类的指针或引用,而不需要任何显示的类型转换。这种方式是多态性的基础,允许通过父类的指针或引用来调用在子类中重写的方法。
class Base {};
class Derived : public Base {};
Derived d;
Base *bp = &d; // 允许,子类指针赋值给父类指针
Base &br = d; // 允许,子类引用赋值给父类引用
2.通过值的赋值:如果尝试将子类的对象通过值的方式赋给父类的对象,这会触发对象切片(Object Slicing)问题。对象切片是指在这种赋值过程中,只有父类部分的成员被复制,子类特有的部分会被切除,这会导致信息的丢失。
class Base {
public:
int baseMember;
};
class Derived : public Base {
public:
int derivedMember;
};
Derived d;
Base b = d; // 赋值操作导致派生类特有的“derivedMember”成员丢失,即发生对象切片
为了避免对象切片,建议总是通过指针或引用来实现子类到父类的赋值或传递。这样不仅保留了对象的完整性,还支持多态性,使得程序能够利用虚函数机制根据对象的实际类型来调用相应的成员函数。
父类到子类对象之间的转换:
基类到子类的转换在C++中被称为向下转型(downcasting)。这种转换不如子类到基类的转换(向上转型)自然和安全。通常,这种转换是不能隐式进行的,因为它可能不安全或逻辑上不合理。 如果你确信某个基类对象实际上是一个特定的派生类对象,你可以使用显式的类型转换操作进行向下转型。
作用域
在C++的继承体系中,作用域的概念进一步扩展,涉及到了基类与派生类之间名字的可见性和访问规则。在这种体系中,理解不同类型的继承(公有继承、保护继承、私有继承)以及它们如何影响成员的访问权限是关键。
上文不同的继承方式已经提过。
在C++的继承体系中,涉及到作用域的特性还包括:
-
名字隐藏:如果派生类中声明了一个与基类中同名的成员(不考虑参数列表),那么派生类中的该成员会隐藏基类中所有同名成员的所有重载版本。即使派生类中的成员和基类中的成员参数列表不同,也会发生隐藏。
-
使用作用域解析运算符(::):可以通过基类的名字加上作用域解析运算符来访问基类中被派生类隐藏的成员。
-
虚函数和多态:在继承体系中,通过使用虚函数可以实现多态。在派生类中重写基类的虚函数时,即使函数签名完全相同,也不会被视为名字隐藏。
要注意的是父类和子类的同名函数不构成函数重载,因为函数重载要求两个函数在同一个作用域。
派生类中的默认成员函数
- 派生类在调用构造函数时,会先自动调用基类的构造函数,初始化基类的成员,如果基类没有默认构造,则必须在派生类的初始化列表当中显式调用基类的构造
- 派生类的拷贝构造必须调用基类的拷贝构造完成基类成员的拷构造
- 派生类的拷贝赋值必须调用基类的拷贝赋值完成基类成员的拷赋值
- 派生类的析构先被调用,完成后会自动调用基类的析构清理基类成员
需要注意:
编译器会对析构函数名进行特殊处理,处理成destructor(),基类析构函数需要加上virtual 声明为虚函数。
在派生类的拷贝构造和拷贝赋值调用基类的拷贝构造和拷贝赋值的传参方式是一个切片,都是将派生类对象直接赋值给基类。
继承与友元
友元不能继承:基类的友元可以访问基类的私有保护成员,但是不能访问派生类的私有保护。
继承与static
基类中定义了一个static成员,在整个继承体系中都只有一个该成员
多继承 ---- 菱形继承
菱形继承的问题是什么?如何解决?
问题:(数据冗余和二义性)
菱形继承的主要问题是导致了基类的多重实例。具体来说,如果基类中有数据成员,那么在最终派生类的对象中,这个数据成员会存在多次拷贝,造成资源浪费和访问混乱。此外,如果基类的成员函数在多个派生路径中被覆盖,可能会导致调用时的二义性。
解决方案:
C++提供了虚继承(virtual inheritance
)来解决菱形继承问题。通过使派生类虚拟地继承其基类,可以确保在继承层次结构中只有一个基类的实例。
class Base {
public:
int data;
};
class Derived1 : virtual public Base {
// ...
};
class Derived2 : virtual public Base {
// ...
};
class FinalDerived : public Derived1, public Derived2 {
// 可以访问Base类的成员,且确保只有一个Base类的实例
};
解决具体细节:
A // 基类
B C // B 和 C 都继承了A
D // D继承了 B 和 C
使用virtual 在腰部修饰继承关系,是重复的成员值存储一份,通过虚表指针和虚基表找到公共虚基类,从而解决了问题。
D对象中将A放到了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这个表叫做虚基表。虚基表中存储了偏移量,通过偏移量可以找到下面的A。
虚继承使用起来虽然可以有效解决菱形继承的问题,但它会使得类的结构更加复杂,并可能引入额外的性能开销。因此,在设计类的继承结构时,应当谨慎使用,并尽量避免出现菱形继承的情况。如果可能的话,最佳的策略是避免菱形继承的发生。
继承和组合
继承 is - a
public 继承,每一个派生类对象都是一个基类对象
组合has - a
组合, B组合了A,每个B对象中都有一个A对象
组合类(内部类)中的protected成员不能使用,只能使用public的成员
总结
在实际的编程实践中,“优先使用组合而非继承”是一个通常的设计原则。这种做法能够最小化类之间的耦合,提高代码的复用性和可维护性。当然,这并不是说继承是有害的,恰当的使用继承能够带来很多便利。但是我们需要清楚,在什么情况下使用继承,什么情况下使用组合,这对于我们来说是非常重要的。