Skip to content

Módulo 8: Estruturas (Structs) e Classes

Bem-vindo ao Módulo 8 do curso "Programação em Arduino: Conceitos Fundamentais sem Hardware". Neste módulo, você irá aprofundar seu conhecimento sobre estruturas (structs) e classes na linguagem de programação Arduino (C/C++). Estruturas e classes são fundamentais para a programação orientada a objetos, permitindo a criação de tipos de dados personalizados e a organização eficiente do código.


Objetivos do Módulo

  • Compreender o conceito de estruturas (structs) e classes na programação.
  • Aprender a declarar e utilizar estruturas em Arduino.
  • Introduzir conceitos básicos de programação orientada a objetos (POO) com classes.
  • Trabalhar com atributos e métodos dentro de classes.
  • Entender o conceito de encapsulamento, herança e polimorfismo.
  • Implementar construtores e destrutores em classes.
  • Resolver exercícios práticos para consolidar o conhecimento sobre estruturas e classes.

1. Introdução às Estruturas (Structs) e Classes

1.1 O que são Estruturas (Structs)?

Uma estrutura (struct) é uma coleção de variáveis agrupadas sob um único nome para representar uma entidade mais complexa. Cada variável dentro de uma estrutura é chamada de membro ou campo da estrutura.

1.2 O que são Classes?

Uma classe é uma extensão das estruturas, incorporando não apenas dados (atributos) mas também funções (métodos) que operam sobre esses dados. Classes são a base da programação orientada a objetos (POO), permitindo a criação de objetos que possuem propriedades e comportamentos.

1.3 Diferenças entre struct e class

  • Acesso Padrão:
  • Em structs, os membros são públicos por padrão.
  • Em classes, os membros são privados por padrão.

  • Funcionalidades:

  • Structs são adequadas para agrupamentos simples de dados.
  • Classes suportam POO completa, incluindo encapsulamento, herança e polimorfismo.

2. Trabalhando com Estruturas (Structs)

2.1 Declaração e Inicialização de Structs

Sintaxe:

struct NomeEstrutura {
    tipo membro1;
    tipo membro2;
    // ...
};

Exemplo:

struct Sensor {
    int id;
    float valor;
};

2.2 Utilizando Structs no Arduino

Exemplo Prático:

struct Sensor {
    int id;
    float valor;
};

Sensor sensor1;
Sensor sensor2;

void setup() {
    Serial.begin(9600);

    sensor1.id = 1;
    sensor1.valor = 23.5;

    sensor2.id = 2;
    sensor2.valor = 47.8;

    Serial.print("Sensor ");
    Serial.print(sensor1.id);
    Serial.print(": ");
    Serial.println(sensor1.valor);

    Serial.print("Sensor ");
    Serial.print(sensor2.id);
    Serial.print(": ");
    Serial.println(sensor2.valor);
}

void loop() {
    // Não há código no loop
}

Explicação:

  • Define uma estrutura Sensor com membros id e valor.
  • Declara duas variáveis sensor1 e sensor2 do tipo Sensor.
  • Inicializa os membros e imprime os valores no Monitor Serial.

3. Introdução à Programação Orientada a Objetos (POO) com Classes

3.1 Conceitos Básicos de POO

  • Objeto: Uma instância de uma classe que possui estado e comportamento.
  • Classe: Um molde para criar objetos, definindo atributos e métodos.
  • Encapsulamento: O ato de esconder os detalhes internos de uma classe e expor apenas o necessário.
  • Herança: Permite que uma classe herde atributos e métodos de outra.
  • Polimorfismo: Permite que objetos de diferentes classes sejam tratados de forma uniforme.

3.2 Benefícios da POO

  • Reutilização de Código: Criação de classes reutilizáveis.
  • Organização: Estruturação lógica do código.
  • Manutenção: Facilita a atualização e correção de código.
  • Escalabilidade: Permite o crescimento do projeto de forma ordenada.

4. Declaração e Uso de Classes

4.1 Declaração de uma Classe

Sintaxe:

class NomeClasse {
public:
    // Atributos
    tipo atributo1;
    tipo atributo2;

    // Métodos
    void metodo1();
    tipo metodo2();
};

Exemplo:

class Motor {
public:
    int rpm;
    float temperatura;

    void ligar() {
        Serial.println("Motor ligado.");
    }

    void desligar() {
        Serial.println("Motor desligado.");
    }

    float lerTemperatura() {
        return temperatura;
    }
};

4.2 Utilizando Classes no Arduino

