15 de maio de 2021

Do bit Ao Byte

Embarcados, Linux e programação

Ponteiros em C/C++

arrays dinâmicos em C++ | Interagir com ponteiros | Alocação de memória | Ponteiros em C/C++ | Socket server com ESP8266 | Socket server com ESP32 | Socket server com Python | Socket client com ESP32 | Sistema de arquivos no ESP8266

Nesse artigo começaremos a ver alguns recursos da linguagem que, ou usamos pouco, ou nem usamos na maioria de nossos sketches. Para falar sobre ponteiros em C/C++, talvez eu leve uns dois ou três artigos e vídeos para ser claro. Certamente não conseguirei abordar tudo com eximia, mas farei o meu melhor para repassar o pouco que sei sobre o assunto.

Essa série foi inspirada por um maker no grupo Arduino Brasil, que considero uns dos melhores grupos sobre Arduino e afins.

Antes de tratar de ponteiros…

…precisamos primeiramente entender as entranhas de uma variável. No livro “C++ Primer Plus” a explicação é muito clara (e extensa) ao tratar do assunto. Uma variável é uma requisição de reserva de memória para um determinado tipo requerido, cuja variável recebe um nome simbólico para que possa ser referenciada no programa. Quando declaramos uma variável para um contador, por exemplo:

uint8_t counter;

Significa que estamos reservando um unsigned char, uma variável de 8 bits, capaz de armazenar números positivos de 0 a 255. Essa é uma forma de economizar recursos, que em microcontroladoras pode ser essencial. Um tipo int em um Atmega328p ocupa 2 bytes, não sendo adequado para interação com valores que não ultrapassem 255. Entender os tipos e seus tamanhos é fundamental para uma boa programação.

Escrevi alguns artigos que são realmente auxiliadores em diversos pontos, como as dicas “Programar em C/C++ para microcontroladoras PIC e Arduino“, no qual cito alguns tipos. Também mostro como usar menos de 8 bits em uma variável, mas isso não significa que a reserva será menor que o tamanho definido para o tipo; apenas deslocamos o estouro de base para outro ponto. Com isso, podemos designar apenas 1 bit para fazer inversão de um valor para, por exemplo, um blink. Com isso, basta incrementar continuamente e o estouro de base o fará retornar ao 0. Leia o artigo supracitado, você não vai se arrepender.

Outro artigo que considero muito importante é sobre o deslocamento de bits. Já discorri a respeito em vários artigos, mas acredito que a melhor referência seja o artigo “Dominando PCF8574 com Arduino“. Serve para qualquer plataforma, apesar de citar o Arduino.

No curso de Raspberry Pi que eu havia feito para a Udemy (e acabei disponibilizando em nosso canal) tem também um vídeo elaborado sobre deslocamento de bits, então, se esse segundo artigo citado não for o suficiente, dê uma olhada no vídeo para entender melhor o conceito. Vamos começar!

Endereços

Quando reservamos memória para uma variável, teremos então um endereço. Para acessar a variável, usamos o nome simbólico e para acessar o endereço utilizamos um “&” na frente desse nome simbólico. Estou escrevendo os exemplos em C++ no console do Linux enquanto escrevo o artigo, por isso você poderá estranhar o formato se programa apenas para Arduino, mas concentre-se nas variáveis.

Se desejar compilar esse exemplo, basta ter o compilador g++ instalado no Linux, Windows ou Mac, e usar a sintaxe:

g++ -o programa variaveis01.cpp

O primeiro código de exemplo, mostrando o valor da variável e o endereço é esse:

#include <iostream>

using namespace std;

int main(){
    cout << "exemplo 01" << endl;
    unsigned char counter = 1;

    cout << "valor" << endl;
    cout << (int) counter << endl;

    cout << "endereco" << endl;
    cout << (long int) &counter << endl;

    return 0;
}

Aqui estamos fazendo uma outra coisa também, que é o casting, convertendo um tipo para outro. Ignore o cast por enquanto, mas apenas para não faltar explicação:

cout << (int) counter << endl;

Aqui utilizamos cout para jogar a saída para stdout, que é a saída padrão. A variável que declaramos é um uint8_t ou unsigned char, como preferir. O cast está convertendo o char para int, dessa maneira:

(int) counter

Usamos também o direcionador, que no caso é “<<“. No final da linha podemos usar “\n” ou, como estamos usando C++, “endl”.

cout é um pequeno objeto em C++ e tem algumas características que não pretendo entrar em detalhes agora para não desviar o foco.

Ponteiro

Agora vamos começar a tratar realmente dos ponteiros.

Uma variável de ponteiro é declarada igual a qualquer outra, mas precedida pelo símbolo “*“. Não importa a posição, desde que atenda um dos critérios abaixo:

char *teste; //costumo usar assim
char* teste;
char * teste;

Atribuindo valor ao ponteiro

Agora preste atenção a essa dica: Se você manja de shell script em Linux, conhece link simbólico. Um ponteiro é semelhante a um link simbólico:

ln -s alvo destino

Se não manja, não se preocupe. Vamos aos detalhes.

Vamos utilizar uma variável chamada valor para armazenar um valor. Vamos criar um ponteiro que apontará para a variável valor:

int valor = 9;
int *ponteiro;

Agora vamos atribuir ao ponteiro o endereço da variável valor, assim poderemos acessar o valor da variável através do ponteiro.

ponteiro = &valor;

Se exibirmos o valor de ambos agora, deverão ser iguais:

cout << "valor: ";
cout << valor;
cout << endl << "ponteiro: ";
cout << *ponteiro << endl;

Repare que aqui estamos fazendo como uma solicitação:

cout, exiba o valor para qual o ponteiro aponta. (cout << *ponteiro;)

Se não colocarmos o asterisco antes do ponteiro na exibição, ele mostrará o endereço do ponteiro, não o valor para qual o ponteiro direciona.

Ponteiros em C/C++

O código para esse exemplo:

#include <iostream>

using namespace std;

int main(){
    cout << "exemplo 02" << endl;
    
    int *ponteiro; //reserva um endereço para o tipo int
    int valor = 9;

    ponteiro = &valor; //colocamos o endereço do valor no ponteiro

    cout << "valor: ";
    cout << valor;
    cout << endl << "endereco do ponteiro: ";
    cout << ponteiro << endl;
    cout << "valor para onde *ponteiro aponta: "; 
    cout << *ponteiro << endl;


    *ponteiro = 8;

    cout << "valor: ";
    cout << valor;
    cout << endl << "ponteiro: ";
    cout << *ponteiro << endl;


    return 0;
}

Agora repare nesse mesmo exemplo e na imagem, onde o valor se torna 8. Repare que invés de mudar o valor na variável valor, mudei o valor através do ponteiro, usando:

*ponteiro = 8;

Mais uma vez, é como dizer ao programa:

Atribua esse valor à variável para qual o ponteiro aponta

Se não tivesse o asterisco, estaríamos mudando o endereço do ponteiro, o que certamente seria um problema, porque estaríamos tentando atribuir um valor inteiro a um ponteiro de um inteiro, de modo que o compilador não deverá fazer a compilação.

Tem um mundo de coisas sobre ponteiros, mas já chegamos em 1.000 palavras nesse parágrafo, por isso vou tentar ser mais sucinto nas próximas linhas e tentarei dividir o conteúdo em mais um ou dois artigos.

Declaração de outros tipos

Podemos declarar ponteiros em C/C++ para qualquer tipo de variável, seguindo a mesma sintaxe. Dependendo do que queremos exibir na saída, pode ser necessário fazer casting, mas esse é assunto para outro artigo.

Ponteiros de arrays em C/C++ e funções

Quando usamos uma função com parâmetro, o parâmetro também é um ponteiro. Se temos uma variável char exemplo e a função foi declarada como funcao(char parametro), dentro da função manipularemos parametro.

Poucas vezes utilizei ponteiros de ponteiros em MCUs, que são comuns em, por exemplo, arrays de strings. Suponhamos que você não use o objeto String do Arduino para armazenar uma string. A string em C é um array de char. Logo, estaremos armazenando um array de arrays. Por exemplo:

