Universidade Federal de Minas Gerais
Instituto de Ciências Exatas
Departamento de Ciência da Computação
Algoritmos e Estruturas de Dados I

Classes Derivam classes e têm amizades

Nos programas na linguagem C++ podemos trabalhar com somente um nível de classe; considere o trecho de programa:

class Animal{/* definição da estrutura e comportamento */};

...

Animal a1, a2, a3;

Mas também podemos definir uma classe a partir de outras; considere este outro trecho de programa:

class Animal{ /*...*/};

class Mamifero:public Animal {/* estrutura e comportamento especifico de mamifero*/};

...

Animal a1;

Mamifero a2, a3;

No primeiro trecho a1, a2 e a3 eram todos animais sem distinção, no segundo trecho a1, a2 e a3 também são animais mas a2 e a3 mais do que serem simplesmente animais são também mamíferos.

Conforme mencionado no material anterior (Classes) na linguagem C++ é possível definir uma classe a partir de outras classes já definidas. Ao definirmos uma classe SUB via derivação de uma outra classe SUPER estamos impondo várias características à classe SUB. Em particular, como será visto, o tipo das instâncias de SUB é “compatível” com o tipo das instâncias de SUPER (mas nem toda instância de SUPER é “compatível” com instâncias de SUB) e a definição de SUB herda a definição de SUPER, o material abaixo discute o significado desta herança de definições. Esta possibilidade de definir uma classe a partir de outras classes é considerado um importante instrumento da atividade de programação! Podemos desdobrar diferentes graus de abstração à medida que criamos hierarquias de classes (a hierarquia é definida pela relação de herança). É fundamental para a boa disciplina só relacionarmos uma classe A com uma classe B através de herança se e somente se todo A “é um” B. A importância de se observar esta disciplina será discutida abaixo, mas por agora observe o exemplo acima: todo mamífero “é um” animal! Este é um exemplo com boa disciplina. Um contra exemplo seria
class Fruta: public Animal{/* exemplo errado de heranca*/};


Já dissemos (para ajudar no entendimento inicial!) que as classes podem ser colocadas em correspondência com conjuntos e os objetos ou instâncias podem ser colocados em correspondência com elementos do respectivo “conjunto”. As classes podem ser relacionadas através da relação de herança e esta relação pode ser colocada em correspondência com a relação de inclusão de conjuntos.

Falando de outro modo: uma classe pode se relacionar com uma outra classe especializando a modelagem de um conceito. Considere o exemplo abaixo:

class A {...}
class A_especializada: public A{...}
...
A a;
A_especializada ae;

 

Na analogia de “classes/objetos” com “conjuntos/elementos” a relação de herança corresponde a dizer que A_especializada é um subconjunto de A, ou ainda, toda instância de A_especializada é uma instância de A (analogamente aos conjuntos: nem toda instância de A é uma instância de A_especializada).

As definições acimas são sintéticas. Se preferir pense em A como sendo Veículo e A_especializada como sendo Caminhão, ou ainda pense em A como sendo Construção e A_especializada como sendo Casa, ou ainda  pense em A como sendo Telefone e A_especializada como sendo "Telefone sem fio". A idéia é que A_especializada  especializa a modelagem de A. Esta relação de herança pode ser descrita de várias formas:

Todos os termos acima podem ser utilizados nos relacionamentos transitivos. Se a classe C especializa B e B especializa A então dizemos que C é herdeira de A ou que C é uma subclasse de A. Podemos distinguir dizendo, por exemplo, que B é herdeira direta de A. Podemos até mesmo usar termos usados em relações de familiares: a classe B é filha de A, a classe C é neta de A.

As instâncias de uma classe A herdeira de uma classe B apresentam o comportamento de instâncias de B e potencialmente apresentam comportamentos adicionais. Por isso podemos dizer que toda instância de A "é uma" instância de B. Em qualquer lugar onde pode aparecer uma instância de B podemos colocar uma instância de A, mas o contrário não é verdadeiro.

 

A importância do conceito de superclasse/subclasse/herança:
podemos desenvolver programas tendo como abstração certas superclasses, posteriormente  podemos utilizar tanto as superclasses quanto as subclasses correspondentes a estas superclasses;  esta característica torna o código mais adaptável e reutilizável.

class Aluno{..}
class Aluno_de_graduacao : public Aluno{...}

Ao elaborarmos um programa relacionado à abstração “Aluno” podemos escrever trechos do programa de forma abrangente utilizando a classe Aluno sempre que for adequado, em outros trechos do programa temos a possibilidade de utilizar não só a classe Aluno mas também suas subclasses (p.ex. Aluno_de_graduacao).

Uma classe derivada, além de ter acesso a sua própria estrutura (e as variáveis globais!), tem acesso à estrutura e comportamentos públicos e protegidos das classes base. Além disso, como será visto à frente uma classe derivada pode redefinir estrutura e comportamento (mas não pode renunciar/repudiar os elementos da herança).

Não devemos usar a relação de herança simplesmente para poder ter acesso à estrutura e comportamento (campos e métodos) de uma classe. Considere a definição de uma classe Formulario a partir de uma classe Celula:
 

class Celula{...}
class Formulario :public Celula{...}  /* contra exemplo!!! */

Exceto por casos muito particulares e intrincados *não* podemos dizer que todo formulário é uma celula! Portanto num caso como este a relação entre as classes correspondentes a "formulário" e "célula"  deve ser uma Relação de Composição. Uma classe A tem uma relação de composição com uma classe B quando a classe A possui campos/membros do tipo da classe B. Certamente podemos imaginar que a classe Formulario, de maneira disciplinada, deve ter em sua composição acesso a objetos do tipo definido pela classe Celula:

class Celula{...}
class Formulario{... <declaração de campos do tipo Celula> ...}

A relação de composição entre a classe A e B pode ser interpretada como sendo a relação "faz parte de" ou ainda a relação "tem". Em termos do exemplo acima: Celula "faz parte de" Formulario, ou ainda Formulario "tem" Celula.
Abaixo demonstramos alguma relações de herança que julgamos adequadas no domínio de formulários e células:
class Celula{...}
class CelulaNumerica : public Celula{...}
class CelulaTexto : public Celula{...}
class CelulaInt : public CelulaNumerica{...}

A expressão "hierarquia de classes" usualmente denota um conjunto de classes e seus relacionamentos de herança.

considere as definições:

class Conta : public Object {...};
class Poupanca : public Conta{ … };

As sentenças acima definem uma classe Conta a partir da qual podemos definir uma hierarquia e no caso acima temo apenas mais uma classe (Poupança) definida a partir dela. Com a modelagem acima fica implícito o fato de que toda instância de Poupanca é também uma instância de Conta. Se no mundo dos bancos ou no mundo financeiro uma poupança não tiver esta característica então não devemos usar esta hierarquia, ou mais especificamente não devemos modelar Poupanca como sendo uma classe que herda estrutura e comportamento da classe Conta.

 

O trecho de código abaixo mostra de forma sintética uma relação de herança entre as classes A, B e C.
class A{public: static int as; int ai;};
int A::as=1;
class B:public A{ public: static int bs; int bi;};
int B::bs=2;
class C:public B{ public: static int cs; int ci;};
int C::cs=3;

Ao instanciarmos objetos destas classes...
    A v1; v1.ai=10;
    B v2; v2.ai=20; v2.bi=30;
    C v3; v3.ai=40; v3.bi=50; v3.ci=60;
 ...as regras de herança se manifestam na forma sugerida pela figura abaixo.

Na figura acima podemos observar regiões de memória comuns às classes A. B e C (p.ex. uma variável estática de A é a mesma herdada por B) e podemos observar que cada objeto tem “fatias” de instanciação correspondentes à hierarquia de onde ele se origina.  Nas implementações do ambiente do C++ muitas vezes não se usa a contiguidade ou superposição física, um esquema mais flexível é obtido com o uso de encadeamento utilizando ponteiros, mas esta figura ilustra alguns aspectos importantes: por exemplo o que acontece quando fazemos uma atribuição do tipo v1=v3;

 

