Sua dose diária de programação

Conceito, 3 exemplos e 3 exercícios: uma pequena conquista de cada vez.

✦   Lição

Operadores

Operadores são os símbolos que fazem contas e combinam valores: +, -, * e / são os aritméticos, e % devolve o resto da divisão. Um detalhe pega muita gente no começo: quando os dois lados de / são inteiros, o resultado também é inteiro e a parte fracionária é descartada, então 7 / 2 vale 3 e 7 % 2 vale 1. Basta um dos lados ser double para a divisão manter as casas decimais.

C# Fundamentos Iniciante 3 exemplos + 3 exercícios
42 lições publicadas 10 linguagens novas lições todo dia útil
C# / Fundamentos

Operadores

por Tatum Winslow
Iniciante

Entenda o conceito

Operadores são os símbolos que fazem contas e combinam valores: +, -, * e / são os aritméticos, e % devolve o resto da divisão. Um detalhe pega muita gente no começo: quando os dois lados de / são inteiros, o resultado também é inteiro e a parte fracionária é descartada, então 7 / 2 vale 3 e 7 % 2 vale 1. Basta um dos lados ser double para a divisão manter as casas decimais.

Para alterar uma variável a partir do valor que ela já tem, existem os operadores de atribuição composta: pontos += 10 é o mesmo que pontos = pontos + 10, e o mesmo vale para -=, *=, /= e %=. Somar ou subtrair 1 é tão comum que ganhou atalhos próprios: ++ e --. Sozinhos numa linha, x++ e ++x fazem a mesma coisa; dentro de uma expressão, o pós-incremento (x++) entrega o valor antigo e o pré-incremento (++x) entrega o valor já somado.

Quando vários operadores aparecem juntos, vale a precedência: *, / e % são avaliados antes de + e -, as comparações vêm depois das contas, e && e || vêm por último. Parênteses mudam essa ordem e deixam a intenção clara para quem lê. Por fim, C# tem operadores para lidar com null: a ?? b devolve a se ele não for nulo e b caso contrário, e a ??= b só atribui b quando a ainda é null.

↳ Pontos-chave
  • Entre inteiros, / descarta a parte fracionária: 1100 / 250 dá 4, e 1100 % 250 dá o resto, 100.
  • Em contas com inteiros, multiplique antes de dividir: sobra * 100 / total dá a porcentagem, enquanto sobra / total * 100 costuma dar 0.
  • x += 5 equivale a x = x + 5; existem também -=, *=, /= e %=.
  • Dentro de uma expressão, x++ usa o valor antigo e depois soma; ++x soma primeiro e usa o valor novo.
  • valor ?? padrao troca um null por um valor padrão, e variavel ??= valor só atribui se a variável ainda for null.

Do conceito à prática

3 exemplos para ver a ideia funcionando.

01Receitas de brigadeiro com divisão inteira e resto

receita.cs
// Quantas receitas de brigadeiro cabem no chocolate da cozinha?
const int ChocolatePorReceita = 250; // gramas
const int DocesPorReceita = 30;
const int Bandejas = 4;

int chocolateDisponivel = 1100;

// Entre inteiros, / descarta a parte fracionária e % devolve o resto
int receitasCompletas = chocolateDisponivel / ChocolatePorReceita;
int sobraChocolate = chocolateDisponivel % ChocolatePorReceita;

Console.WriteLine($"Receitas completas: {receitasCompletas}");
Console.WriteLine($"Chocolate que sobra: {sobraChocolate} g");

// * tem precedência sobre +: a multiplicação acontece primeiro
int totalDoces = receitasCompletas * DocesPorReceita + 6; // 6 doces do pote de ontem
Console.WriteLine($"Total de doces: {totalDoces}");

// Dividindo os doces entre as bandejas
Console.WriteLine($"Doces por bandeja: {totalDoces / Bandejas}");
Console.WriteLine($"Doces fora das bandejas: {totalDoces % Bandejas}");

// % também responde "é divisível?": resto zero quer dizer divisão exata
bool divisaoExata = totalDoces % Bandejas == 0;
Console.WriteLine($"Todas as bandejas iguais e sem sobra? {divisaoExata}");