Exemplo Prático:

class Motor {
public:
    int rpm;
    float temperatura;

    void ligar() {
        Serial.println("Motor ligado.");
    }

    void desligar() {
        Serial.println("Motor desligado.");
    }

    float lerTemperatura() {
        return temperatura;
    }
};

Motor motor1;

void setup() {
    Serial.begin(9600);

    motor1.rpm = 1500;
    motor1.temperatura = 75.0;

    motor1.ligar();
    Serial.print("RPM: ");
    Serial.println(motor1.rpm);
    Serial.print("Temperatura: ");
    Serial.println(motor1.lerTemperatura());
}

void loop() {
    // Não há código no loop
}

Explicação:

  • Define uma classe Motor com atributos rpm e temperatura e métodos ligar, desligar e lerTemperatura.
  • Cria um objeto motor1 da classe Motor.
  • Inicializa os atributos e chama os métodos para ligar o motor e imprimir os valores no Monitor Serial.

5. Construtores e Destrutores

5.1 Construtores

Um construtor é um método especial que é chamado automaticamente quando um objeto é criado. Ele é usado para inicializar os atributos do objeto.

Sintaxe:

class NomeClasse {
public:
    NomeClasse() {
        // Código de inicialização
    }
};

Exemplo:

class Sensor {
public:
    int id;
    float valor;

    // Construtor
    Sensor(int sensorId, float sensorValor) {
        id = sensorId;
        valor = sensorValor;
    }

    void imprimirDados() {
        Serial.print("Sensor ");
        Serial.print(id);
        Serial.print(": ");
        Serial.println(valor);
    }
};

void setup() {
    Serial.begin(9600);

    Sensor sensor1(1, 23.5);
    Sensor sensor2(2, 47.8);

    sensor1.imprimirDados();
    sensor2.imprimirDados();
}

void loop() {
    // Não há código no loop
}

Explicação:

  • A classe Sensor possui um construtor que inicializa os atributos id e valor.
  • Cria dois objetos sensor1 e sensor2 com valores iniciais.
  • Chama o método imprimirDados para exibir os dados no Monitor Serial.

5.2 Destrutores

Um destruidor é um método especial que é chamado automaticamente quando um objeto é destruído ou sai de escopo. É usado para liberar recursos ou realizar tarefas de limpeza.

Sintaxe:

class NomeClasse {
public:
    ~NomeClasse() {
        // Código de limpeza
    }
};

Exemplo:

class Motor {
public:
    Motor() {
        Serial.println("Motor criado.");
    }

    ~Motor() {
        Serial.println("Motor destruído.");
    }

    void ligar() {
        Serial.println("Motor ligado.");
    }
};

void setup() {
    Serial.begin(9600);
    {
        Motor motor1;
        motor1.ligar();
    } // motor1 é destruído aqui
}

void loop() {
    // Não há código no loop
}

Explicação:

  • A classe Motor possui um construtor que imprime uma mensagem ao ser criado e um destruidor que imprime uma mensagem ao ser destruído.
  • O objeto motor1 é criado dentro de um bloco de código {} e é destruído ao final do bloco.

6. Encapsulamento, Herança e Polimorfismo

6.1 Encapsulamento

Encapsulamento é o conceito de esconder os detalhes internos de uma classe e expor apenas o necessário através de métodos públicos.

Exemplo:

class ContaBancaria {
private:
    float saldo;

public:
    ContaBancaria(float saldoInicial) {
        saldo = saldoInicial;
    }

    void depositar(float valor) {
        saldo += valor;
    }

    void sacar(float valor) {
        if(valor <= saldo) {
            saldo -= valor;
        } else {
            Serial.println("Saldo insuficiente.");
        }
    }

    float getSaldo() {
        return saldo;
    }
};

void setup() {
    Serial.begin(9600);
    ContaBancaria minhaConta(1000.0);

    minhaConta.depositar(500.0);
    minhaConta.sacar(200.0);

    Serial.print("Saldo Atual: ");
    Serial.println(minhaConta.getSaldo());
}

void loop() {
    // Não há código no loop
}

Explicação:

  • A classe ContaBancaria encapsula o atributo saldo como privado.
  • Métodos públicos depositar, sacar e getSaldo permitem a manipulação segura do saldo.

6.2 Herança

Herança permite que uma classe (classe derivada) herde atributos e métodos de outra classe (classe base), promovendo a reutilização de código.

Exemplo:

class Veiculo {
public:
    int rodas;

