纯抽象类与其实现之间的数据隔离和纯抽象类的真正含义?

时间:2011-12-29 14:22:46

标签: c++ abstract-class

在Bjarne Stroustrup的书“The C ++ Programming Language”中,声明来自类层次结构中的超类的派生类(在许多情况下)可以访问超类的数据。这本书表明这是一个问题,因为“两个相关但不同的数据集之间的共享是在寻找麻烦。迟早有人会让它们不同步。而且,经验表明,新手程序员倾向于以不必要的方式混乱受保护的数据并导致维护问题“。这里的问题是,如何访问来自祖先类的私人数据是否有问题?听起来好像从父母那里继承资产是一件坏事。

因此,纯抽象类机制允许抽象类与其实现之间的分离。请注意,实现包括数据成员,因为如果数据成员是在抽象类中定义的,抽象类包含实现细节(定义的成员),因此它在抽象和实现之间没有很好地分离。为什么需要这种分离?正如here中所述,其中一个原因是“一种在类设计者和该类用户之间强制签订合同的方法。该类的用户必须为该类声明匹配的成员函数编译。“如书中所述,另一个重要原因是防止实施的变化。拥有抽象类层次结构,可以防止需要编译整个类层次结构的更改。

例如,如果您有一个普通的类,并且该类包含10个数据成员和20个成员函数,并且由十几个类继承,并且在根类下面有多个层。对根类的成员函数(函数签名保持不变)的实现进行了一些更改,并且您必须重新编译整个类层次结构以应用更改其终端类,否则程序会中断。对于抽象类层次结构,除非更改函数签名,否则如果终端类的实现发生更改,则只需重新编译该终端类。因此,大多数代码都受到最大程度的保护。 我的理解是否正确?

我认为他的示例( Ival_box 示例)与Bridge Design Pattern一起尝试并且功能强大,因为Bridge Pattern完全离开了抽象树,并且具有另一个用于实现的类层次结构。 纯抽象类可以被认为等同于Java中的接口,除了在Java中,解释通常是抽象类可以继承一次,而接口允许多个继承,因此共享封装在多个类之间的共享接口中的行为。我认为,这个答案在这种情况下无效,因为C ++允许多重继承。

最后一个问题是,如何在二进制映像中编译和存在派生类?编译器是否将继承的信息填充到派生类中,之后认为它是一个独立的类然后编译? (与预处理类似)

TL;博士

如何访问来自祖先类的私人数据是否有问题?

纯抽象类(也就是Java中的接口)是一种保护源代码免受更改的方法,它将抽象树和实现树分开?

如何用C ++编译抽象类/超类?是否通过从超类填充信息将派生类转换为单个类,然后编译

4 个答案:

答案 0 :(得分:3)

元注:您的问题可能会被问到多个单独的问题。

  

如何访问来自祖先类的私人数据是否有问题?

非正交性。当更改祖先类的受保护部分时,也必须更改所有派生类。这很容易出错,因为更改祖先的程序员可能在他的心理范围内没有派生类。

  

使用抽象类层次结构,除非更改了函数签名,否则如果终端类的实现发生更改,则只需重新编译该终端类。

技术上正确,但你错过了重点。重新编译不是很好,但也不是非常昂贵,因为计算机会这样做。真正的代价是由非正交性引起的人类工作。当一个类有一个公共或受保护的数据成员时,人们会使用它,当它发生变化时,事情就会中断并且必须修复。

BTW,“由十几个类继承,在根类之下有几层”通常是设计糟糕的标志。保持继承层次结构简洁明了。首选基类的成员:当你决定在类Bar中使用类Foo的功能时,没有令人信服的理由让Bar继承Foo,而是使用Foo类型的成员。

  

我认为,这个答案在这种情况下是无效的,因为C ++允许多重继承。

在C ++中,接口和类之间的区别并未实现(制成一个东西),但仍然使用。在许多情况下,构建接口非常有用,例如由抽象虚函数组成的类,因为它们避免了许多可怕的多重继承的麻烦。

  

派生类是如何编译并存在于二进制图像中的?

这是特定于编译器的。暂时不要担心 - 先了解语言,然后再实现它。

答案 1 :(得分:1)

如果根类的公开成员发生更改,则需要重新编译所有内容。

此外,您对如何编译派生类的理解是正确的。事实上,第一个C ++编译器只是一个奇特的C预处理器。

从祖先类访问私有数据是一件麻烦事,因为后代的构建取决于祖先的实现细节。如果祖先需要改变其工作方式,(即使其界面和功能保持不变),所有后代都会中断。

答案 2 :(得分:1)

  

如何从祖先类访问私有数据   要求麻烦?

想象一下,如果一个祖先类具有它维护的某些特性,状态和方法。假设成员数据的某些组合无效(也违反了类不变量)。然后想象一个子类可以在不维护不变量的情况下更改祖先数据。你结束了一个状态无效的情况,不仅如此,父母甚至不知道发生了什么,所以它没有机会抛出异常或以任何方式恢复。不仅如此,即使它最终处于有效状态,如果它在类(公共/受保护)接口之外被突变,祖先类仍可能对其状态“混淆”。

  

纯抽象类(也就是Java中的接口)是一种保护源的方法   反对变更的代码,抽象树和抽象树的分离   实现树?

纯抽象类用于表示接口概念。它本身并不保护源代码免受更改。通常只要接口合适并且可以轻松地将实现写入接口,客户端代码就不必更改,尽管可能需要重建。接口和实现的分离绝对是可取的,并且可以通过使用委托给虚拟pimpl实现(或使用非虚拟接口模式)的非虚拟接口进一步改进。

  

