Aula 1

C

  • Hoje vamos aprender uma nova linguagem, C: uma linguagem de programação que tem todos os recursos do Scratch e muito mais, mas talvez um pouco menos amigável pois é totalmente em texto:

      #include <stdio.h>
    
      int main(void)
      {
          printf("hello, world\n");
      }
    
    • Apesar de as palavras serem novas, as ideias são as mesmas dos blocos "Quando a bandeira verde é clicada" e "diga (hello, world)" do Scratch:
      bloco com rótulo 'Quando a bandeira verde é clicada', bloco com rótulo 'diga (hello, world)'
  • Apesar de parecer complicado, não se esqueça que 2/3 dos alunos do CS50 nunca fizeram ciência da computação antes, então não se assuste! E embora no início, para pegar emprestada uma frase do MIT, tentar absorver todos esses novos conceitos possa parecer como beber de uma mangueira de incêndio, tenha certeza de que até o final do semestre estaremos capacitados e experientes na aprendizagem e aplicação desses conceitos.

  • Podemos comparar vários dos construtos em C com blocos que já vimos e usamos no Scratch. A sintaxe é muito menos importante que os princípios, que já nos foram apresentados.

olá, mundo

  • O bloco “quando a bandeira verde for clicada” no Scratch inicia o programa principal; clicar na bandeira verde faz com que o conjunto direito de blocos abaixo comece. Em C, a primeira linha para o mesmo é int main(void), sobre a qual aprenderemos mais nas próximas semanas, seguida por uma chave aberta {, e uma chave fechada }, envolvendo tudo o que deve estar em nosso programa.

      int main(void)
      {
    
      }
    
  • O bloco “dizer (olá, mundo)” é uma função e mapeia para printf("olá, mundo");. Em C, a função para imprimir algo na tela é printf, onde f significa "format", o que significa que podemos formatar a string impressa de diferentes maneiras. Então, usamos parênteses para passar o que queremos imprimir. Temos que usar aspas duplas para circundar nosso texto para que seja entendido como texto, e finalmente, adicionamos um ponto-e-vírgula ; para finalizar essa linha de código.

  • Para que nosso programa funcione, também precisamos de outra linha no topo, uma linha de cabeçalho #include <stdio.h> que define a função printf que queremos usar. Em algum lugar há um arquivo em nosso computador, stdio.h, que inclui o código que nos permite acessar a função printf, e a linha #include diz ao computador para incluir esse arquivo com nosso programa.
  • Para escrever nosso primeiro programa em Scratch, abrimos o site do Scratch. Da mesma forma, usaremos o CS50 Sandbox para começar a escrever e executar o código da mesma maneira. O CS50 Sandbox é um ambiente virtual baseado em nuvem com as bibliotecas e ferramentas já instaladas para escrever programas em várias linguagens. No topo, há um editor de código simples, onde podemos digitar texto. Abaixo, temos uma janela de terminal, na qual podemos digitar comandos:
    dois painéis, superior rotulado hello.c, inferior rotulado Terminal
  • Digitaremos nosso código do início na parte superior, depois de usar o sinal + para criar um novo arquivo chamado hello.c:
    olá, mundo no editor
  • Finalizamos o arquivo do nosso programa com .c por convenção, para indicar que ele foi concebido como um programa C. Observe que nosso código é colorido, para que certas coisas fiquem mais visíveis.

Compiladores

  • Uma vez que salvamos o código que escrevemos, que é chamado de código-fonte, precisamos convertê-lo em código de máquina, instruções binárias que o computador entende diretamente.
  • Usamos um programa chamado compilador para compilar nosso código-fonte em código de máquina.
  • Para fazer isso, usamos o painel Terminal, que tem um prompt de comando. O $ à esquerda é um prompt, após o qual podemos digitar comandos.
  • Digitamos clang hello.c (onde clang significa "linguagens C", um compilador escrito por um grupo de pessoas). Mas antes de pressionarmos Enter, clicamos no ícone da pasta no canto superior esquerdo do CS50 Sandbox. Vemos nosso arquivo, hello.c. Então, pressionamos Enter na janela do terminal e vemos que agora temos outro arquivo chamado a.out (abreviação de "saída de montagem"). Dentro desse arquivo está o código do nosso programa, em binário. Agora, podemos digitar ./a.out no prompt do terminal para executar o programa a.out em nossa pasta atual. Acabamos de escrever, compilar e executar nosso primeiro programa!

