Resumo da Aula 7

Sumário

Herança: derivação de classes

Os conceitos centrais da programação orientada para objectos são a herança e o polimorfismo.  Esta secção abordará o primeiro destes conceitos.  O segundo será deixado para uma aula posterior.

Suponha-se que se pretendia construir uma aplicação para gestão do pessoal duma empresa.  Uma primeira análise do problema poderia levar à identificação de várias classes de empregado: empregados normais, secretários, chefes, motoristas, chefes dos motoristas, etc.  A mesma análise poderia indicar que a informação a guardar para um empregado normal consistia, para simplificar, apenas em nome e sexo.  Todas as outras classes de empregados possuiriam essa mesma informação adicionada de informação específica.  Por exemplo, para os secretários seria necessário guardar uma referência para o respectivo chefe, para os motoristas uma referência para os veículos à sua guarda, para os chefes informação sobre o nível de chefia e sobre o respectivo departamento, incluindo por exemplo uma lista de referências para os respectivos subordinados.  A análise de problemas desta índole leva também frequentemente à conclusão de que as várias classes identificadas estão relacionadas entre si.  Por exemplo, um chefe é um empregado (embora nem todos os empregados sejam chefes), um secretário é um empregado, um motorista é um empregado, um chefe dos motoristas é um motorista e é um chefe também, etc.

Esta relação é um é extremamente importante e deve ser claramente distinguida da relação tem um (ou é composto por um).  Por exemplo, é claro que um humano é composto, entre outras partes, por um coração, e pode ter, por exemplo, um anel.  Mas um humano, na mesma acepção, não é um coração, tal como não é um anel.  O mesmo se passa, por exemplo, com uma turma de uma universidade.  Uma turma tem um conjunto de alunos, tal como tem um nome, um delegado, etc.  Uma turma não é simplesmente um conjunto (ou lista) de alunos.  Esta diferença deve ser estabelecida mesmo em casos em que as classes identificadas parecem ter apenas um item de informação.  No âmbito de um projecto, por exemplo, as tarefas a realizar podem consistir inicialmente apenas numa duração.  Mas isso não significa que uma tarefa é uma duração.  Uma tarefa tem uma duração, tal como poderá vir a ter outros tipos de informação no futuro.

A relação é um que se referiu é na realidade apenas a forma habitual de referir uma relação mais particular: pode substituir um ou pode ser tratado como um.  É que, na linguagem corrente, também se usa a expressão "é um" para denotar a pertença a uma classe.  Por exemplo, pode-se dizer que o Sr. Fulano é um chefe.  Em C++ isso significaria que fulano seria uma instância da classe Chefe.  Mas a relação é um a que se alude nesta secção é uma relação entre classes, e não entre objectos e as respectivas classes!  Assim, é mais preciso dizer que "um chefe pode substituir um empregado" ou que "um chefe pode se tratado como um empregado".  Normalmente não se usam estas expressões, mas apenas porque elas são demasiado longas e pesadas.

Como se representam relações é um em C++? O que se ganha com esse tipo de relação?  É o assunto da próxima secção.

1.1  Herança pública: relação é um

Voltando ao exemplo do pessoal da empresa, suponha-se que os chefes devem possuir a mesma informação que qualquer empregado e adicionalmente apenas o seu nível de chefia, representado por um inteiro.  Nesse caso, poderiam ser usadas duas abordagens para concretização em C++ dos conceitos de empregado e chefe:
  1. Usa-se uma única classe Empregado, admitindo-se que empregados com nível de chefia zero (0) são empregados normais.
  2. Usam-se duas classes.
A utilização de um única classe, que poderá parecer uma boa solução à primeira vista, tem algumas desvantagens.  A primeira é que, se os chefes tiverem mais informação (e.g., nome do departamento e lista de subordinados), essa informação vai ter de existir mesmo para os empregados normais.  Pior ainda é que é expectável que existam outros tipos de empregados, como as secretárias e os motoristas, cuja informação específica teria de constar também na classe Empregado.  Além disso, a escrita de uma simples operação para mostrar a informação de um empregado no ecrã, exigiria uma lógica de programação que, não sendo difícil, levaria a código difícil de perceber e alterar: consoante se trate de um empregado normal ou um chefe, diferente informação necessita ser mostrada.  Além disso, o reconhecimento de que no pessoal da empresa existe um tipo adicional de empregado, não previsto inicialmente, levaria forçosamente à reescrita total da classe Empregado, complicando ainda mais todas as suas operações.

