初探类继承1


前言

本章完全参考《C++ Primer Plus6th中文版》14章前半部分。
C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。本章将介绍其他方法:

  1. 使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)
  2. 是使用私有或保护继承。

学习这部分内容,最好的方式是:

  • 先理解其思想,基类和派生类到底是什么关系
  • 什么是接口,什么是实现。将自己置身于类的设计者和类的使用者两个角度去思考
  • 最后再来掌握具体的实现方式,代码语法

一、包含对象成员的类

举一个简单的例子,Student类:
类中成员:

  • 姓名
    可以使用字符数组来表示,但这将限制姓名的长度。当然,也可以使用char指针和动态内存分配。较简单的选择是使用string类,因为C++库
    提供了这个类的所有实现代码,且其实现更完美。
  • 考试分数
    可以使用一个定长数组,这限制了数组的长度;可以使用动态内存分配并提供大量的支持代码;也可以设计一个使用动态内存分配的类来表示该数组;还可以在标准C++库中查找一个能够表示这种数据的类。C++库提供了一个这样的类,它就是valarray。

1.1 valarray类简介

这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。valarray被定义为一个模板类,以便能够处理不同的数据类型。
在这里插入图片描述
从中可知,可以创建长度为零的空数组、指定长度的空数组、所有元素度被初始化为指定值的数组、用常规数组中的值进行初始化的数组。当然也可以初始化列表:

valarray<int> v5 = {20, 32, 17,68};

这个类有一些内置方法:

  • operator:访问各个元素;
  • size(): 返回包含的元素数;
  • sum(): 返回所有元素的总和;
  • max(): 返回最大的元素;
  • min(): 返回最小的元素。
    等等,https://cplusplus.com/reference/valarray/valarray/ 。

1.2 Student类的设计

我们可能会这样想(尽管很荒谬,但是有可能)从String和valarray这两个类中派生出Student类,这将是多重公有继承,C++允许但这里并不合适!!!因为Student类和这些类不是is-a关系。这里的关系是has-a
在这里插入图片描述

Student类申明:
在这里插入图片描述
在这里插入图片描述

  • C++包含让程序员能够限制程序结构的特性——使用 explicit 防止单参数构造函数的隐式转换,使用 const 限制方法修改数据,等等。这样做的根本原因是:在编译阶段出现错误优于在运行阶段出现错误。
  • 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。

Student类实现:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


二、私有继承

在原书《C++ Primer Plus6th中文版》中,它认为私有继承是has-a的一种实现。但是我不这样认为,我认为是一种is-implemented-as-a的关系。原书中写到Student拥有string和valarray的性质,我承认确实是可以这样理解。但是我们可不可以换一种方式去理解,并且结合原书中的代码,我更加认为Student是string类和valarray类的具体实现,或者说是实现细节(is-implemented-in-terms-of)

按我的理解:

包含(composition)和私有继承(private inheritance)都是 C++ 中实现代码复用的机制,但它们在语义和用途上有一些区别:

包含(Composition):

  • 在包含中,一个类(称为组合类)包含另一个类(称为成员类)的对象作为其成员变量。
  • 这种关系通常被解释为“有一个”(has-a)关系。例如,如果一个 Car 类包含一个 Engine 类的对象作为成员,那么可以说“汽车有一个引擎”。
  • 组合类可以访问成员类的公有成员,但不能访问其保护或私有成员。
  • 组合是一种更加灵活的方式来实现代码复用,因为它允许在运行时动态改变组合类中的成员类对象。

私有继承(Private Inheritance):

  • 在私有继承中,派生类以私有方式继承基类。这意味着基类的公有和保护成员在派生类中变成了私有成员。
  • 这种关系可以被解释为“是一个实现细节”(is-implemented-in-terms-of)关系。例如,如果一个 Stack 类私有继承自一个 List 类,那么可以说“栈是以列表为实现细节的”。
  • 私有继承不应该被视为“是一个”(is-a)关系,因为派生类的对象不可以被视为基类的对象(即不具有基类的接口)。
  • 私有继承主要用于实现细节的复用,而不是接口的复用。

