Como um simples exercício de função fez eu quebrar a cabeça o dia inteiro

Olá, estou estudando o CS50 de Harvard 2023 e decidi fazer alguns exercícios da Semana 1 (C). O exercício se chama "Half" e você pode encontrá-lo aqui.

Algumas informações e dicas que o exercício dá

Esse exercício foi realmente desafiador e tem algumas 'metas de aprendizagem', que seria trabalhar com diferentes tipos de dados, praticar conversão de tipos, usar operações matemáticas e criar uma função com parâmetros de entrada e valor de retorno.

A descrição diz o seguinte: "Suponha que você esteja comendo fora em um restaurante com um amigo e queira dividir a conta igualmente. Você pode querer antecipar o valor devido antes que a fatura chegue com o imposto adicionado. Neste problema, você completará uma função para calcular o valor que cada um de vocês deve com base no valor da conta, no imposto e na gorjeta."

As dicas são:

  • Observe que o imposto e a gorjeta são inseridos como porcentagens. Você pode querer alterá-los para valores decimais antes de calcular o total.
  • Como a dica é inserida como int, lembre-se de que se você dividir um int por um int você receberá um int!
  • A ordem das operações aqui é exatamente a mesma da álgebra, onde a multiplicação e a divisão são realizadas antes da adição e da subtração.

Essas dicas são boas, especificamente a segunda, pois como vamos utilizar porcentagens, não podemos trabalhar com números inteiros na hora de dividir (10 / 100 = 0).


Se você quiser tentar resolver esse exercício, acesse: CS50 no Google Chrome e faça login com o GitHub.

  1. Clique dentro da janela do terminal e execute cd.
  2. No $prompt, digite mkdir half
  3. Agora execute cd half
  4. Em seguida, copie e cole wget https://cdn.cs50.net/2022/fall/labs/1/half.c em seu terminal para baixar o código de distribuição do laboratório.
  5. Você deve completar a função, half que calcula exatamente metade da conta após a adição de impostos e gorjetas, e retorna esse valor.

O exercício

Abrindo o arquivo gerado, este é código que teremos que modificar:

// Calculate your half of a restaurant bill
// Data types, operations, type casting, return value

#include <cs50.h>
#include <stdio.h>

float half(float bill, float tax, int tip);

int main(void)
{
    float bill_amount = get_float("Bill before tax and tip: ");
    float tax_percent = get_float("Sale Tax Percent: ");
    int tip_percent = get_int("Tip percent: ");

    printf("You will owe $%.2f each!\n", half(bill_amount, tax_percent, tip_percent));
}

// TODO: Complete the function
float half(float bill, float tax, int tip)
{
    return 0.0;
}

Se tentar testar o código: digite make half e depois ./half. Digite os valores para as perguntas e você verá que o resultado será You will owe 0.00 each!

Vamos precisar resolver alguns problemas e neste exercício deve usar função criada. Por termos que modificar a função, por enquanto, esqueceremos a main.

Primeiro, vamos entender algumas coisas nesse código, afinal está em inglês. Bill significa fatura (e eu fiquei um tempo tentando entender quem era Bill), tax é o imposto a ser pago e tip é a gorjeta. Então nós temos uma fatura que recebe um valor x, que está sem imposto e sem gorjeta. Nosso trabalho é adicionar um imposto sobre essa nossa fatura e depois a gorjeta, somente depois dividir entre "eu" e o amigo.

No código, vamos transformar as porcentagens em decimal e, lembrem-se da dica 2, que se dividir um inteiro por outro inteiro, teremos um inteiro, logo, usaremos o float, tanto para dividir o imposto e a gorjeta. Ficará assim:

// TODO: Complete the function
float half(float bill, float tax, int tip)
{
    float tax_decimal = tax_percent / 100.0;
    float tip_decimal = tip_percent / 100.0;
}

O próximo passo será calcular o imposto e taxa sobre a nossa fatura e após isso dividir:

// TODO: Complete the function
float half(float bill_amount, float tax_percent, int tip_percent)
{
    float tax_decimal = tax_percent / 100.0;
    float tip_decimal = tip_percent / 100.0;

    float bill_after_tax = bill_amount * tax_decimal + bill_amount;
    float bill_after_tax_tip = bill_after_tax * tip_decimal + bill_after_tax;
    float invoice_split = bill_after_tax_tip / 2;

    return invoice_split;
}

Até então, fizemos tudo o que o exercício propôs: convertemos inteiro para decimal (float), adicionamos o imposto na fatura primeiro que a gorjeta (o exercício pedia para ser nessa ordem) e por fim, divimos a conta igualmente. Se tentarmos compilar e testar vai dar erro:

Exercício

Foi nesse ponto que eu estagnei, não conseguia achar uma solução por conta própria, pesquisei e vi que tem vídeos explicando e respostas no Stack OverFlow e Reddit, por exemplo. Inclusive, alguns códigos que estavam diferentes do meu, mas com a base semelhante, foi bom ver que havia mais de uma lógica além da minha. Porém, ainda não tinha entendido o que estava errado, até perceber que a linha errada.

float half(float bill, float tax, int tip)

O correto, nesse caso, seria:

float half(float bill_amount, float tax_percent, int tip_percent)

Agora, se compilarmos novamente, funcionará. Inclusive, se testar os exemplos que tem na página do exercício, todos funcionarão. Nosso código no final ficará assim:

// Calculate your half of a restaurant bill
// Data types, operations, type casting, return value

#include <cs50.h>
#include <stdio.h>

float half(float bill, float tax, int tip);

int main(void)
{
    float bill_amount = get_float("Bill before tax and tip: ");
    float tax_percent = get_float("Sale Tax Percent: ");
    int tip_percent = get_int("Tip percent: ");

    printf("You will owe $%.2f each!\n", half(bill_amount, tax_percent, tip_percent));
}

// TODO: Complete the function
float half(float bill_amount, float tax_percent, int tip_percent)
{
    float tax_decimal = tax_percent / 100.0;
    float tip_decimal = tip_percent / 100.0;

    float bill_after_tax = bill_amount * tax_decimal + bill_amount;
    float bill_after_tax_tip = bill_after_tax * tip_decimal + bill_after_tax;
    float invoice_split = bill_after_tax_tip / 2;

    return invoice_split;
}


Dúvida

Agora uma dúvida para quem conhece a Linguagem C: observem a linha 7, ela deveria ser igual a linha 19 ou não? Afinal os parâmetros são diferentes, no meu ponto de vista: bill é diferente de bill_amount, tax é diferente de tax_percent e tip é diferente de tip_percent. Se der, me tirem essa dúvida também.

Obrigado e até a próxima!

bill é diferente de bill_amount, tax é diferente de tax_percent e tip é diferente de tip_percent.

Os nomes dos parâmetros de uma função só existem dentro da função. Fora dela, o que importa é que ela receba uma expressão que resulte em um valor que ela espera receber.

Só para dar um exemplo, eu poderia chamar a função assim:

half(100, 6.25, 18);

Ou seja, em vez de ter variáveis, eu posso muito bem chamar a função passando os valores numéricos diretamente. Claro que para o exercício não serve, mas tecnicamente falando, nada impede que eu faça isso. E ao chamar desta forma, o valor 100 será atribuído ao parâmetro bill_amount, o valor 6.25 será atribuído ao parâmetro tax_percent, e 18, a tip_percent.

Os nomes só existem dentro da função. Fora dela, quem for chamá-la, pode passar qualquer coisa, desde que o resultado seja compatível com os tipos que ela espera.

Tanto que eu também poderia fazer algo assim:

half(valor_batata + valor_cervejas + valor_hamburguer, calcular_taxa_do_dia(), ler_valor_gorjeta());

Assumindo que todas as variáveis valor_batata, valor_cervejas e valor_hamburguer e as funções calcular_taxa_do_dia e ler_valor_gorjeta existam e seus tipos sejam compatíveis com o que a função half espera.