A utilização de várias classes parece portanto ser melhor solução.  Seja então a classe Empregado*:

class Empregado {
  public:
    Empregado(string const& nome, Sexo sexo);

    string const& nome() const;

    Sexo sexo() const;
    void mostra() const;

  private:

    string nome_;
    Sexo sexo_;
};

inline Empregado::Empregado(string const& nome, Sexo sexo)

    : nome_(nome), sexo_(sexo) 
{

}

inline string const& Empregado::nome() const 
{

    return nome_;
}

inline Sexo Empregado::sexo() const 
{

    return sexo_;
}

inline void Empregado::mostra() const 
{

    cout << "Nome: " << nome() << endl
         << "Sexo: " << sexo() << endl;
}

A classe Chefe, por outro lado, acrescenta o nível de chefia como informação adicional, o que obriga à definição de uma nova variável membro, à alteração do construtor, à criação da função membro nível() para inspeccionar o nível de chefia e à alteração do procedimento membro mostra():

class Chefe {
  public:
    Chefe(string const& nome, Sexo sexo, int nível);

    string const& nome() const;

    Sexo sexo() const;
    int nível() const;
    void mostra() const;

  private:

    string nome_;
    Sexo sexo_;
    int nível_;
};

inline Chefe::Chefe(string const& nome, Sexo sexo, int nível)
    : nome_(nome), sexo_(sexo), nível_(nível) 
{

}

inline string const& Chefe::nome() const 
{

    return nome_;
}

inline Sexo Chefe::sexo() const 
{

    return sexo_;
}

inline int Chefe::nível() const 
{

    return nível_;
}

inline void Chefe:: mostra() const 
{

    cout << "Nome: " << nome() << endl
         << "Sexo: " << sexo() << endl
         << "Nível: " << nível() << endl;
}

Podem-se identificar pelo menos dois problemas nesta implementação.  O primeiro é a repetição de código.  Tudo o que consta na classe Empregado consta também na classe Chefe.  Isto, para além do desperdício de esforço, representa também dificuldades acrescidas de manutenção.  Por exemplo, se se identificar à posteriori que o número de contribuinte é informação relevante para todos os empregados, então ter-se-á de alterar não só a classe empregado mas também em todas as classes relacionadas (chefes, secretárias, motoristas, etc.).

O segundo problema é que não há qualquer tipo de relação, do ponto de vista da linguagem C++, entre as duas classes definidas.  Ou seja, não é possível tratar um chefe como se de um empregado apenas se tratasse.  Ou seja,

Chefe o_zé("Zé", masculino, 2);
Empregado outro_zé_como_mero_empregado = o_zé;

é uma inicialização inválida (para o C++ as classes Empregado e Chefe não têm qualquer relação).  O que se pretendia com esta inicialização era que a variável outro_zé_como_mero_empregado ficasse com a informação d'o_zé como mero empregado, i.e., sem aquilo que é específico de um chefe.

A estas operações de retirar o que é específico de uma classe, chama-se em inglês slicing, e chamar-se-há aqui corte, uma vez que cortam informação deixando apenas as "fatias" de informação relevantes.  Esta operação é menos útil do que parece à primeira vista (na realidade até pode ser perigosa).  Bastante mais útil seria que se pudesse colocar o endereço de uma instância ou objecto da classe Chefe num ponteiro para Empregado.  Ou seja:

Chefe o_zé("Zé", masculino, 2);
Empregado* ponteiro_para_o_zé_visto_como_mero_empregado = &o_zé;

Infelizmente, tal também não é possível.

De igual forma seria desejável que referências para Empregados pudessem ser sinónimos de Chefes.  Ou seja:

Chefe o_zé("Zé", masculino, 2);
Empregado& também_sou_o_zé_mas_visto_como_mero_empregado = o_zé;

Mais uma vez, tal ainda não é possível.

Note-se que nestes dois casos, ao contrário da inicialização de um empregado a partir de um chefe, a informação acessível através do ponteiro ou referência para Empregado inclui toda a informação sobre o chefe!  Isto é, não há corte (slicing).  No primeiro caso existiam dois objectos: um Chefe e um Empregado,  que funcionava como um sósia despromovido do Chefe (daí o pouco interesse prático do slicing).  Nos últimos casos existe apenas um objecto, da classe Chefe, que é acessível através de um ponteiro ou de uma referência.

Este comportamento permitiria, por exemplo, guardar todo o pessoal da empresa através de um única lista de ponteiros para Empregado.  Por exemplo, o seguinte código seria possível:

list<Empregado*> empregados;

//
Inserção dos empregados:
empregados.push_back(new Empregado("João Maria", masculino));
empregados.push_back(new Chefe("Ana Maria", feminino, 4));

...

// Visualização dos empregados:
for(list<Empregado*>::iterator i = empregados.begin(); 
    i != empregados.end(); ++i)

    (*i)->mostra();

Infelizmente, tal ainda não é possível.

* Assume-se definido um tipo enumerado Sexo e respectivo operador de escrita num canal:

enum Sexo {masculino, feminino};

inline ostream& operator << (ostream& saída, Sexo s)
{

    return saída << (s == masculino? "masculino" : "feminino");
}

1.1.1  Primeira tentativa de solução do dilema

Uma solução parcial para o primeiro problema apresentado seria construir a classe Chefe à custa da classe Empregado usando uma variável membro de instância dessa classe.  Ou seja:

class Chefe {
  public:
    Chefe(string const& nome, Sexo sexo, int nível);

    string const& nome() const;

    Sexo sexo() const;
    int nível() const;
    void mostra() const;

  private:

    Empregado empregado;
    int nível_;
};

inline Chefe::Chefe(string const& nome, Sexo sexo, int nível)
    : empregado(nome, sexo), nível_(nível) 
{

}

inline string const& Chefe:: nome() const 
{

    return empregado.nome();
}

inline Sexo Chefe::sexo() const 
{

    return empregado.sexo();
}

inline int Chefe::nível() const 
{

    return nível_;
}

inline void Chefe:: mostra() const 
{

    empregado.mostra();
    cout << "Nível: " << nível() << endl;
}

Esta é apenas uma solução parcial.  Por um lado não se conseguiu evitar rescrever os inspectores nome() e sexo(), mesmo que duma forma muito simples e que se limita a delegar no respectivo inspector da classe Empregado.  Por outro lado, o acrescento à posteriori do número de contribuinte na classe Empregado obrigaria a rescrever não só essa classe mas também a classe Chefe, para que esta possuisse o respectivo inspector, o mesmo acontecendo a todas as outras classes criadas à semelhança da classe Chefe: Motorista, Secretário, etc..

Este tipo de solução não é, pois, o mais indicado para representar a relação é um existente entre chefes e empregados.  A utilização de atributos é mais indicada para representar relações de agregação (tem um) ou, melhor ainda, de composição (é composto por um).  Acontece que um chefe não tem um empregado (bom, pelo menos se formos precisos com a linguagem, pois um chefe pode chefiar vários empregados), nem muito menos é composto por um empregado (a teoria do homúnculo há muito que foi abandonada por absurda).

1.1.2  Segunda tentativa

Uma resolução parcial para o segundo problema (possibilidade de inicialização de empregados a partir de chefes) poderia passar por definir um construtor da classe Empregado aceitando um Chefe como argumento.  Por exemplo:

class Empregado {
  public:
    Empregado(string const& nome, Sexo s);
    Empregado(Chefe const& chefe);

    string const& nome() const;

    Sexo sexo() const;
    void mostra() const;

  private:

    string nome_;
    Sexo sexo_;
};

inline Empregado::Empregado(Chefe const& chefe)

    : nome_(chefe.nome()), sexo_(chefe.sexo()) {
}

