Aula 7

1  Resumo da matéria

1.1  Notas sobre const em referências e ponteiros

Atente no seguinte exemplo:
typedef int* Item;

void f(const Item& i) {
    i++;    // erro!
    (*i)++; // ok!
}

void g(const int* p) {
    f(p); // erro!
}

A operação i++ é um erro porque i é uma referência constante para um ponteiro.  Assim, o ponteiro não pode ser alterado.  Mas o seu conteúdo pode, daí que a operação (*i)++ seja totalmente válida.

Finalmente, a invocação de f() a partir de g() é uma erro, dado que implica a inicialização de uma referência constante para int* (a referência i parâmetro de f()) a partir de um const int*, o que descarta o const, sendo esta operação proibida pelo C++!

1.2  Herança: derivação de classes

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 em nome, morada 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 é parte de).  Por exemplo, é claro que uma moeda (no sentido físico do termo, uma chapa metálica) tem uma nacionalidade, tal como tem um valor.  Mas uma moeda, na mesma acepção, não é uma nacionalidade, tal como não é um valor.  O mesmo se passa, por exemplo, com uma turma duma 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 dum projecto, por exemplo, as tarefas 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.

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.2.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.  Mas o pior é que existem 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 de escrita da 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.  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.  Por outro lado, a existência de uma única classe leva a que tenha de se escrever código para tratar um chefe como um empregado (se se pede para mostrar um empregado com nível de chefia diferente de zero, então ele será mostrado como um chefe, o que obriga a escrever código para o despromover ou pelo menos para o mostrar como mero empregado).

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

class Empregado {
    std::string nome_;
    std::string morada_;
    Sexo sexo_;
  public:
    Empregado(std::string n, std::string m, Sexo s)
        : nome_(n), morada_(m), sexo_(s) {
    }
    std::string nome() const {
        return nome_;
    }
    std::string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    void mostra() const {
        std::cout << "Nome: " << nome() << std::endl
                  << "Morada: " << morada() << std::endl
                  << "Sexo: " << sexo() << std::endl;
    }
};
A classe Chefe, por outro lado, acrescenta o nível de chefia como informação adicional, o que obriga à definição de uma variável membro adicional, à alteração do construtor, à criação da função membro nível() para aceder ao nível de chefia e à alteração do procedimento membro mostra():
class Chefe {
    std::string nome_;
    std::string morada_;
    Sexo sexo_;
    int nível_;
  public:
    Chefe(std::string n, std::string m, Sexo s, int ní)
        : nome_(n), morada_(m), sexo_(s), nível_(ní) {
    }
    std::string nome() const {
        return nome_;
    }
    std::string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    int nível() const {
        return nível_;
    }
    void mostra() const {
        std::cout << "Nome: " << nome() << std::endl
                  << "Morada: " << morada() << std::endl
                  << "Sexo: " << sexo() << std::endl
                  << "Nível: " << nível() << std::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 c("Zé", "Rua A", masculino, 2);
Empregado e = c;
é 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 e ficasse com a informação do chefe como mero empregado, i.e., sem aquilo que é específico dum chefe.  A estas operações de retirar o que é específico duma classe, chama-se em inglês "slicing", um vez que tiram apenas as "fatias" de informação relevantes.

Seria também desejável que se pudesse colocar o endereço duma instância da classe Chefe num ponteiro para Empregado.  Ou seja:

Chefe c("Zé", "Rua A", masculino, 2);
Empregado* pe = &c;
De igual forma seria desejável que referências para Empregados pudessem ser sinónimos de Chefes.  Ou seja:
Chefe c("Zé", "Rua A", masculino, 2);
Empregado& re = c;
Note-se que nestes dois casos, ao contrário da inicialização dum 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á "slicing".

Este comportamento permitiria, por exemplo, guardar todo o pessoal da empresa numa única lista de ponteiros para Empregado.  Por exemplo, admitindo que ListaPEmpregado é uma classe de lista em que os itens são do tipo Empregado*, o seguinte código seria possível:

ListaPEmpregado pessoal;
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));
pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    i.item()->mostra();


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

enum Sexo {masculino, feminino};
inline std::ostream& operator << (std::ostream& saída, Sexo s) {
    return saída << (s == masculino? "masculino" : "feminino");
}

1ª Tentativa

