在 C++ 中开发接口类

大家好!我是大聪明-PLUS

介绍

接口类是一种不包含任何数据且主要由纯虚函数构成的类。这种方案实现了实现与接口的完全分离:客户端使用接口类,而其他地方则创建一个派生类,在该派生类中重写纯虚函数并定义一个工厂函数。实现细节对客户端完全隐藏。这实现了真正的封装,而普通类则无法做到这一点。接口类也称为协议类。

使用接口类可以解耦项目不同部分之间的依赖关系,从而简化团队开发并缩短编译和构建时间。接口类简化了灵活、动态解决方案的实现,在这些解决方案中,模块可以在运行时选择性地加载。将接口类用作库(SDK)的 API 可以简化二进制兼容性问题。

接口类广泛用于实现库(SDK)的API、插件接口等等。许多四人帮(GoF)设计模式自然而然地使用接口类来实现。COM接口也可以被视为接口类。然而,不幸的是,基于接口类的解决方案在实现过程中常常会出错。让我们来阐明这个问题。

1. 特殊成员函数、对象创建和删除

本节简要介绍一些 C++ 特性,您需要了解这些特性才能充分理解针对接口类提出的解决方案。

1.1 特殊成员职能

如果程序员没有定义以下列表中的类成员函数------默认构造函数、复制构造函数、复制赋值运算符、析构函数------编译器可以为其生成这些函数。C++11 将移动构造函数和移动赋值运算符添加到了此列表中。这些成员函数被称为特殊成员函数。它们仅在被使用且满足特定于每个函数的附加条件时才会生成。请注意,这种用法可能相当隐蔽(例如,在实现继承时)。如果无法生成所需的函数,则会抛出错误。(移动操作除外,它们会被复制操作替代。)编译器生成的成员函数是公共的且内联的。

特殊成员函数不会被继承;如果派生类中需要特殊成员函数,编译器总是会尝试生成它;基类中是否存在程序员定义的相应成员函数并不会对此产生影响。

程序员可以禁用特殊成员函数的生成。在 C++11 中,这需要在声明特殊成员函数时使用 `[default]` 结构"=delete"。在 C++98 中,相应的成员函数必须声明为私有且未定义。当存在类继承时,在基类中禁用特殊成员函数的生成会影响所有派生类。

如果程序员对编译器生成的成员函数感到满意,在 C++11 中,他们可以显式地表明这一点,而无需简单地省略声明。为此,可以使用声明结构"=default"。这可以提高代码的可读性,并提供额外的访问控制功能。

1.2 创建和删除对象 - 基本细节

使用运算符创建和删除对象new/delete是一种典型的二合一操作。调用运算符时,new首先会为对象分配内存。如果分配成功,则调用构造函数。如果构造函数抛出异常,则释放已分配的内存。调用析构函数时,delete过程则相反:首先调用析构函数,然后释放内存。析构函数不能抛出异常。

当使用运算符new创建对象数组时,首先会为整个数组分配内存。如果分配成功,则会从元素零开始,对每个数组元素调用默认构造函数。如果任何构造函数抛出异常,则会按照构造函数调用的相反顺序,对所有已创建的数组元素调用析构函数,然后释放已分配的内存。要删除数组,必须调用一个运算符delete[](称为delete数组运算符)。在这种情况下,也会按照构造函数调用的相反顺序,对所有数组元素调用析构函数,然后释放已分配的内存。

重要提示!您必须根据要删除的是单个对象还是数组来调用正确形式的运算符delete。必须严格遵守此规则,否则可能会导致未定义行为,这意味着可能会发生任何事情:内存泄漏、崩溃等等。

如果标准内存分配函数无法满足请求,则会抛出异常std::bad_alloc

任何形式的运算符delete都可以安全地应用于空指针。

上述描述需要澄清一点。对于所谓的平凡类型(内置类型、C风格结构),构造函数可能不会被调用,析构函数在任何情况下都不会执行任何操作。另请参阅第1.6节。

1.3. 析构函数访问级别

当运算符delete应用于类指针时,该类的析构函数必须在调用时可访问delete。(此规则存在例外,详见 1.6 节。)因此,通过将析构函数声明为 protected 或 private,程序员可以禁止在delete析构函数不可访问的情况下使用该运算符。需要注意的是,如果一个类没有定义析构函数,编译器会自动定义一个,并且该析构函数将是 public 的(参见 1.1 节)。

1.4. 单个模块中的创建和删除

如果一个运算符new创建了一个对象,那么调用该运算符delete删除该对象的操作必须位于同一模块中。打个比方,"把它放回你找到它的地方"。这条规则众所周知;违反此规则会导致内存分配和释放函数不匹配,这通常会导致程序崩溃。

1.5 多态性缺失

如果设计一个多态类层次结构,其实例通过析构运算符删除delete,则基类必须具有公共虚析构函数。这确保当析构运算符应用于delete基类指针时,实际调用的是对象类型的析构函数。违反此规则可能会导致调用基类析构函数,从而造成资源泄漏。

1.6. 类声明不完整时的删除

该运算符的"全能性"可能会引发一些问题delete。它可以应用于类型指针void*或类指针,即使它们的声明不完整(前向声明)。在这种情况下,不会发生错误;析构函数调用会被直接跳过,而只调用内存释放函数。我们来看一个例子:

复制代码
class` `X`; 
`X*` `CreateX`();

`void` `Foo`()
{
    `X*` `p` `=` `CreateX`();
    `delete` `p`;
}`

即使在调用点delete没有完整的类声明,这段代码也能编译通过X。但是,在 Visual Studio 中编译时,会发出警告:

warning C4150: deletion of pointer to incomplete type 'X'; no destructor called

如果存在X`and`的实现CreateX(),则代码会被链接;如果CreateX()它返回指向由 `and` 运算符创建的对象的指针newFoo()则调用成功,析构函数不会被调用。显然,这可能导致资源泄漏,因此再次强调,密切关注警告信息至关重要。

这种情况并非牵强附会;在使用智能指针类或描述符类时很容易出现这种情况。

2. 纯虚函数和抽象类

接口类的概念源于 C++ 中的一些概念,例如纯虚函数和抽象类。

2.1 纯虚函数

使用该构造声明的虚函数"=0"称为纯虚函数。

复制代码
class` `X`
{
`// ...`
    `virtual` `void` `Foo`() `=` `0`;
};`

与普通虚函数不同,纯虚函数不能被定义(析构函数除外,参见第 2.3 节),但必须在派生类之一中被重写。

2.2 抽象类

抽象类是指至少包含一个纯虚函数的类。从抽象类派生而来且未重写至少一个纯虚函数的类也是抽象类。C++ 标准禁止创建抽象类的实例;只能创建派生的非抽象类的实例。因此,抽象类通常用作基类。相应地,如果抽象类定义了构造函数,则将其声明为 public 是没有意义的;它必须声明为 protected。

2.3. 纯虚析构函数

在某些情况下,将析构函数设为纯虚函数是合理的。然而,这种解决方案有两个缺点。

  1. 必须定义一个纯虚析构函数。(通常使用默认定义,即使用构造函数"=default"。)派生类的析构函数会调用继承链中所有基类的析构函数,因此保证队列最终会到达根节点------纯虚析构函数。
  2. 如果程序员没有在派生类中重写纯虚析构函数,编译器会自动执行此操作(参见 1.1 节)。因此,即使没有显式重写析构函数,从具有纯虚析构函数的抽象类派生的类也可能失去抽象性。

第 4.4 节提供了一个使用纯虚析构函数的示例。

3. 接口类

接口类是一种抽象类,它不包含任何数据,主要由纯虚函数构成。这类类也可能包含常规虚函数(非纯虚函数),例如析构函数。它还可能包含静态成员函数,例如工厂函数。

3.1. 实现

接口类的实现是一个派生类,其中纯虚函数被重写。同一个接口类可以有多个实现,并且有两种可能的模式:水平模式,即多个不同的类继承同一个接口类;垂直模式,即接口类是多态层次结构的根。当然,混合模式也是可能的。

接口类概念的关键在于将接口与实现完全分离------客户端只能使用接口类,而无法访问实现。

3.2 创建对象

实现类的缺失会导致创建对象时出现一些问题。客户端必须创建实现类的实例,并获取指向接口类的指针,才能访问该对象。由于实现类不可用,构造函数无法使用,因此需要使用在实现端定义的工厂函数。该函数通常使用运算符创建对象new,并返回指向已创建对象的指针,该指针会被强制转换为指向接口类的指针。工厂函数可以是接口类的静态成员,但这并非必需;例如,它可以是特殊工厂类(其本身也可以是接口类)的成员,或者是一个自由函数。工厂函数可以返回指向接口类的智能指针,而不是原始指针。此选项将在 3.3.4 节和 4.3.2 节中讨论。

3.3 删除对象

删除对象是一项极其敏感的操作。如果操作不当,可能会导致内存泄漏或重复删除,而这通常会导致程序崩溃。下文将详细讨论此问题,并重点关注如何防止客户端执行错误操作。

主要有四种选择:

  1. 使用运算符delete
  2. 使用特殊的虚拟函数。
  3. 使用外部函数。
  4. 使用智能指针自动删除。

3.3.1 使用运算符delete

为了实现这一点,接口类必须有一个公共虚析构函数。在这种情况下delete,客户端对接口类指针调用的操作符会确保调用实现类的析构函数。这种方法虽然可行,但并不理想。我们在"屏障"的两侧------实现端和客户端------都会遇到操作符调用newdelete而且newdelete如果接口类的实现是在单独的模块中实现的(这种情况很常见),我们就违反了 1.4 节中的规则。

3.3.2 使用特殊虚函数

更先进的方案是创建一个接口类,该类包含一个专门用于删除对象的虚函数。这个函数最终会调用 `[ delete this]`,但这发生在实现层面。这个函数可以有多种名称,例如 `[ ]` ,Delete()但也使用其他变体:Release()`[ Destroy(), ...`Dispose()``Free()``Close()

  1. 允许实现类使用自定义内存分配/释放函数。
  2. 允许您实现更复杂的方案来管理实现对象的生命周期,例如使用引用计数器。

在这种情况下,尝试使用 `delete` 运算符删除对象delete可能编译甚至执行,但这会引发错误。为了防止这种情况,接口类中一个空的或纯虚的受保护析构函数就足够了(参见 1.3 节)。请注意,`delete` 运算符的使用delete可能非常隐蔽;例如,标准的智能指针默认使用 `delete` 运算符删除对象delete,而相应的代码则深深地隐藏在它们的实现中。受保护的析构函数允许在编译时检测到所有此类尝试。

3.3.3 使用外部函数

虽然该方案在创建和删除对象方面具有一定的对称性,但与之前的方案相比,它并无实际优势,反而引入了许多新的问题。因此,不建议使用该方案,也不会对其进行进一步的考虑。

3.3.4 使用智能指针自动删除

在这种情况下,工厂函数返回的不是指向接口类的原始指针,而是相应的智能指针。该智能指针在实现端创建,并封装了一个删除器对象。当客户端的智能指针(或其最后一个副本)超出作用域时,该删除器对象会自动删除实现对象。在这种情况下,可能不需要专门用于删除实现对象的虚函数,但仍然需要受保护的析构函数来防止错误使用 `delete` 运算符delete。(不过,需要注意的是,这种错误的概率已显著降低。)此选项将在 4.3.2 节中详细讨论。

3.4 管理实现类实例生命周期的其他选项

在某些情况下,客户端可能会收到指向接口类的指针,但并不拥有该接口类的所有权。实现对象的生命周期完全由实现方管理。例如,该对象可以是静态单例(这种解决方案在工厂模式中很常见)。另一个例子涉及双向交互;参见第 3.7 节。客户端不应删除此类对象,但此类接口类必须具有受保护的析构函数,以防止错误地使用 `destruct` 运算符delete

3.5. 复制语义

对于接口类,无法使用复制构造函数创建实现对象的副本。因此,如果需要复制,该类必须有一个虚函数,该虚函数创建实现对象的副本并返回指向接口类的指针。这样的函数通常称为虚构造函数,其传统名称为 ` Clone()init` Duplicate()

使用复制赋值运算符并不被禁止,但并非明智之举。复制赋值运算符必须与复制构造函数配对使用。编译器生成的默认运算符毫无意义,它什么也不做。理论上,可以将赋值运算符声明为纯虚函数并对其进行重写,但虚赋值并非推荐做法;此外,这种赋值方式看起来相当不自然:实现类的对象通常是通过指向接口类的指针来访问的,因此赋值语句会是这样的:

复制代码
*х` `=` `*у`;`

最好禁止赋值运算符,如果需要这种语义,则在接口类中设置相应的虚函数。

有两种方法可以阻止分配。

  1. 将赋值运算符声明为已删除(=delete)。如果接口类构成层次结构,则在基类中这样做就足够了。这种方法的缺点是它会影响实现类,并且该禁止操作同样适用于实现类。
  2. 声明一个受保护的赋值运算符,并为其定义一个默认定义(=default)。这不会影响实现类,但在接口类层次结构的情况下,每个类都必须进行这样的声明。

3.6 接口类构造函数

通常情况下,接口类的构造函数不会被声明。在这种情况下,编译器会生成一个默认构造函数,这是实现继承所必需的(参见 1.1 节)。这个构造函数是公共的,但只要声明为受保护的即可。如果接口类中的复制构造函数被声明为删除(`deleted` =delete),则编译器不会生成默认构造函数,必须显式声明该构造函数。当然,它应该使用默认定义(`default`)进行保护=default。原则上,声明这样的受保护构造函数总是允许的。4.4 节提供了一个示例。

3.7 双向交互

接口类便于组织双向交互。如果一个模块可以通过接口类访问,客户端也可以创建某些接口类的实现,并将指向这些实现的指针传递给模块。通过这些指针,模块可以接收来自客户端的服务,也可以向客户端发送数据或通知。

3.8. 智能指针

由于实现类的对象通常通过指针访问,因此很自然地会使用智能指针来管理它们的生命周期。但是,需要注意的是,如果使用第二种删除对象的方法,则必须将自定义删除器(类型)或该类型的实例传递给标准智能指针。否则,智能指针将使用 `delete` 运算符删除对象delete,而代码将无法编译(因为析构函数是受保护的)。

如果接口类支持引用计数器,那么建议不要使用标准的智能指针,而是使用专门为此编写的智能指针;这很容易做到。

3.9 常数成员函数

将接口类的成员函数声明为 const 时应格外谨慎。接口类的一个主要优点是能够将接口与实现完全分离,但成员函数 const 属性带来的限制可能会在开发实现类时造成问题。

3.10. COM接口

COM接口是接口类的一个例子,但需要注意的是,COM是一个与语言无关的标准,COM接口可以用多种语言实现,例如没有析构函数或受保护成员的C语言。在C++中开发COM接口必须遵循COM技术定义的规则。

3.11. 接口类和库

接口类通常被用作整个库(SDK)的接口(API)。在这种情况下,遵循以下方案是合理的。库包含一个可访问的工厂函数,该函数返回指向接口工厂类的指针,该指针用于创建其他接口类的实现类的实例。对于支持显式导出规范的库(例如 Windows DLL),只需要一个导出点:即前面提到的工厂函数。库接口的其余部分可以通过虚函数表访问。这种方案能够以最简单的方式实现灵活的动态解决方案,其中模块可以在运行时选择性地加载。模块通过LoadLibrary()`[或其他平台上的等效方法]` 加载,然后获取工厂函数的地址,库即可完全访问。

4. 接口类及其实现示例

4.1 接口类

由于接口类很少只有一个,因此通常建议创建一个基类。

复制代码
class` `IBase`
{
`protected`:
    `virtual` `~IBase`() `=` `default`; 

`public`:
    `virtual` `void` `Delete`() `=` `0`;  

    `IBase&` `operator=`(`const` `IBase&`) `=` `delete`; 
};`

这是一个示例接口类。

复制代码
class` `IActivatable` : `public` `IBase`
{
`protected`:
    `~IActivatable`() `=` `default`; 

`public`:
    `virtual` `void` `Activate`(`bool` `activate`) `=` `0`;

    `static` `IActivatable*` `CreateInstance`(); 
};`

请注意,基类和接口类中都必须存在受保护的析构函数。基类需要受保护的析构函数,是因为在某些情况下,客户端可能会使用指向该析构函数的指针IBase。接口类也需要受保护的析构函数,因为如果没有受保护的析构函数,编译器将生成一个默认的公共析构函数(参见 1.3 节)。只需在基类中禁止赋值即可;该禁止会传播到所有派生类。

4.2. 实现类

复制代码
class` `Activator` : `private` `IActivatable`
{
`// ...`
`private`:
    `Activator`();

`protected`:
    `~Activator`();

`public`:
    `void` `Delete`() `override`;
    `void` `Activate`(`bool` `activate`) `override`;

    `friend` `IActivatable*` `IActivatable::CreateInstance`();
};

`Activator::Activator`() {`/* ... */`}

`Activator::~Activator`() {`/* ... */`}

`void` `Activator::Delete`() { `delete` `this`; }

`void` `Activator::Activate`(`bool` `activate`) {`/* ... */`}

`IActivatable*` `IActivatable::CreateInstance`()
{
    `return` `static_cast<IActivatable*>`(`new` `Activator`());
}`

在实现类中,析构函数是受保护的,构造函数以及从接口类的继承是私有的,工厂函数被声明为友元,这确保了创建和删除对象的过程的最大程度封装。

4.3. 标准智能标牌

4.3.1. 客户端创建

在客户端创建智能指针时,需要使用自定义删除器。删除器类非常简单(可以嵌套IBase):

复制代码
struct` `BaseDeleter`
{
    `void` `operator`()(`IBase*` `p`) `const` { `p->Delete`(); }
};`

对于std::unique_ptr<>删除器类,它是一个模板参数:

复制代码
template` `<class` `I>` 
`using` `UniquePtr` `=` `std::unique_ptr<I`, `BaseDeleter>`;`

请注意,由于删除器类不包含数据,因此其大小UniquePtr等于原始指针的大小。

以下是一个工厂函数模板:

复制代码
template` `<class` `I>` 
`UniquePtr<I>` `CreateInstance`()
{
    `return` `UniquePtr<I>`(`I::CreateInstance`());
}`

以下是将原始指针转换为智能指针的模板:

复制代码
template` `<class` `I>` 
`UniquePtr<I>` `ToPtr`(`I*` `p`)
{
    `return` `UniquePtr<I>`(`p`);
}`

实例std::shared_ptr<>可以使用实例进行初始化std::unique_ptr<>,因此无需定义返回的特殊函数std::shared_ptr<>。以下是创建类型为 的对象的示例Activator

复制代码
auto` `un1` `=` `CreateInstance<IActivatable>`();
`un1->Activate`(`true`);

`auto` `un2` `=` `ToPtr`(`IActivatable::CreateInstance`());
`un2->Activate`(`true`);

`std::shared_ptr<IActivatable>` `sh` `=` `CreateInstance<IActivatable>`();
`sh->Activate`(`true`);`

由于使用了受保护的析构函数(构造函数必须接受第二个参数------删除器对象),这段错误的代码无法编译:

复制代码
std::shared_ptr<IActivatable>` `sh2`(`IActivatable::CreateInstance`());`

另外,您不能使用该模板std::make_shared<>(),它不支持自定义删除器(相应的代码将无法编译)。

所描述的方案存在一个缺陷:实现对象的虚函数 delete 可以通过智能指针调用,从而导致重复删除。这个问题可以通过将虚函数 delete 设为 protected 并使用不同的删除器类来解决。示例见 4.4 节。

4.3.2. 实施方面的创建

智能指针可以在实现端创建。在这种情况下,客户端会将其作为工厂函数的返回值接收。如果std::shared_ptr<>将指向具有公共析构函数的实现类的指针传递给其构造函数,则不需要用户定义的删除器(也不需要特殊的虚函数来删除实现对象)。在这种情况下,构造函数std::shared_ptr<>(它是一个模板)会根据参数类型创建一个默认的删除器对象,并delete在删除时对指向实现对象的指针应用一个运算符。std::shared_ptr<>删除器对象是智能指针实例(或者更确切地说,是其控制块)的一部分,并且删除器对象的类型不会影响智能指针的类型。在这种情况下,前面的示例可以重写如下。

复制代码
#include <memory>`

`class` `IActivatable`;
`using` `ActPtr` `=` `std::shared_ptr<IActivatable>`;


`class` `IActivatable`
{
`protected`:
    `virtual` `~IActivatable`() `=` `default`; 
    `IActivatable&` `operator=`(`const` `IActivatable&`) `=` `default`; 

`public`:

    `virtual` `void` `Activate`(`bool` `activate`) `=` `0`;

    `static` `ActPtr` `CreateInstance`(); 
};


`class` `Activator` : `public` `IActivatable`
{
`// ...`
`public`:
    `Activator`();  
    `~Activator`(); 

    `void` `Activate`(`bool` `activate`) `override`;
};

`Activator::Activator`() {`/* ... */`}

`Activator::~Activator`() {`/* ... */`}

`void` `Activator::Activate`(`bool` `activate`) {`/* ... */`}

`ActPtr` `IActivatable::CreateInstance`()
{
    `return` `ActPtr`(`new` `Activator`());
}`

对于工厂函数来说,更优的选择是使用模板std::make_shared<>()

复制代码
ActPtr` `IActivatable::CreateInstance`()
{
    `return` `std::make_shared<Activator>`();
}`

在所描述的场景中,你不能使用std::unique_ptr<>,因为它的删除策略略有不同,删除器类是一个模板参数,也就是说,它是智能指针类型的组成部分。

4.4 基类的替代实现

与 C# 或 Java 不同,C++ 没有"接口"这个特定概念;所需的行为是通过虚函数来建模的。这在实现接口类时提供了更大的灵活性。让我们考虑另一种实现方案IBase

复制代码
class` `IBase`
{
`protected`:
    `IBase`() `=` `default`;
    `virtual` `~IBase`() `=` `0`;  
    `virtual` `void` `Delete`(); 

`public`:
    `IBase`(`const` `IBase&`) `=` `delete`;            
    `IBase&` `operator=`(`const` `IBase&`) `=` `delete`; 

    `struct` `Deleter`         
    {
        `void` `operator`()(`IBase*` `p`) `const` { `p->Delete`(); }
    };

    `friend` `struct` `IBase::Deleter`;
};`

需要定义的是纯虚析构函数,Delete()而不是纯虚函数,因此也需要定义它。

复制代码
IBase::~IBase`() `=` `default`;
`void` `IBase::Delete`() { `delete` `this`; }`

其余接口类都继承自 `deleter` IBase。现在,在实现接口类时,无需重写 `deleter` Delete();它定义在基类中,并且由于使用了虚析构函数,确保调用实现类的析构函数。删除器类也自然地嵌套在 `deleter` 中IBaseDelete()由于 `deleter` 被声明为受保护的,因此删除器类是 `deleter` 的友元。这可以防止Delete()在客户端直接调用 `deleter`,从而降低与对象删除相关的错误发生的可能性。这种方法面向智能指针的使用,详见 4.3.1 节。

5. 使用接口类实现异常和集合

5.1 例外情况

如果一个可通过接口类访问的模块被设计成一个使用异常来报告错误的模块,那么可以提出以下异常类的实现选项。

面向客户端的头文件声明了一个接口类IException和一个普通类Exception

复制代码
class` `IException`
{
    `friend` `class` `Exception`;

    `virtual` `IException*` `Clone`() `const` `=` `0`;
    `virtual` `void` `Delete`() `=` `0`;

`protected`:
    `virtual` `~IException`() `=` `default`;

`public`:

    `virtual` `const` `char*` `What`() `const` `=` `0`;
    `virtual` `int` `Code`() `const` `=` `0`;

    `IException&` `operator=`(`const` `IException&`) `=` `delete`;
};

`class` `Exception`
{
    `IException*` `const` `m_Ptr`;

`public`:
    `Exception`(`const` `char*` `what`, `int` `code`);
    `Exception`(`const` `Exception&` `src`) : `m_Ptr`(`src`.`m_Ptr->Clone`()) {}
    `~Exception`() { `m_Ptr->Delete`(); }
    `const` `IException*` `Ptr`() `const` { `return` `m_Ptr`; }
};`

当发生异常时,模块会抛出一个类型为 `Exception` 的异常Exception。客户端捕获此异常,并通过指向 `Exception` 的指针获取相关信息IException。如有必要,客户端可以通过调用 `return` 操作符重新抛出异常throw,或者存储该异常。第一个类构造函数Exception仅在抛出异常时使用,无需从模块导出。其余成员函数均为内联函数,模块和客户端均可访问。

Exception例如,可以按如下方式实现。

实现类IException

复制代码
class` `ExcImpl` : `IException`
{
    `friend` `class` `Exception`;

    `const` `std::string` `m_What`;
    `const` `int` `m_Code`;

    `ExcImpl`(`const` `char*` `what`, `int` `code`);
    `ExcImpl`(`const` `ExcImpl&`) `=` `default`;

    `IException*` `Clone`() `const` `override`;

    `void` `Delete`() `override`;

`protected`:
    `~ExcImpl`() `=` `default`;

`public`:
    `const` `char*` `What`() `const` `override`;
    `int` `Code`() `const` `override`;
};


`ExcImpl::ExcImpl`(`const` `char*` `what`, `int` `code`)
    : `m_What`(`what`), `m_Code`(`code`) {}

`IException*` `ExcImpl::Clone`() `const` { `return` `new` `ExcImpl`(`*this`); }

`void` `ExcImpl::Delete`() { `delete` `this`; }

`const` `char*` `ExcImpl::What`() `const` { `return` `m_What`.`c_str`(); }

`int` `ExcImpl::Code`() `const` { `return` `m_Code`; }`

构造函数定义Exception

复制代码
Exception::Exception`(`const` `char*` `what`, `int` `code`)
    : `m_Ptr`(`new` `ExcImpl`(`what`, `code`)) {}`

请注意,在混合解决方案(.NET 和本机模块)中编程时,如果托管模块是用 C++/CLI 编写的,则此异常能够正确地跨越本机模块和托管模块之间的边界。因此,此异常可以在本机模块中抛出,并在用 C++/CLI 编写的托管类中捕获。

5.2 收藏

集合接口类的模板可能如下所示:

复制代码
template` `<typename` `T>`
`class` `ICollect`
{
`protected`:
    `virtual` `~ICollect`() `=` `default`;

`public`:
    `virtual` `ICollect<T>*` `Clone`() `const` `=` `0`;
    `virtual` `void` `Delete`() `=` `0`;

    `virtual` `bool` `IsEmpty`() `const` `=` `0`;
    `virtual` `int` `GetCount`() `const` `=` `0`;
    `virtual` `T&` `GetItem`(`int` `ind`) `=` `0`;
    `virtual` `const` `T&` `GetItem`(`int` `ind`) `const` `=` `0`;

    `ICollect<T>&` `operator=`(`const` `ICollect<T>&`) `=` `delete`;
};`

现在已经可以很方便地使用这样的集合了,但如果需要的话,可以将指向此类模板类的指针包装在容器类模板中,该容器类模板提供标准库容器风格的接口。

复制代码
template` `<typename` `T>` `class` `ICollect`;

`template` `<typename` `T>` `class` `Iterator`;

`template` `<typename` `T>`
`class` `Contain`
{
    `typedef` `ICollect<T>` `CollType`;

    `CollType*` `m_Coll`;

`public`:
    `typedef` `T` `value_type`;

    `Contain`(`CollType*` `coll`);
    `~Contain`();

    `Contain`(`const` `Contain&` `src`);
    `Contain&` `operator=`(`const` `Contain&` `src`);

    `Contain`(`Contain&&` `src`);
    `Contain&` `operator=`(`Contain&&` `src`);

    `bool` `еmpty`() `const`;
    `int` `size`() `const`;
    `T&` `operator`[](`int` `ind`);
    `const` `T&` `operator`[](`int` `ind`) `const`;

    `Iterator<T>` `begin`();
    `Iterator<T>` `end`();
};`

实现这样的容器并不难。它拥有集合的所有权,这意味着它会在析构函数中释放该集合。这个容器可能无法完全满足标准容器的要求,但这并非必要;关键在于它拥有成员函数begin()end()返回一个迭代器。然而,如果迭代器是按照迭代器标准定义的(参见[Josuttis]),那么就for可以使用范围循环和标准算法来处理这个容器。根据标准库规则定义迭代器的代码相当冗长,因此这里不再赘述。容器和迭代器类模板的定义完全包含在头文件中,因此无需导出任何额外的函数。

6. 接口类和包装类

接口类是相对底层的编程工具。为了获得更舒适的使用体验,建议将它们封装在提供自动对象生命周期管理的包装类中。通常也需要使用异常和容器等标准实现。以上已在 C++ 编程中进行了演示。然而,接口类也可以作为在其他平台(例如 .NET、Java 或 Python)上实现解决方案的功能基础。这些平台使用不同的对象生命周期管理机制和不同的标准接口。在这种情况下,应该使用与目标平台集成并考虑平台特性的技术来创建包装类。例如,对于 .NET Framework,这样的包装类是用 C++/CLI 编写的,并且与上面提到的 C++ 包装类有所不同。

7. 结果

接口类的实现对象由工厂函数创建,该工厂函数返回指向接口类的指针或智能指针。

删除接口类实现对象有三种方法。

  1. 使用运算符delete
  2. 使用特殊的虚拟函数。
  3. 使用智能指针自动删除。

第一种方案中,接口类必须具有公共虚析构函数。

第二种方案中,接口类必须具有受保护的析构函数,以防止错误使用运算符delete。如果在此方案中,使用标准智能指针来管理接口类实现对象的生命周期,则必须向其传递自定义删除器。

第三种方案中,工厂函数返回一个在实现端创建的智能指针,并封装了实现对象的删除过程。在这种情况下,可能不需要专门用于删除实现对象的虚函数,但需要一个受保护的析构函数来防止错误地使用运算符delete

对于实现了接口类的对象,复制语义是通过特殊的虚函数来实现的。

接口类可以简化模块组合;几乎整个模块接口都可以通过虚函数表访问,从而可以轻松实现灵活的动态解决方案,其中模块可以在运行时选择性地加载。

相关推荐
IT 乔峰1 小时前
linux部署DHCP服务端
linux·运维·网络
Hy行者勇哥1 小时前
Linux 系统搭建桌面级云端办公 APP(从快捷方式到自定义应用)
linux·运维·服务器
python百炼成钢2 小时前
52.Linux PWM子系统
linux·运维·服务器·驱动开发
CheungChunChiu2 小时前
Linux 总线模型与 bind/unbind 完整解析
linux·ubuntu·sys·bind/unbind
可可苏饼干2 小时前
ELK(Elastic Stack)日志采集与分析
linux·运维·笔记·elk
大柏怎么被偷了2 小时前
【Git】基本操作
linux·运维·git
小女孩真可爱2 小时前
大模型学习记录(八)---------RAG评估
linux·人工智能·python
我在人间贩卖青春2 小时前
查看文件相关命令
linux·查看文件
番茄你个西红42 小时前
安装KingbaseES时服务器swap的设置
linux·数据库