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).