listaSequencial.cPercorrendo cada bloco do programa completo — do cabeçalho ao main(), função por função, linha por linha
Esta aula revisa o arquivo listaSequencial.c — o programa completo que implementa todos os conceitos das Aulas 29 a 35. Em vez de ver os conceitos isoladamente, aqui você lê o código como um todo e entende como cada parte se encaixa na outra.
main) que orquestra tudo. Ler o código nessa ordem é a melhor forma de dominar o projeto.
O programa implementa as seguintes operações, nesta ordem no arquivo:
| # | Função | O que faz |
|---|---|---|
| 1 | inicializarLista | Zera o contador — prepara a lista para uso |
| 2 | exibirLista | Imprime todos os elementos válidos |
| 3 | tamanho | Retorna quantos elementos estão na lista |
| 4 | tamanhoEmBytes | Retorna o tamanho físico da struct em bytes |
| 5 | primeiroElem | Retorna a chave do primeiro elemento |
| 6 | ultimoElem | Retorna a chave do último elemento |
| 7 | enesimoElem | Retorna a chave do elemento na posição n |
| 8 | reinicializarLista | Esvazia a lista sem liberar memória |
| 9 | inserirElemLista | Insere um registro na posição i com deslocamento |
| 10 | buscaSequencial | Busca linear sem sentinela — O(n) |
| 11 | excluirElemLista | Remove elemento por chave com deslocamento |
| 12 | buscaSentinela | Busca linear otimizada com sentinela — O(n) |
| 13 | main | Programa principal — exercita todas as funções |
As primeiras linhas do arquivo estabelecem o contexto e as dependências do programa:
/******************************************************************************
listaSequencial.c
Este programa gerencia listas lineares sequenciais.
As listas gerenciadas podem ter MAX elementos (posicoes de 0 a MAX-1 no
arranjo A). Alocamos MAX+1 posicoes no arranjo para possibilitar a
criacao de um elemento sentinela.
******************************************************************************/
#include <stdio.h>
#include <stdbool.h>
#define MAX 50
#define ERRO -1
#include <stdio.h> — Biblioteca padrão de entrada e saída. Necessária para printf e scanf.#include <stdbool.h> — Disponível desde C99. Define o tipo bool e as constantes true (1) e false (0). Sem esse include, funções que retornam bool — como inserirElemLista e excluirElemLista — não compilariam.#define MAX 50 — Capacidade máxima da lista. Uma constante simbólica: o pré-processador substitui toda ocorrência de MAX por 50 antes da compilação.#define ERRO -1 — Código sentinela de erro. Quando uma função de busca não encontra o elemento, retorna -1. Usando o nome ERRO em vez do número, o código comunica a intenção claramente.
A fundação de tudo: os três typedefs que definem o que é uma lista neste programa.
typedef int TIPOCHAVE;
typedef struct {
TIPOCHAVE chave;
// outros campos...
} REGISTRO;
typedef struct {
REGISTRO A[MAX+1];
int nroElem;
} LISTA;
Leia de baixo para cima para entender a dependência entre os tipos:
| Tipo | O que é | Depende de |
|---|---|---|
TIPOCHAVE |
Apelido para int. Representa o identificador de um registro. Se a chave mudar de tipo (ex.: CPF como string), só esta linha precisa mudar. |
— |
REGISTRO |
A unidade mínima de dados. Contém pelo menos uma chave. Outros campos de dados (nome, data, valor...) seriam adicionados aqui. |
TIPOCHAVE |
LISTA |
O contêiner completo: um array de até MAX+1 registros + um contador nroElem de quantos estão em uso. |
REGISTRO, MAX |
A[MAX+1] e não A[MAX]? A posição extra A[MAX] é reservada exclusivamente para a técnica de busca com sentinela (função buscaSentinela): colocamos a chave procurada nessa posição para garantir que o loop sempre termine sem precisar checar o limite. Sem o +1, esse acesso seria fora dos limites do array — comportamento indefinido em C.
nroElem na LISTA é o equivalente ao size() de um ArrayList em Java. A capacidade física é sempre MAX; o tamanho lógico (em uso) é nroElem, que varia de 0 a MAX conforme inserções e exclusões.
inicializarLista/* Inicialização da lista sequencial (a lista já está criada e é apontada pelo endereço em l) */ void inicializarLista(LISTA* l) { l->nroElem = 0; } /* inicializarLista */
LISTA declarada com LISTA lista; terá nroElem com valor imprevisível. Esta função corrige isso, zerando o contador antes de qualquer uso.
Três pontos para fixar sobre esta função:
LISTA* l*, a função zeraria uma cópia local e a lista original permaneceria com lixo.
->l é um ponteiro, acessamos campos com -> em vez de .. l->nroElem é idêntico a (*l).nroElem.
nroElem recebe 0. O array A ainda contém lixo — mas as funções ignoram qualquer posição além de nroElem, então isso não causa problema.
main: inicializarLista(&lista); — o & obtém o endereço de lista e o passa para o ponteiro l.
exibirLista/* Exibição da lista sequencial */ void exibirLista(LISTA* l) { int i; printf("Lista: \" "); for (i = 0; i < l->nroElem; i++) printf("%i ", l->A[i].chave); // só lembrando TIPOCHAVE = int printf("\"\n"); } /* exibirLista */
O for percorre exatamente os nroElem primeiros índices do array — de 0 até nroElem - 1. A condição i < l->nroElem garante que nunca acessamos posições além dos elementos válidos.
Lista: " 9 3 12 4 1 " — as aspas fazem parte da saída formatada com \" (aspas escapadas dentro de string). O especificador %i é equivalente a %d para inteiros.
l->A[i].chave: Esta expressão percorre dois níveis de acesso. Primeiro l->A[i] obtém o REGISTRO na posição i. Depois .chave acessa o campo chave desse registro. O -> é usado porque l é ponteiro; o . é usado porque A[i] é um valor (não um ponteiro).
tamanho e tamanhoEmBytes/* Retornar o tamanho da lista (numero de elementos "validos") */ int tamanho(LISTA* l) { return l->nroElem; } /* tamanho */ /* Retornar o tamanho em bytes da lista. Neste caso, isto nao depende do numero de elementos que estao sendo usados, pois a alocacao de memoria eh estatica. A priori, nao precisariamos do ponteiro para a lista, vamos utiliza-lo apenas porque teremos as mesmas funcoes para listas ligadas. */ int tamanhoEmBytes(LISTA* l) { return sizeof(LISTA); } /* tamanhoEmBytes */
| Função | O que mede | Varia com inserções? | Equivalente Java |
|---|---|---|---|
tamanho() |
Elementos em uso (nroElem) |
✅ Sim — cresce e decresce | arrayList.size() |
tamanhoEmBytes() |
Bytes ocupados pela struct LISTA na memória |
❌ Não — alocação estática fixa | Sem equivalente direto |
tamanhoEmBytes recebe LISTA* l se não usa l? O comentário original explica: "A priori, não precisaríamos do ponteiro para a lista, vamos utilizá-lo apenas porque teremos as mesmas funções para listas ligadas." É uma decisão de design para manter a assinatura consistente entre os dois tipos de lista. sizeof(LISTA) é avaliado em tempo de compilação — não depende de l.
Tamanho da lista (em bytes): 208. — com MAX=50 e REGISTRO contendo apenas um int (4 bytes), temos 51 × 4 + 4 = 208 bytes: 51 registros (MAX+1) × 4 bytes cada, mais 4 bytes do campo nroElem.
primeiroElem, ultimoElem e enesimoElem/* Retornar a chave do primeiro elemento (caso haja) e ERRO caso vazia */ TIPOCHAVE primeiroElem(LISTA* l) { if (l->nroElem > 0) return l->A[0].chave; else return ERRO; // lista vazia } /* primeiroElem */ /* Retornar a chave do ultimo elemento (caso haja) e ERRO caso vazia */ TIPOCHAVE ultimoElem(LISTA* l) { if (l->nroElem > 0) return l->A[l->nroElem - 1].chave; else return ERRO; // lista vazia } /* ultimoElem */ /* Retornar a chave do elemento na posição n (0 a MAX-1) */ TIPOCHAVE enesimoElem(LISTA* l, int n) { if ((n >= 0) && (n < l->nroElem)) return l->A[n].chave; else return ERRO; } /* enesimoElem */
As três funções seguem o mesmo padrão de proteção: verificam se o acesso é válido antes de acessar o array. Compare os três casos de acesso:
| Função | Índice acessado | Condição de validade |
|---|---|---|
primeiroElem | A[0] | nroElem > 0 (lista não vazia) |
ultimoElem | A[nroElem - 1] | nroElem > 0 (lista não vazia) |
enesimoElem | A[n] | n >= 0 e n < nroElem |
nroElem - 1 em ultimoElem? Com 5 elementos, eles ocupam os índices 0, 1, 2, 3 e 4. O último está em nroElem - 1 = 4. Usar A[nroElem] acessaria a posição da sentinela — fora dos elementos válidos.
reinicializarLista/* Reinicializar a estrutura */ void reinicializarLista(LISTA* l) { l->nroElem = 0; } /* reinicializarLista */
O código é idêntico ao de inicializarLista. A diferença está na intenção semântica: inicializarLista é chamada na primeira vez, quando a lista acaba de ser criada; reinicializarLista é chamada para esvaziar uma lista que já estava em uso.
A, mas como nroElem voltou a 0, todas as funções os ignoram completamente. Na próxima inserção, a posição 0 será sobrescrita — os dados antigos desaparecem naturalmente. Em Java, arrayList.clear() faz o equivalente (e adicionalmente permite que o garbage collector libere os objetos).
inserirElemLista/* Inserção "direta" na iésima posição (posicao i do arranjo A). Funciona da mesma maneira de um insertionSort: deve-se deslocar todos os elementos a partir da iesima posicao e entao se insere o novo elemento. */ bool inserirElemLista(LISTA* l, REGISTRO reg, int i) { int j; if ((l->nroElem >= MAX) || (i < 0) || (i > l->nroElem)) return false; // lista cheia ou índice inválido for (j = l->nroElem; j > i; j--) l->A[j] = l->A[j-1]; l->A[i] = reg; l->nroElem++; return true; } /* inserirElemLista */
Esta é a função mais complexa do programa. Disseque cada parte:
||:nroElem >= MAX → lista cheiai < 0 → índice negativo inválidoi > nroElem → índice além do último elemento+1false imediatamente.
for (j = nroElem; j > i; j--)A[j-1] para A[j] — abrindo espaço na posição i. Obrigatoriamente da direita para a esquerda para não sobrescrever dados antes de copiá-los.
A[i] = reg — coloca o novo registro na posição liberada.nroElem++ — atualiza o contador. Sem isso, a lista "não saberia" que ganhou um elemento.
Passo a passo para inserir chave 12 na posição i=2 da lista [9, 3, 4, 1] (nroElem=4):
| j | Operação | A[0] | A[1] | A[2] | A[3] | A[4] |
|---|---|---|---|---|---|---|
| — | Estado inicial | 9 | 3 | 4 | 1 | — |
| 4 | A[4] ← A[3] | 9 | 3 | 4 | 1 | 1 |
| 3 | A[3] ← A[2] | 9 | 3 | 4 | 4 | 1 |
| 2 | j > i falso — para | 9 | 3 | 4 | 4 | 1 |
| — | A[2] = 12, nroElem=5 | 9 | 3 | 12 | 4 | 1 |
buscaSequencial/* Busca sequencial em lista ordenada ou não SEM SENTINELA */ int buscaSequencial(LISTA* l, TIPOCHAVE ch) { int i = 0; while (i < l->nroElem) { if (ch == l->A[i].chave) return i; // achou else i++; } return ERRO; // não achou } /* buscaSequencial */
A lógica é direta: percorre do índice 0 ao nroElem-1, comparando a chave procurada ch com a chave de cada elemento. Dois caminhos de saída:
return i — retorna imediatamente o índice onde a chave foi encontrada, sem continuar percorrendo o restante da lista.
while termina quando i == nroElem. A execução sai do loop e cai no return ERRO, que vale -1.
while checa i < nroElem (limite) e o if checa a igualdade da chave. Para n elementos não encontrados, são feitas 2n comparações no total. A buscaSentinela reduz isso a n+1.
TIPOCHAVE ch: ch é o valor que se quer encontrar — a "chave alvo". Dentro do while, l->A[i].chave é a chave do elemento na posição i. A comparação ch == l->A[i].chave verifica se são iguais.
excluirElemLista/* Exclusão do elemento cuja chave seja igual a ch */ bool excluirElemLista(LISTA* l, TIPOCHAVE ch) { int pos, j; pos = buscaSequencial(l, ch); if (pos == ERRO) return false; // não existe for (j = pos; j < l->nroElem - 1; j++) l->A[j] = l->A[j+1]; l->nroElem--; return true; } /* excluirElemLista */
A função tem três etapas encadeadas:
buscaSequencial para encontrar a posição do elemento. Se retornar ERRO, o elemento não existe e a função retorna false sem modificar nada.
for (j = pos; j < nroElem-1; j++) A[j] = A[j+1]nroElem-- — reduz o contador. O dado ainda existe no array mas é ignorado pelas funções a partir de agora.
Visualizando a exclusão do elemento com chave 4 (posição 3) da lista [9, 3, 12, 4, 1]:
| j | Operação | A[0] | A[1] | A[2] | A[3] | A[4] |
|---|---|---|---|---|---|---|
| — | Estado inicial | 9 | 3 | 12 | 4 | 1 |
| 3 | A[3] ← A[4] | 9 | 3 | 12 | 1 | 1 |
| 4 | j < nroElem-1 falso — para | 9 | 3 | 12 | 1 | 1 |
| — | nroElem-- (4) | 9 | 3 | 12 | 1 | ignorado |
excluirElemLista não reimplementa a busca — ela chama buscaSequencial. Esse é um princípio importante de programação: funções devem se construir sobre funções já existentes. O resultado é código mais curto, mais legível e mais fácil de corrigir.
buscaSentinela/* Busca sequencial em lista COM SENTINELA (vetor criado com MAX+1 posições) */ int buscaSentinela(LISTA* l, TIPOCHAVE ch) { int i = 0; l->A[l->nroElem].chave = ch; // sentinela while (l->A[i].chave != ch) i++; if (i > l->nroElem - 1) return ERRO; // não achou else return i; } /* buscaSentinela */
Compare com a buscaSequencial: o while original tinha duas condições. Esta versão tem apenas uma:
| Sem sentinela | Com sentinela | |
|---|---|---|
| Condição do loop | i < nroElem && A[i] != ch | A[i].chave != ch |
| Verificações/iteração | 2 | 1 |
| Como termina | Limite atingido ou chave encontrada | Sempre encontra (real ou sentinela) |
| Confirmação final | Não precisa | if (i > nroElem-1) — parou na sentinela? |
l->A[l->nroElem].chave = ch — coloca a chave procurada na posição A[MAX] (a posição extra reservada).while avança até encontrar ch. Como a sentinela garante que ch existe em algum ponto, o loop sempre para — sem risco de ultrapassar os limites.i <= nroElem-1) ou na sentinela (i == nroElem).
l->A[l->nroElem].chave = ch escreve na posição A[nroElem] — que existe graças ao MAX+1, mas está além dos elementos válidos. Isso é intencional e seguro, pois nunca sobrescreve dados reais. Porém é uma escrita na memória da lista, não apenas uma leitura.
main(): o Programa em AçãoO main exercita todas as funções em sequência. Vamos percorrê-lo em seções:
LISTA lista;
inicializarLista(&lista);
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n", tamanhoEmBytes(&lista));
Lista: " "
Numero de elementos na lista: 0.
Tamanho da lista (em bytes): 208.
LISTA lista; declara a struct na stack. inicializarLista(&lista) zera nroElem. A lista exibida está vazia e já ocupa 208 bytes — a memória é reservada integralmente desde a declaração.
REGISTRO reg; reg.chave = 9; inserirElemLista(&lista, reg, 0); // lista: [9] exibirLista(&lista); reg.chave = 3; inserirElemLista(&lista, reg, 1); // [9, 3] reg.chave = 4; inserirElemLista(&lista, reg, 2); // [9, 3, 4] reg.chave = 1; inserirElemLista(&lista, reg, 3); // [9, 3, 4, 1] reg.chave = 12; inserirElemLista(&lista, reg, 2); // [9, 3, 12, 4, 1] ← inserção no meio! exibirLista(&lista); printf("Numero de elementos na lista: %i.\n", tamanho(&lista)); printf("Tamanho da lista (em bytes): %i.\n", tamanhoEmBytes(&lista));
Lista: " 9 "
Lista: " 9 3 12 4 1 "
Numero de elementos na lista: 5.
Tamanho da lista (em bytes): 208.
[9, 3, 4, 1]. Inserir 12 na posição 2 desloca 4 e 1 para a direita, resultando em [9, 3, 12, 4, 1]. Note que o tamanho em bytes continua 208 — a alocação nunca muda.
printf("Chave 4 encontrada na posicao: %i do arranjo A.\n", buscaSequencial(&lista, 4));
printf("Chave 4 encontrada na posicao: %i do arranjo A.\n", buscaSentinela(&lista, 4));
Chave 4 encontrada na posicao: 3 do arranjo A.
Chave 4 encontrada na posicao: 3 do arranjo A.
Ambas as funções encontram o mesmo elemento e retornam o mesmo índice (3). A lista nesse ponto é [9, 3, 12, 4, 1] — o elemento 4 está no índice 3.
if (excluirElemLista(&lista, 4)) printf("Exclusao bem sucedida: 4.\n");
if (excluirElemLista(&lista, 8)) printf("Exclusao bem sucedida: 8.\n"); // 8 não existe!
if (excluirElemLista(&lista, 9)) printf("Exclusao bem sucedida: 9.\n");
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n", tamanhoEmBytes(&lista));
Exclusao bem sucedida: 4.
Exclusao bem sucedida: 9.
Lista: " 3 12 1 "
Numero de elementos na lista: 3.
Tamanho da lista (em bytes): 208.
excluirElemLista retorna false quando o elemento não existe. O if avalia false e a mensagem não é impressa. Esse é o uso inteligente do valor de retorno bool — o chamador pode tomar decisões baseado no sucesso ou falha da operação.
reinicializarLista(&lista);
exibirLista(&lista);
printf("Numero de elementos na lista: %i.\n", tamanho(&lista));
printf("Tamanho da lista (em bytes): %i.\n", tamanhoEmBytes(&lista));
return 0;
Lista: " "
Numero de elementos na lista: 0.
Tamanho da lista (em bytes): 208.
A lista volta ao estado inicial. Os dados [3, 12, 1] ainda existem fisicamente no array, mas nroElem = 0 faz com que todas as funções os ignorem completamente.
O comentário ao final do arquivo registra a saída real de uma execução:
Lista: " "
Numero de elementos na lista: 0.
Tamanho da lista (em bytes): 208.
Lista: " 9 "
Lista: " 9 3 12 4 1 "
Numero de elementos na lista: 5.
Tamanho da lista (em bytes): 208.
Chave 4 encontrada na posicao: 3 do arranjo A.
Chave 4 encontrada na posicao: 3 do arranjo A.
Exclusao bem sucedida: 4.
Exclusao bem sucedida: 9.
Lista: " 3 12 1 "
Numero de elementos na lista: 3.
Tamanho da lista (em bytes): 208.
Lista: " "
Numero de elementos na lista: 0.
Tamanho da lista (em bytes): 208.
Questões sobre o programa completo listaSequencial.c: