Mostrando postagens com marcador memória. Mostrar todas as postagens
Mostrando postagens com marcador memória. Mostrar todas as postagens

terça-feira, 7 de outubro de 2014

Ponteiros

Como Funcionam os Ponteiros 

Os ints guardam inteiros. Os floats guardam números de ponto flutuante. Os chars guardam caracteres. Ponteiros guardam endereços de memória. Quando você anota o endereço de um colega você está criando um ponteiro. O ponteiro é este seu pedaço de papel. Ele tem anotado um endereço. Qual é o sentido disto? Simples. Quando você anota o endereço de um colega, depois você vai usar este endereço para achá-lo. O C funciona assim. Você anota o endereço de algo numa variável ponteiro para depois usar.

Da mesma maneira, uma agenda, onde são guardados endereços de vários amigos, poderia ser vista como sendo uma matriz de ponteiros no C. 

Um ponteiro também tem tipo. Veja: quando você anota um endereço de um amigo você o trata diferente de quando você anota o endereço de uma firma. Apesar de o endereço dos dois locais ter o mesmo formato (rua, número, bairro, cidade, etc.) eles indicam locais cujos conteúdos são diferentes. Então os dois endereços são ponteiros de tipos diferentes. 

No C quando declaramos ponteiros nós informamos ao compilador para que tipo de variável vamos apontá-lo. Um ponteiro int aponta para um inteiro, isto é, guarda o endereço de um inteiro. 

Declarando e Utilizando Ponteiros 

Para declarar um ponteiro temos a seguinte forma geral:

tipo_do_ponteiro *nome_da_variável; 

É o asterisco (*) que faz o compilador saber que aquela variável não vai guardar um valor mas sim um endereço para aquele tipo especificado. Vamos ver exemplos de declarações: 

int *pt;
char *temp,*pt2; 

O primeiro exemplo declara um ponteiro para um inteiro. O segundo declara dois ponteiros para caracteres. Eles ainda não foram inicializados (como toda variável do C que é apenas declarada). Isto significa que eles apontam para um lugar indefinido. Este lugar pode estar, por exemplo, na porção da memória reservada ao sistema operacional do computador. 

Usar o ponteiro nestas circunstâncias pode levar a um travamento do micro, ou a algo pior. O ponteiro deve ser inicializado (apontado para algum lugar conhecido) antes de ser usado! Isto é de suma importância! 

Para atribuir um valor a um ponteiro recém-criado poderíamos igualá-lo a um valor de memória. Mas, como saber a posição na memória de uma variável do nosso programa? Seria muito difícil saber o endereço de cada variável que usamos, mesmo porque estes endereços são determinados pelo compilador na hora da compilação e realocados na execução. Podemos então deixar que o compilador faça este trabalho por nós. Para saber o endereço de uma variável basta usar o operador &. Veja o exemplo: 

 int count=10; 
 int *pt; 
 pt=&count; 

Criamos um inteiro count com o valor 10 e um apontador para um inteiro pt. A expressão &count nos dá o endereço de count, o qual armazenamos em pt. Simples, não é? 

Repare que não alteramos o valor de count, que continua valendo 10. 

Como nós colocamos um endereço em pt, ele está agora "liberado" para ser usado. Podemos, por exemplo, alterar o valor de count usando pt. Para tanto vamos usar o operador "inverso" do operador &. É o operador *. No exemplo acima, uma vez que fizemos pt=&count a expressão *pt é equivalente ao próprio count. Isto significa que, se quisermos mudar o valor de count para 12, basta fazer *pt=12. 

- Que tal voltarmos à nossa analogia para ver o que está acontecendo. 

Digamos que exista uma firma. Ela é como uma variável que já foi declarada. Você tem um papel em branco onde vai anotar o endereço da firma. O papel é um ponteiro do tipo firma. Você então liga para a firma e pede o seu endereço, o qual você vai anotar no papel. Isto é equivalente, no C, a associar o papel à firma com o operador &. Ou seja, o operador &aplicado à firma é equivalente a você ligar para a mesma e pedir o endereço. Uma vez de posse do endereço no papel você poderia, por exemplo, fazer uma visita à firma. No C você faz uma visita à firma aplicando o operador * ao papel. Uma vez dentro da firma você pode copiar seu conteúdo ou modificá-lo. 

 Uma observação importante: apesar do símbolo ser o mesmo, o operador * (multiplicação) não é o mesmo operador que o * (referência de ponteiros). Para começar o primeiro é binário, e o segundo é unário pré-fixado.

 Aqui vão dois exemplos de usos simples de ponteiros:

#include <stdio.h> 
int main () 
{ 
 int num,valor; 
 int *p; 
 num=55; 
 p=&num; 
/* Pega o endereco de num */ 
 valor=*p; 
 /* Valor e igualado a num de uma maneira indireta */ 
 printf ("\n\n%d\n",valor); 
 printf ("Endereco para onde o ponteiro aponta: %p\n",p); 
 printf ("Valor da variavel apontada: %d\n",*p); 
 return(0); 
}

e:

#include <stdio.h> 
int main () 
{ 
 int num,*p; 
 num=55; 
 p=&num; /* Pega o endereco de num */ 
 printf ("\nValor inicial: %d\n",num); 
 *p=100; /* Muda o valor de num de uma maneira indireta */ 
 printf ("\nValor final: %d\n",num); 
 return(0); 
}

Nos exemplos acima vemos um primeiro exemplo do funcionamento dos ponteiros. No primeiro exemplo, o código %p usado na função printf() indica à função que ela deve imprimir um endereço. 

Podemos fazer algumas operações aritméticas com ponteiros. A primeira, e mais simples, é igualar dois ponteiros. Se temos dois ponteiros p1 e p2 podemos igualá-los fazendo p1=p2. Repare que estamos fazendo com que p1 aponte para o mesmo lugar que p2. Se quisermos que a variável apontada por p1 tenha o mesmo conteúdo da variável apontada por 41 p2 devemos fazer *p1=*p2. Basicamente, depois que se aprende a usar os dois operadores (& e *) fica fácil entender operações com ponteiros.
As próximas operações, também muito usadas, são o incremento e o decremento. Quando incrementamos um ponteiro ele passa a apontar para o próximo valor do mesmo tipo para o qual o ponteiro aponta. Isto é, se temos um ponteiro para um inteiro e o incrementamos ele passa a apontar para o próximo inteiro. Esta é mais uma razão pela qual o compilador precisa saber o tipo de um ponteiro: se você incrementa um ponteiro char* ele anda 1 byte na memória e se você incrementa um ponteiro double* ele anda 8 bytes na memória. O decremento funciona semelhantemente. Supondo que p é um ponteiro, as operações são escritas como: 

p++; 
p--; 

Mais uma vez insisto. Estamos falando de operações com ponteiros e não de operações com o conteúdo das variáveis para as quais eles apontam. Por exemplo, para incrementar o conteúdo da variável apontada pelo ponteiro p, faz-se: 

(*p)++; 

Outras operações aritméticas úteis são a soma e subtração de inteiros com ponteiros. Vamos supor que você queira incrementar um ponteiro de 15. Basta fazer: 

 p=p+15; ou p+=15; 

E se você quiser usar o conteúdo do ponteiro 15 posições adiante: 

 *(p+15); 

A subtração funciona da mesma maneira. Uma outra operação, às vezes útil, é a comparação entre dois ponteiros. Mas que informação recebemos quando comparamos dois ponteiros? Bem, em primeiro lugar, podemos saber se dois ponteiros são iguais ou diferentes (== e !=). No caso de operações do tipo >, <, >= e <= estamos comparando qual ponteiro aponta para uma posição mais alta na memória. Então uma comparação entre ponteiros pode nos dizer qual dos dois está "mais adiante" na memória. A comparação entre dois ponteiros se escreve como a comparação entre outras duas variáveis quaisquer: 

 p1>p2 

Há entretanto operações que você não pode efetuar num ponteiro. Você não pode dividir ou multiplicar ponteiros, adicionar dois ponteiros, adicionar ou subtrair floats ou doubles de ponteiros. 

Matrizes

Pode ser definido como sendo um conjunto de variáveis do mesmo tipo. São também conhecidos por vetores ou array. Sua declaração segue o mesmo formato de variáveis, porém acrescenta-se o número de elementos desejados entre colchetes ([ e ]).

Os elementos são enumerados de 0 (zero) até o número especificado como quantidade menos 1 (n-1). Para matrizes multidimensionais, são fornecidos tantos índices quanto o número de dimensões. Para referenciar uma das variáveis, devemos fornecer o índice do elemento entre colchetes. A linguagem C não verifica se o índice é inválido, permitindo a leitura de "lixo" e alteração de outras posições de memória.


A inicialização pode ser feita apenas na declaração. Se o número de elementos é menor que o tamanho do vetor, os elementos restante são inicializados com zero. Podemos omitir o tamanho do vetor quando ele é inicializado (mas somente quando inicializado!). No caso de matrizes de mais de uma dimensão, somente a primeira dimensão pode ser omitida.

int vet[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
float Mat3x3[3][3] = {{ 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 }};
char nome[60] = “Curso de C Progressivo”;

Sintaxe:
tipo nome_matriz[linhas][colunas];
tipo nome_matriz[profundidade][linhas][colunas];

 Exemplos:
 float fNotas[50][2];
 int iMatriz[2][2][3]; 

 Referenciando elementos de uma matriz:

int iMatriz[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};


Na memória, é representado como:


Isso também pode ser feito para adquirirmos o formato de matriz que quisermos. Ou seja, se fosse uma matriz tridimensional, bastaria ter três dimensionadores, se fosse 4D, então, 4 dimensionadores, se fosse 5D (apesar de geometricamente difícil de imaginar) seriam 5 dimensionadores. E assim por diante...


segunda-feira, 6 de outubro de 2014

Alocação Dinâmica de Memória

Como prometido volto a falar de alocação de memória, em meu último post mencionei alocação estática e dinâmica de memória, e detalhei a alocação estática, agora vamos entrar em mais detalhes sobre a alocação dinâmica de memória.


A alocação dinâmica é o processo que aloca memória em tempo de execução. Ela é utilizada quando não se sabe ao certo quanto de memória será necessário para o armazenamento das informações, podendo ser determinadas em tempo de execução conforme a necessidade do programa. Dessa forma evita-se o desperdício de memória.


As principais características da alocação dinâmica de memória são:

  • Os dados se encontram espalhados na memória do computador em vários blocos;
  • Geralmente as estruturas de dados são compostas por registros que incluem um campo do tipo ponteiro (elo/link), o que nos permite encadear os dados e indicar onde está o dado seguinte na memória (visto que os dados estão espalhados na memória do computador);
  • Podemos ir solicitando mais e mais memória a medida em que precisarmos de mais espaço para armazenar as informações. Isso nos permite criar programas que usam apenas a memória necessária... e por conseqüência, podemos rodar outros programas, em uma mesma máquina, sem que um único programa monopolize toda a memória disponível desta;
  • Podemos liberar espaços de memória quando estes não forem mais necessários ao programa.

Estas estruturas criadas com registros que são encadeados através do uso de ponteiros, que estes blocos espalhados pela memória, vão resultar em estruturas denominadas de “listas encadeadas” de alocação dinâmica. A figura abaixo mostra um exemplo de lista encadeada, baseada na estrutura de dados descrita logo mais abaixo:


Listas encadeadas simples:

Construção de um conjunto de rotinas genéricas e modulares de manipulação de listas encadeadas simples. Vamos aqui descrever uma possível proposta de um conjunto de rotinas empregadas para este fim.

Rotinas básicas de manipulação de listas encadeadas simples:
- Estruturas de dados com alocação dinâmica
- Inserção no inicio da lista, no final da lista, ou de modo ordenado
- Remoção de qualquer elemento da lista (início, final, ou elemento especificado)

Aplicação típica:
- Lista de elementos de tamanho variável

Estrutura de dados:

 typedef int Tipo_Dado;
 typedef struct bloco {
                        Tipo_Dado Dado;
                        struct bloco *Proximo;
                      } Nodo;

Rotinas:

 void inicializa_lista (Nodo **N);
 int insere_inicio_lista (Nodo **N, Tipo_Dado Dado);
 int insere_fim_lista (Nodo **N, Tipo_Dado Dado);
 int insere_ordenando_lista (Nodo **N, Tipo_Dado Dado);
 int remove_inicio_lista (Nodo **N, Tipo_Dado *Dado);
 int remove_fim_lista (Nodo **N, Tipo_Dado *Dado);
 int remove_elemento_lista (Nodo **N, Tipo_Dado Dado);
 int quantidade_lista (Nodo *N);
 void exibe_lista (Nodo *N);
 int pesquisa_lista (Nodo **N, Tipo_Dado Dado);
 int percorre_lista (Nodo **N, Tipo_Dado *Dado);
 void apaga_lista (Nodo **N);

Exemplo: inserção de dados no inicio da lista encadeada

 int insere_inicio_lista (N, Dado)
 Nodo **N;
 Tipo_Dado Dado;
 {
   Nodo *novo;
    novo = (Nodo *) calloc ( 1, sizeof (Nodo) );
    if ( novo == NULL )
         return (ERRO); /* Não conseguiu alocar espaço na memória */
         novo -> Dado = Dado;
         novo -> Proximo = *N;
        *N = novo;
         return (OK);
  }

Exemplo: inserção de dados no final da lista encadeada

 int insere_fim_lista (N, Dado)
 Nodo **N;
 Tipo_Dado Dado;
{
  Nodo *aux, *novo;
  novo = (Nodo *) calloc ( 1, sizeof (Nodo) );
  if ( novo == NULL )
       return(ERRO);
       novo -> Dado = Dado;
       novo -> Proximo = NULL;
  if ( *N == NULL )
       *N = novo;
  else {
           aux = *N;
           while ( aux -> Proximo != NULL )
                      aux = aux -> Proximo;
                      aux -> Proximo = novo;
         }
  return (OK)
 }

Listas duplamente encadeadas

Construção de um conjunto de rotinas genéricas e modulares de manipulação de listas duplamente encadeadas. Vamos aqui descrever uma possível proposta de um conjunto de rotinas empregadas para este fim.


Rotinas básicas de manipulação de listas duplamente encadeadas:

  • Estruturas de dados com alocação dinâmica
  • Inserção no inicio da lista, no final da lista, ou de modo ordenado
  • Remoção de qualquer elemento da lista (início, final, ou elemento especificado)

Aplicação típica:

  • Lista de elementos de tamanho variável, permite percorrer a lista nos dois sentidos


Estrutura de dados:

 typedef int Tipo_Dado;
 typedef struct bloco_ld {
                            Tipo_Dado Dado;
                            struct bloco_ld *Proximo, *Anterior;
                          } Nodo_LD;

Rotinas:

  void inicializa_ld (Nodo_LD **LD);
 void posiciona_inicio_ld (Nodo_LD **LD);
 void posiciona_fim_ld (Nodo_LD **LD);
 int insere_antes_ld (Nodo_LD **LD, Tipo_Dado Dado);
 int insere_depois_ld (Nodo_LD **LD, Tipo_Dado Dado);
 int insere_inicio_ld (Nodo_LD **LD, Tipo_Dado Dado);
 int insere_fim_ld (Nodo_LD **LD, Tipo_Dado Dado);
 int insere_ordenando_ld (Nodo_LD **LD, Tipo_Dado Dado);
 int pesquisa_ld (Nodo_LD **LD, Tipo_Dado Dado);
 int remove_nodo_ld (Nodo_LD **LD, Tipo_Dado *Dado);
 int remove_dado_ld (Nodo_LD **LD, Tipo_Dado *Dado);
 int quantidade_ld (Nodo_LD *LD);
 void exibe_ld (Nodo_LD *LD);
 int percorre_lista (Nodo_LD **LD, Tipo_Dado *Dado);
 void apaga_ld (Nodo_LD **LD);

Exemplo: posiciona o ponteiro no início da lista duplamente encadeada

 void posiciona_inicio_ld (LD)
 Nodo_LD **LD;
{
  if ( *LD != NULL )
                    {
                      while ( (*LD) -> Anterior != NULL )
                      *LD = (*LD) -> Anterior;
                    }
}

Exemplo: inserção de dados antes da posição apontada na lista duplamente encadeada

int insere_antes_ld (LD, Dado)
Nodo_LD **LD;
Tipo_Dado Dado;
{
 Nodo_LD *novo, *aux;
 novo = (Nodo_LD *) calloc ( 1, sizeof (Nodo_LD) );
 if ( novo == NULL )
                    return (ERRO);
                    novo -> Dado = Dado;
if ( *LD == NULL )
                    {
                      *LD = novo;
                      novo -> Anterior = NULL;
                      novo -> Proximo = NULL;
}
else
     {
      aux = (*LD) -> Anterior;
      (*LD) -> Anterior = novo;
      novo -> Proximo = (*LD)
      novo -> Anterior = aux;
      if ( aux != NULL )
      aux -> Proximo = novo;
     }
return (OK);
}

Exemplo: inserção de dados no início da lista duplamente encadeada

int insere_inicio_ld (LD, Dado)
Nodo_LD **LD;
Tipo_Dado Dado;
{
  posiciona_inicio_ld(LD);
  return ( insere_antes_ld (LD, Dado) );
}

Filas, Pilhas, Deques e Ordenação usando alocação dinâmica:

As filas, pilhas e deques podem ser facilmente implementadas usando rotinas genéricas de manipulação de listas, sejam estas simples ou duplamente encadeadas. Uma vez que podemos implementar facilmente rotinas de inserção no início e/ou no fim de uma lista encadeada, e também podemos implementar facilmente rotinas de remoção do início e/ou do fim de uma lista encadeada com alocação dinâmica, por conseqüência, podemos criar rotinas de manipulação destas outras estruturas de dados baseadas nas rotinas descritas anteriormente. É importante salientar que em alguns casos talvez seja interessante criar rotinas mais otimizadas, como por exemplo, na inserção de nodos no final de uma lista. Neste caso, seria interessante que fosse evitada a necessidade de percorrer toda a lista até encontrar o nodo final, cada vez que um novo nodo fosse adicionado no final desta lista.

Filas:

  • Insere no início
  • Retira do fim

Pilhas:

  • Insere no fim
  • Retira do fim

Deques:

  • Insere no início ou no fim
  • Retira do início ou do fim

Ordenação: 

  • A inserção pode ser feita em qualquer posição, o que facilita a ordenação

Caso queiram ter uma menor abstração e aprimorar os conceitos do conteúdo abordado neste post, recomendo o vídeo abaixo:

Estrutura de dados - Alocação dinâmica de memória




"O conhecimento é uma sede insaciável, quanto mais bebemos, mais sede sentimos."
Eduardo Colamego.


domingo, 5 de outubro de 2014

Alocação de Memória

Ao criar um programa em “C” usualmente temos que especificar as variáveis que vamos usar antes de começar a executar o programa, reservando assim um espaço na memória. As variáveis que são alocadas em posições fixas da memória são chamadas de variáveis estáticas, e as variáveis que não possuem uma posição fixa e que são criadas e destruídas durante a execução do programa são chamadas de variáveis dinâmicas.


A alocação de memória no computador pode ser dividida em dois grupos principais:
  • Alocação Estática: os dados tem um tamanho fixo e estão organizados sequencialmente na memória do computador. Um exemplo típico de alocação estática são as variáveis globais, e os vetores (estáticos).
  • Alocação Dinâmica: os dados não precisam ter um tamanho fixo, pois podemos definir para cada dado quanto de memória que desejamos usar. Sendo assim vamos alocar espaços de memória (blocos) que não precisam estar necessariamente organizados de maneira sequencial, podendo estar distribuídos de forma esparsa na memória do computador. Na alocação dinâmica, vamos pedir para alocar/desalocar blocos de memória, de acordo com a nossa necessidade, reservando ou liberando blocos de memória durante a execução de um programa. Para poder “achar” os blocos esparsos na memória usamos as variáveis do tipo Ponteiro (indicadores de endereços de memória). As variáveis locais e os parâmetros passados por valor são alocados dinamicamente.
Neste post vou abordar apenas alocação estática, e no próximo falarei da alocação dinâmica.

Alocação Estática de Memória:


As estruturas de dados para armazenar um conjunto de dados (exemplo: um cadastro com vários registros), podem ser organizadas de formas diferentes, de acordo com a maneira que os dados são inseridos e retirados da memória do computador. As formas mais usuais são: as listas lineares sequenciais (vetores simples), as filas, as pilhas e os deques.

Listas lineares sequenciais:

 

Uma maneira interessante de manipular este tipo de estruturas de dados, é através da construção de um conjunto de rotinas genéricas e modulares de manipulação de listas sequenciais. Vamos aqui descrever uma possível proposta de um conjunto de rotinas empregadas para este fim. 

Rotinas básicas de manipulação de Vetores:
- Estruturas de dados com alocação estática
- Inserção no final do vetor
- Remoção lógica dos elementos

Aplicação típica:
- Pequenos cadastros

Estrutura de dados: 
         typedef int Tipo_Dado;
         typedef struct {
                                 Tipo_Dado Dado [MAX_VETOR];
                                 int Excluido [MAX_VETOR];
                                 int Inicio, Fim;
         } Tipo_Vetor;

Rotinas:
  void inicializa_vetor (Tipo_Vetor *V);
  int insere_vetor (Tipo_Vetor *V; Tipo_Dado Dado);
  int consulta_vetor (Tipo_Vetor V; int Indice; Tipo_Dado *Dado);
  int acha_vetor (Tipo_Vetor V; Tipo_Dado Dado; int *Indice);
  void lista_vetor (Tipo_Vetor V);
  int exclui_vetor (Tipo_Vetor *V; int Indice);
  int atualiza_vetor (Tipo_Vetor *V; int Indice; Tipo_Dado Novo_Dado);
  void compacta_vetor (Tipo_Vetor *V);
  int vazio_vetor (Tipo_Vetor V);
  int quantidade_vetor (Tipo_Vetor V);

Exemplo: inserção de dados no vetor
 int insere_vetor (V, Dado)
 Tipo_Vetor *V;
 Tipo_Dado Dado;
 {
   if (V->Fim < MAX_VETOR) /* Vetor nao esta cheio ? */
     {
       V->Dado[V->Fim]=Dado;
       V->Excluido[V->Fim]=FALSO;
      (V->Fim)++;
       return(OK);
      }
   else
     return(ERRO);
  }
                                    

Filas – FIFO = “First In, First Out”:


Construção de um conjunto de rotinas genéricas e modulares de manipulação de filas. Vamos aqui descrever uma possível proposta de um conjunto de rotinas empregadas para este fim.

Rotinas básicas de manipulação de FILAS usando vetores:
- Estruturas de dados com alocação estática
- Inserção no final da fila
- Remoção do início da fila
- Fila circular

Aplicação típica:
- Lista de elementos a espera de um tratamento

Estrutura de dados:
 typedef int Tipo_Dado;
 typedef struct {
                          Tipo_Dado Dado [MAX_FILA];
                          int Inicio, Fim;
                         } Tipo_Fila;

Rotinas:
 void inicializa_fila (Tipo_Fila *F);
 int insere_fila (Tipo_Fila *F; Tipo_Dado Dado);
 int retira_fila (Tipo_Fila *F; Tipo_Dado *Dado);
 void lista_fila (Tipo_Fila F);
 int consulta_fila (Tipo_Fila F; int Indice; Tipo_Dado *Dado);
 int cheia_fila (Tipo_Fila F);
 int vazia_fila (Tipo_Fila F);
 int quantidade_fila (Tipo_Fila F);
 int acha_fila (Tipo_Fila F; Tipo_Dado Dado; int *Indice);

Exemplo: inserção de dados na fila
 int insere_fila (F, Dado)
 Tipo_Fila *F;
 Tipo_Dado Dado;
 {
   int prox;
   prox=F->Fim+1;
   if (prox == MAX_FILA)
        prox=0;
   if (prox == F->Inicio)
       return(ERRO);
   else
        {
          F->Dado[F->Fim]=Dado;
          F->Fim=prox;
          return(OK);
        } 
  }

Pilhas – LIFO = “Last In, First Out”:


Construção de um conjunto de rotinas genéricas e modulares de manipulação de pilhas. Vamos aqui descrever uma possível proposta de um conjunto de rotinas empregadas para este fim.

Rotinas básicas de manipulação de PILHAS usando vetores:
- Estruturas de dados com alocação estática
- Inserção no topo da pilha
- Remoção do topo da pilha

Aplicação típica:
- Lista de “tarefas pendentes”, passagem de parâmetros nas linguagens de programação.

Estrutura de dados:
 typedef int Tipo_Dado;
 typedef struct {
                         Tipo_Dado Dado [MAX_PILHA];
                         int Base, Topo;
                        } Tipo_Pilha;
Rotinas:
 void inicializa_pilha (Tipo_Pilha *P);
 int insere_pilha (Tipo_Pilha *P; Tipo_Dado Dado);
 int retira_pilha (Tipo_Pilha *P; Tipo_Dado *Dado);
 void exibe_pilha (Tipo_Pilha P);
 int quantidade_pilha (Tipo_Pilha P);
 int cheia_pilha (Tipo_Pilha P);
 int vazia_pilha (Tipo_Pilha P);
 void esvazia_pilha (Tipo_Pilha *P);

Exemplo: inserção de dados na pilha
 int insere_vetor (P, Dado)
 Tipo_Vetor *P;
 Tipo_Dado Dado;
 {
   if (V->Fim < MAX_VETOR) 
     {
       P->Dado[P->Fim]=Dado;
       P->Excluido[P->Fim]=FALSO;
      (P->Fim)++;
       return(OK);
      }
   else
     return(ERRO);
  }

Deque – “Double Ended Queue”:


Construção de um conjunto de rotinas genéricas e modulares de manipulação de deques. Vamos aqui descrever uma possível proposta de um conjunto de rotinas empregadas para este fim.

Rotinas básicas de manipulação de DEQUES usando vetores:
- Estruturas de dados com alocação estática
- Inserção no início ou no final do deque
- Remoção do início ou do final do deque
- Estrutura de dados “circular”

Aplicação típica:
- Lista de elementos com múltiplas formas de considerar/manipular a ordem destes

Estrutura de dados:
 typedef int Tipo_Dado;
 typedef struct {
                         Tipo_Dado Dado [MAX_DEQUE];
                          int Inicio, Fim;
                        } Tipo_Deque;
Rotinas:
 void inicializa_deque (Tipo_Deque *D);
 int insere_inicio_deque (Tipo_Deque *D; Tipo_Dado Dado);
 int insere_final_deque (Tipo_Deque *D, Tipo_Dado Dado);
 int retira_inicio_deque (Tipo_Deque *D; Tipo_Dado *Dado);
 int retira_final_deque (Tipo_Deque *D; Tipo_Dado *Dado);
 void lista_deque (Tipo_Deque D);
 int acha_deque (Tipo_Deque D; Tipo_Dado Dado; int *Indice);
 int cheio_deque (Tipo_Deque D);
 int vazio_deque (Tipo_Deque D);
 int quantidade_deque (Tipo_Deque D);
 void apaga_deque (Tipo_Deque *D);


Caso queiram ter uma menor abstração e aprimorar os conceitos do conteúdo abordado neste post, recomendo os vídeos abaixo: 

Estrutura de dados - Lista estática



Estrutura de dados - Pilhas, Filas e Deque



"A mente que se abre a uma nova ideia jamais voltará ao seu tamanho original."
Albert Einstein