Nesta disciplina não iremos estudar as diversas convenções usuais sobre herança da linguagem C++, mas deve ser notado que existe todo um conjunto de diretrizes sobre o uso de diversos conceitos que são muito importantes na produção de software de qualidade.

Existe um conceito que apoia o desenvolvimento de software de qualidade: o conceito de Abstração de Dados. Se o desenvolvimento de software utilizar abstração de dados tipicamente resolvemos potencialmente o problema de poder modificar uma parte do software sem afetar outras partes. O conceito de abstração de dados depende em parte de sabermos como disciplinar os mecanismos de acesso aos elementos que compõem um programa, em particular as classes e seus membros.

 Nas classes C++ podemos ter seções definidas com os rótulos public, protected e private, na definição de classes podemos relaciona-las em termos de herança também com estes rótulos: public, protected e private. A tabela abaixo delineia grosseiramente o significado:

 

private

protected

public

herança privada

membro inacessível

membro privado

membro privado

herança protegida

membro inacessível

membro protegido

membro protegido

herança pública

membro inacessível

membro protegido

membro público

Membros públicos e privados são “intuitivos”; membros protegidos são aqueles podem ser acessados pelas classes derivadas e descendentes. O mais comum de ser visto em programas C++ é a herança pública (talvez por desconhecimento de como usar devidamente a herança protegida e a herança privada). Para ajudar a fixar estes conceitos faça como exercício alguns programas em C++ para verificar o seguinte esquema:

 

public

protected

private

acesso na classe

sim

sim

sim

acesso na classe derivada

sim

sim

não

acesso fora da hierarquia

sim

não

não

 

Os acessos  em C++ são ainda completados pela relação de amizade (“friendship”/ “friend”). As relações de amizade complementam as regras de acesso da linguagem. A definição de uma classe pode definir uma relação de amizade com (i) uma função ou (ii) uma classe.

O programa abaixo ilustra a definição de uma classe que declara uma função como “amiga”

#include <iostream>

using namespace std;

class A{private: static int m; //declaracao
  public:
  static void mostram(){printf("%d\n",m);};
  friend void ajustam(int);
};
int A::m=0; //definicao
void ajustam(int i){A::m=i;};
int main(int argc, char *argv[])
{
  A::mostram();
  ajustam(123);
  A::mostram();
    system("PAUSE");
    return EXIT_SUCCESS;
}

O programa abaixo ilustra a definição de uma classe que define uma outra classe como “amiga”:
using namespace std;

#include <iostream>

using namespace std;

class A{private: static int m; //declaracao
  public:
  static void mostram(){printf("%d\n",m);};
  friend class B;
};
int A::m=0; //definicao
class B{ public:
static void ajustam(int i){A::m=i;};
};
int main(int argc, char *argv[])
{
  A::mostram();
  B::ajustam(123);
  A::mostram();
    system("PAUSE");
    return EXIT_SUCCESS;
}

Amizade não é herdada… Amizade não é transitiva... Amizade não é simétrica...

---Inicialização de membros estáticos

Parte do material abaixo foi copiado de public dot boulder dot ibm dot com. O trecho de programa abaixo ilustra várias possibilidades de inicialização de membros estáticos:

class C {
  static int i;
  static int j;
  static int k;
  static int l;
  static int m;
  static int n;
  static int p;
  static int q;
  static int r;
  static int s;
  static int f() { return 0; }
  int a;
public:
  C() { a = 0; }
};


C c;
int C::i = C::f();    // initialize with static member function
int C::j = C::i;      // initialize with another static data member
int C::k = c.f();     // initialize with member function from an object
int C::l = c.j;       // initialize with data member from an object
int C::s = c.a;       // initialize with nonstatic data member
int C::r = 1;         // initialize with a constant value


class Y : private C {} y;

int C::m = Y::f();
int C::n = Y::r;
int C::p = y.r;       // error
int C::q = y.f();     // error

A inicialização de C::p e C::q causa erros porque y é um objeto de uma classe que é derivada de forma privada a partir de C e seus membros não acessíveis aos membros de C.

