目录
一、继承的概念:
- 在 C++ 中,允许一个类(称为派生类或子类)从另一个类(称为基类或父类)中继承成员和函数。继承使得派生类可以复用基类的功能,并且可以在此基础上添加新的成员和函数,从而实现代码的复用和扩展。
二、派生类的格式:
三、 继承方式和成员访问控制:
3.1继承方式:
- 在C++中,继承方式指的是在定义派生类时使用的访问控制符,可以是 public、protected 或 private。
- 使用class定义的基类,默认继承方式为private。
- 使用struct定义的基类,默认继承方式为public。
- 派生类可以省略继承方式,省略后会按照基类的默认继承方式来继承。
3.2成员访问控制:
- 成员访问控制:指的是在基类中定义成员时使用的访问控制符,可以是 public、protected 或 private。
3.3继承方式和派生类的关系:
- public 继承方式:
基类的 public 成员在派生类中保持 public。
基类的 protected 成员在派生类中保持 protected。
基类的 private 成员在派生类中不可访问(但仍存在于派生类对象中)。- protected 继承:
基类的 public 成员在派生类中变为 protected。
基类的 protected 成员在派生类中保持 protected。
基类的 private 成员在派生类中不可访问(但仍存在于派生类对象中)。- private 继承:
基类的 public 成员在派生类中变为 private。
基类的 protected 成员在派生类中变为 private。
基类的 private 成员在派生类中不可访问(但仍存在于派生类对象中)。
3.4protect作用域限定符:
- 基类的private成员无法被子类访问,如果想要子类访问某些基类成员,又不想这些成员在类外被访问,就可以使用protect限定这些成员,之后子类使用public或者protect继承方式即可。
四、隐式类型转换:
int a = 5; double b = a;
- 上面代码会发生隐式类型的转换,首先会产生一个double类型的临时变量,然后将5从整形转换为浮点型存储在临时变量中,再将临时变量的值复制给b。
- 隐式类型转换会产生临时变量,临时变量具有常性。
那么如果有一个子类转换为父类,是否会发生隐式类型转换呢?
五、对象切片:
- 对象切片(父子类复制兼容规则):是指将派生类对象的部分(或全部)复制到基类对象中。这通常发生在将派生类对象赋值给基类对象时。包括基类对象/基类指针/基类引用。
- 因此,派生类赋值给基类相当于拷贝派生类的一部分赋值给基类,相当于切割,不会发生隐式类型转换。
- 父类不能赋值给子类,子类有父类没有的成员,所以不行。
所以子类转换为父类,不是隐式类型转换,而是对象切片。
六、继承中的作用域:
6.1基类和派生类有各自的作用域:
- 继承体系中,基类和派生类都有独立的作用域。因此子类和父类可以有同名字的成员。
- 比如基类定义了成员int a;子类也可以定义int a;
6.2分别调用基类和派生类的同名成员:
- 如果直接在派生类调用同名成员,调用的是派生类的成员。
- 想要调用基类的同名成员,就需要限制类域。格式为“ 基类::同名成员”。
- 要限制类域才能调用基类的同名成员的原因是,子类成员隐层了父类的同名成员。
6.3案例演示:
- 基类如下:
- 派生类如下:
- 那么上面的派生类就会有两个函数成员,但是函数名都叫fun,这两个函数是构成函数重载呢还是构成同名函数呢?
- 答案是:构成同名函数。
- 首先,这两个函数不在同一个作用域,只有在同一个作用域的两个同名不同参数的函数才会构成函数重载。
- 其次,同名函数,不考虑返回值和参数,只要名字相同,就会构成同名函数。
6.4总结:
- 能不定义同名成员尽量不要定义。
七、派生类的成员函数:
7.1构造函数:
- 派生类要将他看作是两部分,一部分是基类的部分,一部分是派生类自己的部分。
- 所以派生类的构造函数只要完成自己的成员的初始化和调用父类的拷贝构造即可。不要在派生类的拷贝构造初始化父类的成员。
7.2拷贝构造函数:
7.3函数重载=:
7.4析构函数:
- 由于多态的原因,析构函数会被统一处理成destructor,所以基类的构造函数会和派生类的构造函数产生同名关系。此时基类的构造函数会隐藏派生类的构造函数。
- 在派生类中,析构函数只需要析构自己的成员。当析构完自己的部分,编译器会自动调用基类的构造函数完成基类资源的清理
八、派生类的特殊成员:
8.1如何使得一个类不能被继承?
- 可以通过将类的构造函数声明为私有(private)或将类的析构函数声明为私有(private)。
- 使用C++11引入的关键字final。
8.2派生类能否继承基类的友元?
- 派生类无法继承积累的友元。
8.3派生类能否继承基类的静态成员?
- 在C++中,派生类会继承基类的静态成员。这包括静态变量和静态方法。本质上继承的是对静态成员的使用权。
- 静态成员属于类本身,而不是类的某个实例,因此它们在类的所有实例之间共享。因此无论通过基类还是派生类访问和修改静态成员,都只会影响到同一个静态成员。
九、继承的三个类型:
9.1单继承:
- 一个派生类只继承自一个基类。这是最常见的继承形式,简化了类层次结构,减少了复杂性和潜在的冲突。
9.2多继承:
- 一个派生类继承自多个基类。这种形式允许派生类结合多个基类的特性和行为,但同时也引入了更高的复杂性和潜在的冲突,特别是在基类中有同名成员或方法时。
9.3 菱形继承:
- 多继承可能导致“菱形继承问题”,当一个类通过多个路径继承自同一个基类时,导致基类的多个实例出现。进而引发二义性和资源浪费等问题。
9.4虚拟继承:
- 虚继承(Virtual Inheritance)是C++中为了解决菱形继承问题而引入的机制。通过使用虚继承,派生类会确保基类只有一个实例。
- 使用virtual修饰继承关系
9.5总结:
- 不要用或者少用菱形继承,很麻烦。
十、组合:
10.1组合的概念:
- 通过将对象包含在其他对象中来实现类之间的关联关系。组合常用于构建复杂对象,使得一个类(复合类)可以包含一个或多个其他类(组件类)的实例。
10.2组合的使用格式:
// 引擎类 class Engine { public: void start() { std::cout << "Engine started." << std::endl; } }; // 车轮类 class Wheel { public: void roll() { std::cout << "Wheel rolling." << std::endl; } }; // 汽车类,它包含引擎和车轮 class Car { private: Engine engine; // 组合成员:引擎 Wheel wheels[4]; // 组合成员:车轮数组 public: void start() { engine.start(); // 使用组合成员的方法 for (int i = 0; i < 4; ++i) { wheels[i].roll(); // 使用组合成员的方法 } std::cout << "Car started." << std::endl; } };
10.3组合的使用细节:
- 组合成员可以在构造函数的初始化列表中进行初始化。
- 组合成员通常被声明为私有(private),以确保封装性。通过公共(public)方法或保护(protected)方法来访问这些成员。
- 组合:表示“强”拥有关系,组合对象的生命周期由包含它的对象管理。当包含对象销毁时,组合对象也随之销毁。
- 组合是一种通过在一个类中包含其他类的实例来实现类之间关系的设计原则。它具有灵活性、封装性和低耦合等优点,适用于构建复杂对象和实现代码复用。
十一、继承和组合的对比:
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。
- 使用组合还是继承,要看情况具体分析,如果是is-a的关系就是用继承,如果是has-a的关系就使用组合。
十二、笔试面试题:
- 什么是菱形继承?菱形继承的问题是什么?
- 什么是菱形虚拟继承?如何解决数据冗余和二义性的?
- 继承和组合的区别?什么时候用继承?什么时候用组合?
十三、细节理解:
- 继承体系中派生类不一定要体现出与基类的不同。虽然派生类通常会扩展或修改基类的功能,但这是选择性的,而不是必须的。
class A { public: void f(){ cout<<"A::f()"<<endl; } int a; }; class B : public A { public: void f(int a){cout<<"B::f()"<<endl;} int a; }; int main() { B b; b.f(); return 0; }
- 上面的文件编译会报错,B和A有同名函数,会构成隐藏。
- B会隐藏A的f函数,当调用b.f()时,就不会去找A的f函数。
- 但是B的版本只有有参数int的版本,编译器无法找到无参数的f函数,就会报错。
- 派生类构造函数,会先调用基类构造函数初始化基类成员,再初始化派生类成员。
- 派生类析构函数,先析构派生类成员,在析构基类成员。
class Base1 { public: int _b1; }; class Base2 { public: int _b2; }; class Derive : public Base1, public Base2 { public: int _d; }; int main() { Derive d; Base1* p1 = &d; Base2* p2 = &d; Derive* p3 = &d; return 0; }
+---------+ | _b1 | Base1 部分 +---------+ | _b2 | Base2 部分 +---------+ | _d | Derive 部分 +---------+
- p1和p3指针,都指向_b1,所以他们相同,p2指针指向_b2。
- p1比p2先声明,所以p1小于p2
- 基类对象不一定包含所有基类成员,静态成员就不会被基类包含。
- 静态成员函数不能被声明为虚函数,因为虚函数需要动态绑定,而静态成员函数是与类本身相关联的,而不是与类的对象相关联的。因此,静态成员函数不能通过派生类来重写或覆盖。