Handbook

Programação Procedural

As vezes chamada de imperativa ou estruturada, a programação procedural consiste em executar instruções em sequencia linear, ou seja, em ordem.

Tomemos como exemplo a linguagem C, meramente por sua simplicidade. Assim sendo, imagine que precisamos criar uma aplicação na qual devemos realizar uma série de etapas:

  1. Ligar o carro
  2. Buzinar o carro
  3. Desligar o carro

Podemos ver que o nosso programa controla o estado um carro. Escreveriamos o programa assim:

int main(void) {
  carro_ligar();
  carro_buzinar();
  carro_desligar();

  return 0;
}

Para entendermos o que esta acontecendo nesse programa, vamos quebrá-lo por partes…

int main(void) {
  return 0;
}

Este trecho representa a primeira coisa a ser executada em nosso programa. Nesse caso fazendo absolutamente nada.

return 0; é necessário pois cada programa retorna um código de status, interpretado pelo sistema operacional. No qual o valor 0 representa sucesso, e qualquer valor diferente dele, falha.

Não precisamos saber o que as funções carro_ligar, carro_buzinar e carro_desligar fazem. Apenas que elas são executadas em ordem sequencial, de cima a baixo. E, é claro, liga, buzina e desliga um carro, respectivamente.

No entanto, se em algum momento precisarmos gerenciar vários carros esse método se tornará um problema. Teríamos que ter, por exemplo, carro1_ligar, carro2_ligar, carro3_ligar e assim por diante. Imagine o problema no momento em que teríamos que definir 70 mil funções, uma para cada carro… que nem sabemos quais são…

Para resolver isso, vamos utilizar uma estrutura, um “molde”, genérico, que nosso computador entende.

Computadores não ligam para o que estão manipulando, para eles tudo são números.

struct Carro {
  int chassi;
};

struct é o termo que a linguagem entende que estamos definindo a então estrutura, Carro é o nome que nós demos a ela e as chaves contemplam suas informações (número do chassi).

Para fins de simplificação, o número do chassi será, de fato, um número, e não uma sequencia alfanúmerica de caracteres.

Deste jeito, podemos criar três funções nos moldes das anteriores:

void carro_ligar(Carro carro);
void carro_buzinar(Carro carro);
void carro_desligar(Carro carro);

Agora, a diferença é que podemos chamar estas funções e injetar os valores do carro, e não o contrário. Podemos ter vários carros, e utilizar a exata mesma função.

Observações:

void diz à linguagem que a função não retorna valor algum.

Isto também economiza memória, já que teriamos que criar N funções para N carros, agora temos 1 função para N carros.

struct Carro {
  int chassi;
};

void carro_ligar(Carro carro);
void carro_buzinar(Carro carro);
void carro_desligar(Carro carro);

int main(void) {
  struct Carro carro1 = { 123 };
  struct Carro carro2 = { 456 };
  struct Carro carro3 = { 789 };

  carro_ligar(carro1);
  carro_ligar(carro2);
  carro_ligar(carro3);

  carro_buzinar(carro1);
  carro_buzinar(carro2);
  carro_buzinar(carro3);

  carro_desligar(carro1);
  carro_desligar(carro2);
  carro_desligar(carro3);

  return 0;
}

Este programa liga 3 carros, buzina e desliga-os. Mas ainda temos um problema a resolver. Este código é ineficiente.

Agora somos uma concessionária de carros, e precisamos conduzir testes nas ECUs de cada carro. Para isso usaremos a aplicação exemplificada acima, com algumas modificações.

Agora temos que: para cada carro, ligar e desligar os motores.

Nosso código de teste é apenas um intermediador da ECU para o painel de controle do carro. Ou seja, não temos controle do que acontece dentro do carro (simulando as funções não definidas acima). A variável ligado é responsável por dizer se o carro está, ou não, ligado.

Modificaremos nosso Carro para:

struct Carro {
  int chassi;
  bool ligado;
}

E nossas funções para:

void carro_ligar(Carro carro);
void carro_desligar(Carro carro);

Agora, para testar todos os carros, iremos percorrer uma lista chamada carros:

for (int i = 0; i < TOTAL_DE_CARROS; i++) {
  Carro carro = carros[i];

  carro_ligar(carro);

  if (carro.ligado) carro_desligar(carro);
  else log_err("Carro %d não ligou!", i);
}

log_err faz exatamente o que o nome diz: arquiva um erro.

E por enquanto tudo bem, nossos testes estão prontos. Mas ainda estamos sendo ineficientes.

Para isso, podemos visualizar o que está acontecendo nas imagens abaixo.

bytes

Nesta imagem, temos uma lista de 8 elementos e seus respectivos endereços na memória. Cada elemento tem 4 bytes de tamanho. Contudo, cada Carro ocupa 8 bytes de espaço (5 utilizados, 3 de espaçamento), como é possível ver na imagem abaixo:

carros

Para ler no futuro…

Artigo em inglês sobre alinhamento de memória: structure packing

Esta lista então, pode conter apenas 4 Carros. Mas é quando chamamos as funções que requerem um Carro como parâmetro que vem o problema.

Quando chamamos carro_ligar(), estamos fazendo uma cópia do carro, e enviando para a função. Veja bem, os computadores não são nada além de máquinas gerenciando pilhas. Pilha é um conceito bem conhecido no mundo da computação. Em poucas palavras, o último que entra é o primeiro que sai.

copia-antes

As funções são chamadas assim. Cada argumento é jogado em uma pilha, e então a função precisa manipulá-la a seu favor.

Ao copiarmos o Carro para a função, o que acontece é que esse carro, na prática, ocupará o dobro de memória.

copia-depois

Para fugir desse problema utilizaremos ponteiros.

O que vamos fazer é ao invés de copiarmos todo o Carro para dentro da função, copiar apenas onde ele se localiza.

referencia

Nosso carro fica na posição 0x00, então diremos à função: “ligue o carro que está em 0x00”, ao invés de “ligue este carro aqui”.

Óbviamente em nosso programa atual a redução de carga atual é ínfima, e os ganhos em performance, negligíveis. Porém, no mundo real, trabalharíamos com estruturas inúmeras vezes maiores que essa. E por mais que nesse caso não faça diferença, ainda assim estamos diminuíndo pela metade a quantidade de memória utilizada.

Para aplicar estas mudanças, precisaremos alterar o código:

void carro_ligar(Carro* carro);

Agora, ao chamar a função, utilizaremos a localização do carro, não ele mesmo:

for (int i = 0; i < TOTAL_DE_CARROS; i++) {
  Carro* carro = &carros[i];

  // ...
}

Programa Final

#include <stdbool.h>

struct Carro {
  int chassi;
  bool ligado;
}

void carro_ligar(Carro* carro);
void carro_desligar(Carro* carro);

int main(void) {
  for (int i = 0; i < TOTAL_DE_CARROS; i++) {
    Carro carro = carros[i];

    carro_ligar(carro);

    if (carro->ligado) carro_desligar(carro);
    else log_err("Carro %d não ligou!", i);
  }

  return 0;
}

Com isso, aprendemos como um programa é estruturado na linguagem C, declarações de structs, variáveis, funções, parâmetros, listas, ponteiros e um pouquinho de alinhamento de memória.

Veja outros artigos sobre linguagens de programação: