Compartilhar via


Várias classes base

Uma classe pode ser derivada de mais de uma classe base. Em um modelo de várias heranças (no qual as classes são derivadas de mais de uma classe base), as classes base são especificadas usando o elemento de gramática lista base. Por exemplo, a declaração de classe de CollectionOfBook, derivada de Collection e de Book, pode ser especificada:

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {
};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

A ordem em que as classes base são especificadas não é significativa, exceto em certos casos em que construtores e destruidores são chamados. Nesses casos, a ordem na qual as classes base são especificadas afeta o seguinte:

  • A ordem em que os construtores são chamados. Se seu código depende da parte Book de CollectionOfBook para ser inicializado antes da parte Collection, a ordem de especificação é significante. A inicialização ocorre na ordem em que as classes são especificadas na lista base.

  • A ordem na qual os destruidores são chamados para limpeza. Novamente, se for necessário que uma "parte" específica da classe esteja presente quando outra parte for destruída, a ordem é importante. Os destruidores são chamados na ordem inversa das classes especificadas na lista base.

    Observação

    A ordem de especificação das classes base pode afetar o layout de memória da classe. Não tome decisões de programação com base na ordem dos membros base na memória.

Ao especificar a lista base, você não pode especificar o mesmo nome de classe mais de uma vez. No entanto, é possível que uma classe seja uma base indireta para uma classe derivada mais de uma vez.

Classes base virtuais

Porque uma classe pode ser uma classe base indireta a uma classe derivada mais de uma vez, C++ fornece uma maneira de otimizar a forma como funcionam as classes base. As classes base virtuais oferecem uma maneira de economizar espaço e evitar ambiguidades nas hierarquias de classes que usam a herança múltipla.

Cada objeto não virtual contém uma cópia dos membros de dados definidos na classe base. Essa duplicação perde espaço e exige que você especifique que cópia dos membros da classe base você quer sempre que os acessa.

Quando uma classe base é especificada como base virtual, ela pode atuar como uma base indireta mais de uma vez sem duplicação de seus membros de dados. Uma única cópia dos membros de dados é compartilhada por todas as classes base que ela usa como base virtual.

Ao declarar uma classe base virtual, a palavra-chave virtual aparece nas listas de base das classes derivadas.

Considere a hierarquia de classe na figura a seguir, que ilustra uma linha de almoço simulada:

Diagram of a simulated lunch line.

A classe base é Queue. A Fila do Caixa e a Fila do Almoço herdam da Fila. Por fim, a Fila do Caixa do Almoço herda tanto da Fila do Caixa quanto da Fila do Almoço.

Gráfico simulado da linha de almoço

Na figura, Queue é a classe base para CashierQueue e LunchQueue. No entanto, quando as duas classes são combinadas para formar LunchCashierQueue, o seguinte problema ocorre: a nova classe contém dois subobjetos do tipo Queue, um de CashierQueue e o outro de LunchQueue. A figura a seguir mostra o layout de memória conceitual (o layout de memória real pode ser otimizado):

Diagram of a simulated lunch line object.

A figura mostra um objeto Lunch Cashier Queue com dois subobjetos: Fila de Caixa e Fila de Almoço. Tanto a Fila de Caixa quanto a Fila de Almoço contêm um subobjeto Fila."

Objeto de linha de almoço simulado

Há dois Queue subobjetos no LunchCashierQueue objeto. O código a seguir declara Queue como uma classe base virtual:

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

A palavra-chave virtual garante que apenas uma cópia do subobjeto Queue seja incluída (veja a figura a seguir).

Diagram of a simulated lunch line object, with virtual base classes depicted.

O diagrama mostra um objeto Lunch Cashier Queue, que contém um subobject Fila de Caixa e um subobjeto Lunch Queue. Tanto a Fila de Caixa quanto a Fila de Almoço compartilham o mesmo subobjeto Fila.

Objeto de linha de almoço simulado com classes base virtuais