Esta solução obriga a alterar a classe Empregado, acrescentando um novo construtor, sempre que se cria uma nova especialização do conceito de empregado.  Além disso, só introduz a possibilidade de corte (slicing), que na realidade é pouco interessante e mesmo perigosa.  Não resolve o problema de criar uma lista de ponteiros para empregados que contenha endereços de empregados normais, chefes, secretárias, etc.  Para isso teria de ser possível colocar endereços de chefes, etc. em ponteiros para a classe Empregado.

1.1.3  Solução

A solução em C++ envolve o conceito de herança.  Uma classe pode ser definida por derivação de uma classe base.  Assim, a classe Chefe pode ser derivada a partir da classe Empregado da seguinte forma:

class Chefe : public Empregado {
  public:
    Chefe(string const& nome, Sexo sexo, int nível);

    int nível() const;

    void mostra() const;

  private:

    int nível_;
};

inline Chefe::Chefe(string const& nome, Sexo sexo, int nível)
    : ...
{
    ...
}

inline int Chefe::nível() const
{

    return nível_;
}

inline void Chefe::mostra() const
{

    ...
}

A relação é um introduzida pela derivação acima pode ser representada pelo seguinte diagrama:

A sintaxe para definir classes por derivação corresponde a colocar : após o nome da classe derivada seguida de uma lista de classes base, que podem ser em número arbitrário.  Por exemplo, a definição

...
class D : public A, public B, public C {
    ...
};

cria uma nova classe D derivada das classes base A, B e C.

A classe derivada herda todos os membros das classes base.  No entanto, os membros privados da classe base não ficam acessíveis directamente a partir dos métodos da classe derivada: se não fosse assim, estar-se-ia a "abrir a porta" da parte privada de uma classe a quem quer que definisse uma classe sua derivada, violando-se com isso o princípio do encapsulamento.

A herança ou derivação fez-se usando a palavra chave public.  Isso significa que:

  1. Os membros públicos da classe base são herdados como membros públicos da classe derivada (daí que não seja necessário redefinir as operações nome() e sexo()).  Este facto resolve o primeiro problema identificado, pois acrescentar o número do contribuinte e respectivo inspector na classe Empregado leva à sua inclusão automática na classe Chefe.
  2. Quaisquer instâncias da classe derivada são compatíveis com instâncias da classe base, o mesmo se passando com ponteiros e referências.  Este facto resolve o segundo problema identificado, sendo agora possível inserir todo o pessoal da empresa numa única lista de ponteiros para Empregado.
Assim, a herança pública serve para representar a relação é um pretendida, tendo ainda o efeito secundário simpático de permitir aproveitar todo o código escrito para a classe base.

A derivação poderia ter sido feita também usando a palavra chave private (que é o tipo de herança por omissão para as classes, sendo a herança pública por omissão para as estruturas).  Nesse caso:

  1. Os membros públicos da classe base são herdados como membros privados da classe derivada.
  2. As instâncias da classe derivada não são compatíveis com instâncias da classe base, o mesmo se passando com ponteiros e referências.
A utilidade deste tipo de herança, herança privada, não é a de representar relações é um (por motivos que devem ser óbvios), mas sim a de representar uma forma especial de composição (relação é composto por um), que será explicada mais tarde.

1.1.4  Compatibilidade das classes

A compatibilidade entre a classe derivada e a classe base existe apenas num sentido.  É possível atribuir um chefe a um empregado (porque todos os chefes são empregados) mas não o contrário (porque nem todos os empregados são chefes).  O mesmo se passa com ponteiros e referências.  Ou seja, o seguinte código tornou-se legal:

Chefe o_zé("Zé", masculino, 2);
Empregado um_novo_zé_mero_empregado = o_zé;  // corte (slicing), pouco útil.
Empregado* ponteiro_para_o_zé_como_empregado = &o_zé;
Empregado& também_sou_o_zé_mas_visto_como_mero_empregado = o_zé;