私有继承:使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着 基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成 员函数中使用它们。

深入讨论接口问题:

  • 使用公有继承,基类的公有方法将成为派生类的公有方法。总之,派生类将继承基类的接口;这是 is-a 关系的一部分。
  • 使用私有继承,基类的公有方法将成为派生类的私有方法。总之,派生类不继承基类的接口。因此私有继承提供的特性与包含相同:获得实现,但不获得接口。

上面有句话:基类方法将不会成为派生对象公有接口的一部分
这句话的意思是:设计Student类的设计者可以在派生类(Student类)中,使用基类的方法(public或者protected)。但是作为外来访问者是使用不了私有继承中类的接口的。
提示:当一个派生类以私有方式继承一个基类时,基类的公有成员和保护成员都会成为派生类的私有成员。这意味着派生类的对象不能直接访问这些成员,但派生类的成员函数可以在类的内部访问这些从基类继承而来的成员(包括方法和变量)详情参考初探类继承0

2.1 Student 类示例(新版本)

要进行私有继承,请使用关键字private而不是public来定义类(实际上,private是默认值,因此省略访问限定符也将导致私有继承)。Student类应从两个类派生而来,因此声明将列出这两个类:

class Student : private std::string, private std::valarray<double>{
public:
	... ...
};

在这里插入图片描述
详细解释:

  • 初始化基类方法:新的Student类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。包含版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别。会发现没有name和score这两个成员变量了。
  • **访问基类的方法:**使用私有继承时,只能在派生类的方法中使用基类的方法。但有时候可能希望基类工具是公有的。
    在这里插入图片描述
    私有继承使得能够使用类名和作用域解析运算符来调用基类的方法:
double Student::Average() const{
	if (ArrayDb::size() > 0) return ArrayDb::sum()/ArrayDb::size();
	else return 0;
}
  • **访问基类对象:**例如,Student类的包含版本实现了Name( )方法,它返回string对象成员name;但使用私有继承时,该string对象没有名称。那么,Student类的代码如何访问内部的string对象呢?
    答案是使用强制类型转换。由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结
    果为继承而来的string对象。本书前面介绍过,指针this指向用来调用方法的对象,因此*this为用来调用方法的对象,在这个例子中,为类型为Student的对象。为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:
const string & Student::Name() const{
	return (const string &) *this;
}

// 上述方法返回一个引用,该引用指向用于调用该方法的Student对象中的继承而来的string对象。
  • **访问基类的友元函数:**用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。例如,对于下面的友元函数定义:
ostream & operator<<(ostream & os, const Student & stu){
	os << "Scores for " << (cout String &) stu << ":\n";
	... ...
}

//显式地将stu转换为string对象引用,进而调用函数operator<<(ostream &, const String &)。

引用stu不会自动转换为string引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。

思考:使用包含还是私有继承

在 C++ 中,选择使用包含(组合)还是私有继承主要取决于你的具体需求和设计目的。以下是一些指导原则,可以帮助你做出决定:

  1. 使用场景:

    • 包含(组合):如果你想表示一个“有一个”的关系(例如,学生“有一个”姓名),或者你想重用一个类的实现而不是它的接口,那么使用包含是更合适的。包含是实现代码重用的首选方式。
    • 私有继承:如果你想要重用一个类的接口和实现,并且你确定派生类是一个特殊类型的基类(但你不想在派生类的接口中暴露基类的接口),那么可以考虑使用私有继承。私有继承通常用于实现细节的隐藏和封装。
  2. 接口暴露:

    • 包含:成员对象的公有接口不会成为包含它的类的接口的一部分,除非你显式地提供访问这些成员的方法。
    • 私有继承:基类的公有和保护成员在派生类中变成了私有成员,这意味着它们不会成为派生类的公有接口的一部分,但可以在派生类内部使用。
  3. 语义清晰性:

    • 包含:在语义上更清晰,表示一个类是另一个类的一部分或拥有另一个类的实例。
    • 私有继承:可能会导致一些语义上的混淆,因为它表达的是一种“是一个”关系,但又隐藏了这种关系。
  4. 灵活性和维护性:

    • 包含:通常提供更好的灵活性和维护性。如果将来你需要更改成员对象的类型或实现,通常不需要修改包含它的类的接口。
    • 私有继承:如果基类发生变化,可能会影响到派生类的实现,尽管派生类的公有接口不会受到影响。