    void mover() {
        Serial.println("Veículo em movimento.");
    }
};

class Carro : public Veiculo {
public:
    string modelo;

    void buzinar() {
        Serial.println("Buzinando!");
    }
};

void setup() {
    Serial.begin(9600);
    Carro meuCarro;
    meuCarro.rodas = 4;
    meuCarro.modelo = "Sedan";

    Serial.print("Modelo: ");
    Serial.println(meuCarro.modelo.c_str());
    Serial.print("Rodas: ");
    Serial.println(meuCarro.rodas);

    meuCarro.mover();
    meuCarro.buzinar();
}

void loop() {
    // Não há código no loop
}

Explicação:

  • A classe Carro herda da classe Veiculo, recebendo o atributo rodas e o método mover.
  • Adiciona o atributo modelo e o método buzinar.

6.3 Polimorfismo

Polimorfismo permite que objetos de diferentes classes sejam tratados de forma uniforme através de interfaces comuns.

Exemplo:

class Forma {
public:
    virtual void desenhar() {
        Serial.println("Desenhando uma forma genérica.");
    }
};

class Circulo : public Forma {
public:
    void desenhar() override {
        Serial.println("Desenhando um círculo.");
    }
};

class Retangulo : public Forma {
public:
    void desenhar() override {
        Serial.println("Desenhando um retângulo.");
    }
};

void setup() {
    Serial.begin(9600);

    Forma *formas[2];
    formas[0] = new Circulo();
    formas[1] = new Retangulo();

    for(int i = 0; i < 2; i++) {
        formas[i]->desenhar();
    }

    // Liberação de memória
    for(int i = 0; i < 2; i++) {
        delete formas[i];
    }
}

void loop() {
    // Não há código no loop
}

Explicação:

  • Define uma classe base Forma com um método virtual desenhar.
  • As classes Circulo e Retangulo sobrescrevem o método desenhar.
  • Permite que diferentes objetos sejam chamados de forma polimórfica através de ponteiros da classe base.

7. Exemplos Práticos

7.1 Criando uma Classe para Representar um Sensor

class Sensor {
private:
    int id;
    float valor;

public:
    // Construtor
    Sensor(int sensorId, float sensorValor) {
        id = sensorId;
        valor = sensorValor;
    }

    // Métodos para acessar e modificar os atributos
    int getId() {
        return id;
    }

    float getValor() {
        return valor;
    }

    void setValor(float novoValor) {
        valor = novoValor;
    }

    void imprimirDados() {
        Serial.print("Sensor ");
        Serial.print(id);
        Serial.print(": ");
        Serial.println(valor);
    }
};

void setup() {
    Serial.begin(9600);

    Sensor sensor1(1, 23.5);
    Sensor sensor2(2, 47.8);

    sensor1.imprimirDados();
    sensor2.imprimirDados();

    // Atualizando o valor do sensor1
    sensor1.setValor(25.0);
    Serial.println("Após atualização:");
    sensor1.imprimirDados();
}

void loop() {
    // Não há código no loop
}

Explicação:

  • Define a classe Sensor com atributos privados id e valor.
  • Inclui métodos públicos para acessar e modificar esses atributos.
  • No setup, cria dois objetos sensor1 e sensor2, imprime seus dados e atualiza o valor de sensor1.

7.2 Implementando uma Classe de LED com Métodos para Ligar e Desligar

class LED {
private:
    int pin;

public:
    // Construtor
    LED(int ledPin) {
        pin = ledPin;
        pinMode(pin, OUTPUT);
    }

    // Método para ligar o LED
    void ligar() {
        digitalWrite(pin, HIGH);
    }

    // Método para desligar o LED
    void desligar() {
        digitalWrite(pin, LOW);
    }

    // Método para alternar o estado do LED
    void alternar() {
        int estado = digitalRead(pin);
        digitalWrite(pin, !estado);
    }
};

LED meuLED(13); // LED conectado ao pino 13

void setup() {
    Serial.begin(9600);
    Serial.println("Controlando o LED.");

    meuLED.ligar();
    delay(1000);
    meuLED.desligar();
    delay(1000);
}

void loop() {
    meuLED.alternar();
    delay(500);
}

Explicação:

  • Define a classe LED com um atributo privado pin.
  • Inclui métodos para ligar, desligar e alternar o estado do LED.
  • No setup, liga e desliga o LED uma vez.
  • No loop, alterna o estado do LED a cada meio segundo.

7.3 Criando uma Classe para Gerenciar um Array de Sensores

class GerenciadorSensores {
private:
    Sensor sensores[5];
    int quantidade;

public:
    // Construtor
    GerenciadorSensores() : sensores{Sensor(1, 23.5), Sensor(2, 47.8), Sensor(3, 30.2), Sensor(4, 50.0), Sensor(5, 25.5)} {
        quantidade = 5;
    }

    // Método para imprimir todos os sensores
    void imprimirTodos() {
        for(int i = 0; i < quantidade; i++) {
            sensores[i].imprimirDados();
        }
    }

    // Método para atualizar o valor de um sensor específico
    void atualizarSensor(int id, float novoValor) {
        for(int i = 0; i < quantidade; i++) {
            if(sensores[i].getId() == id) {
                sensores[i].setValor(novoValor);
                Serial.print("Sensor ");
                Serial.print(id);
                Serial.println(" atualizado.");
                return;
            }
        }
        Serial.println("Sensor não encontrado.");
    }
};

GerenciadorSensores gerenciador;

void setup() {
    Serial.begin(9600);
    Serial.println("Gerenciador de Sensores:");

    gerenciador.imprimirTodos();

    // Atualizando o valor do sensor 3
    gerenciador.atualizarSensor(3, 35.0);

    Serial.println("Após atualização:");
    gerenciador.imprimirTodos();
}

void loop() {
    // Não há código no loop
}

Explicação:

  • Define a classe GerenciadorSensores que gerencia um array de 5 objetos Sensor.
  • Inclui métodos para imprimir todos os sensores e atualizar o valor de um sensor específico.
  • No setup, imprime todos os sensores, atualiza o sensor com id 3 e imprime novamente.

8. Exercícios Práticos

Exercício 1: Criar uma Classe para Representar um Veículo

  • Tarefa: Desenvolva uma classe Veiculo que possui atributos como marca, modelo e ano. Inclua métodos para ligar, desligar e exibir as informações do veículo.

  • Exemplo de Código:

class Veiculo {
private:
    String marca;
    String modelo;
    int ano;
    bool ligado;

public:
    // Construtor
    Veiculo(String m, String mo, int a) : marca(m), modelo(mo), ano(a), ligado(false) {}

    // Métodos
    void ligar() {
        if(!ligado) {
            ligado = true;
            Serial.println("Veículo ligado.");
        } else {
            Serial.println("Veículo já está ligado.");
        }
    }

    void desligar() {
        if(ligado) {
            ligado = false;
            Serial.println("Veículo desligado.");
        } else {
            Serial.println("Veículo já está desligado.");
        }
    }

    void exibirInfo() {
        Serial.print("Marca: ");
        Serial.println(marca);
        Serial.print("Modelo: ");
        Serial.println(modelo);
        Serial.print("Ano: ");
        Serial.println(ano);
        Serial.print("Estado: ");
        Serial.println(ligado ? "Ligado" : "Desligado");
    }
};

Veiculo meuCarro("Toyota", "Corolla", 2020);

void setup() {
    Serial.begin(9600);
    meuCarro.exibirInfo();
    meuCarro.ligar();
    meuCarro.exibirInfo();
    meuCarro.desligar();
    meuCarro.exibirInfo();
}

void loop() {
    // Não há código no loop
}

Exercício 2: Implementar Herança com Classes Animal e Cachorro

  • Tarefa: Crie uma classe base Animal com atributos e métodos gerais, e uma classe derivada Cachorro que herda de Animal e adiciona comportamentos específicos.

  • Exemplo de Código:

class Animal {
protected:
    String nome;
    int idade;

public:
    // Construtor
    Animal(String n, int i) : nome(n), idade(i) {}

    // Método para exibir informações
    void exibirInfo() {
        Serial.print("Nome: ");
        Serial.println(nome);
        Serial.print("Idade: ");
        Serial.println(idade);
    }

    // Método virtual
    virtual void emitirSom() {
        Serial.println("Animal emite som.");
    }
};

class Cachorro : public Animal {
public:
    // Construtor
    Cachorro(String n, int i) : Animal(n, i) {}

    // Sobrescreve o método emitirSom
    void emitirSom() override {
        Serial.println("Cachorro diz: Au Au!");
    }
};

void setup() {
    Serial.begin(9600);

    Animal meuAnimal("Genérico", 5);
    Cachorro meuCachorro("Rex", 3);

    Serial.println("Informações do Animal:");
    meuAnimal.exibirInfo();
    meuAnimal.emitirSom();

    Serial.println("\nInformações do Cachorro:");
    meuCachorro.exibirInfo();
    meuCachorro.emitirSom();
}

void loop() {
    // Não há código no loop
}

Explicação:

  • A classe Animal possui atributos nome e idade, e métodos para exibir informações e emitir som.
  • A classe Cachorro herda de Animal e sobrescreve o método emitirSom para emitir um som específico.
  • No setup, cria objetos meuAnimal e meuCachorro e chama seus métodos.

Exercício 3: Implementar Encapsulamento com a Classe Lampada

  • Tarefa: Crie uma classe Lampada que possui atributos privados estado (ligada/desligada) e intensidade. Inclua métodos públicos para ligar, desligar, aumentar e diminuir a intensidade, e exibir o estado atual.

  • Exemplo de Código:

class Lampada {
private:
    bool ligada;
    int intensidade; // De 0 a 100

public:
    // Construtor
    Lampada() : ligada(false), intensidade(0) {}

    // Métodos
    void ligar() {
        ligada = true;
        Serial.println("Lâmpada ligada.");
    }

    void desligar() {
        ligada = false;
        intensidade = 0;
        Serial.println("Lâmpada desligada.");
    }

    void aumentarIntensidade(int valor) {
        if(ligada) {
            intensidade += valor;
            if(intensidade > 100) intensidade = 100;
            Serial.print("Intensidade aumentada para: ");
            Serial.println(intensidade);
        } else {
            Serial.println("Lâmpada está desligada. Não é possível aumentar a intensidade.");
        }
    }

    void diminuirIntensidade(int valor) {
        if(ligada) {
            intensidade -= valor;
            if(intensidade < 0) intensidade = 0;
            Serial.print("Intensidade diminuída para: ");
            Serial.println(intensidade);
        } else {
            Serial.println("Lâmpada está desligada. Não é possível diminuir a intensidade.");
        }
    }

    void exibirEstado() {
        Serial.print("Estado da Lâmpada: ");
        Serial.println(ligada ? "Ligada" : "Desligada");
        Serial.print("Intensidade: ");
        Serial.println(intensidade);
    }
};

Lampada minhaLampada;

void setup() {
    Serial.begin(9600);
    minhaLampada.exibirEstado();

    minhaLampada.ligar();
    minhaLampada.aumentarIntensidade(30);
    minhaLampada.aumentarIntensidade(50);
    minhaLampada.diminuirIntensidade(20);
    minhaLampada.exibirEstado();

    minhaLampada.desligar();
    minhaLampada.exibirEstado();
}

void loop() {
    // Não há código no loop
}

Explicação:

  • A classe Lampada encapsula os atributos ligada e intensidade como privados.
  • Inclui métodos públicos para manipular o estado e a intensidade da lâmpada.
  • No setup, demonstra o uso dos métodos da classe Lampada.

9. Conceitos Importantes

9.1 Encapsulamento

  • Definição: Esconder os detalhes internos de uma classe e expor apenas os necessários através de métodos públicos.
  • Benefícios:
  • Segurança: Protege os dados contra acessos indevidos.
  • Manutenção: Facilita a alteração interna sem afetar o código externo.

9.2 Herança

  • Definição: Permite que uma classe derive atributos e métodos de outra classe.
  • Tipos de Herança:
  • Pública (public): Os membros públicos e protegidos da classe base permanecem públicos e protegidos na classe derivada.
  • Privada (private): Todos os membros herdados se tornam privados na classe derivada.

9.3 Polimorfismo

  • Definição: Permite que objetos de diferentes classes sejam tratados de forma uniforme através de interfaces comuns.
  • Métodos Virtuais: Facilita o polimorfismo, permitindo que métodos sejam sobrescritos em classes derivadas.

9.4 Gerenciamento de Memória

  • Importância: Classes e objetos podem consumir memória significativa, especialmente em microcontroladores com recursos limitados.
  • Boas Práticas:
  • Evitar Alocação Dinâmica Desnecessária: Use objetos estáticos sempre que possível.
  • Liberar Memória Alocada Dinamicamente: Utilize delete e delete[] apropriadamente.

9.5 Boas Práticas de Programação Orientada a Objetos

  • Nomeação Clara: Use nomes significativos para classes, métodos e atributos.
  • Responsabilidade Única: Cada classe deve ter uma única responsabilidade ou funcionalidade.
  • Modularização: Divida o código em módulos e classes que representam entidades do mundo real ou conceitos lógicos.
  • Reutilização de Código: Utilize herança e composição para reutilizar código de forma eficiente.