String

  • Mas depois de executar nosso programa, vemos hello, world$, com o novo prompt na mesma linha que nossa saída. Acontece que precisamos especificar precisamente que precisamos de uma nova linha após o nosso programa, por isso podemos atualizar nosso código para incluir um caractere de nova linha especial, \n:

      #include <stdio.h>
    
      int main(void)
      {
          printf("hello, world\n");
      }
    
    • Agora precisamos nos lembrar de recompilar nosso programa com clang hello.c antes de executar esta nova versão.
  • A linha 2 de nosso programa está intencionalmente em branco, pois queremos iniciar uma nova seção de código, assim como iniciar novos parágrafos em ensaios. Não é estritamente necessário para que nosso programa execute corretamente, mas ajuda os humanos a ler programas mais longos com mais facilidade.

  • Podemos alterar o nome de nosso programa de a.out para outra coisa também. Podemos passar argumentos de linha de comando ou opções adicionais para programas no terminal, dependendo do que o programa foi escrito para entender. Por exemplo, podemos digitar clang -o hello hello.c, e -o hello está dizendo ao programa clang para salvar a saída compilada como apenas hello. Então, possiamo executar apenas ./hello.
  • Em nosso prompt de comando, podemos executar outros comandos, como ls (lista), que mostra os arquivos em nossa pasta atual:

      $ ls
      a.out* hello* hello.c
    
    • O asterisco, *, indica que esses arquivos são executáveis ou que podem ser executados por nosso computador.
  • Podemos usar o comando rm (remover) para excluir um arquivo:

      $ rm a.out
      rm: remove regular file 'a.out'?
    
    • Podemos digitar y ou yes para confirmar e usar ls novamente para ver que ele realmente se foi para sempre.
  • Agora, vamos tentar obter a entrada do usuário, como fizemos no Scratch quando queríamos dizer "ola, David":
    screenshot dos blocos "ask what's your name? and wait", "say join hello, answer"

      string answer = get_string("What's your name?\n");
      printf("hello, %s\n", answer);
    
    • Primeiro, precisamos de uma string, ou parte do texto (especificamente, zero ou mais caracteres em uma sequência entre aspas duplas, como "", "ba", ou “bananas”), que podemos pedir ao usuário, com a função get_string. Passamos o prompt, ou o que queremos perguntar ao usuário, para a função com "What is your name?\n" entre parênteses. À esquerda, queremos criar uma variável, answer, cujo valor será o que o usuário insere. (O sinal de igual = está definindo o valor da direita para a esquerda.) Finalmente, o tipo de variável que queremos é string, então especificamos isso à esquerda de answer.
    • Em seguida, dentro da função printf, queremos o valor de answer no que imprimimos de volta. Usamos um espaço reservado para nossa variável de string, %s, dentro da frase que queremos imprimir, como "hello, %s\n", e então damos printf outro argumento, ou opção, para dizer que queremos que a variável answer seja substituída.
  • Se cometermos um erro, como escrever printf("hello, world"\n); com o \n fora das aspas duplas de nossa string, veremos erros de nosso compilador:

      $ clang -o hello hello.c
      hello.c:5:26: error: expected ')'
          printf("hello, world"\n);
                               ^
      hello.c:5:11: note: to match this '('
          printf("hello, world"\n);
                ^
      1 error generated.
    
    • A primeira linha do erro nos diz para olhar para hello.c, linha 5, coluna 26, onde o compilador esperava um parêntese de fechamento, em vez de uma barra invertida.
  • Para simplificar as coisas (pelo menos no começo), incluiremos uma biblioteca, ou conjunto de códigos, do CS50. A biblioteca nos fornece o tipo de variável string, a função get_string e muito mais. Só temos que escrever uma linha no topo para incluir o arquivo cs50.h:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          string name = get_string("What's your name?\n");
          printf("hello, name\n");
      }
    
  • Então vamos criar um novo arquivo, string.c, com este código:

      #include <stdio.h>
    
      int main(void)
      {
          string name = get_string("What's your name?\n");
          printf("hello, %s\n", name);
      }
    
  • Agora, se tentarmos compilar esse código, obteremos muitas linhas de erro. Às vezes, um erro significa que o compilador começa a interpretar o código correto incorretamente, gerando mais erros do que realmente existem. Então, começamos com nosso primeiro erro:

      $ clang -o string string.c
      string.c:5:5: error: use of undeclared identifier 'string'; did you mean 'stdin'?
        string name = get_string("What's your name?\n");
        ^~~~~~
        stdin
      /usr/include/stdio.h:135:25: note: 'stdin' declared here
      extern struct _IO_FILE *stdin;          /* Standard input stream.  */
    
    • Não queríamos dizer stdin (“entrada padrão”) em vez de string, então essa mensagem de erro não foi útil. Na verdade, precisamos importar outro arquivo que define o tipo string (na verdade, uma roda de treinamento do CS50, como descobriremos nas próximas semanas).
  • Então podemos incluir outro arquivo, cs50.h, que também inclui a função get_string, entre outras.

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          string name = get_string("What's your name?\n");
          printf("hello, %s\n", name);
      }
    
  • Agora, quando tentarmos compilar nosso programa, teremos apenas um erro:

      $ clang -o string string.c
      /tmp/string-aca94d.o: In function `main':
      string.c:(.text+0x19): undefined reference to `get_string'
      clang-7: error: linker command failed with exit code 1 (use -v to see invocation)
    
    • Acontece que também temos que dizer ao nosso compilador para adicionar nosso arquivo de biblioteca CS50 especial, com clang -o string string.c -lcs50, com -l para “link”.
  • Podemos até mesmo abstrair isso e digitar make string. Vemos que, por padrão no CS50 Sandbox, make usa clang para compilar nosso código de string.c para string, com todos os argumentos necessários ou sinalizadores passados.

