当前位置: 首页 > news >正文

C++(十六) 继承 part2

前言

 上半部分:C++(十五)继承 part1-CSDN博客

书接上文,这篇将进一步介绍C++中继承体系的使用与细则。

本文将从以下几部分内容完整地探讨继承体系

  1. 派生类的默认成员函数(主要)
  2. 基类中静态成员
  3. 继承与友元
  4. 多继承&复杂的菱形继承(粗略) 

一、派生类的默认成员函数

首先先回忆以下默认成员函数: 

默认成员函数是指:编译器在我们没有显式定义这些函数时,会自动为类生成的函数。


 接下来我们进入主题——在继承体系中的默认成员函数:

 1.1 派生类默认成员函数生成规则

        C++中,派生类会默认继承基类的成员函数,并且C++会自动为派生类生成一些默认成员函数,其中有一些特定的规则需要我们理解学习。 

1. 构造函数

  • 派生类的构造函数必须调用基类的构造函数,以初始化基类的成员。如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类的其他构造函数。

2. 拷贝构造函数

  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数,以完成基类部分的拷贝初始化。

3. 赋值运算符 (operator=)

  • 派生类的赋值运算符必须调用基类的赋值运算符,以完成基类部分的赋值操作。

4. 析构函数

  • 派生类的析构函数在销毁派生类成员后,会自动调用基类的析构函数,以销毁基类的成员。这确保了派生类对象按正确的顺序销毁成员。

5. 构造顺序

  • 派生类对象的初始化顺序是先调用基类的构造函数,再调用派生类的构造函数。

6. 析构顺序

  • 派生类对象的销毁顺序是先调用派生类的析构函数,再调用基类的析构函数。

7. 析构函数的隐藏

  • 因为某些情况下析构函数需要重写,而重写的条件之一是函数名相同。因此,如果基类的析构函数未加virtual(这部分后面多态会仔细介绍),则派生类的析构函数会隐藏基类的析构函数。

上面是对在继承体系默认成员函数生成规则的系统阐述,接下来我们结合代码看看具体的编译器运作机制:

1.1.1 构造、析构顺序

        我们通过在默认构造函数和默认析构函数中打印相关信息来观察子类和父类中,构造和析构的调用顺序机制。

代码:

#include <iostream>
#include <string>
using namespace std;
// 父类:Person
class Person
{
public:Person(const char* name = "大卫"): _name(name){cout << "  基类构造: Person()" << endl;}~Person(){cout << "  基类析构:~Person()" << endl;delete _phoneNumber;}
protected:string _name;                                     // 姓名string* _phoneNumber = new string("111111111");   //电话号码
};//子类
class Student : public Person
{
public:// 先父后子Student(const char* name = "张三", int studentNumber = 0):Person(name), _studentNumber(studentNumber){cout << "  派生类构造:Student()" << endl;}~Student(){cout << "  派生类构造:~Student()" << endl;}
protected:int _studentNumber;
};int main()
{cout << "实例化Student:s1" << endl;Student s1;return 0;
}

程序运行结果:

实例化Student:s1基类构造: Person()派生类构造:Student()派生类构造:~Student()基类析构:~Person()

代码解析:

构造顺序:

  1. 基类构造函数:当创建一个Student对象(如s1)时,首先调用的是基类Person的构造函数。这是因为在对象的构造过程中,必须先初始化其基类部分。输出 "基类构造:Person()"

  2. 派生类构造函数:在基类部分初始化完成后,才会调用派生类Student的构造函数,用于初始化派生类特有的成员。输出 "派生类构造:Student()"

析构顺序:

  1. 派生类析构函数:当Student对象(如s1)被销毁时,首先调用的是派生类Student的析构函数。这个函数负责清理派生类特有的资源。输出 "派生类析构:~Student()"

  2. 基类析构函数:在派生类部分销毁完成后,调用基类Person的析构函数,用于清理基类部分的资源。输出 "基类析构:~Person()"。注意在基类析构函数中,调用了delete _phoneNumber,释放在堆上分配的内存。