Uma classe pode ter um componente virtual e um componente não virtual de determinado tipo. Isso acontece nas condições ilustradas na figura a seguir:

Diagram of virtual and non virtual components of a class.

O diagrama mostra uma classe base de fila. Uma classe Fila de Caixa e uma classe Fila de Almoço herdam virtualmente de Fila. Uma terceira classe, Takeout Queue, herda não virtualmente da fila. A Fila do Caixa do Almoço herda da Fila do Caixa e da Fila do Almoço. A Fila do Caixa do Almoço para Levar herda da Fila do Caixa do Almoço e da Fila do Takeout.

Componentes virtuais e não virtuais da mesma classe

Na figura, CashierQueue e LunchQueue usam Queue como uma classe base virtual. No entanto, TakeoutQueue especifica Queue como uma classe base, não uma classe base virtual. Portanto, LunchTakeoutCashierQueue tem dois subobjetos do tipo Queue: um do caminho de herança que inclui LunchCashierQueue e outro do caminho que inclui TakeoutQueue. Isso é ilustrado na figura a seguir.

Diagram of the object layout for virtual and non virtual inheritance.

Um objeto Lunch Takeout Cashier Queue é mostrado que contém dois subobjetos: uma Fila de Takeout (que contém um subobjeto Queue) e uma Lunch Cashier Queue. O subobjeto Fila de Caixa de Almoço contém um subobjeto Fila de Caixa e um subobjeto Fila de Almoço, que compartilham um subobjeto Fila.

Layout de objeto com herança virtual e não virtual

Observação

A herança virtual oferece benefícios significativos de tamanho quando comparada com a herança não virtual. No entanto, pode apresentar a sobrecarga adicional de processamento.

Se uma classe derivada substitui uma função virtual que ela herda de uma classe base virtual, e se um construtor ou um destruidor para a classe base derivada chama essa função usando um ponteiro para a classe base virtual, o compilador pode introduzir outros campos "vtordisp" ocultos nas classes com bases virtuais. A opção do compilador /vd0 suprime a adição do membro oculto de deslocamento do construtor/destruidor vtordisp. A /vd1 opção do compilador, o padrão, os habilita onde são necessários. Desative vtordisps somente se tiver certeza de que todos os construtores e destruidores de classe chamam funções virtuais virtualmente.

A opção do compilador /vd afeta um módulo de compilação inteiro. Use o pragma vtordisp para suprimir e habilitar novamente os campos vtordisp classe por classe:

#pragma vtordisp( off )
class GetReal : virtual public { ... };
\#pragma vtordisp( on )

Ambiguidades de nome

A herança múltipla apresenta a possibilidade de que os nomes sejam herdados ao longo de mais de um caminho. Os nomes dos membros da classe ao longo desses caminhos não são necessariamente exclusivos. Esses conflitos de nome são chamados de “ambiguidades”.

Qualquer expressão que se referir a um membro de classe deve fazer uma referência não ambígua. O exemplo a seguir mostra como as ambiguidades se desenvolvem:

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

Dadas as declarações de classe anteriores, o código como o seguinte é ambíguo porque não está claro se se b refere ao b in ou in BA :

C *pc = new C;

pc->b();

Considere o exemplo anterior. Como o nome a é um membro da classe e da classeBA, o compilador não pode discernir qual a designa a função a ser chamada. O acesso a um membro é ambíguo se pode referenciar mais de uma função, objeto, tipo ou enumerador.

O compilador detecta ambiguidades executando testes nesta ordem:

  1. Se o acesso ao nome é ambíguo (como somente descrito), será gerada uma mensagem de erro.

  2. Se as funções sobrecarregadas forem inequívocas, elas serão resolvidas.

  3. Se o acesso ao nome viola a permissão de acesso a membros, será gerada uma mensagem de erro. (Para mais informações, confira Controle de acesso a membros.)

