使用头/源文件来分离接口和实现

Use of header/source files to separate interface and implementation

本文关键字:分离 接口 实现 源文件      更新时间:2023-10-16

在c++中,类通常这样声明:

// Object.h
class Object
{
    void doSomething();
}
// Object.cpp
#include "Object.h"
void Object::doSomething()
{
    // do something
}

我理解这改善了编译时间,因为将类放在一个文件中使您无论何时更改实现或接口(参见此)都可以重新编译它。

然而,从面向对象的角度来看,我看不出将接口与实现分离有什么帮助。我读过很多其他的问题和答案,但我的问题是,如果你定义一个类的方法正确(在单独的头/源文件),那么你怎么能做一个不同的实现?如果在两个不同的地方定义Object::method,那么编译器如何知道调用哪一个?在不同的命名空间中声明Object::方法定义吗?

如果你想在同一个程序中使用一个接口和多个实现,那么你可以使用抽象虚基。

一样:

class Printer {
    public:
    virtual void print_string(const char *s) = 0;
    virtual ~Printer();
};

然后你可以有实现:

class EpsonPrinter : public Printer {
    public:
    void print_string(const char *s) override;
};
class LexmarkPrinter : public Printer {
     public:
     void print_string(const char *s) override;
};

另一方面,如果您正在查看实现操作系统独立性的代码,则可能有几个子目录,每个子目录对应一个操作系统。头文件是相同的,但Windows的源文件仅为Windows构建,Linux/POSIX的源文件仅为Linux构建。

然而,从[一个]面向对象的角度来看,我看不出将接口与实现分离有什么帮助。

从面向对象的角度来看,它不能帮助,也不打算这样做。这是c++的一个文本包含特性,它继承自C语言,而C语言并不直接支持面向对象编程。

模块化的文本包含是一个从汇编语言借来的特性。它几乎是面向对象编程的对立面,或者基本上是计算机程序组织领域中任何好的东西的对立面。

文本包含允许c++编译器与不存储任何符号类型信息的古老对象文件格式进行互操作。Object.cpp文件被编译为这种对象格式,从而产生Object.o文件或Object.obj文件或您的平台上的其他文件。当程序的其他部分使用这个模块时,它们几乎完全信任在Object.h中写的关于它的信息。除了带有偏移量和大小等数字信息的符号外,Object.o文件中没有任何有用的内容。如果头文件中的信息不能正确地反映Object.obj,你就会有未定义的行为(在某些情况下,通过c++对函数重载的支持来缓解,这将使不匹配的函数调用变成不可解析的符号,这要归功于名称混淆)。

例如,如果头文件声明了一个变量extern int foo;,但目标文件是编译double foo = 0.0;的结果,这意味着程序的其余部分正在访问double对象作为int。防止这种情况发生的原因是Object.cpp包含自己的头文件(从而迫使编译器捕获声明和定义之间的不匹配),并且您有一个合理的构建系统,确保如果有任何内容涉及Object.h,则重建Object.cpp。如果该检查是基于时间戳的,那么您还必须有一个正常的文件系统和版本控制系统,它们不会对时间戳做奇怪的事情。

如果你在两个不同的地方定义Object::method,那么编译器如何知道调用哪一个?

它不会,事实上,如果你这样做,你将违反"一个定义规则",根据标准,这将导致未定义的行为,不需要诊断。

如果你想为一个类接口定义多个实现,你应该以某种方式使用继承。

一种可能的方法是,使用虚基类并覆盖不同子类中的一些方法。

如果希望将类的实例作为值类型操作,那么可以使用pImpl习惯用法,并结合虚拟继承。因此,您将有一个类,即"指针"类,它公开接口,并持有指向抽象虚拟基类类型的指针。然后,在.cpp文件中,您将定义虚拟基类,并定义它的多个子类,pImpl类的不同构造函数将实例化不同的子类作为实现。

如果您想使用静态多态性,而不是运行时多态性,您可以使用CRTP习惯用法(它最终仍然基于继承,而不是虚拟继承)。