Blocos do Scratch em C

  • O bloco “definir [contador] para (0)” está criando uma variável e, em C, escreveríamos int contador = 0;, onde int especifica que o tipo da nossa variável é um número inteiro. bloco com a etiqueta 'definir contador para (0)' - block labeled 'set counter to (0)'

  • “alterar [contador] por (1)” é contador = contador + 1; em C. (Em C, = não é como um sinal de igual em uma equação, onde estamos dizendo que contador é igual a contador + 1. Em vez disso, = é um operador de atribuição que significa “copiar o valor à direita no valor à esquerda”.) E observe que não precisamos mais dizer int, pois presume-se que já especificamos anteriormente que contador é um int, com algum valor existente. Também podemos dizer contador += 1; ou contador++;, ambos os quais são “sintaxe simplificada” ou atalhos que têm o mesmo efeito com menos caracteres para digitar. bloco com a etiqueta 'alterar contador por (1)' - block labeled 'change counter by (1)'

  • Uma condição seria mapeada para: bloco com a etiqueta 'se < (x) < (y) > então', dentro do qual há um bloco com a etiqueta 'dizer (x é menor que y)' - block labeled 'if < (x) < (y)> then', inside which there is a block labeled 'say (x is less than y)'

c if (x < y) { printf("x é menor que y\n"); }

  • Observe que, em C, usamos { e } (bem como indentação) para indicar como as linhas de código devem ser aninhadas.

  • Também podemos ter condições se-senão: bloco com a etiqueta 'se < (x) < (y) > então', dentro do qual há um bloco com a etiqueta 'dizer (x é menor que y)', o bloco pai também tem um 'senão', dentro do qual há um bloco com a etiqueta 'dizer (x não é menor que y)' - block labeled 'if < (x) < (y)> then', inside which there is a block labeled 'say (x is less than y)', parent block also has an 'else', inside which there is a block labeled 'say (x is not less than y)'

c if (x < y) { printf("x é menor que y\n"); } else { printf("x não é menor que y\n"); }

  • Observe que as linhas de código que não são, elas mesmas, uma ação (if... e as chaves) não terminam em ponto e vírgula.

  • E até mesmo senão se: bloco com a etiqueta 'se < (x) < (y) > então', dentro do qual há um bloco com a etiqueta 'dizer (x é menor que y)', o bloco pai também tem um 'senão', dentro do qual há um bloco aninhado com a etiqueta 'se < (x) > (y) > então', dentro do qual há um bloco com a etiqueta 'dizer (x é maior que y)', o bloco pai também tem um 'senão', dentro do qual há um bloco com a etiqueta 'se < (x) = (y) > então', dentro do qual há um bloco com a etiqueta 'dizer (x é igual a y)' - block labeled 'if < (x) < (y)> then', inside which there is a block labeled 'say (x is less than y)', parent block also has an 'else', inside which is a nesting of a block labeled 'if < (x) > (y) > then', inside which there is a block labeled 'say (x is greater than y)', parent block also has an 'else', inside which there is a block labeled 'if < (x) = (y) > then', inside which there is a block labeled 'say (x is equal to y)'

c if (x < y) { printf("x é menor que y\n"); } else if (x > y) { printf("x é maior que y\n"); } else if (x == y) { printf("x é igual a y\n"); }

  • Observe que, para comparar dois valores em C, usamos ==, dois sinais de igual.
  • E, logicamente, não precisamos de if (x == y) na condição final, pois esse é o único caso restante, e podemos simplesmente dizer else.

  • Os loops podem ser escritos como o seguinte: bloco com a etiqueta 'para sempre', dentro do qual há um bloco com a etiqueta 'dizer (olá, mundo)' - block labeled 'forever', inside which there is a block labeled 'say (hello, world)'

c while (true) { printf("olá, mundo\n"); }

  • A palavra-chave while também requer uma condição, então usamos true como a expressão booleana para garantir que nosso loop seja executado para sempre. Nosso programa verificará se a expressão é avaliada como true (o que sempre será o caso) e, em seguida, executará as linhas dentro das chaves. Em seguida, repetirá isso até que a expressão não seja mais verdadeira (o que não mudará neste caso).

  • Poderíamos fazer algo um certo número de vezes com while: bloco com a etiqueta 'repetir (50)', dentro do qual há um bloco com a etiqueta 'dizer (olá, mundo)' - block labeled 'repeat (50)', inside which there is a block labeled 'say (hello, world)'

c int i = 0; while (i < 50) { printf("olá, mundo\n"); i++; }

  • Criamos uma variável, i, e a definimos como 0. Então, enquanto i < 50, executamos algumas linhas de código e adicionamos 1 a i após cada execução.
  • As chaves em torno das duas linhas dentro do loop while indicam que essas linhas serão repetidas e podemos adicionar linhas adicionais ao nosso programa depois, se quisermos.

  • Para fazer a mesma repetição, mais comumente podemos usar a palavra-chave for:

c for (int i = 0; i < 50; i++) { printf("olá, mundo\n"); }

  • Novamente, primeiro criamos uma variável chamada i e a definimos como 0. Então, verificamos se i < 50 toda vez que alcançamos o início do loop, antes de executar qualquer parte do código dentro dele. Se essa expressão for verdadeira, então executamos o código interno. Finalmente, depois de executar o código interno, usamos i++ para adicionar um a i e o loop se repete.

Tipos, formatos, operadores

  • Há outros tipos que podemos usar para as nossas variáveis
    • bool, uma expressão booleana de true ou false
    • char, um único caractere como a ou 2
    • double, um valor de ponto flutuante com ainda mais dígitos
    • float, um valor de ponto flutuante, ou número real com um valor decimal
    • int, inteiros até um determinado tamanho, ou número de bits
    • long, inteiros com mais bits, para que possam contar mais alto
    • string, uma cadeia de caracteres
  • E a biblioteca CS50 tem funções correspondentes para obter entrada de vários tipos:
    • get_char
    • get_double
    • get_float
    • get_int
    • get_long
    • get_string
  • Para printf, também, há diferentes espaços reservados para cada tipo:
    • %c para caracteres
    • %f para flutuantes, duplos
    • %i para ints
    • %li para longos
    • %s para strings
  • E existem alguns operadores matemáticos que podemos usar:
    • + para adição
    • - para subtração
    • * para multiplicação
    • / para divisão
    • % para resto

Mais exemplos

  • Para cada um desses exemplos, você pode clicar nos links do sandbox para executar e editar suas próprias cópias deles.
  • Em int.c, obtemos e imprimimos um inteiro:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int idade = get_int("Qual é a sua idade?\n");
          int dias = idade * 365;
          printf("Você tem pelo menos %i dias de idade.\n", dias);
      }
    
    • Observe que usamos %i para imprimir um inteiro.
    • Agora podemos executar make int e executar nosso programa com ./int.
    • Podemos combinar linhas e remover a variável dias com:

        int idade = get_int("Qual é a sua idade?\n");
        printf("Você tem pelo menos %i dias de idade.\n", idade * 365);
      
    • Ou mesmo combinar tudo em uma linha:

        printf("Você tem pelo menos %i dias de idade.\n", get_int("Qual é a sua idade?\n") * 365);
      
    • No entanto, quando uma linha fica muito longa ou complicada, pode ser melhor manter duas ou até três linhas para facilitar a leitura.

  • Em float.c, podemos obter números decimais (chamados de valores de ponto flutuante em computadores, porque o ponto decimal pode "flutuar" entre os dígitos, dependendo do número):

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          float preço = get_float("Qual é o preço?\n");
          printf("Seu total é %f.\n", preço * 1.0625);
      }
    
    • Agora, se compilarmos e executarmos nosso programa, veremos um preço impresso com impostos.
    • Podemos especificar o número de dígitos impressos após a vírgula decimal com um espaço reservado como %.2f para dois dígitos após a vírgula decimal.
  • Com parity.c, podemos verificar se um número é par ou ímpar:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int n = get_int("n: ");
    
          if (n % 2 == 0)
          {
              printf("par\n");
          }
          else
          {
              printf("ímpar\n");
          }
      }
    
    • Com o operador % (módulo), podemos obter o restante de n após sua divisão por 2. Se o restante for 0, sabemos que n é par. Caso contrário, sabemos que n é ímpar.
    • Funções como get_int da biblioteca CS50 fazem a verificação de erros, onde apenas as entradas do usuário que correspondem ao tipo desejado são aceitas.
  • Em conditions.c, transformamos os trechos de condição anteriores em um programa:

      // Condições e operadores relacionais
    
      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          // Solicita ao usuário x
          int x = get_int("x: ");
    
          // Solicita ao usuário y
          int y = get_int("y: ");
    
          // Compara x e y
          if (x < y)
          {
              printf("x é menor que y\n");
          }
          else if (x > y)
          {
              printf("x é maior que y\n");
          }
          else
          {
              printf("x é igual a y\n");
          }
      }
    
    • As linhas que começam com // são comentários, ou nota para humanos que o compilador irá ignorar.
    • Para David compilar e executar este programa em seu sandbox, ele primeiro precisava executar cd src1 no terminal. Isso altera o diretório ou pasta para aquele no qual ele salvou todos os arquivos de origem da aula. Então, ele poderia executar make conditions e ./conditions. Com pwd, ele pode ver que está em uma pasta src1 (dentro de outras pastas). E cd por si só, sem argumentos, nos levará de volta para nossa pasta padrão no sandbox.
  • Em agree.c, podemos pedir ao usuário para confirmar ou negar algo:

      // Operadores lógicos
    
      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          // Solicita ao usuário que concorde
          char c = get_char("Você concorda?\n");
    
          // Verifica se concordou
          if (c == 'S' || c == 's')
          {
              printf("Concordo.\n");
          }
          else if (c == 'N' || c == 'n')
          {
              printf("Não concordo.\n");
          }
      }
    
    • Usamos duas barras verticais, ||, para indicar um "ou" lógico, seja qual for a expressão que pode ser verdadeira para que a condição seja seguida.
    • E se nenhuma das expressões for verdadeira, nada acontecerá, pois nosso programa não tem um loop.
  • Vamos implementar o programa de tosse da semana 0:

      #include <stdio.h>
    
      int main(void)
      {
          printf("tosse\n");
          printf("tosse\n");
          printf("tosse\n");
      }
    
  • Podemos usar um loop for:

      #include <stdio.h>
    
      int main(void)
      {
          for (int i = 0; i < 3; i++)
          {
              printf("tosse\n");
          }
      }
    
    • Por convenção, os programadores tendem a começar a contar em 0 e, portanto, i terá os valores de 0, 1 e 2 antes de parar, para um total de três iterações. Também poderíamos escrever for (int i = 1; i <= 3; i++) para o mesmo efeito final.
  • Podemos mover a linha printf para sua própria função:

      #include <stdio.h>
    
      void tosse(void);
    
      int main(void)
      {
          for (int i = 0; i < 3; i++)
          {
              tosse();
          }
      }
    
      void tosse(void)
      {
          printf("tosse\n");
      }
    
    • Declaramos uma nova função com void tosse(void); antes que nossa função main a chame. O compilador C lê nosso código de cima para baixo, então precisamos dizer a ele que a função tosse existe, antes de usá-la. Então, após nossa função main, podemos implementar a função tosse. Dessa forma, o compilador sabe que a função existe e podemos manter nossa função main perto do topo.
    • E nossa função tosse não recebe nenhuma entrada, então temos tosse(void).
  • Podemos abstrair a tosse ainda mais:

      #include <stdio.h>
    
      void tosse(int n);
    
      int main(void)
      {
          tosse(3);
      }
    
      void tosse(int n)
      {
          for (int i = 0; i < n; i++)
          {
              printf("tosse\n");
          }
      }
    
    • Agora, quando queremos imprimir "tosse" qualquer número de vezes, podemos simplesmente chamar a mesma função. Observe que, com void tosse(int n), indicamos que a função tosse recebe como entrada um int, que chamamos de n. E dentro de tosse, usamos n em nosso loop for para imprimir "tosse" o número correto de vezes.
  • Vamos dar uma olhada em positivo.c:

      #include <cs50.h>
      #include <stdio.h>
    
      int obter_ inteiro_positivo(void);
    
      int main(void)
      {
          int i = obter_inteiro_positivo();
          printf("%i\n", i);
      }
    
      // Solicita ao usuário um inteiro positivo
      int obter_ inteiro_positivo(void)
      {
          int n;
          faça
          {
              n = get_int("%s", "Inteiro positivo: ");
          }
          Enquanto (n < 1);
          Retorno n;
      }
    
    • A biblioteca CS50 não tem uma função obter_inteiro_positivo, mas podemos escrever uma nós mesmos. Nossa função int obter_inteiro_positivo(void) solicitará ao usuário um int e retornará esse int, que nossa função main armazena como i. Em obter_inteiro_positivo, inicializamos uma variável, int n, sem atribuir um valor a ela ainda. Então, temos uma nova construção, faça ... enquanto, que faz algo primeiro, então verifica uma condição e repete até que a condição deixe de ser verdadeira.
    • Assim que o loop terminar porque temos um n que não é "< 1", podemos retorná-lo com a palavra-chave return. E de volta à nossa função main, podemos definir int i para esse valor.

