Aula 3 - Estruturas de Repetição e suas Aplicações
Nesta aula veremos como fazer um programa repetir um conjunto de instruções: as estruturas while, do-while e for. Veremos também os padrões mais comuns que aparecem em quase todo laço (contadores, acumuladores e flags), e uma série de aplicações práticas que combinam tudo isso.
1. Por que repetir instruções?
Muitos problemas exigem executar o mesmo conjunto de passos várias vezes: somar todos os números digitados pelo usuário, imprimir a tabuada de um número, validar uma entrada até que ela seja digitada corretamente. Copiar e colar a mesma instrução dezenas (ou milhares) de vezes não é viável — para isso, usamos comandos de repetição, também chamados de laços (loops).
A linguagem C oferece três comandos de repetição: while, do-while e for. Todos resolvem, no fundo, o mesmo tipo de problema — a diferença está em qual deles deixa o código mais claro para cada situação.
2. Estrutura while
A estrutura while executa um bloco de código enquanto uma condição permanecer verdadeira. Ela é ideal quando não sabemos previamente quantas vezes o laço será executado — o término depende de algo que só é conhecido durante a execução:
int numero = -1; while (numero != 0) { printf("Digite um numero (0 para sair): "); scanf("%d", &numero); }
A condição é testada antes de cada execução do bloco. Se ela já começar falsa, o bloco pode não ser executado nenhuma vez — repare que, no exemplo, numero foi inicializado com -1 (qualquer valor diferente de 0) exatamente para garantir que o laço execute pelo menos a primeira vez.
Cuidado com laços infinitos. Se a condição do while nunca se tornar falsa, o laço nunca termina. Sempre verifique se existe, dentro do bloco, alguma instrução capaz de eventualmente tornar a condição falsa (no exemplo acima, isso depende do próprio usuário digitar 0).
|
3. Estrutura do-while
A estrutura do-while é parecida com o while, mas a condição é testada depois de executar o bloco pelo menos uma vez:
int numero; do { printf("Digite um numero (0 para sair): "); scanf("%d", &numero); } while (numero != 0);
Note que, dessa vez, numero nem precisou ser inicializado antes — o bloco sempre roda pelo menos uma vez, então o valor de numero já estará definido (pela leitura do scanf) no momento em que a condição for testada pela primeira vez.
O do-while é especialmente comum em menus: queremos sempre mostrar o menu pelo menos uma vez, e só parar de repeti-lo quando o usuário escolher a opção de sair.
4. Simulando uma contagem com while
Até agora, usamos o while para repetir algo até uma condição deixar de ser satisfeita, sem nos preocupar em contar quantas vezes o laço executa. Mas nada impede de usar um while justamente para contar — por exemplo, para imprimir os números de 0 a 9:
int i = 0; // inicialização, antes do laço while (i < 10) { // condição, testada a cada repetição printf("%d\n", i); i++; // atualização, ao final de cada repetição }
Repare que esse laço segue sempre o mesmo roteiro: uma variável (i) é inicializada antes do laço começar; a cada repetição, uma condição envolvendo essa variável é testada; e, ao final de cada repetição, a variável é atualizada (aqui, incrementada com i++). Esse padrão — inicializar, testar, atualizar — é tão comum que a linguagem C oferece uma estrutura própria só para ele, que veremos a seguir.
5. Estrutura for
A estrutura for é usada principalmente quando já sabemos quantas repetições serão necessárias. Ela reúne, em uma única linha, exatamente as três partes que vimos na seção anterior:
for (inicialização; condição; atualização) {
instruções;
}
| Parte | Quando executa | Função |
|---|---|---|
| Inicialização | Uma única vez, antes da primeira verificação da condição. | Cria e inicializa a variável de controle do laço. |
| Condição | Antes de cada repetição. | Decide se o laço continua (verdadeira) ou termina (falsa). |
| Atualização | Depois de cada repetição do bloco. | Normalmente incrementa ou decrementa a variável de controle. |
for (int i = 0; i < 10; i++) { printf("%d\n", i); }
Nesse exemplo, sabemos exatamente quantas vezes o laço executa: dez vezes, uma para cada valor de i entre 0 e 9. O for costuma ser a estrutura mais usada para percorrer vetores (assunto da próxima aula), já que o número de posições do vetor é conhecido.
Compare esse for com o while da seção anterior — os dois fazem exatamente a mesma coisa:
// com while // com for int i = 0; for (int i = 0; i < 10; i++) { while (i < 10) { printf("%d\n", i); printf("%d\n", i); } i++; }
O for não faz nada que o while já não fizesse — ele apenas junta, em uma única linha, as três partes (inicialização, condição, atualização) que no while ficavam espalhadas em lugares diferentes do código. Isso também evita um erro comum: esquecer de escrever o i++ lá no final do bloco do while, o que causaria um laço infinito.
Então, quando usar cada um? Use while quando o laço depende de um teste cujo resultado só é conhecido durante a execução (não sabemos, de antemão, quantas vezes ele vai rodar — por exemplo, "até o usuário digitar 0"). Use for quando o laço depende de um contador cujos limites já são conhecidos antes de começar (por exemplo, "de 0 até 9", ou "uma vez para cada posição de um vetor de tamanho n", assunto da próxima aula).
|
6. Contadores, acumuladores e flags
Independente de qual das três estruturas é usada, a grande maioria dos laços utiliza uma (ou mais) destas três variáveis auxiliares:
| Padrão | Para que serve | Inicializado com |
|---|---|---|
| Contador | Contar quantas vezes algo acontece. | 0, incrementado (++) a cada ocorrência. |
| Acumulador | Acumular uma soma (ou produto) ao longo do laço. | 0 para soma (1 para produto), atualizado a cada iteração. |
| Flag | Guardar um valor lógico (verdadeiro/falso) que "lembra" se algo já aconteceu. | Geralmente 0 (falso), alterado para 1 (verdadeiro) quando o evento ocorre. |
Um exemplo com os três padrões juntos: ler n números e, ao final, informar quantos são pares (contador), a soma de todos eles (acumulador), e se algum deles foi negativo (flag):
#include <stdio.h> int main() { int n, numero, i; int contadorPares = 0; // contador int soma = 0; // acumulador int achouNegativo = 0; // flag printf("Quantos numeros? "); scanf("%d", &n); for (i = 0; i < n; i++) { printf("Digite um numero: "); scanf("%d", &numero); if (numero % 2 == 0) { contadorPares++; // conta mais um par } soma += numero; // acumula a soma if (numero < 0) { achouNegativo = 1; // liga a flag } } printf("Numeros pares: %d\n", contadorPares); printf("Soma total: %d\n", soma); printf("Achou negativo? %s\n", achouNegativo ? "Sim" : "Nao"); return 0; }
Por que inicializar contador e acumulador com 0? Se essas variáveis não fossem inicializadas, elas começariam com um valor indeterminado (lixo de memória), e o resultado final estaria incorreto (ou imprevisível) desde a primeira iteração. Sempre inicialize contadores e acumuladores antes do laço começar.
|
7. Comparando as três estruturas
| Estrutura | Quando usar | Exemplo típico |
|---|---|---|
while |
Não sabemos previamente quantas repetições serão necessárias. | Ler valores até o usuário digitar um valor de parada. |
for |
Sabemos antecipadamente quantas repetições serão realizadas. | Percorrer um vetor de tamanho n (próxima aula). |
do-while |
O bloco precisa ser executado pelo menos uma vez, antes do primeiro teste. | Menus e programas interativos. |
Assim como discutimos com if/else e switch na Aula 2, a diferença entre essas estruturas não está no que cada uma consegue resolver (qualquer uma delas pode ser adaptada para resolver praticamente qualquer laço), mas em qual deixa a intenção do programador mais clara para quem lê o código depois.
8. Aplicações de comandos de repetição
Vejamos agora alguns padrões práticos, muito comuns, que combinam laços com os conceitos das últimas aulas.
Validação de entrada com while
Um uso muito comum de while é insistir até que o usuário digite um valor válido:
int idade; printf("Digite sua idade: "); scanf("%d", &idade); while (idade < 0 || idade > 130) { printf("Idade invalida! Digite novamente: "); scanf("%d", &idade); }
Laços aninhados: tabuada completa
Assim como o if pode ser aninhado dentro de outro if, um laço pode ser aninhado dentro de outro. Um exemplo clássico é imprimir a tabuada de multiplicação de 1 a 5:
int i, j; for (i = 1; i <= 5; i++) { for (j = 1; j <= 10; j++) { printf("%d x %d = %d\n", i, j, i * j); } }
Para cada um dos 5 valores de i (laço externo), o laço interno percorre os 10 valores de j por completo — o corpo mais interno executa, no total, 5 * 10 = 50 vezes.
Contagem regressiva com for decrescente
Um for também pode contar "para trás", bastando ajustar a inicialização, a condição e a atualização:
for (int i = 10; i >= 1; i--) { printf("%d\n", i); } printf("Fim!\n");
Fatorial: acumulador de produto
O mesmo padrão de acumulador usado para somas também funciona para produtos, bastando inicializar com 1 em vez de 0 (o elemento neutro da multiplicação):
int n, fatorial = 1; printf("Digite um numero: "); scanf("%d", &n); for (int i = 1; i <= n; i++) { fatorial *= i; // fatorial = fatorial * i } printf("%d! = %d\n", n, fatorial);
Somatório de uma fórmula: aproximando π
Um padrão de acumulador mais desafiador aparece quando cada termo da soma vem de uma fórmula que muda a cada repetição, em vez de simplesmente somar os números lidos. A série de Leibniz, por exemplo, aproxima π somando termos alternados, cada um com um sinal e um denominador diferentes:
termo(k) = (-1)^k / (2k + 1), para k = 0, 1, 2, 3, ... pi ≈ 4 * (termo(0) + termo(1) + termo(2) + ... + termo(N))
int n, k; float soma = 0, sinal; printf("Quantos termos somar? "); scanf("%d", &n); for (k = 0; k < n; k++) { if (k % 2 == 0) { sinal = 1; } else { sinal = -1; // alterna +1, -1, +1, -1, ... } soma += sinal / (2.0 * k + 1); } printf("Aproximacao de pi: %.5f\n", 4 * soma);
Por que isso é mais difícil do que parece. Aqui, cada termo somado não é simplesmente o valor lido do usuário (como em um acumulador simples): ele depende de k através de uma fórmula, com sinal alternando e denominador crescendo. Antes de programar, é preciso primeiro conseguir escrever a expressão de termo(k) corretamente — o código em si (um for com um acumulador) é o de sempre, mas interpretar a fórmula matemática é o passo que costuma gerar mais erros.
|
Decaimento com tolerância: while controlado por erro
Nem sempre sabemos, de antemão, quantas repetições serão necessárias — às vezes só sabemos que devemos parar quando um valor ficar "pequeno o suficiente". Um exemplo clássico é o decaimento radioativo: a cada período de meia-vida, a quantidade de material restante cai pela metade. Queremos saber quantos períodos são necessários até restar menos que uma certa quantidade mínima (tolerância):
float quantidade, tolerancia; int periodos = 0; printf("Quantidade inicial do material: "); scanf("%f", &quantidade); printf("Tolerancia (quantidade minima): "); scanf("%f", &tolerancia); while (quantidade > tolerancia) { quantidade = quantidade / 2; // meia-vida: cai pela metade a cada periodo periodos++; } printf("Apos %d periodo(s), restam %.4f (abaixo da tolerancia)\n", periodos, quantidade);
Por que precisa ser while, e não for. Não há como saber, antes de rodar o programa, quantos períodos serão necessários — isso depende dos valores de quantidade e tolerancia digitados, que só existem em tempo de execução. Esse é exatamente o critério da Seção 7 para escolher while: usamos quando o número de repetições depende de um teste (aqui, "ainda estou acima da tolerância?"), não de um contador já conhecido. O mesmo padrão — repetir até que um erro/diferença fique pequeno o suficiente — reaparece bastante em métodos numéricos mais avançados.
|
9. Boas práticas com laços
|
Resumo
|
Exercícios
Nível fácil
-
Escreva um programa que use um
forpara imprimir todos os números de 1 a 20. - Escreva um programa que leia 10 números inteiros e imprima a soma de todos eles, usando um acumulador.
-
Escreva um programa que use um
do-whilepara exibir um menu com as opções "1 - Somar", "2 - Subtrair" e "3 - Sair", repetindo o menu até que o usuário escolha a opção 3.
Nível médio
-
Escreva um programa que leia números inteiros até que o usuário digite
-1, e ao final informe quantos números foram digitados (sem contar o-1) e a média deles. -
Escreva um programa que leia um número inteiro
ne verifique, usando um laço, se ele é primo (dica: use um contador de divisores, ou uma flag que é ligada assim que um divisor diferente de 1 e dené encontrado). -
Escreva um programa que imprima, usando laços aninhados, um triângulo de asteriscos com
nlinhas (o valor denlido do usuário), como no exemplo abaixo paran = 4:* ** *** ****
Nível difícil
-
Escreva um programa que leia um número inteiro
ne determine, usando apenas laços (sem funções prontas), se ele é um número palíndromo (lido da esquerda para a direita, é igual a lido da direita para a esquerda — por exemplo, 1221 e 7 são palíndromos, mas 123 não é). Dica: use divisão inteira e módulo para extrair os dígitos um a um e construir o número invertido. -
Escreva um programa que leia um número inteiro
ne imprima osnprimeiros termos da sequência de Fibonacci (0, 1, 1, 2, 3, 5, 8, ...), usando apenas variáveis simples dentro de um laço (sem vetores) para guardar os dois últimos termos calculados.
Sugestões de Respostas dos Exercícios
Exercício 1
#include <stdio.h> int main() { int i; for (i = 1; i <= 20; i++) { printf("%d\n", i); } return 0; }
Exercício 2
#include <stdio.h> int main() { int i, numero; int soma = 0; for (i = 0; i < 10; i++) { printf("Digite um numero: "); scanf("%d", &numero); soma += numero; } printf("Soma: %d\n", soma); return 0; }
Exercício 3
#include <stdio.h> int main() { int opcao, a, b; do { printf("1 - Somar\n2 - Subtrair\n3 - Sair\nEscolha: "); scanf("%d", &opcao); if (opcao == 1) { printf("Digite dois numeros: "); scanf("%d %d", &a, &b); printf("Soma: %d\n", a + b); } else if (opcao == 2) { printf("Digite dois numeros: "); scanf("%d %d", &a, &b); printf("Subtracao: %d\n", a - b); } } while (opcao != 3); return 0; }
O do-while é ideal aqui: o menu precisa ser mostrado pelo menos uma vez, e continua sendo repetido até que a condição (opcao != 3) se torne falsa.
Exercício 4
#include <stdio.h> int main() { int numero; int contador = 0; int soma = 0; printf("Digite numeros (-1 para parar): "); scanf("%d", &numero); while (numero != -1) { soma += numero; contador++; printf("Digite numeros (-1 para parar): "); scanf("%d", &numero); } if (contador > 0) { printf("Quantidade: %d\n", contador); printf("Media: %.2f\n", (float) soma / contador); } else { printf("Nenhum numero foi digitado.\n"); } return 0; }
Note a verificação if (contador > 0) antes de calcular a média: sem ela, se o usuário digitasse -1 logo de cara, o programa tentaria dividir por zero.
Exercício 5
#include <stdio.h> int main() { int n, i; int ehPrimo = 1; // flag: assume primo, até prova em contrário printf("Digite um numero: "); scanf("%d", &n); if (n < 2) { ehPrimo = 0; } else { for (i = 2; i < n; i++) { if (n % i == 0) { ehPrimo = 0; // encontrou um divisor: nao e primo } } } if (ehPrimo) { printf("%d e primo\n", n); } else { printf("%d nao e primo\n", n); } return 0; }
A flag ehPrimo começa "otimista" (verdadeira) e só é desligada (0) se algum divisor entre 2 e n-1 for encontrado. O laço continua até o fim mesmo depois de encontrar um divisor (uma versão mais eficiente poderia parar antes, usando break, mas o laço completo já resolve corretamente o problema).
Exercício 6
#include <stdio.h> int main() { int n, i, j; printf("Digite o numero de linhas: "); scanf("%d", &n); for (i = 1; i <= n; i++) { for (j = 1; j <= i; j++) { printf("*"); } printf("\n"); } return 0; }
O laço externo (i) controla a linha atual; o laço interno (j) imprime exatamente i asteriscos naquela linha — por isso o laço interno vai até i, e não até um valor fixo.
Exercício 7
#include <stdio.h> int main() { int n, original, invertido = 0, digito; printf("Digite um numero: "); scanf("%d", &n); original = n; while (n > 0) { digito = n % 10; // extrai o último dígito invertido = invertido * 10 + digito; // "empilha" esse dígito no final do invertido n = n / 10; // descarta o último dígito } if (original == invertido) { printf("%d e palindromo\n", original); } else { printf("%d nao e palindromo\n", original); } return 0; }
A cada iteração, extraímos o último dígito de n (n % 10) e o colocamos no final do número invertido que estamos construindo (invertido * 10 + digito), depois descartamos esse dígito de n (n / 10). Ao final, invertido contém os dígitos de original na ordem inversa, e basta compará-los.
Exercício 8
#include <stdio.h> int main() { int n, i; int anterior = 0, atual = 1, proximo; printf("Quantos termos? "); scanf("%d", &n); for (i = 0; i < n; i++) { if (i == 0) { printf("%d\n", anterior); } else { printf("%d\n", atual); proximo = anterior + atual; anterior = atual; atual = proximo; } } return 0; }
Em vez de guardar toda a sequência (o que exigiria um vetor, assunto da próxima aula), guardamos apenas os dois últimos termos (anterior e atual). A cada passo, calculamos o próximo termo como a soma dos dois anteriores, e "avançamos" os nomes das variáveis: o que era atual vira anterior, e o novo termo vira atual.