Resumindo, uma coisa são os nomes dos parâmetros, que só existem dentro da função. Outra coisa são os valores dos argumentos que passamos para a função ao chamá-la. Estes podem ser quaisquer expressões (nem precisam ser variáveis), desde que os tipos sejam compatíveis com os respectivos parâmetros. Ou seja, as variáveis que vc leu no main não precisam necessariamente ter os mesmos nomes dos parâmetros definidos na função.


Por fim, a função poderia ser simplificada, pois não vejo a necessidade de tantas variáveis intermediárias:

float half(float bill, float tax, int tip) {
    float tax_multiplier = 1 + (tax / 100);
    float tip_multiplier = 1.0 + (tip / 100.0);
    return (bill * tax_multiplier * tip_multiplier) / 2;
}

No caso do tax_multiplier, como tax é um float, toda a expressão resultará em um float. Já tip é um int, então eu preciso dividir por 100.0 (o .0 no final força que toda a expressão seja float, evitando o arredondamento que ocorreria se eu dividisse por 100, já que este é um literal inteiro).


Ou ainda, caso seja permitido criar funções auxiliares:

// converte um valor de porcentagem para o respectivo multiplicador (ex: 6.5 vira 1.065)
float convert_perc(float perc) {
    return 1.0 + (perc / 100.0);
}

float half(float bill, float tax, int tip) {
    return (bill * convert_perc(tax) * convert_perc(tip)) / 2;
}
Boa noite, obrigado por responder! > *Outra coisa são os valores dos argumentos que passamos para a função ao chamá-la* Então, por exemplo, `fatura` (ou bill) pode receber mais de um argumento (valor_batata + valor_cerveja + valor hamburguer)? Neste exercício em específico, seria necessário criar uma função que calcule esses três argumentos ou apenas substituindo `valor_batata + valor_cerveja + valor hamburguer` por algum valor (10.0 + 12.0 + 25.0, por exemplo) no half funcionaria? > *Por fim, a função poderia ser simplificada* Isso é mesmo, percebi quando estava dando erro na compilação e precisei pesquisar para ver o que estava errado. Muitos dos códigos estavam mais simplificados que o meu, mas com o tempo irei melhorando. O importante é não desistir e sempre continuar estudando. Obrigado e bom domingo!
> *pode receber mais de um argumento (valor_batata + valor_cerveja + valor hamburguer)* Não é "mais de um argumento", é um só. Mas vamos por partes. A função foi definida assim: ~~~c float half(float bill, float tax, int tip) { // etc... } ~~~ Ou seja, ela possui três parâmetros: o primeiro é um `float` e o nome é `bill`, o segundo também é `float` e o nome é `tax`, e o terceiro é um `int` chamado `tip`. Agora, de onde vêm esses valores? Isso é algo definido por quem for chamar a função. Por exemplo, se eu chamo a função assim: ~~~c half(100, 6.25, 18); ~~~ Repare que os argumentos são separados por vírgula. Ou seja, os três argumentos são os números `100`, `6.25` e `18`. E como o primeiro argumento é `100`, este é o valor que será colocado em `bill`. Agora, se eu chamo a função assim: ~~~c half(valor_batata + valor_cervejas + valor_hamburguer, 2.5, 10); ~~~ Ainda são três argumentos separados por vírgula. O segundo é `2.5` e o terceiro é `10`. E o primeiro argumento? Ele é **toda a expressão** `valor_batata + valor_cervejas + valor_hamburguer`. Não são vários argumentos, é um só. E o **resultado desta expressão** é o valor que será colocado em `bill`. Isso quer dizer que primeiro ele calcula a expressão (no caso, soma os 3 valores), e só depois o resultado é passado para a função. Seria o equivalente a fazer isso: ~~~c float total = valor_batata + valor_cervejas + valor_hamburguer; half(total, 2.5, 10); ~~~ Mas neste caso eu optei por não usar a variável `total`, e em disso passar o resultado da soma diretamente para a função. Claro, tudo isso assumindo que as variáveis existem e possuem valores adequados. Mas o ponto aqui é que continua sendo um único argumento. Só que não precisa ser necessariamente uma variável: ele pode ser qualquer expressão válida, desde que o resultado seja um valor de um tipo compatível com o que a função espera.
> *Isso quer dizer que primeiro ele calcula a expressão (no caso, soma os 3 valores), e só depois o resultado é passado para a função.* Sua explicação fez sentido para mim, obrigado. Agora é só continuar praticando...

Eae, tudo bem ? O erro da foto estava sendo causado por causa dos diferentes nomes colocados na declaração do parâmetro e na utilização de uma variável que "não existe". Provavelmente era algo assim:

float half(float bill, float tax, int tip)
{
    float tax_decimal = tax_percent / 100.0; // deveria ser "tax" e não "tax_percent"
    ...

E em relação a sua dúvida: Não há problema em fazer a declaração da função com parâmetros de nomes diferentes desde que eles tenham o mesmo tipo e que sejam utilizados os nomes certos no escopo da função. Então, a única coisa que importa para o compilador é que o formato seja (float, float, int), pouco importando se os parâmetros tenham diferentes nomes. Inclusive, se você mantivesse os nomes "bill, tax e tip" na linha 19 o código ainda funcionaria desde que, é claro, você também mudasse a chamada destes parâmetros dentro da função.

Ficaria assim:

float half(float bill, float tax, int tip) {
    float tax_decimal = tax / 100.0;
    float tip_decimal = tip / 100.0;

    float bill_after_tax = bill * tax_decimal + bill;
    float bill_after_tax_tip = bill_after_tax * tip_decimal + bill_after_tax;
    float invoice_split = bill_after_tax_tip / 2;

    return invoice_split;
}

Qualquer dúvida é só chamar.

Boa noite, obrigado por responder. > O erro da foto estava sendo causado por causa dos diferentes nomes colocados na declaração do parâmetro e na utilização de uma variável que "não existe". As variáveis declaradas "não existem" porquê são chamadas antes de `main`? Além disso, como que `bill_after_tax` (e tax/tip_decimal) sabe que `bill` terá os valores de `bill_amount`? Seria por causa do `float half(float bill, float tax, int tip)`?
> As variáveis declaradas "não existem" porquê são chamadas antes de main? Não, as variáveis não existem porque elas não foram declaradas neste escopo. ```c float half(float bill, float tax, int tip) { float tax_decimal = tax_percent / 100.0; // deveria ser "tax" e não "tax_percent" ... ``` No código acima, você estaria tentando tirar um valor de uma variável chamada "tax_percent", mas no escopo ela não existe. O que existe, no entanto, é a variável chamada "tax".
> Além disso, como que bill_after_tax (e tax/tip_decimal) sabe que bill terá os valores de bill_amount? Seria por causa do float half(float bill, float tax, int tip)? Como você está criando a função e nomeando os parâmetros nesta ordem (float bill, float tax, int tip), sempre que você chamar a função, os parâmetros serão gravados nesta ordem. Por exemplo: ```c float aaa = 50.0; float bbb = 10.0; int ccc = 15; half(aaa, bbb, ccc); ``` Como mostra o exemplo acima, os valores recebidos pela função independem dos nomes dados as variáveis utilizada nas chamadas.
Espero ter ajudado
> *Não, as variáveis não existem porque elas não foram declaradas neste escopo.* Agora eu entendi perfeitamente > Espero ter ajudado Ajudou e muito, muito obrigado.

e aí !

a linha antes da main() é chamada de "assinatura da função" e não precisa nem ter nomes nos argumentos, ou seja, poderia ser escrita assim:

float half(float, float, int);

mas é uma boa prática colocar o nome do parâmetro para servir de documentação pra quem usar. sei que, provavelmente, tu ainda estás no início do aprendizado, mas em sistemas em C, as assinaturas de funções geralmente são colocadas em um arquivo separado das implementações (que é a função depois da main). assinaturas vão em arquivos .h e implementações em arquivos .c.

espero ter