Telas

  • Podemos desejar um programa que imprima parte de uma tela de um videogame, como Super Mario Bros. Em mario0.c, temos:

      // Imprime uma linha de 4 pontos de interrogação
    
      #include <stdio.h>
    
      int main(void)
      {
          printf("????\n");
      }
    
  • Podemos pedir para o usuário um número de pontos de interrogação e, em seguida, imprimi-los, com mario2.c:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int n;
          do
          {
              n = get_int("Largura: ");
          }
          while (n < 1);
          for (int i = 0; i < n; i++)
          {
              printf("?");
          }
          printf("\n");
      }
    
  • E podemos imprimir um conjunto bidimensional de blocos com mario8.c:

      // Imprime uma grade nxn de tijolos com um loop
    
      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int n;
          do
          {
              n = get_int("Tamanho: ");
          }
          while (n < 1);
          for (int i = 0; i < n; i++)
          {
              for (int j = 0; j < n; j++)
              {
                  printf("#");
              }
              printf("\n");
          }
      }
    
    • Observe que temos dois loops aninhados, onde o loop externo usa i para fazer tudo dentro das vezes n e o loop interno usa j, uma variável diferente, para fazer algo n vezes para cada uma daquelas vezes. Em outras palavras, o loop externo imprime n “linhas” ou linhas, e o loop interno imprime n “colunas” ou caracteres “#”, em cada linha.
  • Outros exemplos não abordados em aula estão disponíveis em “Código-fonte” para a Semana 1.

