Reforço de Algoritmos e Programação - Instituto de Informática (UFRGS) - Prof. Dennis Giovani Balreira



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.
  • Mantenha uma indentação consistente (4 espaços por nível) em todo o programa.
  • Use else if em vez de vários if independentes quando as condições forem mutuamente exclusivas.
  • Não esqueça o break em cada case do switch, a menos que o fall-through seja intencional (e "óbvio").
  • Não use switch para testar faixas de valores ou combinações de condições — para isso, use if/else.

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", &nota);

    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

  • Expressões relacionais (> < >= <= == !=) comparam valores e resultam em verdadeiro (diferente de 0) ou falso (0).
  • Expressões lógicas (&& || !) combinam ou invertem expressões relacionais; &&/|| avaliam em curto-circuito.
  • if: executa um bloco se a condição for verdadeira. if/else: escolhe entre dois blocos.
  • else if: encadeia várias condições mutuamente exclusivas; a ordem dos testes importa.
  • Indentação não é exigida pelo compilador, mas é essencial para a legibilidade — use sempre 4 espaços por nível e chaves em todo bloco.
  • switch: compara uma única variável com valores constantes; não esqueça o break em cada case, sob risco de fall-through.
  • Use if/else para comparações e combinações de condições; use switch para várias alternativas do valor exato de uma única variável.

Exercícios

Nível fácil

  1. Escreva um programa que leia um número inteiro e informe se ele é par ou ímpar.
  2. Escreva um programa que leia dois números inteiros e informe qual é o maior, ou se são iguais.
  3. Escreva um programa que leia um caractere representando uma opção de menu ('A', 'B' ou 'C') e imprima, usando switch, uma mensagem diferente para cada opção, e "Opcao invalida" para qualquer outro caractere.

Nível médio

  1. 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).
  2. 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).
  3. 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 com default.

Nível difícil

  1. 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.
  2. 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.