// Multiplicar antes de dividir evita perder tudo na divisão inteira
int percentualErrado = sobraChocolate / ChocolatePorReceita * 100;
int percentualCerto = sobraChocolate * 100 / ChocolatePorReceita;
Console.WriteLine($"Sobra em relação a uma receita: {percentualCerto}% (e não {percentualErrado}%)");

Com 1100 g de chocolate e 250 g por receita, / entre inteiros dá 4 receitas completas e % mostra os 100 g que sobram. O mesmo par de operadores divide os 126 doces entre 4 bandejas (31 em cada, 2 de fora), e totalDoces % Bandejas == 0 mostra o uso clássico do resto: testar se uma divisão é exata.

A última conta mostra por que a ordem importa na divisão inteira: 100 / 250 já vira 0 antes da multiplicação, então o resultado é 0%. Multiplicando primeiro, 10000 / 250 dá os 40% corretos. Repare também em receitasCompletas * DocesPorReceita + 6: a multiplicação acontece antes da soma, sem precisar de parênteses.

02Placar de fliperama com atribuição composta e ++/--

placar.cs
// Placar de uma partida de fliperama com atribuição composta e ++/--
int pontos = 0;
int vidas = 3;
int combo = 0;

pontos += 500;      // o mesmo que pontos = pontos + 500
combo++;            // pós-incremento numa linha sozinha: só soma 1
pontos += 300;
combo++;
Console.WriteLine($"Pontos: {pontos} | combo: {combo} | vidas: {vidas}");

// Com dois acertos seguidos, o combo dobra o placar
pontos *= 2;
Console.WriteLine($"Combo x2 aplicado: {pontos} pontos");

// O jogador perde uma vida e paga uma penalidade
vidas--;
pontos -= 150;
combo = 0;
Console.WriteLine($"Levou dano! Pontos: {pontos} | vidas: {vidas}");

// Dentro de uma expressão, pré e pós-incremento dão resultados diferentes
int fase = 1;
Console.WriteLine($"Pós-incremento mostra {fase++}, e depois fase vale {fase}");
Console.WriteLine($"Pré-incremento já mostra {++fase}");

// /= e %= também existem
int moedas = 47;
int vidasExtras = moedas / 20;
moedas %= 20;       // fica só o que não virou vida extra
vidas += vidasExtras;
Console.WriteLine($"Vidas extras: {vidasExtras} | moedas restantes: {moedas} | vidas: {vidas}");

Os operadores +=, -= e *= atualizam o placar a partir do valor atual, sem repetir o nome da variável dos dois lados. combo++ e vidas-- aparecem sozinhos numa linha, onde só importa o efeito de somar ou subtrair 1.

O trecho da fase mostra a diferença entre as duas formas dentro de uma expressão: fase++ imprime 1 e só depois a variável passa a 2; ++fase soma antes e já imprime 3. No fim, / calcula quantas vidas extras as 47 moedas compram e moedas %= 20 guarda na própria variável só as 7 que sobraram.

03Multa da biblioteca com precedência, ?? e ??=

emprestimo.cs
// Ficha de empréstimo da biblioteca: precedência, ?? e ??=
const int DiasPadrao = 14;
const int MultaPorDiaCentavos = 120;

string? apelidoLeitor = null;   // leitor não cadastrou apelido
string nomeLeitor = "Marina Costa";
int? diasRenovados = null;      // null: ainda não renovou

// ?? devolve o lado esquerdo se ele não for null; senão, o direito
string exibido = apelidoLeitor ?? nomeLeitor;
Console.WriteLine($"Leitor: {exibido}");

// ??= só atribui quando a variável ainda é null
diasRenovados ??= 0;
Console.WriteLine($"Dias renovados: {diasRenovados}");
diasRenovados ??= 7;            // não muda nada: já tem valor
Console.WriteLine($"Depois do segundo ??=: {diasRenovados}");

int diasComLivro = 20;
// Sem parênteses, * e / vêm antes de + e -; parênteses deixam a intenção clara
int prazo = DiasPadrao + diasRenovados.Value;
int diasAtraso = diasComLivro - prazo;
int multa = diasAtraso * MultaPorDiaCentavos + 50; // 50 centavos de taxa fixa
int multaSemParenteses = diasComLivro - prazo * MultaPorDiaCentavos;
int multaComParenteses = (diasComLivro - prazo) * MultaPorDiaCentavos;