# Memória imprecisão e overflow

  • Nosso computador possui memória em chips de hardware chamados RAM, memória de acesso aleatório. Nossos programas utilizam essa RAM para armazenar dados enquanto são executados, porém essa memória é finita. Portanto, com um número finito de bits não podemos representar todos os números possiveis (dos quais existe um número infinito). Então nosso computador tem um determinado número de bits para cada float e int e tem de arredondar para o valor decimal mais próximo em determinado ponto.
  • Com floats.c podemos ver o que acontece quando usamos floats:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          // Solicita o x ao usuário
          float x = get_float("x: ");
    
          // Solicita o y ao usuário
          float y = get_float("y: ");
    
          // Executa a divisão
          printf("x / y = %.50f\n", x / y);
      }
    
    • Com %50f, podemos especificar o número de casas decimais exibidos.
    • Hmm, agora obtemos...

        x: 1
        y: 10
        x / y = 0.10000000149011611938476562500000000000000000000000
      
    • Acontece que isso se chama imprecisão de ponto flutuante, onde não temos bits suficientes para armazenar todos os valores possíveis então o computador tem de armazenar o valor mais próximo possível de 1 dividido por 10.

  • Podemos ver um problema similar em overflow.c:

      #include <stdio.h>
      #include <unistd.h>
    
      int main(void)
      {
          for (int i = 1; ; i *= 2)
          {
              printf("%i\n", i);
              sleep(1);
          }
      }
    
    • No nosso laço for, definimos i como 1 e o dobramos com *= 2. (E continuaremos fazendo isso para sempre, então não há condição para verificação.)
    • Também usamos a função sleep do unistd.h para permitir que nosso programa pause sempre.
    • Agora, quando executamos esse programa veremos o número crescer cada vez mais até que:

        1073741824
        overflow.c:6:25: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'
        -2147483648
        0
        0
        ...
      
    • Acontece que nosso programa reconheceu que um inteiro assinado (um inteiro com um sinal positivo ou negativo) não poderia armazenar esse próximo valor e exibiu um erro. Então, uma vez que ele tentou dobrá-lo de qualquer forma, i se tornou um número negativo e então 0.

    • Esse problema é chamado de overflow de inteiro, onde um inteiro pode ser somente tão grande antes que acabem os bits e ele "reinicie". Podemos imaginar adicionar um ao número 999 decimal. O último digito se torna 0, pegamos o 1 emprestado para que o próximo digito se torne 0 e obtemos 1000. Mas se tivéssemos apenas três digitos, acabaríamos com 000 pois não há lugar para o 1 final!
  • O problema da virada do milênio surgiu porque muitos programas armazenavam o ano-calendário com apenas dois digitos, como 98 para 1998 e 99 para 1999. Porém quando o ano 2000 se aproximou, os programas teriam armazenado 00, levando a confusão entre os anos 1900 e 2000.

  • Um avião Boeing 787 também teve um bug onde um contador no gerador tinha overflow após um certo número de dias de operação contínua, visto que o número de segundos que havia rodado já não cabia mais naquele contador.
  • Portanto, vimos alguns problemas que podem acontecer, mas agora entendemos por que e como prevení-los.
  • Com o problema definido nesta semana, usaremos o CS50 Lab, construído no CS50 Sandbox, para escrever alguns programas com orientações para nos guiar.