如何在C ++中编译抽象类/超类?它会转变吗?   通过填充信息将派生类转换为单个类   超类,然后编译

超类被视为派生类的一部分。具体细节因实施而略有不同。

答案 3 :(得分:1)

与几个O.O合作过语言,我学到了一些东西,有助于用C ++编程。 这适用于您的问题。

这是一个漫长而无聊的问题,但是,它可能值得花时间。

具体课程,首先

我通常从具体类开始,当几个类共享共同特征时,作为方法或字段,然后,我设计一个共同的超类。许多开发人员(使用任何语言)似乎都会直接参与超级课程,以及后来的儿童具体课程。

O.O.的设计程序,可能看起来是超级类,抽象或不抽象,首先设计。但是,在现实世界中,大多数课程都是具体的,而超级课程则是后来的。即使在设计中,U.M.L。或代码超类首先出现。

这个例子,包括数据&方法:


//  ....

#include <iostream>

class Employee {
  public:
    Employee();                         // constructor;

    void SayHello();
    void Work();
};

class Student {
  public:
    Student();                          // constructor;

    void SayHello();
    void Study();
};

//  ....

成为这个:


//  ....

#include <iostream>

class Person {
  public:
    Person();                         // constructor;

    virtual void SayHello() = 0;
};

class Employee {
  public:
    Employee();                         // constructor;

    virtual void SayHello();
    void Work();
};

class Student {
  public:
    Student();                         // constructor;

    virtual void SayHello();
    void Study();
};

//  ....

前面这些示例仅显示方法,而不是数据,但适用相同的原则。

避免私有字段或方法

我通常做的事情,它是为了避免“私人”部分。如果我需要隐藏,我使用“protected”,而不是。

所以,这个:


//  ....

#include <iostream>
#include <cstring>

class Person {
  private:
    char[512] Name;

  public:
    Person();                         // constructor;

    virtual void SayHello() = 0;    
};

//  ....

成为这个:


//  ....

#include <iostream>
#include <cstring>

class Person {
  protected:
    char[512] Name;

  public:
    Person();                         // constructor;

    char[512] Name;

    virtual void SayHello() = 0;    
};

//  ....

如果您正在使用类,无论是否抽象,您可以考虑,您或其他开发人员可以编写子类并访问父类字段或方法。

使用属性(使用访问方法),而不是普通数据字段

有些事情让我感到困扰,它在“C ++”和“C ++”中缺乏“真正的属性”。 “Java” 的。

访问,修改,控制或更新对象及其直通属性的数据的方法之一,即使用称为加速器的方法接收的字段。

属性可以用C ++实现,使用模板,或者只是手工编写方法。

所以这是直接非孤立的字段示例:


//  ....

#include <iostream>
#include <cstring>

class Person {
  public:
    Person();                         // constructor;

    char[512] Name;

    virtual void SayHello() = 0;    
};

int main (...) {
  Person thisPerson = new Person();

  strcpy(thisPerson.Name, "John Doe");

  cout << thisPerson.Name << "\n";

  delete thisPerson;
}

//  ....

成为,这个孤立的属性示例:


//  ....

#include <iostream>
#include <cstring>

class Person {
  protected:
    // isolated data field
    char[512] Name;

  public:
    Person();                         // constructor;

    virtual void SayHello() = 0;

    // public data field accesor reader method or "getter"
    const char* getName();

    // public data field accesor writer method or "setter"
    void setName(const char* AName);
};

const char Person::getName() {
  char char* Result = this.FName;

  return Result;
}   

void setName(const char* AName) {
  bool IsValid = false;

  // do some validation before actually modifing field
  // ...

  if (IsValid)
  {
    strcpy(this.FName, AName);
  }
}   

int main (...) {
  Person thisPerson = new Person();

      // should be read as "thisPerson.Name = "John Doe";"
  thisPerson.setName("John Doe");

      // should be read as "cout << thisPerson.Name;"
  cout << thisPerson.getName() << "\n";

  delete thisPerson;
}

//  ....

许多C ++开发人员不喜欢“属性”,因为缺乏控制。我认为您应该习惯于将数据作为您的问题进行访问,但任何功能都应该“正确使用”。

属性访问方法应该是虚拟的

在某些语言中,可以实现属性的概念,直接访问对象的数据字段,通过方法读取和写入,或混合两种方法。为了使事情变得更复杂,方法可以是“虚拟的”或“非虚拟的”。

当我使用属性时,我总是使用虚方法来访问它们。速度和速度有一个penanalty记忆,但是,它值得。具有虚拟覆盖方法的属性允许控制&amp;访问对象的数据。

<强>摘要

您的问题涉及如何实施数据,如何隔离数据,以及如何访问类中的数据,以及如何在子类中实现这些功能。

我在Stackoverflow中已经阅读了类似的问题,但是,我得出结论,有几个问题与C ++中缺少“真实属性”有关。

您可能希望管理您的数据访问权限。通过使用“属性”来隔离。

并且,可能想尝试学习其他O.O.,其中实现了“属性”的概念,例如:VB(.NET),C#,Delphi,D或Vala。

然后,在需要时,尝试在C ++程序中实现这个概念。我有C ++中的类,它具有普通数据字段,字段被视为“属性”,如Java与访问方法一样,或者在需要时混合两者。

这并不意味着你重写了你正在使用的应用程序,或者试图探究哪种编程语言更好,只是从其他编程语言中学到一些可能有助于你工作的东西。