DEV Community

Cover image for Orientação a Objetos em C? Implementando uma interface do zero.
Andrey Arthur Sousa e Silva
Andrey Arthur Sousa e Silva

Posted on

Orientação a Objetos em C? Implementando uma interface do zero.

Sempre fui um cara curioso quando se trata de computação, daquele tipo que pensa: "Ok, entendi como usar, mas como isso funciona de verdade?". Nesse processo, sempre faço um exercício de imaginação: se eu tivesse que implementar isso do zero, como faria? Neste artigo, vamos explorar como interfaces funcionam na orientação a objetos usando Java e, em seguida, implementar uma versão rudimentar de interface em C.

Vamos ao nosso exemplo

O nosso exemplo será muito simples: calcularemos o preço de um veículo. Se for um carro, o preço será calculado com base na velocidade que ele pode alcançar; se for uma moto, o preço será calculado com base nas cilindradas. Começamos definindo o comportamento de um veículo com uma interface:

public class Main {
    public interface Vehicle {
        Integer price();
    }
}
Enter fullscreen mode Exit fullscreen mode

Nada demais aqui, apenas um método que retorna um inteiro. Agora vamos implementar a classe do carro:

public class Main {
    // ...

    public static class Car implements Vehicle {
        private final Integer speed;

        public Car(Integer speed) {
            this.speed = speed;
        }