Console.WriteLine($"Prazo: {prazo} dias | atraso: {diasAtraso} dias");
Console.WriteLine($"Multa com taxa: R$ {multa / 100},{multa % 100:D2}");
Console.WriteLine($"Sem parênteses: {multaSemParenteses} | com parênteses: {multaComParenteses}");

// Operadores de comparação e lógicos também são operadores: o resultado é bool
bool podeEmprestarOutro = diasAtraso <= 0 || multa < 500;
Console.WriteLine($"Pode emprestar outro livro? {podeEmprestarOutro}");

Como o leitor não tem apelido, apelidoLeitor ?? nomeLeitor usa o nome completo. Já diasRenovados ??= 0 troca o null por 0, e a segunda tentativa, ??= 7, não altera nada porque a variável já tem valor. Depois disso o programa pode usar diasRenovados.Value com segurança.

A comparação entre multaSemParenteses e multaComParenteses mostra a precedência na prática: sem parênteses, prazo * MultaPorDiaCentavos é calculado primeiro e o resultado vira -1660, um valor sem sentido; com parênteses, o atraso é calculado antes e a multa fica em 720 centavos. A última linha lembra que comparações e || também são operadores, avaliados depois das contas.


Agora é com você

Tente resolver no rascunho antes de abrir a resposta. Os códigos não são executados nesta página.

01Horas de estudo da semanaFácil

Some os minutos estudados de segunda a sexta (45, 30, 60, 50 e 25) e mostre o total em minutos, o mesmo tempo em horas e minutos e a média de minutos por dia.

↳ Requisitos
  • Acumular o total com o operador +=, uma linha por dia.
  • Calcular as horas com / e os minutos restantes com %, usando só inteiros.
  • Guardar o valor 60 numa constante com nome.
  • Produzir a saída idêntica à esperada.
código inicial
const int MinutosPorHora = 60;

int totalMinutos = 0;

// acumule os minutos de segunda (45), terça (30), quarta (60), quinta (50) e sexta (25)

// separe horas e minutos

// imprima total, horas e minutos e a média por dia
saída esperada
Total da semana: 210 minutos
Equivale a 3 h 30 min
Média por dia: 42 minutos
Ver dica

Pense em quantas vezes 60 cabe no total e no que sobra depois disso: cada uma dessas perguntas tem um operador próprio.

Ver resposta
// Tempo de estudo da semana convertido em horas e minutos
const int MinutosPorHora = 60;

int totalMinutos = 0;

// += acumula o tempo de cada dia
totalMinutos += 45; // segunda
totalMinutos += 30; // terça
totalMinutos += 60; // quarta
totalMinutos += 50; // quinta
totalMinutos += 25; // sexta

// / dá as horas inteiras e % dá os minutos que sobram
int horas = totalMinutos / MinutosPorHora;
int minutos = totalMinutos % MinutosPorHora;

Console.WriteLine($"Total da semana: {totalMinutos} minutos");
Console.WriteLine($"Equivale a {horas} h {minutos} min");
Console.WriteLine($"Média por dia: {totalMinutos / 5} minutos");

Passo a passo

  1. MinutosPorHora dá nome ao número 60, deixando claro o que as contas fazem.
  2. totalMinutos começa em 0, porque += precisa de um valor inicial para somar.
  3. Cada totalMinutos += ... acumula o tempo de um dia; ao final a variável vale 210.
  4. totalMinutos / MinutosPorHora é uma divisão entre inteiros, então dá 3, as horas completas, sem casas decimais.
  5. totalMinutos % MinutosPorHora devolve o resto dessa divisão, 30, que são os minutos que não fecham uma hora.
  6. A média usa a mesma divisão inteira: 210 / 5 dá exatamente 42.

Por que funciona

Converter minutos em horas e minutos é exatamente a pergunta que / e % respondem juntos: quantas vezes 60 cabe no total e quanto sobra. Como 3 * 60 + 30 volta a dar 210, nenhum minuto se perde. A atribuição composta += deixa o acúmulo legível, uma linha por dia.

