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 artigoGeraçã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.