Geração de Código de Simples para C - Um Meio Alternativo de Gerar Código para Envios de Mensagem
Antes de ler este texto, leia o artigo Geração e Otimização de Código
Orientado a Objetos.
Cada classe tem um número associado a ela. Se a classe não herda de ninguém, o número é 0. Então várias classes podem ter o mesmo número. As subclasses
possuem números 1, 2, ... Todas as subclasses de uma classe possuem números diferentes. Classes de hierarquias diferentes fatalmente possuirão números iguais. Não tem importância, elas nunca se confundirão. Ao compilar uma classe A, o código gerado deve ser
typedef
struct St_A {
int n;
// declaração da variáveis de instância
};
seguido da translação dos métodos de A para C. O campo n deve conter o número da classe, inicializado no contrutor da classe. O construtor de uma classe A que não herda de ningúem (número 0) deve ser
class_A
*new_A()
{
class_A *t;
if ( (t = malloc(sizeof(class_A))) != NULL
)
t->n = 0; // número da classe A
return t;
}
É
fundamental que o número de cada classe seja dado após o término da análise sintática e criação da ASA. Utilize o seguinte algoritmo para numerar as classes:
para cada classe C da ASA, faça:
if C ainda não tem número e C não tem superclasse
then
numereClasse(C, 0);
endif
int numereClasse( ClassDec aClass, int n) {
aClass.setClassNumber(n);
para cada subclasse S de aClass,
faça:
n = numereClasse(S, ++n);
}
Se você numerar as classes à medida em que for encontrando-as, a codificação de envio de mensagem dada abaixo não funcionará em alguns casos. É absolutamente fundamental que, se uma classe tem um número N, então as suas subclasses (se elas existirem) devem ter números N+1, N+2,
N+3, .... . Isto poderia não ocorrer se a numeração é feita à medida em que as
classes forem encontradas. Para enxergar isto, usaremos B(A) para significar
que a classe B foi declarada e herda de A. Se as classes
A B(A) C(A)
G(B) D(C) F(D) E(C)
comporem um programa e os números forem dados à medida que as classes forem sendo
encontradas, os números serão:
A-0
B-1 C-2 G-3
D-4 F-5 E-5
As subclasses D e E de C receberão números 4 e 5, enquanto que C tem número 2. O número entre
eles, 3, foi dado a uma classe que não é subclasse de C. Não pode.
Quando um método m for definido em uma classe C e ele não for uma redefinição de um método de alguma das
superclasses, então deve ser criado um vetor para ele do tipo Func. Se a classe não tem superclasse, então um vetor deve ser criado para cada um dos métodos da classe. O tamanho do vetor deve ser 1 + número de subclasses da classe C. A primeira posição do vetor deve conter um ponteiro para o método m da classe C. A segunda, um ponteiro para o método da classe X subclasse de C que possui número igual ao (número de C) + 1. Para exemplificar, veremos dois exemplos.
Exemplo 1: a classe A é raiz de uma hierarquia (não herda de ninguém) com subclasses B e C. D herda de C. Associaremos os números 0, 1, 2 e 3 para as classes A, B, C e D, respectivamente. Suponha que a classe A defina um método m redefinido por B, mas que não é redefinido por C. A classe D redefine o método m. De acordo com as regras do artigo “Geração de Código em C para Simples”, os nomes dos métodos m no código em C serão A_m, B_m, D_m. A classe C utilizará o método m de A. Isto é, em
var c : C;
begin
c =
C.new();
c.m();
end
o método chamado em “c.m()” será o método m da classe A. O compilador, ao gerar código, deverá criar um vetor para m com ponteiros para os métodos que cada uma das classes da hierarquia de A irá utilizar. O código em C para o vetor para m deve ser
Func dt_m_A[] { // dispatch table of method m of class A
A_m,
// 0 : método m para a classe A
B_m,
// 1 : método m para a classe B
A_m,
// 2 : método m para a classe C: como C não redefine m, utilizará o m herdado de A
D_m
// 3 : método m para a classe D
};
A posição i do vetor dt_m_A aponta para o método que a classe de número i deve utilizar. Note que, como a classe C não define um método m, ela utiliza o método herdado de A. Então a posição de número 2 do vetor acima aponta para o método m de A. Assumindo que a, b, c e d sejam variáveis das classes A, B, C e D, a geração de código para
a.m();
b.m();
c.m();
d.m();
é
dt_m_A[a->n](a);
dt_m_A[b->n](b);
dt_m_A[c->n](c);
dt_m_A[d->n](d);
Sem considerar as conversões de tipo. Veja esta codificação para um
exemplo completo. Neste exemplo, utilizamos classNumber no lugar da variável n
que informa o número da classe. O arquivo “sda.txt” contém a saída do programa “ok-ger20.c”
depois de compilado e executado. O arquivo “ok-ger20-metodo-antigo.c” contém o
arquivo “ok-ger20.s” compilado utilizando o método antigo de compilação, o que
está no artigo Geração de
Código em C para Simples. O arquivo “ok-ger20.c” contém o arquivo “ok-ger20.s”
compilado segundo o método descrito nesta página (o que vocês devem utilizar).
Se b apontar realmente para um objeto da classe B, então b->n será igual a 1 e o método chamado será B_m. Se a variável “a” apontar para um objeto de D (subclasse de A), então a->n será 3 e o método chamado será D_m. Então este esquema implementa polimorfismo perfeitamente.
Exemplo 2: um problema acontece quando A tiver uma superclasse. O número da classe A será diferente de 0. Digamos que seja 2 e que esta classe define um método m não definido pelas suas superclasses. O vetor dt_m_A deveria ser algo como
Func dt_m_A[] { // dispatch table of method m of class A
NULL,
// 0: nenhuma classe correspondente
NULL,
// 1: nenhuma classe correspondente
A_m,
// 2 : método m para a classe A
B_m,
// 3 : método m para a classe B
A_m,
// 4 : método m para a classe C: como C não redefine m,
utilizará o m herdado de A
D_m
// 5 : método m para a classe D
};
Naturalmente, agora os números de B, C e D seriam 3, 4 e 5. Colocando NULL
nas duas primeiras posições, o envio de mensagem ficaria como antes, algo como
dt_m_A[a->n](a);
Contudo, não
precisamos desperdiçar as duas primeiras posições do vetor dt_m_A. Podemos começar do 0 e, no envio de uma mensagem, subtrair o número da classe A, que é a primeira que em m foi definido:
dt_m_A[a->n - 2](a);
dt_m_A[b->n - 2](b);
dt_m_A[c->n - 2](c);
dt_m_A[d->n - 2](d);
Assim o vetor dt_m_A deveria continuar como no exemplo 1. É desta forma que o envio de mensagem deve ser implementado no segundo trabalho. Observe que não sabemos o valor de a->n: a variável “a” pode apontar para um objeto de uma subclasse de A e o valor de n pode ser 2, 3, 4 ou 5.