本文从三个级别描述继承:初学者,中级和高级。 专家号 而且没有关于SOLID的一句话。 老实说
初学者
什么是继承?
继承是OOP的基本原则之一。 根据它,一个类可以使用另一个类的变量和方法作为自己的类。
继承数据的类称为子类,派生类或子类。 继承数据或方法的类称为超类,基类或父类。 术语“父母”和“孩子”对于理解继承非常有用。 当孩子接受其父母的特征时,派生类将接受基类的方法和变量。
继承非常有用,因为它允许您构造和重用代码,而代码又可以重用 可以大大加快开发过程。 尽管如此,继承仍应谨慎使用,因为超类的大多数更改都会影响所有子类,这可能导致无法预料的后果。
在此示例中,未在Computer
子类中声明或定义turn_on()
方法和serial_number
变量。 但是,可以使用它们,因为它们是从基类继承的。
重要说明 :私有变量和方法不能被继承。
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } private: int pincode = 87654321; }; class Computer: public Device {}; int main() { Computer Computer_instance; Computer_instance.turn_on(); cout << "Serial number is: " << Computer_instance.serial_number << endl; // cout << "Pin code is: " << Computer_instance.pincode << endl; // will cause compile time error return 0; }
继承类型
C ++中有几种继承类型:
- 公共-公共(
public
)和受保护( protected
)数据是在不更改对其访问级别的情况下继承的; - protected(
protected
)-所有继承的数据都受到保护; - 私有-所有继承的数据都变为私有。
对于Device
基类,数据访问级别不会改变,但是由于Computer
派生类将数据继承为私有数据,因此该数据对于Computer
类而言变为私有。
#include <iostream> using namespace std; class Device { public: int serial_number = 12345678; void turn_on() { cout << "Device is on" << endl; } }; class Computer: private Device { public: void say_hello() { turn_on(); cout << "Welcome to Windows 95!" << endl; } }; int main() { Device Device_instance; Computer Computer_instance; cout << "\t Device" << endl; cout << "Serial number is: "<< Device_instance.serial_number << endl; Device_instance.turn_on(); // cout << "Serial number is: " << Computer_instance.serial_number << endl; // Computer_instance.turn_on(); // will cause compile time error cout << "\t Computer" << endl; Computer_instance.say_hello(); return 0; }
现在, Computer
类与任何私有方法一样,使用turn_on()
方法:可以从该类内部调用turn_on()
,但是尝试直接从main
调用它会在编译时导致错误。 对于基类Device
, turn_on()
方法保持公共状态,可以从main
调用。
构造函数和析构函数
在C ++中,不继承构造函数和析构函数。 但是,当子类初始化其对象时将调用它们。 从基类开始到最后一个派生类结束,构造函数被逐层调用。 析构函数的调用顺序相反。
重要说明:本文不介绍虚拟析构函数。 可以在例如Habr的本文中找到有关此主题的其他材料。
#include <iostream> using namespace std; class Device { public: // constructor Device() { cout << "Device constructor called" << endl; } // destructor ~Device() { cout << "Device destructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } ~Computer() { cout << "Computer destructor called" << endl; } }; class Laptop: public Computer { public: Laptop() { cout << "Laptop constructor called" << endl; } ~Laptop() { cout << "Laptop destructor called" << endl; } }; int main() { cout << "\tConstructors" << endl; Laptop Laptop_instance; cout << "\tDestructors" << endl; return 0; }
构造函数: Device
-> Computer
-> Laptop
。
析构函数: Laptop
-> Computer
-> Device
。
多重继承
当子类具有两个或多个超类时,会发生多重继承。 在此示例中, Laptop
类同时继承Monitor
和Computer
。
#include <iostream> using namespace std; class Computer { public: void turn_on() { cout << "Welcome to Windows 95" << endl; } }; class Monitor { public: void show_image() { cout << "Imagine image here" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.show_image(); return 0; }
多重继承问题
多重继承需要仔细的设计,因为它可能导致无法预料的后果。 这些后果大多数是由继承中的模棱两可引起的。 在此示例中, Laptop
从两个父级继承了turn_on()
方法,尚不清楚应调用哪个方法。
#include <iostream> using namespace std; class Computer { private: void turn_on() { cout << "Computer is on." << endl; } }; class Monitor { public: void turn_on() { cout << "Monitor is on." << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will cause compile time error return 0; }
尽管不继承私有数据,但通过将对数据的访问级别更改为私有来解决歧义继承是不可能的。 编译时,首先搜索方法或变量,然后搜索方法或变量的访问级别。
菱形问题

在支持多重继承的语言中,钻石问题是经典问题。 当类B
和C
继承A
,而类D
继承B
和C
时,会发生此问题C
例如,类A
, B
和C
定义了print_letter()
方法。 如果类D
将调用print_letter()
,则不清楚应调用哪种方法-类A
, B
或C
不同的语言具有解决菱形问题的不同方法。 在C ++中,问题的解决方案由程序员自行决定。
菱形问题主要是设计问题,应在设计阶段提出。 在开发阶段,可以解决以下问题:
- 调用特定超类的方法;
- 将子类的对象称为特定超类的对象;
- 覆盖最后一个子类中有问题的方法(在代码中,
Laptop
子类中的turn_on()
)。
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } }; class Computer: public Device {}; class Monitor: public Device {}; class Laptop: public Computer, public Monitor { /* public: void turn_on() { cout << "Laptop is on." << endl; } // uncommenting this function will resolve diamond problem */ }; int main() { Laptop Laptop_instance; // Laptop_instance.turn_on(); // will produce compile time error // if Laptop.turn_on function is commented out // calling method of specific superclass Laptop_instance.Monitor::turn_on(); // treating Laptop instance as Monitor instance via static cast static_cast<Monitor&>( Laptop_instance ).turn_on(); return 0; }
如果在Laptop中没有覆盖turn_on()
方法,则调用Laptop_instance.turn_on()
将导致编译错误。 Laptop
对象可以同时访问两个turn_on()
方法turn_on()
: Device:Computer:Laptop.turn_on()
和Device:Monitor:Laptop.turn_on()
。
钻石问题:构造函数和析构函数
由于在C ++中,当子类的对象被初始化时,所有父类的构造函数都被调用,因此出现另一个问题:基类Device
的构造函数将被调用两次。
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } }; class Computer: public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; return 0; }
虚拟继承
虚拟继承可防止多个基类对象出现在继承层次结构中。 因此,基类Device
的构造函数将仅被调用一次,并且调用turn_on()
方法而不在子类中对其进行重写不会导致编译错误。
#include <iostream> using namespace std; class Device { public: Device() { cout << "Device constructor called" << endl; } void turn_on() { cout << "Device is on." << endl; } }; class Computer: virtual public Device { public: Computer() { cout << "Computer constructor called" << endl; } }; class Monitor: virtual public Device { public: Monitor() { cout << "Monitor constructor called" << endl; } }; class Laptop: public Computer, public Monitor {}; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); return 0; }
注意 :如果子类Laptop
不真正继承Device
类( class Laptop: public Computer, public Monitor, public Device {};
),则Computer
和Monitor
类中的虚拟继承将不允许菱形继承。
抽象类
在C ++中,至少存在一个纯虚方法的类被视为抽象类。 如果在子类中未覆盖虚拟方法,则代码将无法编译。 同样,在C ++中,不可能创建抽象类的对象-尝试也会在编译期间导致错误。
#include <iostream> using namespace std; class Device { public: void turn_on() { cout << "Device is on." << endl; } virtual void say_hello() = 0; }; class Laptop: public Device { public: void say_hello() { cout << "Hello world!" << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); Laptop_instance.say_hello(); // Device Device_instance; // will cause compile time error return 0; }
介面
与某些OOP语言不同,C ++不提供单独的关键字来表示接口。 但是,可以通过创建一个纯抽象类(仅包含方法声明的类)来实现接口。 此类通常也称为抽象基类(ABC)。
#include <iostream> using namespace std; class Device { public: virtual void turn_on() = 0; }; class Laptop: public Device { public: void turn_on() { cout << "Device is on." << endl; } }; int main() { Laptop Laptop_instance; Laptop_instance.turn_on(); // Device Device_instance; // will cause compile time error return 0; }
进阶
尽管继承是OOP的基本原则,但应谨慎使用。 务必要考虑的是,将要使用的任何代码都可能会更改,并且可以以对开发人员不明显的方式使用。
从已实现或部分实现的类继承
如果继承不是来自接口(在C ++上下文中为纯抽象类),而是来自存在任何实现的类,则应考虑将继承人通过最可能的连接方式连接到父类。 对父类的大多数更改都可能影响继承人,从而可能导致意外行为。 继承人行为的这种变化并不总是很明显-在已经测试过且有效的代码中可能会发生错误。 复杂的类层次结构的存在加剧了这种情况。 永远值得记住的是,不仅可以由编写代码的人来更改代码,而且对于他的同事而言,可能不考虑对作者而言显而易见的继承路径。
相比之下,值得注意的是,从部分实现的类继承具有不可否认的优势。 库和框架通常按以下方式工作:它们为用户提供一个抽象类,其中包含几种虚拟方法和许多已实现的方法。 因此,已经完成了最大的工作量-已经编写了复杂的逻辑,并且用户只能根据自己的需求定制现成的解决方案。
介面
接口(纯抽象类)的继承将继承作为构造代码和保护用户的机会。 由于该接口描述了实现类将执行的工作,但没有描述实现类,因此该接口的任何用户都可以免受实现此接口的类的更改的影响。
界面:用法示例
首先,值得注意的是,该示例与多态性概念密切相关,但是将在从纯抽象类继承的上下文中考虑该示例。
必须从单独的配置文件配置运行抽象业务逻辑的应用程序。 在开发的早期阶段,该配置文件的格式尚未完全形成。 在接口后面传递文件解析具有多个优点。
关于配置文件的格式缺乏清晰性并不会减慢主程序的开发过程。 两名开发人员可以并行工作-一名负责业务逻辑,另一名负责解析器。 由于他们通过此界面进行交互,因此每个人都可以独立工作。 这种方法使使用代码对单元测试进行编码变得更加容易,因为可以使用模拟为该接口编写必要的测试。
同样,当更改配置文件格式时,应用程序的业务逻辑不受影响。 唯一需要从一种格式完全过渡到另一种格式的事情是编写一个已经存在的抽象类(解析器类)的新实现。 此外,返回原始文件格式所需的工作最少,只需将一个现有的解析器替换为另一个即可。
结论
继承有很多好处,但是必须谨慎设计以避免产生机会的问题。 在继承的上下文中,C ++提供了广泛的工具,为程序员打开了无数的可能性。
SOLID很好。