Erros comuns

  • Esquecer de inicializar o acumulador: int totalMinutos; seguido de totalMinutos += 45; não compila, com o erro de uso de variável local não atribuída. Comece com int totalMinutos = 0;.
  • Escrever totalMinutos =+ 45;: isso compila, mas significa totalMinutos = +45, que substitui o valor em vez de somar, e o total fica 25. O operador é +=.
  • Usar double para as horas: double horas = totalMinutos / 60.0; dá 3.5 e não separa as horas dos minutos. Para esse formato, use / e % com inteiros.

Outra forma de resolver

Para durações, a biblioteca padrão tem TimeSpan: var tempo = TimeSpan.FromMinutes(totalMinutos); e então tempo.Hours e tempo.Minutes. É preferível quando o programa também soma datas ou passa de 24 horas (aí entra TotalHours); para aprender os operadores, / e % mostram a conta por trás.

Saída esperada

Total da semana: 210 minutos
Equivale a 3 h 30 min
Média por dia: 42 minutos

02Movimento de estoque de cadernosMédio

Uma loja abre o dia com 40 cadernos. Registre, nesta ordem, a venda de 12 unidades, a chegada de 2 caixas de 15, uma devolução e uma avaria, mostrando o estoque após cada movimento; depois calcule a porcentagem do estoque inicial que foi vendida e mostre o resultado da ordem errada de cálculo.

↳ Requisitos
  • Usar -= para as vendas, += para o lote e ++ e -- para a devolução e a avaria.
  • Calcular a porcentagem só com inteiros, multiplicando por 100 antes de dividir.
  • Mostrar também o resultado de dividir antes de multiplicar, para comparar.
  • Produzir a saída idêntica à esperada.
código inicial
const int EstoqueInicial = 40;
const int UnidadesPorCaixa = 15;

int estoque = EstoqueInicial;
Console.WriteLine($"Abertura: {estoque} unidades");

int vendidos = 12;

// registre vendas, lote, devolução e avaria, imprimindo o estoque após cada uma

// calcule a porcentagem vendida nas duas ordens
saída esperada
Abertura: 40 unidades
Após as vendas: 28
Após o lote: 58
Após a devolução: 59
Após a avaria: 58
Vendido: 30% do estoque inicial
Ordem errada daria: 0%
Ver dica

Na divisão entre inteiros, qualquer conta que dê menos de 1 vira 0; organize a expressão para que a divisão seja o último passo.

Ver resposta
// Movimento de estoque de cadernos ao longo de um dia na loja
const int EstoqueInicial = 40;
const int UnidadesPorCaixa = 15;

int estoque = EstoqueInicial;
Console.WriteLine($"Abertura: {estoque} unidades");

int vendidos = 12;
estoque -= vendidos;                 // vendas do dia
Console.WriteLine($"Após as vendas: {estoque}");

estoque += 2 * UnidadesPorCaixa;     // chegaram 2 caixas
Console.WriteLine($"Após o lote: {estoque}");

estoque++;                           // um cliente devolveu um caderno
Console.WriteLine($"Após a devolução: {estoque}");

estoque--;                           // um caderno chegou amassado
Console.WriteLine($"Após a avaria: {estoque}");

// Divisão inteira: 12 / 40 dá 0, então a multiplicação vem antes
Console.WriteLine($"Vendido: {vendidos * 100 / EstoqueInicial}% do estoque inicial");
Console.WriteLine($"Ordem errada daria: {vendidos / EstoqueInicial * 100}%");

Passo a passo

  1. As constantes EstoqueInicial e UnidadesPorCaixa guardam os dados fixos; o estoque inicial é reaproveitado no cálculo da porcentagem.
  2. estoque -= vendidos desconta as 12 vendas e deixa 28 unidades.
  3. estoque += 2 * UnidadesPorCaixa soma as duas caixas; a multiplicação é avaliada antes do +=, então entram 30 unidades.
  4. estoque++ e estoque-- ajustam uma unidade cada; sozinhos na linha, só o efeito de somar ou subtrair importa.
  5. vendidos * 100 / EstoqueInicial calcula 1200 / 40, que é exatamente 30.
  6. A última linha mostra a ordem errada: 12 / 40 já vira 0 na divisão inteira, e 0 * 100 continua 0.

