组合VS继承

ops/2024/11/20 9:08:31/

类的组合

一.组合的相关定义

1.定义:组合:是一种“has-a”关系,即一个类的对象“有”另一个类的对象。例如,一个汽车类可能包含轮胎类的对象。

例(1):

class Desk
{private:unsigned int iHeight;unsigned int iLength;public:unsigned int get_height(){return this->iHeight;}unsigned int get_length(){return this->iLength;}
};class Room
{private:class Desk desk;public:unsigned int get_desk_height(){return this->desk.get_height();}
};

在上面的例子,Room类中有一个Desk类的对象desk,即“房间中有一张椅子”,那么Room类和Desk类就是组合关系。

例(2):

class Engine {
public:void start() {// 启动引擎的代码}
};class Car {
private:Engine engine; // Car类包含一个Engine类的对象
public:void startCar() {engine.start(); // 使用包含的Engine对象来启动汽车}
};

在这个例子中,Car类有一个Engine类的成员对象engineCar类的方法startCar通过调用engine对象的start方法来启动汽车。 

 2.组合关系中的构造和析构

(1)构造函数

  • 原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。
  • 声明形式:
类名::类名(对象成员所需的形参,本类成员形参):对象1(参数),对象2(参数),......
{
//函数体其他语句
}

构造组合类对象时的初始化次序

  • 首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序。
    • 成员对象构造函数调用顺序:按对象成员的声明顺序,先声明者先构造。
    • 初始化列表中未出现的成员对象,调用用默认构造函数(即无形参的)初始化
  • 处理完初始化列表之后,再执行构造函数的函数体。

示例解释

假设我们有一个Car类,它有两个成员对象:EngineWheel。每个成员对象都有自己的构造函数,需要特定的参数来初始化。Car类的构造函数需要传递这些参数给成员对象的构造函数。

class Engine {
public:Engine(int horsepower) : horsepower_(horsepower) {}
private:int horsepower_;//引擎的马力
};class Wheel {
public:Wheel(double diameter) : diameter_(diameter) {}
private:double diameter_;//轮子的直径
};class Car {
public:// Car类的构造函数Car(int engineHorsepower, double wheelDiameter, std::string carModel): engine_(engineHorsepower), wheel_(wheelDiameter), carModel_(carModel) {// 构造函数体中的其他语句std::cout << "Car " << carModel_ << " is being constructed." << std::endl;}private:Engine engine_;Wheel wheel_;std::string carModel_;//车的型号
};
  • Car类的构造函数接受三个参数:engineHorsepowerwheelDiametercarModel
  • 在成员初始化列表中,engine_(engineHorsepower)wheel_(wheelDiameter)分别初始化EngineWheel对象,传递相应的参数给它们的构造函数。
  • carModel_(carModel)初始化carModel_成员变量。

注释与讲解:

成员初始化列表

在构造函数的参数列表之后,使用冒号:开始的是成员初始化列表,它用于在构造函数体执行之前初始化类的成员变量和成员对象:

  • engine_(engineHorsepower)

    • 这表示使用engineHorsepower参数来初始化Car类的engine_成员对象。engine_的构造函数将接收这个参数,并用它来设置引擎的马力。
  • wheel_(wheelDiameter)

    • 这表示使用wheelDiameter参数来初始化Car类的wheel_成员对象。wheel_的构造函数将接收这个参数,并用它来设置轮子的直径。
  • carModel_(carModel)

    • 这表示使用carModel参数来初始化Car类的carModel_成员变量。这个参数直接赋值给carModel_字符串。

(2)构造和析构顺序

  • 构造由内而外:Container(上面例子中的Room类)的构造函数首先调用Component(上面例子中的Desk类)的 default 构造函数然后才真正执行自己的构造函数。
  • 析构由外而内:Container(上面例子中的Room类)的析构函数首先调用Container(上面例子中的Room类)的析构函数然后才执行Component的析构函数。(当一个类中包含多个对象成员时,C++的析构顺序遵循成员在类中声明的逆序。也就是说,最后声明的成员对象的析构函数会先被调用,而第一个声明的成员对象的析构函数会最后被调用。)

3.组合的优点

  • 灵活性:组合提供了更大的灵活性,因为组合的对象可以独立于包含它们的类而存在。
  • 重用性:组合允许代码重用,因为现有的类可以被用作构建更复杂系统的构建块。
  • 松耦合:组合倾向于创建松耦合的设计,因为类的内部实现细节被隐藏起来,只通过接口与外界交互。
  • 多态性:组合可以与多态性结合使用,允许在运行时动态替换组件。

类的继承 

1.继承的概念:允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数) 和 属性(成员变量),这样产生新的类,称派生类

继承代表了 is-a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。

// 基类
class Animal {// eat() 函数// sleep() 函数
};//派生类
class Dog : public Animal {// bark() 函数
};

2.继承的相关定义:

基类 的private的任何成员 都无法被访问

继承方式为 public子类 可以使用 父类的protect 和 public 成员

继承方式为 protected时子类 可以使用 父类的protect 和 public 成员

解释及补充:

  • protected: 只有在该类及其子类中可以访问(如果一个类继承了另一个类,那么子类可以访问父类中的protected成员。),外部类不能访问(protected成员不是完全私有的,但它们也不能被外部类直接访问,这意味着其他非成员函数和非子类不能访问这些protected成员)。
  • public: 可以在任何地方访问,不受限制。

公有继承
基类的public和protected成员的访问属性在派生类中保持不变,但基类的private成员不可直接访问。

派生类中的成员函数可以直接访问基类中的public 和 protected 成员,但不能直接访问基类的private成员。
通过派生类的对象只能访问基类的public成员。

私有继承
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员。
通过派生类的对象不能直接访问基类中的任何成员。

3.继承中的作用域

  • 在继承体系中 基类 和  派生类  都有独立的作用域。
  • 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的 直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类::基类成员显示访问)
  • 需要注意的是如果是成员函数的隐藏,只要函数名相同! 就构成隐藏。
  • 注意在实际中在继承体系里面最好不要定义同名的成员。

class Base {
public:void func() {std::cout << "Base function" << std::endl;}
};class Derived : public Base {
public:void func() {std::cout << "Derived function" << std::endl;}
};int main() {Derived d;d.func(); // 调用Derived的func,隐藏了Base的func// Base类的func在Derived对象中被隐藏,无法直接访问return 0;
}

Derived类有一个与Base类同名的成员函数func()。当通过Derived类的对象调用func()时,会调用Derived类的版本,从而隐藏了Base类的func()

 4.继承关系中的构造和析构

(1)构造函数

  • 如果基类没有默认构造函数,那么派生类必须在其构造函数的成员初始化列表中显式调用基类的构造函数。
#include <iostream>
#include <string>// 基类 Person
class Person {
public:// 基类带参数的构造函数Person(const std::string& name, int age) : name_(name), age_(age) {std::cout << "Person constructor called." << std::endl;}void display() const {std::cout << "Name: " << name_ << ", Age: " << age_ << std::endl;}private:std::string name_;int age_;
};// 派生类 Student,继承自 Person
class Student : public Person {
public:// 派生类带参数的构造函数,显式调用基类的构造函数Student(const std::string& name, int age, const std::string& studentID): Person(name, age), studentID_(studentID) {std::cout << "Student constructor called." << std::endl;}void display() const {Person::display(); // 调用基类的display函数std::cout << "Student ID: " << studentID_ << std::endl;}private:std::string studentID_;
};int main() {Student student("John Doe", 20, "S12345");student.display();return 0;
}

 

注:基类的构造函数,析构函数,重载的赋值运算符不能被派生类继承 

(2)构造和析构顺序

  • 构造由内而外:子类的构造函数首先调用父类的defalut构造函数,然后执行自己。形如下面的代码:
Derived::Derived(...):Base(){...};
  • 析构由外而内:子类的析构函数首先执行自己,然后调用父类的析构函数。形如下面的代码:
Derived::~Derived(...){... ~Base()};

 5.继承方式

(1)单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承:

(2) 多继承:⼀个派生类有两个或 以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面后面继承的基类在后面派生类成员在放到最后面:

6. 继承的优点

  • 代码重用:

    • 继承允许派生类重用基类的代码,减少重复代码,提高开发效率。
  • 扩展性:

    • 派生类可以扩展基类的功能,添加新的方法或属性,以满足特定的需求。
  • 维护性:

    • 由于代码重用,当基类中的代码需要修改时,所有派生类都会自动获得这些修改,这简化了代码的维护。
  • 多态性:

    • 继承支持多态性,允许使用基类指针或引用来引用派生类对象,这使得可以编写更通用的代码。
  • 代码组织:

    • 继承有助于组织代码结构,形成层次结构,使得代码更加清晰和易于理解。
  • 实现细节隐藏:

    • 基类可以将其实现细节隐藏在私有成员中,只暴露必要的接口给派生类,这有助于封装和数据隐藏。
  • 接口统一:

    • 派生类继承基类的接口,这有助于保持接口的一致性,使得使用不同派生类的代码可以更加通用。
  • 减少错误:

    • 由于重用了经过测试的基类代码,可以减少新代码中的错误。
  • 提高性能:

    • 在某些情况下,继承可以减少对象创建的开销,因为派生类可以共享基类的成员。