总的来说,包含(组合)通常是首选的设计选择,因为它提供了更好的封装和灵活性。私有继承应该在有特定需求且明确需要继承的特性时才考虑使用。在实际设计中,建议尽量使用包含来实现代码的重用和类之间的关系,只在确实需要继承的特性时才使用继承(无论是公有还是私有)。

2.2 使用保护继承(protected)

保护继承是私有继承的变体。

class Student : protected std::string, protected std::valarray<double>{... ...};

使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。

相同点:

  • 和私有私有继承一样,基类的接口在派生类中也是可用的。

差异点:

  • 在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。

    • 使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;(简单来说,孙子辈以下的都不能继承爷爷辈的财产)
    • 使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

在这里插入图片描述

2.3 使用using重新定义访问权限

使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,两种方法:

  1. 定义一个使用该基类方法的派生类方法。例如,假设希望Student类能够使用valarray类的sum( )方法,可以在Student类的声明中声明一个sum( )方法,然后像下面这样定义该方法:
double Student::sum() const   // public Student method
{
	return std::valarray<double>::sum();   // using privately-inherited method
}

// 这样Student对象便能够调用Student::sum( ),后者进而将valarray<double>::sum( )方法应用于被包含的valarray对象(
  1. 另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。例如,假设希望通过Student类能够使用valarray的方法min( )和max( ),可以在studenti.h的公有部分加入如下using声明:
class Student : private std::string, private std::valarray<double>{
... ...
public:
	using std::valarray<double>::min;
	using std::valarray<double>::max;
}

// 上述using声明使得valarray<double>::min()可用,就像它们是Student的公有方法一样

注意:using声明只使用成员名——没有圆括号、函数特征标和返回类型。


总结

在面向对象编程中,接口(外部人员能否调用,即生成的对象能否调用)和实现(类的设计者能否定义)是两个重要的概念:

  1. 接口(Interface):一个类的接口是指它对外提供的一组公有的方法(函数)和属性(变量)。这些方法和属性定义了其他代码可以如何与该类的对象进行交互。接口描述了类的行为,但不涉及具体的实现细节。例如,在一个形状类中,getArea()getPerimeter() 方法可以是它的接口的一部分,因为它们定义了形状的基本行为,但不涉及这些行为是如何实现的。

  2. 实现(Implementation):一个类的实现是指它的内部逻辑,即它的方法是如何被定义和执行的。实现包括了类的私有属性和方法的具体代码,这些代码实现了类的接口所定义的行为。继续上面的形状类的例子,getArea() 方法的具体计算公式(如 π * radius * radius 对于圆形)就是这个方法的实现。

公有继承:

  • 继承接口:派生类继承了基类的公有接口。这意味着派生类的对象可以使用基类中定义的公有方法和属性。在上述形状类的例子中,如果有一个子类 Circle 继承自基类 Shape,那么 Circle 类的对象可以使用 getArea()getPerimeter() 方法,因为它们是 Shape 类的接口的一部分。

  • 可能还有实现:派生类不仅继承了基类的接口,还继承了基类的实现。这意味着如果基类中的方法有具体的实现代码,那么在派生类中,这些方法默认会沿用这些实现代码。派生类可以选择保留这些实现,也可以重写(override)这些方法来提供自己的实现。

公有继承体现了“是一个”(is-a)的关系,即派生类是基类的一个特殊版本。通过公有继承,派生类不仅可以重用基类的代码,还可以通过扩展和定制来增强自己的功能。

包含(Composition):