        @Override
        public Integer price() {
            return speed * 60;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Clássico: um construtor e a implementação do método price, multiplicando a velocidade por 60. Agora, vamos implementar a classe da moto:

public class Main {
    // ...

    public static class Motorcycle implements Vehicle {
        private final Integer cc;

        public Motorcycle(Integer cc) {
            this.cc = cc;
        }

        @Override
        public Integer price() {
            return cc * 10;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Praticamente igual, com a única diferença de que agora estamos multiplicando as cilindradas por 10. Vamos então implementar um método que imprime o preço de um veículo:

public class Main {
    // ...

    public static void printVehiclePrice(Vehicle vehicle) {
        System.out.println("$" + vehicle.price() + ".00");
    }
}
Enter fullscreen mode Exit fullscreen mode

Sem segredos. Por fim, nosso método main:

public class Main {
    // ...

    public static void main(String[] args) {
        Car car = new Car(120);
        Motorcycle motorcycle = new Motorcycle(1000);

        printVehiclePrice(car);
        printVehiclePrice(motorcycle);
    }
}
Enter fullscreen mode Exit fullscreen mode
$ java Main.java
$7200.00
$10000.00
Enter fullscreen mode Exit fullscreen mode

É esse o modelo que queremos alcançar, mas agora do zero, em C.

Como vamos resolver esse problema?

Quando penso em objetos, a primeira coisa que me vem à mente é um conjunto de dados que representa um estado e métodos que manipulam e gerenciam esse estado. A maneira mais direta de representar um conjunto de dados em C é uma struct. Já para métodos, a forma mais próxima seria uma função que recebe o estado como argumento. Esse estado corresponderia ao this em uma classe, por exemplo. Um exemplo prático seria:

typedef struct {
    int height_in_cm;
    int weight_in_kg;
} Person;

float person_bmi(Person *person) {
    float height_in_meters = (float)person->height_in_cm / 100;
    float bmi =
        (float)person->weight_in_kg / (height_in_meters * height_in_meters);

    return bmi;
}
Enter fullscreen mode Exit fullscreen mode

Aqui definimos os dados de uma pessoa na struct Person e utilizamos esses dados para fazer um cálculo simples. Isso é uma das coisas mais próximas de uma classe que podemos ter em C. Talvez usar ponteiros de função dentro da struct também seja uma boa ideia? Bom, isso fica para um próximo artigo.

Ok, temos uma espécie de classe. Agora, como podemos definir uma interface em C? Se pensarmos bem, um compilador/interpretador não faz mágica para adivinhar quais classes implementam uma interface. Ele consegue determinar isso em tempo de compilação e trocar todas as partes onde usamos interfaces pelos tipos concretos. No programa compilado, as interfaces nem sequer existem.

Como o compilador de C não nos oferece essa possibilidade, teremos que implementar todo esse esquema sozinhos. Precisamos saber todos os tipos que implementam nossa interface e dar um jeito de usar as funções dessas implementações.

Implementando uma interface em C

Para começar, vamos definir o esqueleto da nossa interface rudimentar. Criaremos um enum com as diferentes implementações e a assinatura das nossas funções.

#include <stdio.h>
#include <stdlib.h>

typedef enum { VEHICLE_CAR, VEHICLE_MOTORCYCLE } VehicleType;

typedef struct {
    VehicleType type;
} Vehicle;

void vehicle_free(Vehicle *vehicle);

int vehicle_price(Vehicle *vehicle);
Enter fullscreen mode Exit fullscreen mode

Aqui definimos nosso enum com as implementações que faremos posteriormente. Pode não parecer, mas essa é a parte mais importante. Em seguida, declaramos as funções vehicle_free, que explicarei mais adiante, e vehicle_price, que desejamos implementar nas nossas "classes". Agora vamos à implementação do carro:

// ...

typedef struct {
    VehicleType type;
    int speed;
} Car;

Car *car_init(int speed) {
    Car *car = malloc(sizeof(Car));

    car->type = VEHICLE_CAR;
    car->speed = speed;

    return car;
}

void car_free(Car *car) {
    free(car);
}

int car_price(Car *car) {
    return car->speed * 60;
}
Enter fullscreen mode Exit fullscreen mode

A função car_init inicializa um novo "objeto" Car na memória. Em Java, isso seria feito automaticamente com new. Aqui, precisamos fazer manualmente. A função vehicle_free será usada para liberar a memória alocada por qualquer "objeto" inicializado anteriormente, utilizando implementações como car_free. A implementação da Motorcycle é bem parecida:

// ...

typedef struct {
    VehicleType type;
    int cc;
} Motorcycle;

Motorcycle *motorcycle_init(int cc) {
    Motorcycle *motorcycle = malloc(sizeof(Motorcycle));

    motorcycle->type = VEHICLE_MOTORCYCLE;
    motorcycle->cc = cc;

    return motorcycle;
}

void motorcycle_free(Motorcycle *motorcycle) {
    free(motorcycle);
}

int motorcycle_price(Motorcycle *motorcycle) {
    return motorcycle->cc * 10;
}
Enter fullscreen mode Exit fullscreen mode

Praticamente igual, só muda que agora inicializamos com VEHICLE_MOTORCYCLE e multiplicamos por 10. Agora vamos pra função que imprime o preço do veículo:

// ...

void print_vehicle_price(Vehicle *vehicle) {
    printf("$%d.00\n", vehicle_price(vehicle));
}
Enter fullscreen mode Exit fullscreen mode

Tão simples... Vendo assim nem parece que estamos tendo todo esse trabalho. Agora, por último, e mais importante, temos que implementar as funções que declaramos na definição da nossa interface lá em cima, lembra? Para a nossa sorte, nem precisamos pensar tanto nessa implementação. Sempre teremos um simples switch/case exaustivo, nada mais.

// ...

void vehicle_free(Vehicle *vehicle) {
    switch (vehicle->type) {
    case VEHICLE_CAR:
        car_free((Car *)vehicle);
        break;
    case VEHICLE_MOTORCYCLE:
        motorcycle_free((Motorcycle *)vehicle);
        break;
    }
}

int vehicle_price(Vehicle *vehicle) {
    switch (vehicle->type) {
    case VEHICLE_CAR:
        return car_price((Car *)vehicle);
    case VEHICLE_MOTORCYCLE:
        return motorcycle_price((Motorcycle *)vehicle);
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora podemos usar tudo o que fizemos:

// ...

int main(void) {
    Car *car = car_init(120);
    Motorcycle *motorcycle = motorcycle_init(1000);

    print_vehicle_price((Vehicle *)car);
    print_vehicle_price((Vehicle *)motorcycle);

    vehicle_free((Vehicle *)car);
    vehicle_free((Vehicle *)motorcycle);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode
$ gcc -o main main.c
$ ./main
$7200.00
$10000.00
Enter fullscreen mode Exit fullscreen mode

Funcionou! Mas você pode estar pensando: "Ok, mas para que serve?".

Um caso de uso real

Um dos meus tipos de projetos favoritos são parsers, desde interpretadores até simples parsers de expressões matemáticas. Geralmente quando você está implementando um desses, vai esbarrar em algo chamado AST (Abstract Syntax Tree). Como o próprio nome diz, ela é uma árvore que vai representar a sintaxe que você está processando, por exemplo, uma declaração de variável int foo = 10; é um nó da AST que contém mais três outros nós, um nó de tipo, para o int, um nó de identificador, para o foo, e um nó de expressão para o 10, esse que contém outro nó de inteiro com o valor numérico 10. Viu como fica complexo?

Quando fazemos isso em C, temos que escolher entre uma struct gigante com diversos campos, para reprensentar qualquer possível nó da AST, ou várias structs pequenas implementando uma definição abstrata, representando cada uma um nó diferente, como fizemos aqui, com a nossa "interface". Se quiser ver um exemplo simples, nesse parser de expressões matemáticas eu implemento a segunda forma.

Conclusão

Nada que um compilador ou interpretador faz é mágica. Tentar implementar algo por conta própria é sempre um exercício interessante. Espero que tenha sido uma leitura proveitosa. Obrigado!

Top comments (0)