O programa abaixo ilustra a denominada “herança multipla”:
#include <cstdlib>
#include <iostream>

using namespace std;
class A{public: A(){x=1; y=1;}; int x; int y;};
class B{public: B(){x=10;}; int x;};

class C: public A, public B{public: C(){cout<<y<<"\n";};};

int main(int argc, char *argv[]){
  C v;
  system("PAUSE");
  return EXIT_SUCCESS;
}

A menção à y não tem ambiguidade, mas como especificar em C o acesso a x?

No material sobre classes foi visto que ao ser instanciado um objeto é feita a chamada ao construtor e quando o objeto deixa de existir é invocado o destruidor correspondente. O programa abaixo ilustra a chamada dos construtores da classe base até a a classe derivada e posteriormente a chamada dos destruidores da classe derivada até a classe base.

#include <cstdlib>
#include <iostream>
using namespace std;

class A{public:
      A(){cout<<"avo: ola!\n";};
     ~A(){cout<<"avo: adeus!\n";}; };
class B :public A{public:
      B(){cout<<"mae: ola!\n";};
     ~B(){cout<<"mae: adeus!\n";}; };
class C : public B{ public:
      C(){cout<<"filha: ola!\n";};
     ~C(){cout<<"filha: adeus!\n";}; };
void f(){
  C v;
}
int main(int argc, char *argv[]){
  f();
  cout<<"alocacao via ponteiro:\n";
   C *p=new C();
  /*...*/
  delete p;
  system("PAUSE");
  return EXIT_SUCCESS;
}

Escreva um programa que ilustra a ordem de execução dos construtores e destruidores de uma classe quando existe herança múltipla.

 

O programa acima ilustra o fato de que o compilador C++ gera de maneira IMPLÍCITA as chamadas dos construtores “default”. Além disso o compilador gera também um construtor default se o programador não o definir. Quando o programador define construtores diferentes dos construtores default (construtor com parâmetros) o compilador não mais gera um construtor default e a correspondente chamada (implícita). Compile o seguinte  trecho de programa e verifique a mensagem de erro:

class A{public: A(int x){};};
class B:public A {public: B(){};};

>>>>>> no matching function for call to ‘A::A()’

Lista de Inicialização (initialization list)

 

A linguagem C++  permite o encadeamento das chamadas de construtores, de forma similar a o que ocorre no encadeamento das definições de herança:

class A{public: A(int x){};};
class B:public A {public: B(int y):A(3){};};

Observe que o construtor B(int) especifica (separado por dois pontos!) a chamada do construtor de A com o parâmetro real 3. Mais do que isso a sintaxe da linguagem permite estender a idéia de iniciação de “objetos” também para tipos fundamentais, observe o trecho de programa:

class A{public: int x; int y;
      A():x(2){y=x;}};

Neste trecho a iniciação do campo x é tratada como se fosse uma chamada de construtor, enquanto y é tratado dentro do construtor. Este aspecto da linguagem C++ é conhecido como “Initialization Lists” (Listas de iniciação). Podemos definir campos com o modificador “const”, um campo modificado por const só pode corresponder a um único valor e não pode ser modificado. Uma lista de iniciação é um bom local para “iniciar” um valor para um campo do tipo const:

class A{public:
      A():ic(2){}
      int obter_ic(){return ic;}
      private: const int ic;
};

Sim... um campo const pode ser inconstante:
class A{public:
      A():ic(2){}
      A(int op):ic(op){}
      int obter_ic(){return ic;}
      private: const int ic;
};

Se definirmos um objeto com o construtor default (A obj;) o campo ic terá valor 2. Se ao definirmos o objeto invocarmos o construtor que tem um parâmetro inteiro então o campo ic terá o valor do parâmetro(p.ex. para A v(5); resulta em ic com o valor 5).

No caso de herança multipla separamos as chamadas aos construtores por vírgula, p.ex.:

class A1{public: A1(int x){}};
class A2{public: A2(int x){}};
class B:public A1, A2 {public: B(int y):A1(3), A2(4){}};

A lista de iniciações não altera a ordem de execução determinada pela “lista de herança”:

No exemplo acima podemos escrever:
|class B:public A1, A2 {public: B(int y):A2(3), A1(4){}};
A ordem de construção continuará sendo a ordem de definição na herança (instanciação de A1 seguido da instanciação de A2) a lista de iniciação é consultada parasaber se o contrutor é chamado com parâmetros e quais os valores.

Na linguagem C++ o elemento “struct” da linguagem C foi estendido para funcionar como uma classe, ou seja, na linguagem C uma “struct” só agrupa e abriga dados, mas na linguagem C++ podemos agrupar e abrigar em uma “struct” a definição de funções. Na linguagem C++ a definição de “struct” e “class” é bastante <<similar>>. O acesso default de membros de “struct” é public enquanto que  o acesso default de membros de “class” é private e o modo default de herança é private.

 

A linguagem C++ permite a definição de uma função como sendo virtual.

class A{public: virtual void f(){}}

No trecho acima a classe A é definida utilizando a função f com o modificador “virtual”. O modificador virtual neste caso serve para indicar que o programador deseja a chamada “ligação dinâmica” (dynamic binding). Observe os dois programas abaixo:

Programa 1:

#include <iostream>
using namespace std;

struct A {
   void f() { cout << "Class A" << endl; }
};
struct B: A {
   void f() { cout << "Class B" << endl; }
};
void g(A& arg) {
   arg.f();
}
int main() {
   B x;
   g(x);
}
Programa 2:

#include <iostream>
using namespace std;

struct A {
   virtual void f() { cout << "Class A" << endl; }
};
struct B: A {
   void f() { cout << "Class B" << endl; }
};
void g(A& arg) {
   arg.f();
}
int main() {
   B x;
   g(x);
}
 

Execute os dois programas e verifique a diferença. No primeiro programa não é usado o modificador “virtual” e a função g() opera sobre instâncias do tipo A. No segundo programa o programador sinaliza que ele deseja que sejam chamadas as funções “mais especificas” em função do parâmetro, a função f em A é “virtual”. Esta determinação de qual função usar é feita em tempo de execução! é dinâmica!

 

No caso acima foi usada uma referência, o programa abaixo ilustra a diferença entre diferentes possibilidades de argumentos. Tente imaginar como que as diferentes opções de tentativa de “otimização” de código podem ser incentivadas ou bloqueadas dependendo da forma como uma função é invocada.

#include <cstdlib>
#include <iostream>

using namespace std;

class A{public: virtual void m(){cout<< "sou um A" <<endl;}};
class B: public A{public: void m(){cout<<"Sou um B"<<endl;}};

void f(A p){p.m();}
void g(A& p){p.m();}

void h(A *p){p->m();}

int main(int argc, char *argv[]){
    B b;
    f(b);
    g(b);

    h(&b);
    system("PAUSE");
    return EXIT_SUCCESS;
}

Observe que o programador deve ser cuidadoso também com relação ao processo de construção & destruição das instâncias nas hierarquias. Considere o programa abaixo:
#include <cstdlib>
#include <iostream>
using namespace std;

class Base{
      public:
      Base(){cout<<"construtor da Base, ciente"<<endl;}
      /* virtual */~Base(){cout<<"destruidor da Base, ciente"<<endl;}
      };
     
class Derivada: public Base{
      public:
      Derivada(){cout<<"construtor da Derivada, ciente"<<endl;}
      ~Derivada(){cout<<"destruidor da Derivada, ciente"<<endl;}
      };

int main(int argc, char *argv[]){
   
    Derivada *pd;
    pd= new Derivada();
    delete pd;
   
    cout<<"----------------"<<endl;
   
    Base *pb;
    pb=new Derivada();
    delete pb;

    system("PAUSE");

    return EXIT_SUCCESS;
}
Para que as instâncias das classes derivadas sejam tratadas de forma adequada na hierarquia de destruição é necessário que o destruidor da base tenha o qualificador “virtual”. Mas o programa abaixo mostra que o qualificador virtual para o destruidor pode aparecer não só na base, mas em outros pontos da hierarquia:

#include <cstdlib>
#include <iostream>
using namespace std;

class Base{
      public:
      Base(){cout<<"construtor da Base, ciente"<<endl;}
      ~Base(){cout<<"destruidor da Base, ciente"<<endl;}
      };
     
class Derivada1: public Base{
      public:
      Derivada1(){cout<<"construtor da Derivada1, ciente"<<endl;}
      virtual ~Derivada1(){cout<<"destruidor da Derivada1, ciente"<<endl;}
      };
     
class Derivada2: public Derivada1{
      public:
      Derivada2(){cout<<"construtor da Derivada2, ciente"<<endl;}
      ~Derivada2(){cout<<"destruidor da Derivada2, ciente"<<endl;}
      };

int main(int argc, char *argv[]){
   
    Derivada2 *pd;
    pd= new Derivada2();
    delete pd;
   
    cout<<"----------------"<<endl;
   
    Base *pb;
    pb=new Derivada2();
    delete pb;

    cout<<"----------------"<<endl;
   
    Derivada1 *pd1;
    pd1=new Derivada2();
    delete pd1;

    system("PAUSE");
    return EXIT_SUCCESS;
}

O modificador “virtual” está ligado ao problema de edição e ligação estática e edição e ligação dinâmica e o material aqui é apenas introdutório, para alertar para diversos conceitos importantes que são vistos em tópicos avançados de Programação C++. Por exemplo, para alertar, existem vários aspectos abstrusos da linguagem sobre em que momentos do processo de construção e destruição de instâncias podemos chamar funções virtuais. Também relacionado a isto são os vários tipos de “ponteiros inteligentes” (oxímoro?) que a linguagem e bibliotecas oferecem.

 

 

---------

Uma outra caracteristica da linguagem C++ é a possibilidade de uma função “ocultar” outra função sem no entanto redefini-la. Tente prever o resultado de:
#include <iostream>
using namespace std;

struct A {
   virtual void f() { cout << "Class A" << endl; }
};

struct B: A {
   void f(int) { cout << "Class B" << endl; }
};

struct C: B {
   void f() { cout << "Class C" << endl; }
};

int main() {
   B b; C c;
   A* pa1 = &b;
   A* pa2 = &c;
//   b.f();
   pa1->f();
   pa2->f();
}
No programa acima a função B::f(int) apenas “esconde” ou “oculta” (“hide”) a função A::f(), mas na classe C há a redefinição. Dada uma invocação de função o compilador C++ deve aplicar um procedimento de decisão bastante elaborado para saber se deve dar uma mensagem de erro ou qual das “funções candidatas” deve ser invocada.

 

O uso de funções de mesmo nome entre diferentes classes (diferentes escopos de nomes) de uma mesma hierarquia é denominado de sobrescrita (override) ou “passar por cima” e não deve ser confundido com a sobrecarga (overload) que é o uso de funções de mesmo nome (mas diferentes assinaturas!) em um mesmo escopo de nomes.

 

Exercícios

1.  Execute o programa abaixo e explique porque ao ser criada uma instância de Derivada também é criada uma instância de Base.
#include <cstdlib>
#include <iostream>

using namespace std;

class Base {public: Base(){printf("B\n");}};
class Derivada :public Base {
  public: Derivada(){printf("D\n");}
};

int main(int argc, char *argv[])
{
    Derivada d;
    system("PAUSE");
    return EXIT_SUCCESS;
}

2.      Faça um programa que mostre que os construtores também podem utilizar parâmetros default (p.ex. class C{...C(int i=10, double d=20){...} ...};)

3.      Reescreva o programa acima substituindo os parâmetros default por listas de inicialização.

4.      O que é escrito em função da execução da chamada da função f(x)?
class A {public: virtual void m(){cout<<”A\n”;}};
class B :public A{public: void m(){A::m(); cout<<”B\n”;}};
class C :public B{public: void m(){B::m(); cout<<”C\n”;}};
void f(A &a){a.m();}
...
C x; f(x);

5.      Escreva um programa similar ao anterior, mas com hierarquia mais extensa. (p.ex. A, B, C e D).