Quando uma expressão gera uma ambiguidade por meio de uma herança, você pode solucioná-la manualmente qualificando o nome em questão com seu nome de classe. Para fazer com que o exemplo acima seja compilado corretamente sem ambiguidades, use códigos como:

C *pc = new C;

pc->B::a();

Observação

Quando C é declarado, ele tem o potencial para causar erros quando B é referenciado no escopo de C. Nenhum erro é emitido, no entanto, até que uma referência não qualificada a B seja feita no escopo de C.

Dominância

É possível que mais de um nome (função, objeto ou enumerador) seja alcançado por meio de um gráfico de herança. Esses casos são considerados ambíguos com classes base não virtuais. Eles também são ambíguos com as classes base virtuais, a menos que um dos nomes "domine" os outros.

Um nome domina outro nome se for definido em ambas as classes e uma classe for derivada da outra. O nome dominante é o nome na classe derivada; esse nome é usado quando uma ambiguidade poderia surgir de outra forma, conforme mostrado no seguinte exemplo:

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
    int a;
};

class B : public virtual A {
public:
    int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
    D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

Conversões ambíguas

As conversões explícitas e implícitas de ponteiros ou as referências aos tipos da classe podem causar ambiguidades. A figura a seguir, a conversão ambígua dos ponteiros para as classes base, mostra o seguinte:

  • A declaração de um objeto de tipo D.

  • O efeito da aplicação do endereço do operador (&) a esse objeto. O operador de endereço sempre fornece o endereço base do objeto.

  • O efeito de converter explicitamente o ponteiro obtido usando o operador address-of para o tipo de classe base A. Coagir o endereço do objeto a ser digitado A* nem sempre fornece ao compilador informações suficientes sobre qual subobjeto de tipo A selecionar, nesse caso, existem dois subobjetos.

Diagram showing how the conversion of pointers to base classes can be ambiguous.

O diagrama primeiro mostra uma hierarquia de herança: A é a classe base. B e C herdam de A. D herda de B e C. Em seguida, o layout de memória é mostrado para o objeto D. Há três subobjetos em D: B (que inclui um subobjeto A) e C (que inclui um subobjeto A). O código & d aponta para o A no subobjeto B. O código ( * A ) & d aponta para o subobjeto B e o subobjeto C.

Conversão ambígua de ponteiros em classes base

A conversão para tipo (ponteiro para A) é ambígua porque não há como discernir qual subobjeto de tipo A*A é o correto. Você pode evitar a ambiguidade especificando explicitamente qual subobjeto pretende usar, da seguinte maneira:

(A *)(B *)&d       // Use B subobject.
(A *)(C *)&d       // Use C subobject.

Ambiguidades e classes base virtuais

Se forem usadas classes base virtuais, as funções, os objetos, os tipos e os enumeradores poderão ser alcançados por meio de caminhos de herança múltipla. Como há apenas uma instância da classe base, não há ambiguidade ao acessar esses nomes.

A figura a seguir mostra como os objetos são compostos usando a herança virtual e não virtual.

Diagram showing virtual derivation and nonvirtual derivation.

O diagrama primeiro mostra uma hierarquia de herança: A é a classe base. B e C praticamente herdam de A. D virtualmente herda de B e C. Em seguida, o layout de D é mostrado. D contém os subobjetos B e C, que compartilham o subobjeto A. Em seguida, o layout é mostrado como se a mesma hierarquia tivesse sido derivada usando herança não virtual. Nesse caso, D contém os subobjetos B e C. Tanto B quanto C contêm sua própria cópia do subobjeto A.

Derivação virtual e não virtual

Na figura, o acesso a qualquer membro da classe A por meio de classes base não virtuais causa uma ambiguidade; o compilador não tem nenhuma informação que explique se ele deve usar o subobjeto associado a B ou o subobjeto associado a C. No entanto, quando A é especificado como uma classe base virtual, não há dúvida de qual subobjeto está sendo acessado.

Confira também

Herança