Elmord's Magic Valley

Software, lingüística e rock'n'roll. Às vezes em Português, sometimes in English.

Orientação a objetos: faça você mesmo!

2012-04-11 23:41 -0300. Tags: comp, prog, em-portugues

É possível escrever um programa orientado a objetos em C? Claro que sim! Embora não tenhamos suporte da linguagem em nível sintático a orientação a objetos, nada impede que façamos na mão o que uma linguagem com suporte nativo a OO está fazendo por baixo dos panos para nós. Implementar OO manualmente é interessante para entendermos como as coisas realmente funcionam. Ready for the fun?

O que é orientação a objetos

Antes de começarmos a implementar OO, temos uma pequena dificuldade: não há uma definição universalmente aceita de o que é OO. Para os fins deste post, assumiremos que as características mais importantes da orientação a objetos são a herança e o polimorfismo. Herança é a habilidade de definir novos tipos de objetos estendendo tipos já existentes (e.g., criar um tipo Funcionário a partir de um tipo Pessoa, com todos os dados que uma Pessoa tem e possivelmente outros dados, e suportando todas as funções/métodos que uma Pessoa suporta e possivelmente outros métodos). Polimorfismo é a habilidade de escrever funções que aceitam diferentes tipos de objetos (e.g., uma função que opera sobre Pessoas também operará automaticamente com Funcionários, já que Funcionários também são Pessoas). Consideraremos aqui apenas herança simples, i.e., uma classe/tipo pode estender apenas uma classe, não podendo incorporar atributos e métodos de mais de uma classe.

Do que é feito um objeto

Um objeto é basicamente uma struct, com um campo para cada atributo do objeto. Adicionalmente, a struct tem um campo escondido: um ponteiro para uma tabela de métodos (também conhecida como virtual table, ou vtable, no mundo C++).

typedef struct pessoa_t {
    struct pessoa_vtable_t *vtable;
    char nome[16];
    int idade;
    char sexo;
} pessoa_t;

A tabela de métodos contém ponteiros para as funções que implementam cada um dos métodos do objeto. Criamos uma struct representando a tabela, e inicializamos uma tabela com ponteiros para os métodos adequados:

// Implementação do método 'imprime()' de um objeto do tipo 'pessoa_t'.
void imprime_pessoa(pessoa_t *p) {
    printf("Esta pessoa se chama %s, e tem %d anos.\n", p->nome, p->idade);
}

// Implementação do método 'cumprimenta()' de um objeto do tipo 'pessoa_t'.
void cumprimenta_pessoa(pessoa_t *p) {
    printf("Olá, %s!\n", p->nome);
}

// Tipo da vtable de pessoa_t.
typedef struct pessoa_vtable_t {
    void (*imprime)(pessoa_t *);
    void (*cumprimenta)(pessoa_t *);
} pessoa_vtable_t;

// A vtable propriamente dita.
pessoa_vtable_t pessoa_vtable = { imprime_pessoa, cumprimenta_pessoa };

Agora, temos que escrever um construtor para a classe pessoa_t. O construtor deve alocar um novo objeto, inicializar os atributos apropriadamente, e fazer o campo vtable apontar para a tabela de métodos que acabamos de definir:

pessoa_t *make_pessoa(char *nome, int idade, char sexo) {
    pessoa_t *new = malloc(sizeof(pessoa_t));
    strcpy(new->nome, nome);
    new->idade = idade;
    new->sexo = sexo;

    new->vtable = &pessoa_vtable;
    return new;
}

Pronto! Temos nossa classe pessoa_t. Agora podemos instanciar objetos da classe e chamar seus métodos:

int main() {
    pessoa_t *p = make_pessoa("Hildur", 18, 'f');
    p->vtable->imprime(p);
    p->vtable->cumprimenta(p);
    return 0;
}

É um tanto quanto desagradável usar a sintaxe objeto->vtable->método(objeto, arg1, arg2); podemos melhorar um pouco a situação criando uma função wrapper para cada método:

void imprime(pessoa_t *p)     { p->vtable->imprime(p); }
void cumprimenta(pessoa_t *p) { p->vtable->cumprimenta(p); }

Agora podemos reescrever nossa main como:

int main() {
    pessoa_t *p = make_pessoa("Hildur", 18, 'f');
    imprime(p);
    cumprimenta(p);
}

Herança e polimorfismo

Até agora só complicamos as coisas, adicionando uma tabela de métodos às nossas structs e chamando funções através dela, sem ganho aparente. A vantagem disso tudo será agora, quando definiremos uma nova classe, funcionario_t, que estende pessoa_t, adicionando um campo salario:

typedef struct funcionario_t {
    struct funcionario_vtable_t *vtable;
    char nome[16];
    int idade;
    char sexo;
    float salario;
} funcionario_t;

O truque aqui é que os campos que são comuns a pessoa_t e funcionario_t têm os mesmos tipos e são definidos na mesma ordem, e portanto têm os mesmos endereços a partir do começo da estrutura (assumindo um compilador terráqueo). Assim, se passarmos um funcionario_t para uma função que espera uma pessoa_t, essa função poderá acessar os campos da estrutura normalmente, type warnings à parte.

Falta definirmos a tabela de métodos. Vamos manter o método cumprimenta intacto, de modo que um funcionário é cumprimentado como qualquer pessoa. Mas vamos substituir o método imprime para imprimir o salário, e vamos adicionar um método que multiplica o salário do funcionário por um fator:

typedef struct funcionario_vtable_t {
    void (*imprime)(funcionario_t *);
    void (*cumprimenta)(funcionario_t *);
    void (*multiplica_salario)(funcionario_t *, float fator);
} funcionario_vtable_t;

Novamente, os ponteiros para métodos comuns a pessoa_t e funcionario_t têm a mesma posição na estrutura. Falta inicializar a tabela:

void imprime_funcionario(funcionario_t *f) {
    imprime_pessoa(f);   // Chamamos o método da superclasse.
    printf("Esta criatura ganha %f dinheiros.\n", f->salario);
}

void multiplica_salario_funcionario(funcionario_t *f, float fator) {
    f->salario *= fator;
}

funcionario_vtable_t funcionario_vtable =
    { imprime_funcionario, cumprimenta_pessoa, multiplica_salario_funcionario };

Note que copiamos o método cumprimenta_pessoa intacto na tabela de métodos de funcionario_t. Note também que na definição da função que implementa o método imprime do funcionário chamamos o método imprime de pessoa_t, passando como argumento o funcionário. O compilador nos dará um warning pelo conflito de tipos, mas nada de grave acontecerá durante a execução, pois os campos estão nas posições esperadas.

Por fim, nosso construtor:

funcionario_t *make_funcionario(char *nome, int idade, char sexo, float salario) {
    funcionario_t *new = malloc(sizeof(funcionario_t));
    strcpy(new->nome, nome);
    new->idade = idade;
    new->sexo = sexo;
    new->salario = salario;

    new->vtable = &funcionario_vtable;
    return new;
}

Se tivéssemos separado a alocação e a inicialização do objeto em funções separadas, poderíamos chamar a função de inicialização de pessoa_t na inicialização do funcionário. A vida tem dessas coisas.

E por fim [2], mais uma função wrapper para facilitar nossa vida:

void multiplica_salario(funcionario_t *f, float fator) {
    f->vtable->multiplica_salario(f, fator);
}

Note novamente que as funções wrapper que escrevemos antes para pessoa_t continuam funcionando para funcionario_t, warnings à parte, já que os ponteiros correspondentes na tabela de métodos dos dois tipos de objetos ficam nas mesmas posições.

Será que funciona?

int main() {
    pessoa_t *p1 = make_pessoa("Hildur", 18, 'f');
    funcionario_t *p2 = make_funcionario("Elmord", 20, 'm', 1000);

    imprime(p1);
    imprime(p2);
    cumprimenta(p1);
    cumprimenta(p2);
    multiplica_salario(p2, 1.5);
    imprime(p2);

    return 0;
}

O código final desenvolvido neste post pode ser obtido aqui.

Comentários / Comments (1)

Carol Nogueira, 2012-04-14 06:31:41 -0300 #

Obrigada, Cara! Agora falta só a apostila e um áudio para eu colocar no mp3 =D


Deixe um comentário / Leave a comment

Main menu

Posts recentes

Comentários recentes

Tags

em-portugues (213) comp (137) prog (68) in-english (50) life (47) pldesign (35) unix (34) lang (32) random (28) about (27) mind (25) lisp (23) mundane (22) fenius (20) ramble (17) web (17) img (13) rant (12) hel (12) privacy (10) scheme (10) freedom (8) copyright (7) bash (7) music (7) academia (7) lash (7) esperanto (7) home (6) mestrado (6) shell (6) conlang (5) emacs (5) misc (5) latex (4) editor (4) book (4) php (4) worldly (4) android (4) politics (4) etymology (4) wrong (3) security (3) tour-de-scheme (3) kbd (3) c (3) film (3) network (3) cook (2) poem (2) physics (2) wm (2) treta (2) philosophy (2) comic (2) lows (2) llvm (2) perl (1) en-esperanto (1) audio (1) old-chinese (1) kindle (1) german (1) pointless (1) translation (1)

Elsewhere

Quod vide


Copyright © 2010-2020 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.