Por que funciona

Cada movimento do estoque é uma alteração do valor atual, e é para isso que existem -=, +=, ++ e --: o código fica curto e mostra a intenção sem repetir estoque = estoque .... Na porcentagem, a precedência entre * e / é a mesma, então a avaliação segue da esquerda para a direita; por isso escrever a multiplicação primeiro preserva o resultado na divisão inteira.

Erros comuns

  • Calcular a porcentagem com vendidos / EstoqueInicial * 100: compila, mas dá 0%, porque a divisão inteira acontece antes. Multiplique por 100 primeiro.
  • Escrever estoque + 2 * UnidadesPorCaixa; sozinho numa linha: dá erro de compilação, porque uma expressão que só calcula não é uma instrução válida em C#. Use estoque += ....
  • Mudar o valor de EstoqueInicial no meio do programa: EstoqueInicial -= vendidos; não compila, porque constantes não podem ser alteradas. Guarde o valor variável em estoque.

Outra forma de resolver

A porcentagem também pode ser calculada com double: double percentual = (double)vendidos / EstoqueInicial * 100;. É a escolha certa quando a conta não fecha em número inteiro, como 7 de 40 (17.5), mas a forma de exibir o número passa a depender da cultura (vírgula ou ponto decimal).

Saída esperada

Abertura: 40 unidades
Após as vendas: 28
Após o lote: 58
Após a devolução: 59
Após a avaria: 58
Vendido: 30% do estoque inicial
Ordem errada daria: 0%

03Pontuação das fases de um jogo de plataformaDesafio

Escreva uma função local CalcularPontos(string fase, int moedas, int inimigos, int danos, int? bonusTempo) que calcule e imprima os pontos de uma fase: 10 por moeda, 50 por inimigo, menos 5 por dano, mais o bônus de tempo (ausente vale zero) e mais 10% quando não houver dano ou o número de inimigos for par. Some três fases, mostre as vidas extras (uma a cada 1000 pontos), quanto falta para a próxima e grave o total como primeiro recorde.

↳ Requisitos
  • Calcular a base da fase numa única expressão, sem parênteses, aproveitando a precedência de * sobre + e -.
  • Tratar o bônus ausente com ?? e gravar o recorde, que começa como int? recorde = null, com ??=.
  • Testar a paridade dos inimigos com % e calcular os 10% com divisão inteira.
  • Usar as fases Floresta (34, 6, 3, 120), Caverna (21, 9, 7, sem bônus) e Castelo (40, 12, 10, 200) e produzir a saída idêntica à esperada.
código inicial
const int PontosPorMoeda = 10;
const int PontosPorInimigo = 50;
const int PenalidadePorDano = 5;
const int PontosPorVidaExtra = 1000;

int? recorde = null;
int total = 0;

// some as três fases chamando CalcularPontos

// grave o recorde com ??= e mostre total, vidas extras, quanto falta e o recorde

int CalcularPontos(string fase, int moedas, int inimigos, int danos, int? bonusTempo)
{
    // calcule, imprima e devolva os pontos da fase
    return 0;
}
saída esperada
Floresta: 819 pontos
Caverna: 625 pontos
Castelo: 1265 pontos
Total: 2709 pontos
Vidas extras: 2
Faltam 291 pontos para a próxima vida
Recorde salvo: 2709
Ver dica

Faça a conta da Floresta no papel antes de programar (base, bônus de tempo e só então os 10%); se o seu programa der 819, a ordem dos operadores está certa.

Ver resposta
// Pontuação das fases de um jogo de plataforma
const int PontosPorMoeda = 10;
const int PontosPorInimigo = 50;
const int PenalidadePorDano = 5;
const int PontosPorVidaExtra = 1000;

int? recorde = null;   // nenhum recorde salvo ainda
int total = 0;

total += CalcularPontos("Floresta", 34, 6, 3, 120);
total += CalcularPontos("Caverna", 21, 9, 7, null);
total += CalcularPontos("Castelo", 40, 12, 10, 200);

// ??= grava o primeiro recorde só se ainda não existir nenhum
recorde ??= total;

Console.WriteLine($"Total: {total} pontos");
Console.WriteLine($"Vidas extras: {total / PontosPorVidaExtra}");
Console.WriteLine($"Faltam {PontosPorVidaExtra - total % PontosPorVidaExtra} pontos para a próxima vida");
Console.WriteLine($"Recorde salvo: {recorde}");

int CalcularPontos(string fase, int moedas, int inimigos, int danos, int? bonusTempo)
{
    // * vem antes de + e -, então cada produto é calculado primeiro
    int pontos = moedas * PontosPorMoeda + inimigos * PontosPorInimigo - danos * PenalidadePorDano;

    // Fase sem bônus de tempo (null) vale como zero
    pontos += bonusTempo ?? 0;

    // Bônus de 10% para fase sem dano nenhum ou com número par de inimigos derrotados
    if (danos == 0 || inimigos % 2 == 0)
    {
        pontos += pontos / 10;
    }

    Console.WriteLine($"{fase}: {pontos} pontos");
    return pontos;
}

Passo a passo

  1. As constantes guardam as regras de pontuação, e recorde é um int? que começa em null para indicar que ainda não há recorde.
  2. Cada chamada a CalcularPontos devolve os pontos da fase, e total += os acumula; a fase Caverna recebe null como bônus de tempo.
  3. Na função, moedas * PontosPorMoeda + inimigos * PontosPorInimigo - danos * PenalidadePorDano não precisa de parênteses: os três produtos são calculados antes da soma e da subtração.
  4. pontos += bonusTempo ?? 0 troca o null por zero; sem o ??, somar um int? a um int produziria outro int? e a atribuição não compilaria.
  5. A condição danos == 0 || inimigos % 2 == 0 usa o resto para testar se o número de inimigos é par; % e == são avaliados antes de ||.
  6. pontos += pontos / 10 aplica 10% com divisão inteira: na Floresta, 745 / 10 dá 74, e a fase fecha com 819.
  7. Fora da função, recorde ??= total só grava o total porque o recorde ainda era null.
  8. total / PontosPorVidaExtra dá as vidas extras e PontosPorVidaExtra - total % PontosPorVidaExtra calcula quanto falta para a próxima: % tem precedência sobre -.

Por que funciona

Toda a pontuação é feita com expressões que dependem da precedência: produtos antes de somas, resto antes da comparação, comparações antes do ||. Os valores que podem não existir (o bônus de tempo e o recorde) são int?, e os operadores ?? e ??= resolvem o null num único ponto, sem if espalhados. A divisão inteira aparece duas vezes de propósito: no bônus de 10%, arredondando para baixo, e na contagem de vidas extras, junto com %.

Erros comuns

  • Somar o bônus direto: pontos += bonusTempo; não compila, porque não existe conversão implícita de int? para int. Use bonusTempo ?? 0.
  • Esquecer a precedência no que falta para a vida: (PontosPorVidaExtra - total) % PontosPorVidaExtra dá -709, porque os parênteses obrigam a subtração a acontecer antes do resto. Sem eles, % vem primeiro e o resultado é 291.
  • Calcular os 10% com pontos * (10 / 100): 10 / 100 é 0 em divisão inteira, então o bônus some. Use pontos / 10 ou pontos * 10 / 100.
  • Testar par com inimigos / 2 == 0: a divisão só é zero para 0 ou 1 inimigo, e a fase Castelo perderia o bônus. O teste de paridade usa o resto, %.

Outra forma de resolver

Os ajustes da fase também podem ser escritos como uma única expressão com o ternário: int pontos = baseFase + (bonusTempo ?? 0); return danos == 0 || inimigos % 2 == 0 ? pontos + pontos / 10 : pontos;. Fica mais compacto e combina com funções curtas; a versão com += e if é mais fácil de depurar linha a linha e de imprimir antes de devolver o valor.

Saída esperada

Floresta: 819 pontos
Caverna: 625 pontos
Castelo: 1265 pontos
Total: 2709 pontos
Vidas extras: 2
Faltam 291 pontos para a próxima vida
Recorde salvo: 2709
← Anterior na trilha de C#Decisões com condicionais

Últimas lições

Ver o arquivo completo →