No entanto, se se souber, como no exemplo acima, que um ponteiro ou referência de uma classe base de facto referenciam um objecto (ou instância) de uma dada classe derivada, é possível fazer atribuições no sentido inverso, embora apenas por intermédio do operador static_cast<tipo>():

Chefe* ponteiro_para_o_zé_agora_como_chefe = 
    static_cast<Chefe*>(ponteiro_para_o_zé_como_empregado);

Chefe& também_sou_o_zé = 
    static_cast<Chefe&>(também_sou_o_zé_mas_visto_como_mero_empregado);

Raramente tais conversões são úteis.  Aliás, normalmente são sinal de fraco desenho das classes usadas para resolver o problema.

1.2  Construtores, construtores por omissão e cópia e atribuição por cópia

Como inicializar os atributos das classes base herdados por uma classe derivada?  O C++ proporciona uma forma simples: invocam-se os construtores das classes base nas listas de inicializadores dos construtores da classe derivada.  Ou seja:

inline Chefe::Chefe(string const& nome, Sexo sexo, int nível)
    : Empregado(nome, sexo), nível_(nível)
{

}

Se não se invocar explicitamente o construtor de uma classe base, o compilador gerará automaticamente uma invocação ao construtor por omissão dessa classe.  Para isso deverá existir um construtor por omissão na classe base, de outro modo ocorrerá um erro de compilação.

A ordem pela qual as inicializações têm lugar num construtor é a que se segue:

  1. Primeiro são inicializadas as classes base através da invocação dos respectivos construtores, por ordem de aparecimento na especificação das heranças na definição da classe derivada.
  2. Depois são inicializadas os atributos de instância da classe derivada, por ordem de declaração, através da invocação dos respectivos construtores.
  3. Finalmente é executado o corpo do construtor.
Por exemplo, durante a construção de uma instância da classe D que se segue

class Z {
    ...
};

class D : A, B, C {
  public:
    D(int i, Z const& z);
    ~D();

    ...

  private:
    static int const dim = 10;
    int i;
    Z const z;
    float m[dim];
    int* pi;
};

D::D(int i, Z const& z)
    : i(i), z(z), pi(new int(10))
{

    for(int i = 0; i != dim; ++i)
        m[i] = 0;
}

D::~D() 
{

    delete pi;
}

são invocados, por ordem:
  1. Construtor de A, para inicialização dos atributos herdados de A.
  2. Construtor de B, para inicialização dos atributos herdados de B.
  3. Construtor de C, para inicialização dos atributos herdados de C.
  4. Construtor de int, para inicialização do atributo i.
  5. Construtor de Z, para inicialização do atributo constante z.
  6. Construtor de pi, para inicialização do atributo pi com o endereço de uma nova variável dinâmica.
  7. Atributos de tipos básicos ou deles derivados não têm quaisquer construtores invocados implicitamente.  É o caso do atributo m.
  8. Corpo do construtor, onde se inicializa a matriz m.
Note-se que a constante membro dim é de classe, e não de instância, pelo que é inicializada no início do programa.  Recorda-se que uma variável ou constante diz-se membro de classe se for partilhada por todas as instâncias dessa classe.

Como para qualquer outra classe, a linguagem tentará sempre fornecer implicitamente:

  1. Um construtor por omissão (desde que não se tenha declarado qualquer construtor na classe).  Este construtor por omissão fornecido automaticamente limita-se a invocar os construtores por omissão de todas as classes base e de todos os atributos de instância (excepto se forem de tipos básicos e noutros casos particulares).
  2. Um construtor por cópia (desde que não se tenha declarado explicitamente na classe).  Este construtor por cópia fornecido implicitamente limita-se a invocar os construtores por cópia de todas as classes base e de todas os atributos de instância.
  3. Um operador de atribuição por cópia (desde que não seja declarado explicitamente na classe).  Este operador fornecido automaticamente limita-se a invocar os operadores de atribuição por cópia de todas as classes base e de todos os atributos variáveis de instância (como é óbvio, este operador não poderá ser fornecido se existirem atributos constantes ou referências instância).
