一、引言
在面向对象编程(OOP)的世界中,继承是一个核心概念,它允许我们定义一个类(称为基类或父类)作为另一个类(称为派生类或子类)的基础。通过继承,子类可以继承基类的属性和方法,同时还可以添加或覆盖基类的某些部分,从而实现代码的复用和扩展。
二、继承的基本概念
定义继承
在C++中,继承是通过冒号(:)和访问修饰符(如public
、protected
、private
)来定义的。子类通过继承可以获取基类的公有和保护成员,但无法直接访问基类的私有成员。
继承的分类
- 公有继承(public inheritance):基类的公有和保护成员在派生类中保持其访问属性不变,而基类的私有成员在派生类中仍然不可访问。
- 私有继承(private inheritance):基类的公有和保护成员在派生类中变为私有成员,基类的私有成员在派生类中仍然不可访问。
- 保护继承(protected inheritance):与私有继承类似,但基类的公有和保护成员在派生类中变为保护成员。
继承的作用域和访问权限
在派生类中,基类的成员可以通过作用域解析运算符(::)来访问。同时,派生类可以添加新的成员,这些成员与基类的成员在作用域上是分开的。
三、继承的实现
编写基类和派生类的基本结构
cpp
class Base {
public:
void foo() { /* ... */ }
protected:
int protectedVar;
};
class Derived : public Base {
public:
void bar() { foo(); /* 访问基类的公有成员 */ }
void accessProtected() { protectedVar = 10; /* 访问基类的保护成员 */ }
};
演示如何在派生类中访问基类的公有和保护成员
如上面的示例所示,派生类可以通过直接调用或访问基类的公有和保护成员。
讨论派生类如何覆盖(重写)基类的虚函数
当基类中的函数被声明为虚函数时,派生类可以提供一个与基类函数同名、同参数列表、同返回类型的函数来覆盖它。这样,当通过基类指针或引用调用该函数时,将执行派生类中的版本(即多态性)。
cpp
class Base {
public:
virtual void print() { std::cout << "Base" << std::endl; }
};
class Derived : public Base {
public:
void print() override { std::cout << "Derived" << std::endl; }
};
四、继承中的构造函数和析构函数
构造函数的调用顺序
基类的构造函数在派生类的构造函数之前被调用。如果基类有多个构造函数,派生类的构造函数可以通过初始化列表来指定要调用的基类构造函数。
析构函数的调用顺序
派生类的析构函数在基类的析构函数之后被调用。这确保了派生类中的资源在基类之前被正确释放。
五、虚函数和多态性
虚函数的定义和作用
虚函数是基类中声明为virtual
的成员函数。它允许派生类覆盖该函数并提供自己的实现。通过基类指针或引用调用虚函数时,将执行与指针或引用实际指向的对象类型相对应的版本(即动态绑定)。
演示如何通过虚函数实现多态性
cpp
class Base {
public:
virtual void print() { std::cout << "Base" << std::endl; }
};
class Derived : public Base {
public:
void print() override { std::cout << "Derived" << std::endl; }
};
抽象基类和纯虚函数的概念
抽象基类是至少包含一个纯虚函数的类。纯虚函数是在声明时使用= 0
语法的虚函数。抽象基类不能被实例化,只能作为其他类的基类使用。派生类必须为纯虚函数提供实现,除非派生类本身也是抽象类。
六、继承与组合(聚合)的比较
继承和组合的定义
- 继承:子类继承基类的属性和方法。
- 组合(聚合):一个类的对象包含另一个类的对象作为其成员。
继承和组合的使用场景
- 继承:适用于"是"(is-a)关系,即子类是基类的一种特殊类型。
- 组合:适用于"有"(has-a)关系,即一个类包含另一个类的对象作为其部分。
继承和组合的优缺点比较
- 继承:优点包括代码复用、易于扩展;缺点包括可能导致类层次结构过于复杂、破坏封装性等。
- 组合:优点包括降低类之间的耦合度、提高代码的可维护性和可重用性;缺点包括可能导致更多的内存开销和复杂性增加等。
七、多重继承与菱形继承
多重继承的概念和语法
多重继承允许一个类同时从多个基类中继承。这在某些情况下可以提供更大的灵活性,但也可能导致更复杂的问题,如名称冲突和歧义性。在C++中,多重继承通过在冒号后列出多个基类来实现。
cpp
class Base1 {
public:
void func1() { /* ... */ }
};
class Base2 {
public:
void func2() { /* ... */ }
};
class Derived : public Base1, public Base2 {
public:
void useBoth() {
func1();
func2();
}
};
菱形继承(钻石问题)及其解决方法
菱形继承(也称为钻石问题)发生在多重继承中,当两个基类都继承自同一个公共基类时。这可能导致公共基类的成员在派生类中被多次继承,从而产生多个副本。为了避免这个问题,C++引入了虚继承的概念。
虚继承允许一个类通过虚方式继承其基类。这样,当另一个类从这个类派生时,它将只继承基类的一个副本。
cpp
class CommonBase {
public:
void commonFunc() { /* ... */ }
};
class Base1 : virtual public CommonBase {
// ...
};
class Base2 : virtual public CommonBase {
// ...
};
class Derived : public Base1, public Base2 {
// ...
};
在上面的示例中,Base1
和Base2
都通过虚方式继承自CommonBase
。因此,当Derived
从Base1
和Base2
派生时,它只继承CommonBase
的一个副本。
虚继承的引入和原理
虚继承的主要目的是解决菱形继承中的多重继承问题。它通过引入一个共享的基类副本来实现这一点。当派生类从多个通过虚方式继承同一基类的基类派生时,它将只继承该基类的一个副本,并通过一个共享的指针来访问它。这样可以确保基类的数据成员在派生类中只有一个副本,避免了数据冗余和歧义性。
八、继承中的类型转换和切片问题
向上转型(隐式类型转换)
向上转型是将派生类指针或引用转换为基类指针或引用的过程。这是自动发生的,不需要显式转换。通过向上转型,我们可以将派生类对象视为基类对象来处理,从而利用基类的接口。
向下转型(显式类型转换)和dynamic_cast
向下转型是将基类指针或引用转换为派生类指针或引用的过程。由于这可能导致类型不匹配的问题,因此必须显式进行。在C++中,我们可以使用dynamic_cast
运算符来进行向下转型。但是,请注意,dynamic_cast
只能在包含虚函数的类之间使用,并且只能在运行时进行类型检查。
切片问题及其避免方法
切片问题发生在将派生类对象赋值给基类对象时。由于基类不包含派生类特有的成员,因此这些成员将被丢弃(即"切片"掉)。为了避免切片问题,我们应该使用指针或引用来处理对象,而不是直接赋值。这样可以确保派生类对象的完整性得到保留。
九、继承与友元和继承与静态成员
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。