Uma solução parcial para o primeiro problema apresentado seria construir a classe Chefe à custa da classe Empregado colocando uma váriavel membro de instância dessa classe.  Ou seja:
class Chefe {
    Empregado e;
    int nível_;
  public:
    Chefe(std::string n, std::string m, Sexo s, int ní)
        : e(n, m, s), nível_(ní) {
    }
    std::string nome() const {
        return e.nome();
    }
    std::string morada() const {
        return e.morada();
    }
    Sexo sexo() const {
        return e.sexo();
    }
    int nível() const {
        return nível_;
    }
    void mostra() const {
        e.mostra();
        std::cout << "Nível: " << nível() << std::endl;
    }
};
Esta é apenas uma solução parcial porque por um lado não se conseguiu evitar reescrever as funções membro nome(), morada() e sexo(), e por outro lado o acrescento à posteriori do número de contribuinte na classe Empregado obrigará a reescrever não só essa classe mas também a classe Chefe, para que esta possua uma função membro para lhe aceder.

Este tipo de solução não é pois o mais indicado para representar relações é um, embora o seja para representar relações tem um.

2ª Tentativa

Um resolução parcial para o 2º 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 {
    std::string nome_;
    std::string morada_;
    Sexo sexo_;
  public:
    Empregado(std::string n, std::string m, Sexo s)
        : nome_(n), morada_(m), sexo_(s) {
    }
    Empregado(const Chefe& c)
        : nome_(c.nome()), morada_(c.morada()), sexo_(c.sexo()) {
    }
    std::string nome() const {
        return nome_;
    }
    std::string morada() const {
        return morada_;
    }
    Sexo sexo() const {
        return sexo_;
    }
    void mostra() const {
        std::cout << "Nome: " << nome() << std::endl
                  << "Morada: " << morada() << std::endl
                  << "Sexo: " << sexo() << std::endl;
    }
};
Mas 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, não resolve o problema de criar uma lista de empregados que contenha empregados normais, chefes, secretárias, etc.

Solução

A solução em C++ envolve o conceito de herança.  Uma classe pode ser definida por derivação à custa duma classe base.  Assim, a classe Chefe pode ser derivada a partir da classe Empregado da seguinte forma:
class Chefe : public Empregado {
    int nível_;
  public:
    Chefe(std::string n, std::string m, Sexo s, int ní);
    int nível() const {
        return nível_;
    }
    void mostra() const;
};
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 todas os membros das classes base.  No entanto, os membros privados da classe base não ficam acessíveis directamente às funções membro da classe derivada.  Isto deve-se a que proceder de outro modo "abriria a porta" da parte privada duma classe a quem quer que definisse uma classe sua derivada, violando assim o princípio do encapsulamento.

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

  1. Os membro públicos da classe base são herdados como membros públicos da classe derivada (daí que não seja necessário redefinir as funções membro nome(), morada() e sexo()).  Este facto resolve o primeiro problema identificado, pois acrescentar o número do contribuinte e respectiva função membro 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.
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, que não serve para representar relações é um (por motivos que devem ser óbvios), será explicada mais tarde.

Compatibilidade das classes

Note-se que a compatibilidade dos tipos 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 c("Zé", "Rua A", masculino, 2);
Empregado e = c;
Empregado* pe = &c;
Empregado& re = c;
No entanto, se se souber, como no exemplo acima, que um ponteiro ou referência duma classe base de facto referenciam um objecto (ou instância) duma dada classe derivada, é possível fazer atribuições no sentido inverso, embora apenas por intermédio do operador static_cast<tipo>():
Chefe* pc = static_cast<Chefe*>(pe);
Chefe& rc = static_cast<Chefe&>(re);

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

Como inicializar os membros das classes base duma classe derivada?  O C++ proporciona uma forma simples: podem-se invocar os construtores das classes base nas listas de inicializadores dos construtores da classe derivada.  Ou seja:
    inline Chefe::Chefe(std::string n, std::string m,
                        Sexo s, int ní)
        : Empregado(n, m, s), nível_(ní) {
    }
Se não se invocar explicitamente o construtor de uma classe base, o compilador gerará automaticamente uma chamada ao construtor por omissão dessa classe.  Claro que 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 é a que se segue:

  1. Primeiro são inicializadas as classes base, por ordem de aparecimento na especificação das heranças na definição da classe derivada, através da invocação dos respectivos construtores.
  2. Depois são inicializadas as variáveis e constantes membro 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 da classe D que se segue