De igual forma é fornecido automaticamente um destrutor (desde que não seja declarado explicitamente) que invoca os destrutores de todas as classes base e de todas as variáveis e constantes membro de instância.  A ordem pela qual as operações ocorrem é a seguinte:
  1. Primeiro é executado o corpo do destrutor.
  2. Depois são invocados os destrutores de todos os atributos de instância da classe derivada, por ordem inversa de declaração.
  3. Finalmente são invocados os destrutores de todas as classes base por ordem inversa à do seu aparecimento na especificação das heranças na definição da classe derivada.
Para o exemplo acima, o destrutor da classe D invoca por ordem:
  1. Corpo do destrutor, onde se devolve à memória livre a variável dinâmica cujo endereço se guarda em pi.
  2. m e pi, não têm quaisquer destrutores invocados.  Um ponteiro não tem destrutor qualquer que seja o tipo apontado.  Uma matriz tem um destrutor que consiste em destruir cada um dos seus membros, pela ordem inversa à da construção, excepto quando, como neste caso, os elementos da matriz são de tipos básicos do C++.
  3. Destrutor de Z, para a constante membro z.
  4. Destrutor de C, para os atributos herdados de C.
  5. Destrutor de B, para os atributos herdados de B.
  6. Destrutor de A, para os atributos herdados de A.

1.3  Ocultação

A classe derivada pode declarar operações com o mesmo nome que a classe base.  Chama-se a este processo ocultação, pois tais operações ocultam as operações de igual nome na classe base, que por isso deixam de ser invocáveis directamente (não confundir ocultação nem com sobrecarga, assunto já visto anteriormente, nem com sobreposição, a ver na próxima aula).  Isto permite especializar na classe derivada uma operação definida na classe base.  É o caso da operação mostra() para a classe Chefe:

inline void Chefe::mostra() const
{

    Empregado::mostra();
    cout << "Nível: " << nível() << endl;
}

Repare-se que, como o procedimento com o mesmo nome da classe Empregado faz parte daquilo que se pretende fazer, a primeira instrução do procedimento é a sua invocação.  Para isso foi necessário preceder o nome do procedimento de Empregado:: (operador de resolução de âmbito), de modo a ficar claro que é a versão original que deve ser executada.  De outro modo estar-se a invocar recursivamente a operação Chefe::mostra().

Dada a definição da classe, o seguinte código

Chefe chefe("Zé", masculino, 2);
chefe.mostra();

resulta em

Nome: Zé
Sexo: masculino
Nível: 2

Por outro lado, se se pretendesse mostrar o chefe como mero empregado, poder-se-ia usar

chefe.Empregado::mostra();

que resultaria em

Nome: Zé
Sexo: masculino

Ou seja, é possível invocar uma operação de uma classe base, mesmo que oculta.

1.4  Hierarquias de classes

A derivação pode ser feita em sucessão, com o que se conseguem hierarquias de classes.  Por exemplo, se se pretendesse modelar a classificação dos seres vivos poder-se-ia definir:

class SerVivo {
    ...
};
class Animal /* reino */ : public SerVivo {
    ...
};
class Vegetal /* reino */ : public SerVivo {
    ...
};
class Cordado /* filo */ : public Animal {
    ...
};
class Vertebrado /* subfilo */ : public Cordado {
    ...
};
class Mamífero /* classe */ : public Vertebrado {
    ...
};
class Primata /* ordem */ : public Mamífero {
    ...
};
class Hominídeo /* família */ : public Primata {
    ...
};
class Homo /* género */ : public Hominídeo {
    ...
};
class Homo_sapiens /* espécie */ : public Homo {
    ...
};
class Homo_sapien_sapiens /* subespécie */
    : public Homo_sapiens {
    ...
};

Na figura que se segue pode-se ver um possível diagrama para as relações entre classes de veículos das forças armadas:

São subclasses de  uma classe todas as classes que dela derivem, directa ou indirectamente.  São superclasses de uma classe as classes de que ela derive, directa ou indirectamente.  Assim, as subclasses de VeículoAéreo no diagrama acima são Helicóptero, Avião e Hidroavião.

Fica como exercício para o leitor desenhar o diagrama do primeiro exemplo e o esqueleto da definição em C++ das classes do segundo exemplo.