组合和继承相关重用 

  • 代码重用

    • 可以通过创建新类来复用代码,而不必重头开始编写。
    • 可以使用别人已经开发并调试好的类。
  • 类的重用

    • 在新类中使用其他类的对象。即新类由很多种类的对象组成,这种方法成为组合。
    • 在现有类的基础上创建新类,在其中添加新代码,这种方法称为继承。

 

组合与继承使用的场景

继承的使用场景:

  1. IS-A 关系:当存在一种“IS-A”的关系时,即子类是父类的一种,可以使用继承。例如,苹果是水果的一种。

  2. 代码复用如果多个类有共同的属性和方法,可以通过继承一个基类来避免代码重复。

  3. 多态:继承是实现多态的一种方式,通过继承,子类可以覆盖父类的方法,以实现不同的行为。

  4. 类型层次:当你需要构建一个类型层次结构,并且这些类型共享某些公共接口或实现时。

组合的使用场景:

  1. HAS-A 关系:当一个类需要另一个类的功能,但不是其子类时,可以使用组合。例如,汽车拥有引擎,但汽车不是引擎的子类。

  2. 动态行为组合可以提供更大的灵活性,因为可以在运行时改变所包含对象的状态。

  3. 避免过多继承:过深的继承层次会导致代码的脆弱性和高耦合性,组合可以作为一种更灵活的替代方案。

  4. 功能复用:当你只需要复用某个类的功能,而不想继承其所有属性和方法时,组合是更好的选择。


http://www.ppmy.cn/ops/135194.html

相关文章

Chrome 浏览器 131 版本新特性

Chrome 浏览器 131 版本新特性 一、Chrome 浏览器 131 版本更新 1. 在 iOS 上使用 Google Lens 搜索 自 Chrome 126 版本以来&#xff0c;用户可以通过 Google Lens 搜索屏幕上看到的任何图片或文字。 要使用此功能&#xff0c;请访问网站&#xff0c;并点击聚焦时出现在地…

【氮化镓】用于低压射频电源的具有80.4% PAE的Si基E-Mode AlN/GaN HEMT

引言 本文是一篇关于增强型(E-mode)AlN/GaN高电子迁移率晶体管(HEMTs)的研究论文,晶体管是在硅衬底上制造的,并在3.6 GHz频率下展示了80.4%的峰值功率附加效率(PAE)。文章首先介绍了GaN器件在微波和毫米波功率放大器中的应用,特别是在雷达、卫星通信和民用移动通信系…

AWS IAM

一、介绍 1、简介 AWS Identity and Access Management (IAM) 是 Amazon Web Services 提供的一项服务&#xff0c;用于管理 AWS 资源的访问权限。通过 IAM&#xff0c;可以安全地控制用户、组和角色对 AWS 服务和资源的访问权限。IAM 是 AWS 安全模型的核心组成部分&#xf…

Go语言中的自定义错误及其使用

在Go语言中&#xff0c;错误处理是程序设计的一个重要部分。Go的error接口是一个内建的接口&#xff0c;任何实现了Error()方法的类型都可以作为一个错误。自定义错误类型可以提供更多的错误上下文信息&#xff0c;使得错误处理更加灵活和强大。 自定义错误类型 在Go中定义一…

GRU(门控循环单元)详解

1️⃣ GRU介绍 前面介绍的LSTM可以有效缓解RNN的梯度消失问题&#xff0c;但是其内部结构比较复杂&#xff0c;因此衍生出了更加简化的GRU。GRU把输入门和遗忘门整合成一个更新门&#xff0c;并且合并了细胞状态和隐藏状态。于2014年被提出 2️⃣ 原理介绍 GRU的结构和最简单…

Zookeeper的简单使用Centos环境下

目录 前言 一、ZOokeeper是什么&#xff1f; 二、安装Zookeeper 1.进入官网下载 2.解压到服务器 3.配置文件 三.使用Zookeeper 3.1启动相关指令 3.2其他指令 3.3ACL权限 总结 前言 记录下安装zookeeper的一次经历 一、ZOokeeper是什么&#xff1f; ZooKeeper是一…

缓存工具类编写

缓存工具类编写 一般操作 在外面日常开发中&#xff0c;经常会有为了减少数据库压力&#xff0c;而将数据保存到缓存中并设置一个过期时间的操作。日常代码如下&#xff1a; Autowired private RedisTemplate<String, String> redisTemplate;public Object queryDataW…

华为VPN技术

1.启动设备 2.配置IP地址 [FW1]int g1/0/0 [FW1-GigabitEthernet1/0/0]ip add 192.168.1.254 24 [FW1-GigabitEthernet1/0/0]int g1/0/1 [FW1-GigabitEthernet1/0/1]ip add 100.1.1.1 24 [FW1-GigabitEthernet1/0/1]service-manage ping permit [FW2]int g1/0/0 [FW2-Gi…