class Z {
    ...
};
class D : A, B, C {
    static const int dim = 10;
    int i;
    const Z z;
    float m[dim];
    int* pi;
  public:
    D(int ii, const Z& zz) : i(ii), z(zz) {
        for(int i = 0; i != dim; ++i)
            m[i] = 0;
        pi = new int;
    }
    ~D() {
        delete pi;
    }
    ...
};
são invocados, por ordem:
  1. Construtor de A, para inicialização das variáveis membro herdadas de A.
  2. Construtor de B, para inicialização das variáveis membro herdadas de B.
  3. Construtor de C, para inicialização das variáveis membro herdadas de C.
  4. Construtor de int, para inicialização da variável membro i.
  5. Construtor de Z, para inicialização da constante membro z.
  6. m e pi, sendo derivadas de tipos básicos, não têm quaisquer construtores invocados.
  7. Corpo do construtor, onde se inicializa a matriz m e se cria uma variável dinâmica cujo endereço se guarda na variável membro pi.
Note-se que a constante membro dim é de classe, e não de instância, pelo que é inicializada no início do programa.

Como para qualquer outra classe, o compilador tentará sempre criar automaticamente um construtor por omissão (se não se tiver definido qualquer construtor na classe), que invoca os construtores por omissão de todas as classe base, um construtor por cópia (desde que não seja definido explicitamente na classe), que invoca os construtores por cópia das classe base, e um operador de atribuição por cópia (desde que não seja definido explicitamente na classe), que invocará os operadores de atribuição por cópia das classes base.

De igual forma é criado automaticamente um destrutor (desde que não seja definido explicitamente) que invoca os destrutores das classes base.  A ordem pela qual as operações ocorrem é a seguinte:

  1. Primeiro é executado o corpo do destrutor.
  2. Depois são invocados os destrutores das variáveis e constantes membro de instância da classe base, por ordem inversa de declaração.
  3. Finalmente são invocados os destrutores das classe 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 A 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, sendo derivadas de tipos básicos, não têm quaisquer destrutores invocados.
  3. Destrutor de Z, para a constante membro z.
  4. Destrutor de C, para as variáveis membro herdadas de C.
  5. Destrutor de B, para as variáveis membro herdadas de B.
  6. Destrutor de A, para as variáveis membro herdadas de A.

Sobreposição

A classe derivada pode definir funções ou procedimentos com exactamente a mesma assinatura funções ou procedimentos existentes em classe base, ou seja, com o mesmo nome e os mesmos parâmetros *.  Chama-se a este processo sobreposição (ou substituição, não confundir com sobrecarga, em que só o nome da função ou procedimento se mantém).  Isto permite sobrepor a uma função ou procedimento definidos por uma classe base uma função ou procedimento especializada para a classe derivada.  É o caso do procedimento mostra() para a classe Chefe:
    void Chefe::mostra() const {
        Empregado::mostra();
        std::cout << "Nível: " << nível() << std::endl;
    }
Repare-se que, como a versão original do procedimento já fazia parte do trabalho, a primeira operação do procedimento que se lhe sobrepôs é invocar a versão original.  Para o fazer 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.

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

Chefe c("Zé", "Rua A", masculino, 2);
c.mostra();
resulta em
Nome: Zé
Morada: Rua A
Sexo: masculino
Nível: 2
Por outro lado, se se pretendesse mostrar o chefe como mero empregado, poder-se-ia usar
c.Empregado::mostra();
que resultaria em
Nome: Zé
Morada: Rua A
Sexo: masculino
* O tipo de devolução pode ser diferente, mas tem de obedecer a algumas regras.  Ver [2, pág. 425].

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 {
    ...
};

Problemas

Voltando ao exemplo da lista de pessoal duma empresa, com a classe Chefe derivada publicamente da classe Empregado, torna-se possível o código que antes era inválido:
ListaPEmpregado pessoal;
// Inserção dos empregados:
pessoal.põeFim(new Empregado("Manela", "Rua B", feminino));
pessoal.põeFim(new Chefe("Zé", "Rua A", masculino, 2));
...
// Visualização dos empregados:
for(ListaPEmpregado::Iterador i = pessoal.primeiro();
    i != pessoal.fim();
    ++i)
    i.item()->mostra();
