第九章 组合与继承
一、比较 is-a 关系和 is-like-a 关系
1 is-a 关系
表示严格的继承关系。
含义:
派生类是基类的一种特殊类型。
例如:
Dog is a Animal代码:
classAnimal{};classDog:publicAnimal{};特点:
- 派生类对象可以当作基类对象使用
- 满足替代原则
例如:
Animal*p=newDog();2 is-like-a 关系
表示行为相似,但不是严格继承关系。
例如:
Airplane is like a Bird飞机和鸟都会飞,但:
- 飞机不是鸟
- 不应该继承
3 区别总结
| 关系 | 含义 |
|---|---|
| is-a | 真实继承关系 |
| is-like-a | 功能相似但不是继承 |
原则:
只有满足 is-a 才应该使用继承二、public / private / protected 继承区别
C++中继承方式分为public继承、protected继承和private继承。它们的主要区别在于基类成员在派生类中的访问权限不同。
在public继承中,基类的public成员在派生类中仍为public,protected成员仍为protected;
在protected继承中,基类的public和protected成员在派生类中均变为protected;
在private继承中,基类的public和protected成员在派生类中均变为private;
而基类的private成员在派生类中始终不可直接访问。
三、派生类对象的内存布局与初始化
派生类对象的内存由基类子对象和派生类自身成员组成,基类部分通常位于对象的前部,多个基类时按继承顺序依次排列。
在初始化过程中,构造函数的执行顺序为:首先调用基类的构造函数(按继承顺序),然后初始化成员对象(按声明顺序),最后执行派生类自身的构造函数体。
析构顺序与构造顺序相反。
1 内存布局
派生类对象在内存中:
+----------------+ | 基类成员 | +----------------+ | 派生类成员 | +----------------+例如:
classA{intx;};classB:publicA{inty;};内存:
B对象 +------+ | x | +------+ | y | +------+四、多重继承可能导致的问题
多重继承可能带来以下问题:
- 二义性问题:当多个基类中存在同名成员时,派生类对象访问时会产生不明确性;
- 菱形继承问题:当多个基类继承自同一父类时,会导致基类数据被重复继承,从而产生数据冗余和访问二义性;
- 结构复杂性:多重继承会使类层次结构复杂,增加程序理解和维护难度;
- 构造与析构复杂:多个基类的初始化顺序和调用关系较复杂。
通常可通过虚继承来解决菱形继承带来的问题。
多重继承:
classC:publicA,publicB{};1 命名冲突
如果两个基类有同名成员:
classA{voidfunc();};classB{voidfunc();};调用:
C c;c.func();// 二义性解决:
c.A::func();2 菱形继承问题
例如:
A / \ B C \ / D代码:
classA{};classB:publicA{};classC:publicA{};classD:publicB,publicC{};问题:
D中会有两个A导致:
- 内存浪费
- 二义性
解决方法
使用虚继承
classB:virtualpublicA{};classC:virtualpublicA{};这样:
D只有一个A五、替代原则(Liskov Substitution Principle)
替代原则(里氏替换原则)是指在程序中,子类对象必须能够替代其父类对象使用,并且不影响程序的正确性和功能。
即在任何使用父类对象的地方,都可以透明地使用子类对象,而不会改变程序的行为。该原则要求子类在继承父类时不能改变父类的原有语义,应保证行为的一致性。
举例
classBird{public:virtualvoidfly();};classSparrow:publicBird{};使用:
Bird*b=newSparrow();b->fly();是合理的。
错误设计例子
classBird{public:virtualvoidfly();};classPenguin:publicBird{};企鹅不会飞。
如果写:
Bird*b=newPenguin();b->fly();// 不合理违反替代原则。
原则总结
设计继承关系时必须满足:
子类可以完全替代父类否则:
不应该使用继承七.如果要在派生类中覆盖基类的成员函数,在定义派生类成员函数时必须满足什么要求?
如果派生类要覆盖基类的成员函数,基类函数必须声明为虚函数,派生类函数的函数名、参数列表必须与基类完全相同,返回类型必须一致或满足协变返回类型,同时访问权限不能比基类更严格。通常建议使用override关键字以保证函数正确覆盖。
一.继承和组合是面向对象的两种重要的代码复用机制,阐述它们有什么区别,如何选择应用?
继承和组合是面向对象中两种重要的代码复用机制。
继承表示类之间的“is-a”关系,子类可以继承父类的属性和方法,实现代码复用,并支持多态。但继承耦合性较高,灵活性较差,且容易破坏封装。
组合表示“has-a”关系,一个类通过包含另一个类的对象来实现功能复用。组合具有较低的耦合性,更好的封装性和更高的灵活性,可以在运行时动态替换组件。
1 继承(Inheritance)
继承表示is-a关系。
含义:
派生类是一种特殊的基类。
例如:
class Animal{}; class Dog : public Animal{};这里:
Dog is a Animal特点:
- 可以复用基类的成员
- 支持多态
- 体现类之间的层次关系
优点:
- 代码复用
- 支持运行时多态
缺点:
- 耦合度较高
- 继承结构不易修改
2 组合(Composition)
组合表示has-a关系。
含义:
一个类包含另一个类的对象。
例如:
class Engine{}; class Car{ private: Engine engine; };这里:
Car has a Engine特点:
- 类中包含其他类对象
- 通过对象组合实现功能
优点:
- 灵活性高
- 耦合度低
- 更容易扩展
缺点:
- 不能直接复用接口
3 区别总结
| 项目 | 继承 | 组合 |
|---|---|---|
| 关系 | is-a | has-a |
| 耦合程度 | 高 | 低 |
| 是否支持多态 | 支持 | 一般不支持 |
| 灵活性 | 较低 | 较高 |
4 如何选择
一般原则:
如果是 is-a 关系 → 使用继承 如果是 has-a 关系 → 使用组合现代软件设计中常说:
优先使用组合,而不是继承第十章 虚函数与多态性
1. 派生类向基类转换导致类型信息丢失怎么办?
当派生类对象通过基类指针或引用访问时,会发生向上转型(upcasting),此时只能看到基类部分的信息。
如果希望根据对象的实际类型调用对应函数,需要:使用虚函数(virtual function)。
示例:
classBase{public:virtualvoidshow(){cout<<"Base"<<endl;}};classDerived:publicBase{public:voidshow(){cout<<"Derived"<<endl;}};Base*p=newDerived();p->show();// 调用 Derived::show()如果没有virtual:
只会调用 Base::show()2. 什么是动态绑定?它与虚函数有什么关系?
动态绑定(Dynamic Binding):
在程序运行时根据对象的实际类型决定调用哪个函数。
特点:
- 又称晚绑定(Late Binding)
- 发生在通过指针或引用调用虚函数时
关系:
虚函数是实现动态绑定的机制没有虚函数:
函数调用在编译时确定(静态绑定)3. 使用虚函数实现多态的一般过程
实现多态通常需要三个步骤:
① 在基类中声明虚函数
classBase{public:virtualvoidshow();};② 在派生类中重写虚函数
classDerived:publicBase{public:voidshow();};③ 使用基类指针或引用调用函数
Base*p=newDerived();p->show();运行结果:
调用 Derived 的 show()这就是运行时多态。
4. 抽象类有什么作用?如何定义?
作用
抽象类用于:
- 描述一类对象的共同特征
- 作为基类供派生类继承
- 实现接口规范
特点:
抽象类不能创建对象定义方式
只要类中包含纯虚函数,这个类就是抽象类。
纯虚函数写法:
virtual返回类型 函数名()=0;示例:
classShape{public:virtualdoublearea()=0;};5. 什么时候使用抽象类或纯抽象类?
一般在以下情况使用:
① 只定义接口,不提供实现
例如:
图形类 动物类 设备接口② 需要统一操作不同类型对象
例如:
Shape ├─ Circle ├─ Rectangle └─ Triangle统一调用:
Shape*p;p->area();③ 需要强制派生类实现某些函数
纯虚函数要求:
派生类必须重写6. dynamic_cast 的作用(举例)
dynamic_cast用于运行时安全类型转换,通常用于基类指针转换为派生类指针。
示例:
classBase{public:virtual~Base(){}};classDerived:publicBase{public:voidfunc(){cout<<"Derived function"<<endl;}};Base*p=newDerived();Derived*d=dynamic_cast<Derived*>(p);if(d){d->func();}如果转换失败:
返回 nullptr作用总结:
用于多态类型的安全向下转型7. 如何理解C++中的多态机制?
**多态(Polymorphism)**指:
同一个接口对不同对象产生不同的行为。
在 C++ 中主要通过虚函数 + 继承 + 基类指针/引用实现。
示例:
classAnimal{public:virtualvoidspeak(){cout<<"Animal sound"<<endl;}};classDog:publicAnimal{public:voidspeak(){cout<<"Dog bark"<<endl;}};classCat:publicAnimal{public:voidspeak(){cout<<"Cat meow"<<endl;}};调用:
Animal*p;p=newDog();p->speak();p=newCat();p->speak();输出:
Dog bark Cat meow同一个函数speak()表现不同。
一页速记版(考试背诵版)
动态绑定是指在程序运行时根据对象的实际类型决定调用哪个函数,它通常通过虚函数实现。实现多态的一般过程是:在基类中声明虚函数,在派生类中重写该函数,然后通过基类指针或引用调用函数。抽象类是包含纯虚函数的类,不能实例化对象,主要用于定义接口规范并作为基类供派生类继承。dynamic_cast用于运行时安全类型转换,常用于基类指针向派生类指针的转换。C++的多态机制是指通过继承和虚函数,使得同一个接口在不同对象上表现出不同的行为。
这两题是C++虚函数章节最经典的简答题,我给你整理成考试标准答案 + 易理解版。
1. 什么是虚函数?什么是纯虚函数?为什么要引入它们?
(1)什么是虚函数
**虚函数(Virtual Function)**是指在基类中使用virtual关键字声明的成员函数。
特点:
- 可以在派生类中被重写(override)
- 通过基类指针或引用调用时,根据对象的实际类型决定调用哪个函数
示例:
classBase{public:virtualvoidshow(){cout<<"Base show"<<endl;}};classDerived:publicBase{public:voidshow(){cout<<"Derived show"<<endl;}};Base*p=newDerived();p->show();// 调用 Derived::show()(2)什么是纯虚函数
**纯虚函数(Pure Virtual Function)**是没有函数实现的虚函数。
声明方式:
virtual返回类型 函数名()=0;例如:
classShape{public:virtualdoublearea()=0;};特点:
- 包含纯虚函数的类称为抽象类
- 抽象类不能创建对象
- 派生类必须实现纯虚函数
(3)为什么引入虚函数
引入虚函数是为了实现:
① 运行时多态
同一个接口,对不同对象产生不同的行为。
例如:
Animal*p=newDog();p->speak();② 动态绑定
函数调用在运行时决定。
(4)为什么引入纯虚函数
纯虚函数主要用于:
① 定义接口规范
规定派生类必须实现某些函数。
例如:
Shape ├── Circle ├── Rectangle └── Triangle所有图形都必须实现:
area()② 实现抽象类
用于表示抽象概念。
例如:
Shape Animal Device2. 析构函数应该是虚函数吗?为什么?
一般来说:
如果类可能被继承,析构函数应该定义为虚函数。
原因
当使用基类指针删除派生类对象时,如果析构函数不是虚函数,只会调用基类析构函数。
示例:
classBase{public:~Base(){cout<<"Base destructor"<<endl;}};classDerived:publicBase{public:~Derived(){cout<<"Derived destructor"<<endl;}};Base*p=newDerived();deletep;如果析构函数不是虚函数:
只调用 Base destructorDerived的资源不会释放。
正确写法
classBase{public:virtual~Base(){cout<<"Base destructor"<<endl;}};此时执行:
deletep;调用顺序:
Derived destructor Base destructor总结
如果一个类作为基类使用,其析构函数通常应该声明为虚函数,以保证通过基类指针删除派生类对象时能够正确调用派生类析构函数,从而避免资源泄露。考试背诵版(简化)
虚函数是在基类中使用virtual声明的成员函数,在派生类中可以被重写,通过基类指针或引用调用时,根据对象的实际类型决定调用哪个函数,从而实现运行时多态。纯虚函数是没有函数体的虚函数,其形式为virtual 函数 = 0,包含纯虚函数的类称为抽象类,抽象类不能创建对象,派生类必须实现纯虚函数。引入虚函数是为了实现动态绑定和多态,引入纯虚函数是为了定义接口规范。
如果一个类作为基类使用,其析构函数通常应该声明为虚函数,因为当通过基类指针删除派生类对象时,虚析构函数可以保证先调用派生类析构函数再调用基类析构函数,从而正确释放资源。
这两题其实是在考C++多态 + 容器存储对象时的对象切片问题,很多人容易答不完整。我给你整理成考试标准答案 + 原理解释版。
5. 能否用 vector 或 vector<Component&> 代替 vector<Component*>?
结论:
不能使用 vector<Component>,也不能使用 vector<Component&>原因如下。
(1)为什么不能使用 vector
如果使用
vector<Component>会发生对象切片(Object Slicing)。
原因:
Component是基类,而Part、Assembly等是派生类。
当派生类对象放入vector<Component>时:
派生类对象 → 转换为基类对象结果:
派生类特有的数据和行为会被丢弃例如:
classComponent{};classPart:publicComponent{};存入:
vector<Component>v;Part p;v.push_back(p);结果:
p 会被复制成 Component 对象此时:
Part 的成员全部消失并且多态也无法实现。
(2)为什么不能使用 vector<Component&>
C++ 标准规定:
容器不能存储引用类型因此:
vector<Component&>是非法的。
原因:引用必须在初始化时绑定对象,且不能重新绑定,而容器需要频繁移动和赋值元素。
(3)为什么使用 vector<Component*>
使用指针:
vector<Component*>可以:
1️⃣ 保存不同派生类对象
例如:
Part*Assembly*2️⃣ 保持对象真实类型
3️⃣ 实现多态调用
例如:
Component*c=newPart();c->print();运行时调用:
Part::print()总结
vector<Component> 会导致对象切片,丢失派生类信息; vector<Component&> 在C++中是不允许的; 因此必须使用 vector<Component*> 来保存不同类型的组件对象, 从而保持多态性。6. 为什么说“面向对象编程必须使用指针和引用”?
这句话的意思是:
要实现多态,必须通过指针或引用访问对象(1)对象本身不能实现多态
例如:
Base b=Derived();b.show();发生:
对象切片结果:
只调用 Base::show()因为:
b 已经变成 Base 对象(2)只有指针或引用才能保持对象真实类型
例如:
Base*p=newDerived();p->show();此时:
p 指向 Derived 对象运行时:
调用 Derived::show()这就是:
动态绑定(3)多态必须满足三个条件
1️⃣ 继承
2️⃣ 虚函数
3️⃣基类指针或引用
例如:
Animal*p;p=newDog();p=newCat();调用:
p->speak();不同对象表现不同。
总结理解
这句话的含义是:
在C++中,如果直接使用对象进行赋值或传递, 会发生对象切片,丢失派生类信息; 而使用指针或引用可以保持对象的真实类型, 从而通过虚函数实现运行时多态。因此,在面向对象编程中,经常需要使用指针或引用来操作对象。
考试简答版(推荐写法)
在组件库存管理程序中,不能使用vector<Component>,因为当派生类对象存入该容器时会发生对象切片,派生类特有的数据和行为会丢失,从而无法实现多态;也不能使用vector<Component&>,因为C++标准容器不允许存储引用类型。因此需要使用vector<Component*>来保存组件对象,从而能够存储不同派生类对象并保持多态。
“无法直接使用对象进行面向对象编程”是因为如果直接使用对象赋值或传递,会发生对象切片,丢失派生类信息,而使用基类指针或引用可以保持对象的真实类型,并通过虚函数实现运行时多态,因此在C++面向对象编程中通常需要通过指针或引用来操作对象。
这些是C++模板(Template)章节的典型简答题,我给你整理成标准答案 + 简洁考试版,考试基本按这个写就可以。
第十一章 模板与泛型编程
1. C++的模板机制解决什么问题?
**模板(Template)**主要用于解决代码复用问题。
在很多情况下,不同数据类型需要执行相同的算法或操作,如果不用模板,就需要为每种类型写一套函数或类。
例如:
intmax(inta,intb);doublemax(doublea,doubleb);floatmax(floata,floatb);代码会大量重复。
使用模板:
template<typenameT>Tmax(T a,T b){returna>b?a:b;}一个模板即可适用于多种类型。
总结:
C++模板机制通过参数化类型,使函数或类能够适用于多种数据类型,从而提高代码复用性,减少重复代码,并提高程序的通用性。
2. 什么叫函数模板实例?函数模板在什么时候被实例化?
函数模板实例是指:
编译器根据函数模板和具体的数据类型生成的具体函数。
例如:
模板:
template<typenameT>Tadd(T a,T b){returna+b;}调用:
add(1,2);add(1.5,2.3);编译器会生成:
intadd(int,int);doubleadd(double,double);这些具体函数就叫函数模板实例。
实例化时刻:
函数模板在被调用时由编译器自动实例化也就是说:
只有当模板被使用时才会生成具体函数3. 什么叫类模板实例?类模板在什么时候被实例化?
类模板实例是指:
根据类模板和具体类型参数生成的具体类。
例如:
template<typenameT>classBox{public:T data;};使用:
Box<int>b1;Box<double>b2;编译器会生成:
classBox<int>classBox<double>这些就是类模板实例。
实例化时刻:
类模板在使用具体类型创建对象时进行实例化也就是说:
Box<int> b1;这一行代码会触发实例化。
4. 模板的显式实例化有什么作用?
**显式实例化(Explicit Instantiation)**是指:
程序员显式指定模板生成某种类型的实例。
语法:
template返回类型 函数名<类型>(参数);例如:
templateintadd<int>(int,int);作用:
1️⃣ 强制编译器生成指定类型的模板实例
2️⃣ 提前生成代码,避免多次实例化
3️⃣ 在模板实现分离(.h/.cpp)时解决链接问题
例如:
模板定义在cpp中时,需要显式实例化一页考试简答版(推荐背)
1️⃣ C++模板机制通过参数化类型,使函数或类能够适用于多种数据类型,从而提高代码复用性,减少重复代码,提高程序的通用性。
2️⃣ 函数模板实例是编译器根据函数模板和具体类型参数生成的具体函数。函数模板通常在被调用时由编译器自动实例化。
3️⃣ 类模板实例是根据类模板和具体类型参数生成的具体类。类模板一般在使用具体类型创建对象时进行实例化。
4️⃣ 模板的显式实例化是指程序员显式指定模板生成某种类型的实例,其作用是强制编译器生成特定类型的模板代码,并可用于解决模板分离编译时的链接问题。
一、模板的特点
C++模板是一种参数化类型机制,它允许函数或类在定义时不指定具体类型,而在使用时再确定具体类型。模板具有代码复用性高、类型安全、编译期实例化和支持泛型编程等特点。模板通常用于不同数据类型执行相同操作、实现通用数据结构以及编写通用算法等场景。