总结:

  • 构造顺序:当创建派生类对象时,构造顺序总是先基类后派生类
  • 析构顺序:当销毁派生类对象时,析构顺序总是先派生类后基类。 

 1.1.2 拷贝构造

        同样我们在分别在基类和派生类的拷贝构造打印信息来帮助我们观察,当派生类拷贝构造时候运行的机制。

#include <iostream>
#include <string>
// 父类:Person
class Person
{
public:Person(const char* name = "大卫"): _name(name){cout << "  基类构造: Person()" << endl;}Person(const Person& p): _name(p._name){//动态申请一个新string初始化成*p._phoneNumber内容_phoneNumber = new string(*p._phoneNumber);cout << "  基类拷贝构造:Person(const Person& p)" << endl;}~Person(){cout << "  基类析构:~Person()" << endl;delete _phoneNumber;}
protected:string _name; // 姓名string* _phoneNumber = new string("111111111");
};
//子类
class Student : public Person
{
public:// 先父后子Student(const char* name = "张三", int studentNumber = 0):Person(name), _studentNumber(studentNumber){cout << "  派生类构造:Student()" << endl;}Student(const Student& s):Person(s), _studentNumber(s._studentNumber){cout << "  派生类拷贝构造:Student(const Student& p)" << endl;}~Student(){cout << "  派生类构造:~Student()" << endl;}
protected:int _studentNumber;
};int main()
{cout << "实例化Student:s1" << endl;Student s1;cout << endl;cout << "拷贝构造Student s2:" << endl;Student s2(s1);cout << endl;return 0;
}

 程序运行结果:

实例化Student:s1基类构造: Person()派生类构造:Student()拷贝构造Student s2:基类拷贝构造:Person(const Person& p)派生类拷贝构造:Student(const Student& p)派生类构造:~Student()基类析构:~Person()派生类构造:~Student()基类析构:~Person()

代码解析:

        通过打印信息我们可以观察到,先打印基类拷贝构造,再打印派生类拷贝构造。之后分别析构2个对象s1 和 s2,打印4条析构信息,符合析构时先派生类后基类。

拷贝构造的顺序:

  1. 基类拷贝构造函数:当使用现有的Student对象(s1)来初始化新的Student对象(s2)时,首先调用的是基类Person的拷贝构造函数。这是因为在创建派生类对象的过程中,必须先拷贝初始化其基类部分。输出 "基类拷贝构造:Person(const Person& p)"

  2. 派生类拷贝构造函数:在基类部分拷贝完成后,调用派生类Student的拷贝构造函数,用于拷贝派生类特有的成员。输出 "派生类拷贝构造:Student(const Student& p)"

总结:

  • 顺序:派生类的拷贝构造函数会先调用基类的拷贝构造函数,以确保基类部分正确拷贝。

  • 自动生成:如果用户未显式定义拷贝构造函数,编译器将自动生成一个默认拷贝构造函数。对于派生类,这个默认的拷贝构造函数将递归地调用基类的拷贝构造函数。

【注意】深拷贝与浅拷贝:如果类中有指针成员,必须特别注意拷贝构造函数的实现,避免默认的浅拷贝引发资源管理问题(如重复释放同一指针)。

 1.1.3 赋值运算符

#include <iostream>
#include <string>
// 父类:Person
class Person
{
public:Person(const char* name = "大卫"): _name(name){cout << "  基类构造: Person()" << endl;}Person(const Person& p): _name(p._name){cout << "  基类拷贝构造:Person(const Person& p)" << endl;_phoneNumber = new string(*p._phoneNumber);}Person& operator=(const Person& p){cout << "  基类赋值:Person& operator=(const Person& p)" << endl;if (this != &p){_name = p._name;// 释放旧的内存delete _phoneNumber;// 分配新内存并复制内容_phoneNumber = new string(*p._phoneNumber);}return *this;}~Person(){cout << "  基类析构:~Person()" << endl;delete _phoneNumber;}
protected:string _name; // 姓名string* _phoneNumber = new string("111111111");
};
//子类
class Student : public Person
{
public:// 先父后子Student(const char* name = "张三", int studentNumber = 0):Person(name), _studentNumber(studentNumber){cout << "  派生类构造:Student()" << endl;}Student(const Student& s):Person(s), _studentNumber(s._studentNumber){cout << "  派生类拷贝构造:Student(const Student& p)" << endl;}Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_studentNumber = s._studentNumber;}cout << "  派生类赋值:Student& operator=(const Student& s)" << endl;return *this;}~Student(){cout << "  派生类构造:~Student()" << endl;}
protected:int _studentNumber;
};int main()
{cout << "实例化Student:s1" << endl;Student s1;//cout << endl;//cout << "拷贝构造Student s2:" << endl;//Student s2(s1);cout << endl;cout << "实例化Student: s3:" << endl;Student s3("李四", 12185415);cout << endl;cout << "赋值 s1 = s3:" << endl;s1 = s3;cout << endl;cout << "赋值(切片) p = s1;" << endl;Person p = s1;cout << endl;return 0;
}

 代码运行结果:

实例化Student:s1基类构造: Person()派生类构造:Student()实例化Student: s3:基类构造: Person()派生类构造:Student()赋值 s1 = s3:基类赋值:Person& operator=(const Person& p)派生类赋值:Student& operator=(const Student& s)赋值(切片) p = s1;基类拷贝构造:Person(const Person& p)基类析构:~Person()派生类构造:~Student()基类析构:~Person()派生类构造:~Student()基类析构:~Person()

代码分析:

        在1.1.1和1.1.2中分析过的这里就不在赘述,看看在新增的代码:父类和子类运赋值运算符和main函数中赋值。

  •  顺序:在代码中,当执行 s1 = s3;时,首先调用基类Person的赋值运算符 (Person& operator=(const Person& p)),然后调用派生类Student的赋值运算符 (Student& operator=(const Student& s)),这是为了确保Person部分和Student特有的部分都被正确赋值。
  • 资源管理:基类Person的赋值运算符会释放旧的_phoneNumber指针并分配新的内存。这避免了浅拷贝带来的问题。
  • 切片:当派生类对象赋值给基类对象时( Person p = s1;),会发生切片操作。此时,派生类的特有部分会被切掉,只保留基类部分。
    • Person p = s1;调用的是基类的拷贝构造函数 (Person(const Person& p)),因为pPerson类型,切掉了Student类型的特有部分。

总结:

        赋值运算符顺序:派生类的赋值运算符必须首先调用基类的赋值运算符,确保基类部分被正确赋值,然后再处理派生类的特有部分。 


二、 基类中静态成员

2.1 静态成员的定义与特性

1.静态数据成员:属于整个类,而不是某个对象。静态成员在类的所有对象间共享,并且在类的所有实例化之前就已经存在。它们在程序启动时被初始化一次。

  • 静态成员函数:不能访问类的非静态成员,因为静态成员函数不与任何具体对象绑定,但可以访问静态数据成员和其他静态成员函数。
class Person
{
public:// 静态数据成员,各个对象共享这一个变量static int personCount;//... ...// 静态成员函数static int getPersonCount(){//不能访问类的非静态成员//可访问静态数据成员return personCount;}protected:string _name;             // 非静态成员string* _phoneNumber ;    // 非静态成员
};// 静态数据成员初始化
int Person::personCount = 0;

2.2.静态成员的初始化 

        静态数据成员必须在类外部进行定义和初始化。这与非静态数据成员不同,后者通常在构造函数或成员初始化列表中进行初始化。

class Person
{
public:// 静态数据成员 类内声名static int personCount;// 静态成员函数static int getPersonCount(){return personCount;}
//... ...
};// 静态数据成员初始化 类外定义初始化
int Person::personCount = 0;

2.3使用注意事项&示例

使用注意事项

  • 静态成员不与任何对象实例绑定,可以通过类名直接访问
  • 由于静态数据成员在类的所有对象之间共享,修改静态数据成员的值会影响所有对象。
  • 静态成员函数无法访问非静态成员变量和成员函数,因为它们不依赖于类的实例。

 示例:

下面的代码中,每次调用Person类构造函数,变量personCount会+1,析构一次会 -1.

#include <iostream>
#include <string>
using namespace std;class Person
{
public:// 静态数据成员static int personCount;Person(const char* name = "大卫"): _name(name){cout << "  基类构造:Person()" << endl;personCount++;  // 每实例化一个Person对象,personCount增加}~Person(){cout << "  基类析构:~Person()" << endl;personCount--;  // 每销毁一个Person对象,personCount减少delete _phoneNumber;}// 静态成员函数static int getPersonCount(){//实现最简单的返回值的逻辑return personCount;}protected:string _name; // 姓名string* _phoneNumber = new string("12315");
};// 静态数据成员初始化
int Person::personCount = 0;class Student : public Person
{
public:Student(const char* name = "张三", int studentNumber = 0): Person(name), _studentNumber(studentNumber){}~Student(){}protected:int _studentNumber;
};int main()
{
//通过Person::getPersonCount()可随时调用静态成员函数,就算s1还没实例化也可以调用cout << "当前Person实例数量:" << Person::getPersonCount() << endl;cout << endl;cout << "实例化Student:s1" << endl;Student s1;cout << "当前Person实例数量:" << s1.personCount << endl;     
//因为是公有继承,且personCount是公有可以直接访问cout << endl;cout << "实例化Student:s2:" << endl;Student s2("李四", 12185415);cout << "当前Person实例数量:" << s2.personCount << endl;  
//通过打印结果观察,可以看出s1、s2、s3访问的是同一个personCount,
//也就是:类的所有对象之间共享personCountcout << endl;cout << "实例化Student:s3:" << endl;Student s3("王五", 125554);cout << "当前Person实例数量:" << s3.personCount << endl;cout << endl;return 0;
}

 程序运行结果:

当前Person实例数量:0实例化Student:s1基类构造:Person()
当前Person实例数量:1实例化Student:s2:基类构造:Person()
当前Person实例数量:2实例化Student:s3:基类构造:Person()
当前Person实例数量:3基类析构:~Person()基类析构:~Person()基类析构:~Person()

  • 每创建一个 PersonStudent 对象,personCount 会增加1。
  • 每销毁一个对象,personCount 会减少1。
  • 可以通过类名 Person::getPersonCount() 直接访问静态成员函数,而不需要通过对象实例。  

这部分内容还是比较简单,所以这里关于静态成员部分就介绍这么多。

更多的变化小伙伴可以自行实践、摸索,欢迎大家一起讨论学习~~

三、 继承与友元

先回顾一下友元:

           友元(friend)是一个能够访问类的私有和受保护成员的函数或另一个类。友元可以是函数(普通函数或成员函数)或整个类。

        大家记住在不看派生类的情况下,基类就是一个普通的类,下面就是一个最普通的友元函数的用法。


class Person
{
public:// 友元函数friend string getName(const Person& person);Person(const char* name = "大卫"): _name(name){}~Person(){delete _phoneNumber;}protected:string _name; string* _phoneNumber = new string("12315");
};string getName(const Person& person)
{//友元函数内可以访问Person类的私有和受保护成员return person._name;
}

回顾完友元基本的用法之后我们继续结合继承体系的内容探讨友元:

继承与友元的关系

这部分内容比较简单,只需要搞清楚2个问题就好:

        1.基类中的友元关系会不会继承呢,换而言之:在基类中声明的友元函数能否访问派生类的保护和私有成员呢?

        2.在派生类中声明的友元函数能否访问基类中的私有和受保护成员呢?

先说结论: 

1.基类中的友元

  • 基类中的友元函数或友元类能够访问该基类的私有和受保护成员。
  • 但是,基类中的友元并不会自动成为派生类的友元。换句话说,基类的友元没有权利访问派生类的私有成员,除非它也被显式地声明为派生类的友元

2.派生类中的友元

  • 派生类可以有自己的友元,派生类的友元能够访问该派生类的私有和受保护成员。
  • 基类的友元关系不会自动继承到派生类。派生类的友元不能直接访问基类的私有成员(我们上一章学过在派生类中,基类的私有成员本来就是不可见的),除非基类中的成员是protected,或者基类的友元函数允许这种访问。

3.子类不能成为父类的友元

  • 派生类不能自动访问基类的私有成员,即使派生类是基类的子类。要让派生类能够访问基类的私有成员,可以将派生类声明为基类的友元。

结合代码说明:

#include <iostream>
#include <string>
using namespace std;class Person
{
public:// 友元函数,能够访问Person类的私有和受保护成员friend string getName(const Person& person);Person(const char* name = "大卫"): _name(name){}~Person(){delete _phoneNumber;}protected:string _name; // 姓名string* _phoneNumber = new string("12315");
};// 基类的友元函数定义
string getName(const Person& person)
{
//基类的友元没有权利访问派生类的私有成员,这里不能访问_studentNumber
//除非getName也声明为Student 的友元才能通过传参Student类在这里访问_studentNumberreturn person._name;
}class Student : public Person
{
public:Student(const char* name = "张三", int studentNumber = 0): Person(name), _studentNumber(studentNumber){}~Student(){}// 友元函数定义在派生类中friend int getStudentNumber(const Student& student);protected:int _studentNumber; // 学号
};// 派生类的友元函数定义
int getStudentNumber(const Student& student)
{//这里可以通过student._name访问基类保护成员//没有实际作用,只是为演示语法所以只是做简单的打印cout << "p1的名字: " << student._name << endl;return student._studentNumber;
}int main()
{Person p1("张三");Student s1("李四", 123456);cout << "p1的名字: " << getName(p1) << endl << endl;    // 访问基类友元函数cout << "s1的名字: " << getName(s1) << endl << endl;         // 派生类可以通过基类的友元函数访问基类成员//对象切片, s1 对象会切片成 Person 类型传给getName()cout << "s1的学号: " << getStudentNumber(s1) << endl; // 访问派生类友元函数return 0;
}

程序运行结果:

p1的名字: 张三s1的名字: 李四student._name: 李四
s1的学号: 123456

代码分析: 

基类中的友元

  • getName 函数被声明为 Person 类的友元,因此它可以访问 Person 类的 _name_phoneNumber 成员。
  • 当我们在 main 函数中调用 getName(p1)getName(s1) 时,友元函数能够访问基类的私有成员,无论是 Person 对象还是 Student 对象。

派生类中的友元

  • Student 类中,由于它继承了 Person 类,所以 Student 也拥有了 _name_phoneNumber 成员。
  • 所以,友元函数getStudentNumber 函数能访问 Student 对象的私有成员,和Person 类的protected成员_name,无法访问 Person 类的私有成员

 四、多继承&复杂的菱形继承

4.1多继承 

4.1.1 什么是多继承? 

        多继承是指一个类同时继承多个基类的能力。一个子类可以从两个或多个基类继承成员数据和成员函数。这种机制允许子类同时具备多个基类的功能。

多对一继承 

示例:

/****************  多继承  ********************/
class Base1 {// Base1的成员
};class Base2 {// Base2的成员
};class Derived : public Base1, public Base2 {// Derived类的成员
};

Derived 类继承了 Base1Base2 类的所有成员和函数。

4.1.2多继承的优缺点:

优点: 

  1. 代码复用:多继承允许子类从多个基类中继承功能,从而可以重用基类的代码。
  2. 功能组合:通过继承多个基类,子类可以结合这些基类的功能,使得子类具有更复杂的功能。
  3. 灵活性:多继承使得开发者可以在设计类时更加灵活,特别是在处理复杂系统时。

多继承的缺点和挑战:

  • 复杂性:多继承增加了类层次结构的复杂性,尤其在多个基类有相同的成员时,容易产生歧义。
  • 菱形继承问题:多继承可能导致菱形继承问题,即子类通过多个路径继承同一个基类,导致同一个基类的成员被继承多次。
  • 名称冲突:如果多个基类中有同名的成员函数或变量,子类在继承这些基类时会发生名称冲突,必须明确指定使用哪一个基类的成员。
  • 难以维护:由于多继承的复杂性,随着项目规模的扩大,代码的维护和调试变得更加困难。

4.1.3 解决多继承问题的技术 :虚拟继承

        虚拟继承:其出现的主要目的是解决菱形继承中的“二义性”问题。当一个派生类从两个或多个基类继承,而这些基类本身又继承自同一个共同的基类时,最终的派生类将从这个共同基类继承多份成员,这就导致了二义性问题。为了避免这种问题,可以使用虚拟继承。

结合代码说明: 

 考虑以下的非虚拟继承情况:

class Base {
public:int value;
};//普通公有继承
class Derived1 : public Base {// ...
};//普通公有继承
class Derived2 : public Base {// ...
};class Derived3 : public Derived1, public Derived2 {// ...
};

问题: Derived3,是不是会有2,份value呢?如果Derived3访问value时范围的是哪一份value?

  Derived1Derived2 都继承了 Base 类,所以 Derived3 类从 Base 继承了两份 value 成员变量。这会导致在 Derived3 中访问 value 时出现二义性,因为编译器无法确定访问的是 Derived1 中的 value 还是 Derived2 中的 value

虚拟继承的解决方案 :

        通过虚拟继承来确保 Base 类的成员只会被继承一次:

class Base {
public:int value;
};//使用关键字:virtual
class Derived1 : virtual public Base {// ...
};//使用关键字:virtual
class Derived2 : virtual public Base {// ...
};class Derived3 : public Derived1, public Derived2 {// ...
};在这个例子中,Derived3 只会继承一次 Base 的成员 value 

在腰部位置设置虚拟继承:

      Base/    \1      2    <- virtual (腰部)\    /Derived3

4.1.4使用多继承的注意事项

  • 设计简单的继承结构:如果可以,应尽量避免复杂的多继承结构,保持类设计的简单和清晰。
  • 明确继承路径:在设计类时,应该明确并记录好继承路径,特别是在处理多重继承和虚拟继承时。
  • 合理使用虚拟继承:只有在确实需要避免菱形继承问题时才使用虚拟继承,因为它也带来了额外的复杂性。


4.2 一对多继承

1对多继承是指一个基类被多个子类继承的情况。例如,一个 Person类可以被 Student、Teacher 等多个子类继承。这种继承方式比较常见,也是面向对象编程中的基础概念。

class Person{// ...
};class Teacher: public Person{// ...
};class Student : public Person{// ...
};

4.3 一 对 一 对 一 的连续继承 

        此种语法也是合法且非常常用的: 

        下面是一个 1 对 1 对 1 的连续继承的例子,其中 Derived2 类继承自 Derived1,而 Derived1 类又继承自 Base 类。每个派生类都可以扩展或修改其基类的行为。 

#include <iostream>// 基类
class Base {// ...
};// 派生类 Derived1,从 Base 继承
class Derived1 : public Base{// ...
};
// 派生类 Derived2,从 Derived1 继承
class Derived2 : public Derived1{// ...
};
继承结构:Base                                            |                Derived1               |Derived2                   

4.4 菱形继承结构

只要有多继承,就会纯在菱形继承(在实际中我们应该尽量避免菱形继承

(1) 假设有四个类,分别是 Base、A、B 和 Derived其中 A 和 B 都继承自 Base,而 Derived 同时继承自 A 和 B。此时,继承关系形成一个菱形的结构:(1) 假设有五个类,分别是 Base、A、B、C 和 Derived其中 A 和 B 都继承自 Base,C继承自B,而 Derived 同时继承自 A 和 C。此时,依旧构成菱形继承:Base                    Base                            /    \                  /    \A      B                A      B  \    /                 |      |Derived                |      C\    /(1)                    Derived (2)

下面是关于多继承和菱形继承的总结和注意事项,感兴趣的朋友可以看看:

1.多继承的复杂性:多继承会带来菱形继承的问题,而菱形继承会引发菱形虚拟继承,导致底层实现复杂化。这不仅增加了代码的理解难度,还可能导致调试和维护时的困难。

2.性能问题:多继承,特别是虚拟继承,会引入额外的指针间接访问和虚表开销,从而影响性能。虽然在大多数情况下,这些开销可能不会对性能产生显著影响,但在性能要求较高的应用中,这些开销可能变得不可忽视。

3.谨慎使用多继承避免设计出复杂的多继承结构,特别是菱形继承,是良好的编码实践。

 4.多继承可以认为是C++的缺陷之一,很多后来的oo语言都没有多继承,如Java。


4.5继承和组合使用的选择 

        继承和组合是两种常见的代码复用方式,各自有优缺点。 (此处理解需要我们今后在大量的练习和使用经验下慢慢体会)

继承(白箱复用)

  • 白箱复用:继承被称为白箱复用,因为派生类可以看到基类的内部实现。通过继承,派生类可以直接访问和扩展基类的功能。
  • 破坏封装:继承在一定程度上破坏了基类的封装,基类的任何改变都可能对派生类产生影响。因此,继承带来了较高的耦合度,派生类与基类之间的依赖性较强。
  • 适用场景:继承适合用在“是一个”(is-a)关系的场景中,比如“猫是动物”。同时,继承是实现多态性的基础,当你需要在派生类中实现基类的通用接口时,继承是必不可少的。

组合(黑箱复用)

  • 黑箱复用:组合被称为黑箱复用,因为组合类内部的对象细节对外不可见。通过组合,一个类可以拥有其他类的实例,并通过这些实例来实现复杂的功能。
  • 降低耦合度:组合类之间的依赖性较低,修改某个类通常不会影响其他类,从而提高了代码的维护性和可扩展性。
  • 优先选择:在大多数情况下,组合优于继承,因为它更灵活、更易维护。组合适合用在“有一个”(has-a)关系的场景中,比如“车有一个引擎”。

继承与组合的平衡

  • 何时使用继承:当类之间有明显的“是一个”关系,并且需要实现多态时,继承是合适的选择。比如,在一个动物分类系统中,动物 的一种,因此继承是合理的。
  • 何时使用组合:当你希望保持类之间的独立性和低耦合度时,组合更为适合。比如,如果一个类需要使用其他类的功能,但不希望依赖它们的具体实现,组合是更好的选择。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 无线通信里的一些参数(dB dBm RSRP RSRQ RSSI SIN) / 天线增益
  • TD综合教程——粒子切换和音轮(附思路和工程文件)
  • 物联网之ESP32控制GPIO输出点亮LED、闪烁LED灯
  • 数据库中LIKE 和 NOT LIKE的用法辨析
  • [Python学习日记-12] 双色球彩票程序练习(使用到列表、判断、循环等)
  • 运营有哪几种?
  • MySQL——表操作
  • Django+Vue3前后端分离学习(二)(重写User类)
  • 代码随想录-- 二叉树 -- 二叉树的所有路径
  • 未来的去中心化网络:Web3与AI的深度融合探讨
  • 操作系统:线程实现方式
  • 最大时间
  • Open3D 四元数、欧拉角及旋转向量转旋转矩阵的方式
  • application/x-www-form-urlencoded与multipart/form-data与application/json的区别
  • 多态与接口(Golang)
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • 【每日笔记】【Go学习笔记】2019-01-10 codis proxy处理流程
  • CentOS7 安装JDK
  • docker python 配置
  • ECMAScript6(0):ES6简明参考手册
  • es6
  • IOS评论框不贴底(ios12新bug)
  • JavaScript 无符号位移运算符 三个大于号 的使用方法
  • JS+CSS实现数字滚动
  • Linux CTF 逆向入门
  • Linux链接文件
  • MySQL的数据类型
  • spring-boot List转Page
  • vue-cli3搭建项目
  • 将回调地狱按在地上摩擦的Promise
  • 七牛云假注销小指南
  • 前端学习笔记之观察者模式
  • 如何解决微信端直接跳WAP端
  • 如何胜任知名企业的商业数据分析师?
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 在Unity中实现一个简单的消息管理器
  • 【运维趟坑回忆录 开篇】初入初创, 一脸懵
  • #[Composer学习笔记]Part1:安装composer并通过composer创建一个项目
  • #绘制圆心_R语言——绘制一个诚意满满的圆 祝你2021圆圆满满
  • (1) caustics\
  • (javaweb)Http协议
  • (ZT)薛涌:谈贫说富
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (力扣)1314.矩阵区域和
  • (四)c52学习之旅-流水LED灯
  • (太强大了) - Linux 性能监控、测试、优化工具
  • (转)德国人的记事本
  • *p++,*(p++),*++p,(*p)++区别?
  • .bashrc在哪里,alias妙用
  • .net core Redis 使用有序集合实现延迟队列
  • .net 程序发生了一个不可捕获的异常
  • .net 反编译_.net反编译的相关问题
  • .NET版Word处理控件Aspose.words功能演示:在ASP.NET MVC中创建MS Word编辑器
  • .NET文档生成工具ADB使用图文教程
  • .pyc文件还原.py文件_Python什么情况下会生成pyc文件?