1.5  Problemas por resolver

Voltando ao exemplo da lista de pessoal de uma empresa, com a classe Chefe derivada publicamente da classe Empregado, torna-se possível o código que antes era inválido:

list<Empregado*> empregados;

//
Inserção dos empregados:
empregados.push_back(new Empregado("João Maria", masculino));
empregados.push_back(new Chefe("Ana Maria", feminino, 4));

...

// Visualização dos empregados:
for(list<Empregado*>::iterator i = empregados.begin(); 
    i != empregados.end(); ++i)

    (*i)->mostra();

A situação é ilustrada pelo seguinte diagrama:

O único problema é que, como a operação mostra() é invocada através de um ponteiro para Empregado, todos os empregados são mostrados como simples empregados, apesar de entre eles haver empregados que são chefes, secretárias, ou motoristas.  A solução para este problema será vista na próxima aula, onde se apresentam as noções de polimorfismo, ligação estática e dinâmica e sua implementação em C++.

1.6  Herança privada: redução/alteração de interfaces

Viu-se atrás que se a herança não for pública mas privada, então a classe derivada deixa de ter uma relação é um com a classe base.  Além disso, todos os membros das classes base privadas tornam-se membros privados da classe derivada.  Que utilização poderá este tipo de herança ter?

Suponha-se que se pretende, mais uma vez, implementar o conceito de pilha de inteiros, na forma da classe PilhaDeInt, de novo sem quaisquer limitações quanto ao número de itens, excepto as impostas pela memória disponível e pelos limites do tipo int.  Uma vez que a classe ListaDeInt está já desenvolvida, não impõe restrições quanto ao comprimento das listas, e possui todas as operações necessárias para implementar uma pilha, é natural pensar em implementar o conceito de pilha com base na implementação das listas já existentes.  Neste caso a relação que existe entre as duas classes é:

a classe PinhaInt funciona como uma ListaDeInt, mas apenas permite...

Então, uma primeira implementação das pilhas poderia ser:

class PilhaDeInt {
  public:
    typedef ListaDeInt::Item Item;

    int altura() const;
    bool estáVazia() const;
    Item const& topo() const;

    Item& topo();

    void põe(Item const& novo_item);
    void tiraItem();

  private:

    ListaDeInt lista;
};

int PilhaDeInt:: altura() const 
{

    return lista.comprimento();
}

bool PilhaDeInt::
estáVazia() const 
{

    return lista.estáVazia();
}

Item const& PilhaDeInt:: topo() const 
{

    return lista.trás();
}

Item& PilhaDeInt::
topo() 
{

    return lista.trás();
}

void PilhaDeInt::
põe(Item const& novo_item) 
{

    lista.põeAtrás(novo_item);
}

void PilhaDeInt::tiraItem()
{

    lista.tiraDeTrás();
}

Esta solução usa composição para representar a relação entre as duas classes, e os métodos definidos delegam na classe ListaDeInt as suas tarefas.

Um resultado muito semelhante pode ser obtido por herança privada:

class PilhaDeInt : private ListaDeInt {
  public:
    typedef ListaDeInt::Item Item;

    int altura() const;
    bool estáVazia() const;
    Item const& topo() const;

    Item& topo();
    void põe(Item const& novo_item);
    void tiraItem();
};

int PilhaDeInt::altura() const
{

    return comprimento();
}

bool PilhaDeInt::estáVazia() const 
{

    return ListaDeInt::estáVazia();
}

Item const& PilhaDeInt:: topo() const 
{

    return trás();
}

Item& PilhaDeInt::
topo() 
{

    return trás();
}

void PilhaDeInt::
põe(Item const& novo_item) 
{

    põeAtrás(novo_item);
}

void PilhaDeInt::
tiraItem() 
{

    tiraDeTrás();
}

Esta solução é porventura demasiado parecida com a anterior para se perceberem as vantagens imediatamente.  Mas observe-se o que se passa com os membros Item e estáVazia().  Eles são redefinidos apenas porque a herança foi feita de forma privada, e portanto o utilizador da classe PilhaDeInt de outra forma não tem acesso aos membros correspondentes da classe base.  Nos outros casos, como os nomes das operações das listas usados directamente pela pilha não têm os nomes que se pretendiam, a definição de novas operações justifica-se, desde que sejam inline, para não incorrer nos custos da invocação de rotinas.

Esta solução pode ser vista como uma alteração da interface da classe ListaDeInt de modo a proporcionar apenas os serviços de uma pilha.

O C++ proporciona uma forma simples de abrir excepções quanto ao tipo de herança.  Se se pretender tornar públicos membros cuja herança foi feita de uma forma privada, basta usar uma declaração de utilização do respectivo nome na parte pública da classe derivada.  Assim, a classe PilhaDeInt pode ser definida como:

class PilhaDeInt : private ListaDeInt {
  public:
    using ListaDeInt::Item;

    int altura() const;


    using ListaDeInt::estáVazia;


    Item const& topo() const;


    Item& topo();

    void põe(Item item);
    void tiraItem();
};

int PilhaDeInt::altura() const 
{

    return comprimento();
}

Item const& PilhaDeInt::
topo() const 
{

    return trás();
}

Item& PilhaDeInt::
topo() 
{

    return trás();
}

void PilhaDeInt::
põe(Item item) 
{

    põeAtrás(item);
}

void PilhaDeInt::
tiraItem() 
{

    tiraDeTrás();
}

Note-se que usar herança pública neste caso seria o mesmo que afirmar que um pilha é uma lista, o que manifestamente não é verdade.  Note-se ainda que, se se considerarem os nomes usados na lista como apropriados para a pilha, pode-se fazer simplesmente:

class PilhaDeInt : private ListaDeInt {
  public:
    using ListaDeInt::Item;

    using ListaDeInt::comprimento;

    using ListaDeInt::estáVazia;
    using ListaDeInt::trás;

    using ListaDeInt::põeAtrás;

    using ListaDeInt::tiraDeTrás;
};

o que corresponde simplesmente a reduzir a interface da classe ListaDeInt de modo a proporcionar apenas os serviços de uma pilha.

A herança privada deve ser usada com muita parcimónia, pois introduz uma ligação muito forte entre a classe PilhaDeInt e a classe ListaDeInt.  Regra geral, portanto, deve-se usar a composição para este tipo de relações e usar herança pública para representar relações é um.

1.7  Políticas de acesso

Em termos de políticas de acesso, as regras podem ser resumidas como se segue:
  1. Se um membro de uma classe é privado, só lhe podem aceder membros ou amigos dessa mesma classe.
  2. Se um membro de uma classe é público, o acesso não tem restrições.
  3. Os membros públicos de uma classe estão sempre acessíveis a membros e amigos de qualquer classe derivada.
  4. Os membros públicos de uma classe só são acessíveis por intermédio de instâncias de uma classe derivada se essa derivação se tiver feito duma forma pública (herança pública).
Por exemplo:

class A {
  private:
    int i;
  public:
    int u;
};

class B : private A {
  public:
    void f();
};

void B::f()
{

    i = 1; // erro: i é privada de A.
    u = 1;
}

class C : public A {
  public:
    void f();
};

void C::f()
{

    i = 1; // erro: i é privada de A.
    u = 1;
}

void g() {
    A a;
    B b;
    C c;

    a.i = 1; // erro: i é privada de A.
    a.u = 1;
    b.i = 1; // erro: i é privada de A.
    b.u = 1; // erro: u é privada de B embora seja pública de A (herança privada).
    c.i = 1; // erro: i é privada de A.
    c.u = 1;

    a = b; // erro: um B não é um A (herança privada).
    b = a; // erro: um A não é um B.
    a = c;
    c = a; // erro: um A não é um C.
}

Mais tarde se estudarão as propriedades de uma terceira categoria de acesso: protected.

Leitura recomendada

Recomenda-se a leitura do Capítulo 14 de [1].

Referências

[1]  Michael Main e Walter Savitch, "Data Structures and Other Objects Using C++", Addison-Wesley, Reading Massachusetts, 1997.