是另一种常见的面向对象设计技术,它用于表示一种“有一个”(has-a)关系。在包含关系中,一个类(称为容器类)包含另一个类的对象作为其成员变量。这种关系意味着容器类拥有被包含类的对象,并且可以使用这个对象来扩展自己的功能。

  • 获得实现:通过包含或组合,一个类(容器类)可以在其内部持有另一个类(成员类)的一个实例。容器类可以利用这个成员类实例的方法和属性来实现自己的功能。这意味着容器类可以重用成员类的代码,从而获得其实现。

  • 不获得接口:尽管容器类获得了成员类的实现,但成员类的接口并不自动成为容器类的接口的一部分。换句话说,成员类的公有方法不会直接成为容器类的公有方法。如果容器类想要向外界提供成员类的功能,它需要显式地在自己的接口中定义相应的方法,并在这些方法中调用成员类的相应方法。

这种设计方式有助于封装和解耦,使得容器类和成员类可以独立变化而不互相影响,从而提高了代码的可维护性和可重用性。

私有继承:

在私有继承中,派生类以私有的方式继承基类。这意味着基类的公有和保护成员在派生类中变成了私有成员。这对于派生类的实现和接口有以下影响:

  1. 获得实现:派生类获得了基类的实现,因为它可以在自己的内部使用基类的方法和属性(包括公有和保护的)。这意味着派生类可以重用基类的代码来实现自己的功能,就像在包含或组合关系中一样。

  2. 不获得接口:尽管派生类获得了基类的实现,但基类的接口并不直接成为派生类的接口的一部分。这是因为基类的公有成员在派生类中变成了私有成员,所以它们不能被派生类的对象在外部直接访问。如果派生类想要向外界提供基类的功能,它需要显式地在自己的公有接口中定义相应的方法,并在这些方法中调用基类的相应方法。

私有继承通常用于实现细节的封装,而不是表示两个类之间的“是一个”关系。它允许派生类在内部利用基类的功能,同时隐藏这些功能的细节,从而提高了代码的封装性和可维护性。

最后小节:

当然!以下是对公有继承、包含(组合)、和私有继承在实现和接口方面的总结:

  1. 公有继承

    • 获得实现:派生类继承了基类的所有公有和保护成员,包括方法和属性。
    • 获得接口:基类的公有成员成为派生类的接口的一部分,这意味着派生类的对象可以直接调用这些继承来的方法。
  2. 包含(组合)

    • 获得实现:容器类包含成员类的一个实例,可以在内部使用这个实例的方法和属性来实现自己的功能。
    • 不获得接口:成员类的接口不自动成为容器类的接口的一部分。如果容器类想向外界提供成员类的功能,需要显式定义自己的接口。
  3. 私有继承

    • 获得实现:派生类继承了基类的所有公有和保护成员,但它们在派生类中变成了私有成员。
    • 不获得接口:基类的公有成员不会成为派生类的接口的一部分。派生类需要显式定义自己的公有接口来提供外界访问的功能。

在选择这三种关系中的哪一种时,通常考虑以下因素:

  • 公有继承:用于表示“是一个”(is-a)关系,适用于派生类需要扩展基类的功能或需要基类的接口的场景。
  • 包含(组合):用于表示“有一个”(has-a)关系,适用于需要重用另一个类的实现但不需要其接口的场景。
  • 私有继承:用于实现细节的封装,适用于需要重用另一个类的实现和接口但不想将其暴露给外界的场景。参考用Array类来实现Stack类

具体语法则围绕着:

  • 实现函数的定义,具体点就是怎么在类设计中调用基类的函数,以及自己重写定义。
  • 实现基类的公有接口。

参考文献:

  1. 《C++ Primer Plus 6th中文版》
  2. 查漏补缺——C/C++(类0)
  3. 初探类继承0
  4. https://github.com/ShujiaHuang/Cpp-Primer-Plus-6th
  5. C++ 一篇搞懂继承的常见特性(小林coding)
  • 34
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值