Aula 2 - Expressões Relacionais, Lógicas e Condicionais
Nesta aula veremos como um programa pode tomar decisões: comparar valores, combinar condições, e executar diferentes trechos de código dependendo do resultado dessas comparações. Esses conceitos — expressões relacionais, expressões lógicas e comandos condicionais (if, if/else e switch) — são a base de praticamente todo programa não trivial.
1. Expressões relacionais
Uma expressão relacional compara dois valores e produz um resultado lógico: verdadeiro ou falso. Em C, não existe um tipo booleano "puro" por padrão (isso mudou um pouco com o C99, mas não usaremos aqui) — o resultado de uma comparação é um int, onde 0 significa falso e qualquer valor diferente de 0 significa verdadeiro (por convenção, 1).
| Operador | Significado | Exemplo |
|---|---|---|
> |
Maior que | idade > 18 |
< |
Menor que | nota < 6.0 |
>= |
Maior ou igual | idade >= 18 |
<= |
Menor ou igual | nota <= 10.0 |
== |
Igual | opcao == 1 |
!= |
Diferente | letra != 'S' |
O erro mais comum da linguagem C. = é atribuição (Aula 1); == é comparação. Escrever if (idade = 18) por engano, no lugar de if (idade == 18), é um erro clássico: o compilador normalmente aceita (embora costume emitir um aviso com -Wall), mas o programa passa a atribuir 18 a idade em vez de compará-la — e, como o resultado da atribuição é o próprio valor atribuído (18, diferente de zero), a condição do if sempre será considerada verdadeira. Sempre revise seus ifs com atenção a esse detalhe.
|
2. Expressões lógicas
Os operadores lógicos combinam ou invertem o resultado de expressões relacionais, permitindo construir condições mais elaboradas:
| Operador | Nome | Significado | Exemplo |
|---|---|---|---|
&& |
E lógico (AND) | Verdadeiro somente se ambos os lados forem verdadeiros. | idade >= 18 && nota >= 6.0 |
|| |
OU lógico (OR) | Verdadeiro se pelo menos um dos lados for verdadeiro. | dia == 6 || dia == 7 |
! |
Negação (NOT) | Inverte o valor lógico da expressão. | !encontrou |
Por exemplo, para verificar se um aluno foi aprovado (nota mínima 6.0 e frequência mínima de 75%):
if (nota >= 6.0 && frequencia >= 75) { printf("Aprovado.\n"); }
Curto-circuito (short-circuit). Em a && b, se a já for falso, C nem chega a avaliar b — o resultado já é falso de qualquer forma. De modo semelhante, em a || b, se a já for verdadeiro, b não é avaliado. Isso não é só uma otimização: é comum (e seguro) escrever código como indice >= 0 && vetor[indice] > 0, contando que a segunda parte só será avaliada se a primeira garantir que o acesso é seguro.
|
3. Estrutura if
A estrutura if executa um bloco de instruções somente quando uma determinada condição é verdadeira:
if (nota >= 6.0) { printf("Aprovado!\n"); }
Caso a condição seja falsa, nenhuma instrução do bloco é executada, e o programa simplesmente continua depois do if.
4. Estrutura if/else
Quando queremos executar um bloco se a condição for verdadeira, e outro bloco se for falsa, usamos if/else:
if (nota >= 6.0) { printf("Aprovado!\n"); } else { printf("Reprovado!\n"); }
Exatamente um dos dois blocos será executado — nunca os dois, e nunca nenhum.
Condicionais aninhadas e else if
É comum que um teste dependa do resultado de outro. Nesses casos, podemos "aninhar" um if dentro de outro:
if (nota >= 6.0) { if (frequencia >= 75) { printf("Aprovado!\n"); } else { printf("Reprovado por frequência.\n"); } } else { printf("Reprovado por nota.\n"); }
Quando existem várias condições mutuamente exclusivas (isto é, no máximo uma delas pode ser verdadeira por vez), uma sequência de else if costuma ser mais clara do que ifs aninhados:
if (nota >= 9.0) { printf("Conceito A\n"); } else if (nota >= 7.5) { printf("Conceito B\n"); } else if (nota >= 6.0) { printf("Conceito C\n"); } else { printf("Conceito D\n"); }
Repare que, assim que uma das condições é verdadeira, o programa executa aquele bloco e pula todos os outros — mesmo que uma condição posterior também fosse verdadeira. Por isso, a ordem dos testes importa: aqui, testamos da nota mais alta para a mais baixa, para que cada faixa seja verificada corretamente.
5. Indentação
A linguagem C não exige indentação para funcionar — o compilador ignora espaços em branco extras. Ainda assim, a indentação é uma das ferramentas mais importantes para tornar um programa legível, deixando claro visualmente quais instruções pertencem a qual bloco.
Compare os dois trechos abaixo — ambos fazem exatamente a mesma coisa:
Indentação inadequada (evite):
if (nota >= 6.0){ printf("Aprovado\n"); if(frequencia>=75){ printf("Frequência suficiente\n"); } else{ printf("Reprovado por frequência\n"); } }
Indentação correta (adote sempre):
if (nota >= 6.0) { printf("Aprovado\n"); if (frequencia >= 75) { printf("Frequência suficiente\n"); } else { printf("Reprovado por frequência\n"); } }
Na versão indentada, é possível ver imediatamente, só pela posição do texto, que o segundo if está dentro do primeiro. Na versão sem indentação, essa relação fica escondida, e o código se torna muito mais difícil de ler, revisar e corrigir.
Convenção adotada nesta disciplina. Utilize sempre quatro espaços por nível de bloco (não tabulações misturadas com espaços), e sempre utilize chaves { }, mesmo quando o bloco tiver apenas uma instrução — isso evita um erro comum: adicionar uma segunda linha ao bloco mais tarde e esquecer de acrescentar as chaves, fazendo com que ela passe a rodar fora do if por engano.
|
6. Estrutura switch
Quando existem diversas alternativas baseadas no valor de uma mesma variável (inteira ou caractere), a estrutura switch costuma deixar o código mais organizado do que uma longa cadeia de else if:
switch (opcao) { case 1: printf("Inserir\n"); break; case 2: printf("Remover\n"); break; case 3: printf("Buscar\n"); break; default: printf("Opcao invalida\n"); }
O switch compara o valor de opcao com cada case, na ordem em que aparecem, e executa o bloco correspondente ao primeiro valor igual encontrado. O default (opcional) é executado quando nenhum case corresponde ao valor.
break é essencial. A instrução break encerra o switch imediatamente. Se ela for esquecida, a execução continua caindo nos cases seguintes, mesmo que o valor não corresponda — comportamento chamado de fall-through. Às vezes isso é usado de propósito (para agrupar vários cases com o mesmo tratamento), mas na grande maioria das vezes é um bug: sempre confira se cada case termina com break, a menos que o fall-through seja intencional (e, nesse caso, comente o código explicando por quê).
|
Um exemplo intencional de fall-through, agrupando dias do fim de semana:
switch (dia) { case 6: // sábado: sem break, "cai" para o case 7 case 7: printf("Fim de semana\n"); break; default: printf("Dia de semana\n"); }
7. if/else versus switch: quando usar cada um
| Aspecto | if / else if |
switch |
|---|---|---|
| Tipos de condição | Qualquer expressão relacional/lógica (>, <, &&, intervalos, múltiplas variáveis, etc.). |
Apenas igualdade com valores constantes de uma única variável (inteira ou char). |
| Quando usar | Condições envolvendo faixas de valores (nota >= 6.0), múltiplas variáveis, ou operadores lógicos. |
Um menu de opções, um código de erro, um dia da semana — vários valores possíveis para uma variável específica. |
| Legibilidade | Pode ficar longo e repetitivo com muitas alternativas para a mesma variável. | Mais organizado e direto quando há muitas alternativas para o mesmo valor. |
| Risco comum | Esquecer o else final, ou testar as condições na ordem errada. |
Esquecer o break, causando fall-through não intencional. |
Uma heurística simples para não travar na hora de escolher: pense primeiro no switch. Se a decisão for só "qual valor exato uma única variável tem entre algumas opções conhecidas", o switch resolve bem, e deixa o código mais organizado. Assim que a decisão precisar de algo que o switch não faz — uma faixa de valores (nota >= 6.0), mais de uma variável, ou operadores lógicos (&&/||) — parta para o if/else.
Se sair numa ilha deserta, leve o if/else. Ele é o mais genérico dos dois: tudo que um switch resolve, um if/else também resolve (só que talvez com mais linhas); mas o contrário não é verdade — existem muitas decisões (faixas de valores, várias variáveis, condições combinadas) que o switch simplesmente não consegue expressar. Por isso, na dúvida, o if/else nunca falha; o switch é uma opção mais especializada, para quando ele se encaixa perfeitamente no problema.
|
Boas práticas.
|
8. Programa completo de exemplo
Um programa que lê a nota de um aluno e imprime seu conceito, combinando expressões relacionais, lógicas e uma cadeia de else if:
#include <stdio.h> int main() { float nota; printf("Digite a nota final: "); scanf("%f", ¬a); if (nota < 0.0 || nota > 10.0) { printf("Nota invalida.\n"); } else if (nota >= 9.0) { printf("Conceito A\n"); } else if (nota >= 7.5) { printf("Conceito B\n"); } else if (nota >= 6.0) { printf("Conceito C\n"); } else { printf("Conceito D\n"); } return 0; }
Resumo
|
Exercícios
Nível fácil
- Escreva um programa que leia um número inteiro e informe se ele é par ou ímpar.
- Escreva um programa que leia dois números inteiros e informe qual é o maior, ou se são iguais.
-
Escreva um programa que leia um caractere representando uma opção de menu (
'A','B'ou'C') e imprima, usandoswitch, uma mensagem diferente para cada opção, e"Opcao invalida"para qualquer outro caractere.
Nível médio
-
Escreva um programa que leia a idade de uma pessoa e informe sua faixa etária, usando
else if: "Criança" (até 12 anos), "Adolescente" (13 a 17 anos), "Adulto" (18 a 59 anos) ou "Idoso" (60 anos ou mais). - Escreva um programa que leia três números inteiros representando os lados de um triângulo, e informe se eles realmente formam um triângulo válido (a soma de quaisquer dois lados deve ser maior que o terceiro) e, em caso afirmativo, se o triângulo é equilátero (três lados iguais), isósceles (exatamente dois lados iguais) ou escaleno (todos diferentes).
-
Escreva um programa que leia o mês (um número de 1 a 12) e imprima, usando
switch, o nome da estação do ano correspondente no hemisfério sul (considere dezembro, janeiro e fevereiro como verão; março, abril e maio como outono; e assim por diante), tratando meses inválidos comdefault.
Nível difícil
- Escreva um programa que leia três números inteiros e determine, sem usar nenhum comando de repetição, qual deles é o do meio (a mediana), usando apenas expressões relacionais, lógicas e comandos condicionais.
- Escreva um programa que leia o dia, o mês e o ano de uma data, e verifique se essa data é válida (por exemplo, dia 31 não é válido para todos os meses, e fevereiro pode ter 28 ou 29 dias). Considere o cálculo de ano bissexto: um ano é bissexto se for divisível por 4, exceto os anos divisíveis por 100 que não sejam também divisíveis por 400.
Sugestões de Respostas dos Exercícios
Exercício 1
#include <stdio.h> int main() { int n; printf("Digite um numero: "); scanf("%d", &n); if (n % 2 == 0) { printf("Par\n"); } else { printf("Impar\n"); } return 0; }
Exercício 2
#include <stdio.h> int main() { int a, b; printf("Digite dois numeros: "); scanf("%d %d", &a, &b); if (a > b) { printf("O maior e %d\n", a); } else if (b > a) { printf("O maior e %d\n", b); } else { printf("Os numeros sao iguais\n"); } return 0; }
Exercício 3
#include <stdio.h> int main() { char opcao; printf("Escolha uma opcao (A, B ou C): "); scanf("%c", &opcao); switch (opcao) { case 'A': printf("Voce escolheu Inserir\n"); break; case 'B': printf("Voce escolheu Remover\n"); break; case 'C': printf("Voce escolheu Buscar\n"); break; default: printf("Opcao invalida\n"); } return 0; }
Exercício 4
#include <stdio.h> int main() { int idade; printf("Digite a idade: "); scanf("%d", &idade); if (idade <= 12) { printf("Crianca\n"); } else if (idade <= 17) { printf("Adolescente\n"); } else if (idade <= 59) { printf("Adulto\n"); } else { printf("Idoso\n"); } return 0; }
Note que a ordem dos testes (do menor limite para o maior) é o que permite usar apenas <= em cada else if: se o código chegou até, por exemplo, o terceiro teste, já sabemos que idade é maior que 17 (senão um dos testes anteriores já teria sido verdadeiro), então idade <= 59 sozinho já identifica corretamente a faixa de 18 a 59 anos.
Exercício 5
#include <stdio.h> int main() { int a, b, c; printf("Digite os tres lados: "); scanf("%d %d %d", &a, &b, &c); if (a + b <= c || a + c <= b || b + c <= a) { printf("Nao forma um triangulo valido\n"); } else if (a == b && b == c) { printf("Triangulo equilatero\n"); } else if (a == b || b == c || a == c) { printf("Triangulo isosceles\n"); } else { printf("Triangulo escaleno\n"); } return 0; }
A validade do triângulo é verificada pela "desigualdade triangular": a soma de quaisquer dois lados deve ser maior que o terceiro. Se qualquer uma das três somas não for maior (ou seja, for menor ou igual), o triângulo é inválido.
Exercício 6
#include <stdio.h> int main() { int mes; printf("Digite o mes (1 a 12): "); scanf("%d", &mes); switch (mes) { case 12: case 1: case 2: printf("Verao\n"); break; case 3: case 4: case 5: printf("Outono\n"); break; case 6: case 7: case 8: printf("Inverno\n"); break; case 9: case 10: case 11: printf("Primavera\n"); break; default: printf("Mes invalido\n"); } return 0; }
Este é um exemplo de fall-through intencional: agrupamos três cases seguidos sem break entre eles, para que os três meses de uma mesma estação caiam no mesmo bloco de printf.
Exercício 7
#include <stdio.h> int main() { int a, b, c, mediana; printf("Digite tres numeros: "); scanf("%d %d %d", &a, &b, &c); if ((a >= b && a <= c) || (a >= c && a <= b)) { mediana = a; } else if ((b >= a && b <= c) || (b >= c && b <= a)) { mediana = b; } else { mediana = c; } printf("A mediana e %d\n", mediana); return 0; }
A ideia é: um número é a mediana se ele estiver "entre" os outros dois — ou seja, se for maior ou igual a um deles e menor ou igual ao outro (em qualquer uma das duas ordens possíveis). Testamos essa condição primeiro para a, depois para b; se nenhum dos dois for a mediana, só resta que seja c.
Exercício 8
#include <stdio.h> int main() { int dia, mes, ano; int bissexto; int valido; printf("Digite dia, mes e ano: "); scanf("%d %d %d", &dia, &mes, &ano); bissexto = (ano % 4 == 0 && ano % 100 != 0) || (ano % 400 == 0); valido = 1; // otimista: assume válido, e descarta nos testes abaixo if (mes < 1 || mes > 12 || dia < 1) { valido = 0; } else if (mes == 2) { if (bissexto && dia > 29) { valido = 0; } else if (!bissexto && dia > 28) { valido = 0; } } else if (mes == 4 || mes == 6 || mes == 9 || mes == 11) { if (dia > 30) { valido = 0; } } else { if (dia > 31) { valido = 0; } } if (valido) { printf("Data valida\n"); } else { printf("Data invalida\n"); } return 0; }
A verificação de ano bissexto segue exatamente a regra do enunciado: divisível por 4 e não divisível por 100, ou divisível por 400 (isso cobre corretamente casos como o ano 2000, bissexto, e o ano 1900, não bissexto). A partir daí, tratamos separadamente três casos de número máximo de dias: fevereiro (28 ou 29, dependendo do bissexto), os meses com 30 dias, e os demais meses, com 31.