#include <iostream>
#include <cstring>

using namespace std;

int main(){
    cout << "exemplo 02" << endl;

    char palavra2[8];

    strcpy(palavra2,"palavra");

    cout << palavra2 << endl;

    return 0;
}

Aqui temos a declaração de um ponteiro de 8 posições, sendo que a oitava receberá o terminador nulo atribuído pela função strcpy. Como reservamos o valor de um tamanho predefinido, foi fácil fazer essa atribuição, mas se declararmos um ponteiro sem indicar o tamanho do que ele vai alocar, o mais provável é que obtenhamos uma falha de segmentação por invasão de memória. Então, como usar o ponteiro de tamanho dinâmico?

Para fazer alocação dinâmica, usamos em C o mallocfree. Usamos alocação dinâmica quando não podemos garantir um valor de comprimento constante sempre. Se quisermos armazenar um tipo inteiro de 10 posições, devemos fazer algo como:

#include <iostream>
#include <cstring>

using namespace std;

int main(){

    uint8_t posicoes = 8;
    cout << "exemplo 03" << endl;

    int *myValue = (int *) malloc(posicoes * sizeof(int));

    return 0;
}

A função malloc aloca o tipo void, portanto fazemos um cast (int*) e alocamos posições multiplicada pelo tamanho do tipo. Para ficar interessante e melhorar o entendimento, vamos fazer com char:

#include <iostream>
#include <cstring>

using namespace std;

int main(){

    uint8_t tamanho = 7;
    cout << "exemplo 03" << endl;
   
    char *myValue = (char *) malloc(tamanho * sizeof(char));

    for (uint8_t i=0; i<7;i++){
        cin >>myValue[i];
    }

    cout << myValue << endl;

    return 0;
}

Desse modo recebemos byte a byte pelo terminal do sistema, que fica parecido com receber pela serial do Arduino. Daí a saída fica assim:

Ponteiros em C/C++

Ponteiro não é um assunto simples. Suponhamos que desejássemos imprimir “abc” na saída padrão. Eis duas formas:

#include <iostream>
#include <cstring>

using namespace std;

char myString[4] = {'a','b','c',0};

char *myPointer = (char*) "abc\0";

int main(){
    cout << myString << endl;
    cout << myPointer << endl;
    return 0;
}

Na primeira, definimos o tamanho e atribuímos a cada posição seu respectivo valor. Na segunda, passamos um array diretamente. Se usarmos a função sizeof(), veremos tamanhos diferentes. Mas vou deixar mais detalhes para outro artigo, vou apenas mostrar o ponteiro de ponteiro:

#include <iostream>
#include <cstring>

using namespace std;

char myString[4] = {'a','b','c',0};

char *myPointer[] =  {(char*) "abc\0", (char*) "def\0"};

int main(){
    cout << myString << endl;
    cout << myPointer[1] << endl;


    return 0;
}

Repare que o ponteiro está recebendo um colchete ao final, que indica outra reserva de tamanho indefinido. Para imprimir o valor, do array de char na segunda posição, usamos myPointer[1] (porque começa em 0, não se esqueça).

O resultado seria:

Do mesmo modo, podemos passar um ponteiro de ponteiro para, por exemplo, uma função. O parâmetro de uma função já é um ponteiro, tenha isso em mente. Além disso, podemos receber qualquer tipo no parâmetro se declararmos a função com um ponteiro de ponteiro do tipo void e depois fazer casting:

#include <iostream>
#include <cstring>

using namespace std;



void teste(void *valor){
    char *result = (char*) valor;
    cout << result << endl;
}


int main(){
    teste((void*) "batata");
    return 0;
}

Claro que esses exemplos são figurativos, na prática usamos em situações específicas que fazem todo o sentido. O propósito aqui é esclarecer gradativamente a “forma” de implementar e por essa razão, outros artigos relacionados serão necessários.

Para as próximas duas semanas já temos artigos agendados, então devemos retornar ao tema em seguida. Até a próxima!

 

Revisão: Ricardo Amaral de Andrade