O único problema é que, como o procedimento mostra() é executado a partir dum ponteiro para Empregado, todos os empregados são mostrados como empregados básicos, apesar de entre eles haver empregados que são chefes, secretárias, ou motoristas.  A solução para este problema será vista na Aula 8.

1.2.2  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 um relação é um com as classes 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 PilhaInt, mas agora sem quaisquer limitações quanto ao número de itens, excepto a memória disponível.  Uma vez que a classe ListaInt está já desenvolvida, não impões restrições quanto ao comprimento das listas, e possui todos os métodos necessários para implementar uma pilha, é natural pensar em implementar o conceito de pilha com base na implementação das listas já existentes.

Admita-se que a classe ListaInt, definida na Aula 5, Secção 1.3, possuia os seguintes métodos adicionais:

Então uma primeira implementação das pilhas poderia ser:
class PilhaInt {
    ListaInt l;
  public:
    typedef ListaInt::Item Item;
    void põe(Item item) {
        l.põeFim(item);
    }
    void tira() {
        l.tiraÚltimo();
    }
    Item topo() const {
        return l.trás();
    }
    Item& topo() {
        return l.trás();
    }
    bool vazia() const {
        return l.vazia();
    }
    int altura() const {
        return l.comprimento();
    }
};
Mas um resultado muito semelhante poderia ser obtido por herança privada:
class PilhaInt : private ListaInt {
  public:
    typedef ListaInt::Item Item;
    void põe(Item item) {
        põeFim(item);
    }
    void tira() {
        tiraÚltimo();
    }
    Item topo() const {
        return trás();
    }
    Item& topo() {
        return trás();
    }
    bool vazia() const {
        return ListaInt::vazia();
    }
    int altura() const {
        return comprimento();
    }
};
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 vazia().  Eles são sobrepostos às definições originais apenas porque a herança foi feita de forma privada, e portanto o utilizador da classe PilhaInt de outra forma não teria acesso a esses membros.  Nos outros casos, como os nomes das funções e procedimentos das listas usados directamente pela pilha não têm os nomes que se pretendiam, a definição de novas funções e procedimentos justifica-se, desde que sejam inline, para não incorrer nos custos da invocação de funções.

Esta solução pode ser vista como um alteração da interface da classe ListaInt de modo a proporcionar 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 duma forma privada, basta usar uma declaração de utilização do respectivo nome na parte pública da classe derivada.  Assim, a classe PilhaInt poderia ser definida como:

class PilhaInt : private ListaInt {
  public:
    using ListaInt::Item;
    void põe(Item item) {
        põeFim(item);
    }
    void tira() {
        tiraÚltimo();
    }
    Item topo() const {
        return trás();
    }
    Item& topo() {
        return trás();
    }
    using ListaInt::vazia;
    int altura() const {
        return comprimento();
    }
};


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 PilhaInt : private ListaInt {
  public:
    using ListaInt::Item;
    using ListaInt::põeFim;
    using ListaInt::tiraÚltimo;
    using ListaInt::trás;
    using ListaInt::vazia;
    using ListaInt::comprimento;
};
o que corresponde simplesmente a reduzir a interface da classe ListaInt de modo a proporcionar apenas os serviços de uma pilha.

1.2.3  Políticas de acesso

Em termos de políticas de acesso, as regras podem ser resumidas como se segue:
  1. Se um membro duma classe é privado, só lhe podem aceder membros ou amigos dessa mesma classe.
  2. Se um membro duma classe é público, o acesso não tem restrições.
  3. Os membros públicos duma classe estão sempre acessíveis a membros e amigos de qualquer classe derivada.
  4. Os membros públicos duma 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() {
        i = 1; // erro: i é privada de A.
        u = 1;
    }
};

class C : public A {
  public:
    void 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.i = 1; // erro: i é privada de B (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 classe de acesso: protected.

1.3  Leitura recomendada

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

2  Exercícios

1.  A classe ListaInt definida em aulas anteriores não tem alguns métodos importantes.  Acrescente à classe ListaInt os seguintes métodos: 2.  Construa a classe FilaInt por herança privada de ListaInt.

3  Referências

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

[2]  Bjarne Stroustrup, "The C++ Programming Language", 3ª edição, Addison-Wesley, Reading, Massachusetts, 1997. *

# Existem 10 exemplares na biblioteca do ISCTE.

* Existe um exemplar na biblioteca do ISCTE.