类的组合
一.组合的相关定义
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
类的成员对象engine
。Car
类的方法startCar
通过调用engine
对象的start
方法来启动汽车。
2.组合关系中的构造和析构
(1)构造函数
- 原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。
- 声明形式:
类名::类名(对象成员所需的形参,本类成员形参):对象1(参数),对象2(参数),......
{
//函数体其他语句
}
构造组合类对象时的初始化次序
- 首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中定义的次序。
- 成员对象构造函数调用顺序:按对象成员的声明顺序,先声明者先构造。
- 初始化列表中未出现的成员对象,调用用默认构造函数(即无形参的)初始化
- 处理完初始化列表之后,再执行构造函数的函数体。
示例解释
假设我们有一个
Car
类,它有两个成员对象:Engine
和Wheel
。每个成员对象都有自己的构造函数,需要特定的参数来初始化。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
类的构造函数接受三个参数:engineHorsepower
、wheelDiameter
和carModel
。- 在成员初始化列表中,
engine_(engineHorsepower)
和wheel_(wheelDiameter)
分别初始化Engine
和Wheel
对象,传递相应的参数给它们的构造函数。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. 继承的优点
代码重用:
- 继承允许派生类重用基类的代码,减少重复代码,提高开发效率。
扩展性:
- 派生类可以扩展基类的功能,添加新的方法或属性,以满足特定的需求。
维护性:
- 由于代码重用,当基类中的代码需要修改时,所有派生类都会自动获得这些修改,这简化了代码的维护。
多态性:
- 继承支持多态性,允许使用基类指针或引用来引用派生类对象,这使得可以编写更通用的代码。
代码组织:
- 继承有助于组织代码结构,形成层次结构,使得代码更加清晰和易于理解。
实现细节隐藏:
- 基类可以将其实现细节隐藏在私有成员中,只暴露必要的接口给派生类,这有助于封装和数据隐藏。
接口统一:
- 派生类继承基类的接口,这有助于保持接口的一致性,使得使用不同派生类的代码可以更加通用。
减少错误:
- 由于重用了经过测试的基类代码,可以减少新代码中的错误。
提高性能:
- 在某些情况下,继承可以减少对象创建的开销,因为派生类可以共享基类的成员。
组合和继承相关重用
代码重用
- 可以通过创建新类来复用代码,而不必重头开始编写。
- 可以使用别人已经开发并调试好的类。
类的重用
- 在新类中使用其他类的对象。即新类由很多种类的对象组成,这种方法成为组合。
- 在现有类的基础上创建新类,在其中添加新代码,这种方法称为继承。
组合与继承使用的场景
继承的使用场景:
IS-A 关系:当存在一种“IS-A”的关系时,即子类是父类的一种,可以使用继承。例如,苹果是水果的一种。
代码复用:如果多个类有共同的属性和方法,可以通过继承一个基类来避免代码重复。
多态:继承是实现多态的一种方式,通过继承,子类可以覆盖父类的方法,以实现不同的行为。
类型层次:当你需要构建一个类型层次结构,并且这些类型共享某些公共接口或实现时。
组合的使用场景:
HAS-A 关系:当一个类需要另一个类的功能,但不是其子类时,可以使用组合。例如,汽车拥有引擎,但汽车不是引擎的子类。
动态行为:组合可以提供更大的灵活性,因为可以在运行时改变所包含对象的状态。
避免过多继承:过深的继承层次会导致代码的脆弱性和高耦合性,组合可以作为一种更灵活的替代方案。
功能复用:当你只需要复用某个类的功能,而不想继承其所有属性和方法时,组合是更好的选择。