Clase 1

C

  • Hoy aprenderemos un nuevo lenguaje, C: un lenguaje de programación que tiene todas las funciones de Scratch y más, pero quizás sea un poco menos amigable ya que es puramente de texto:

      #include <stdio.h>
    
      int main(void)
      {
          printf("hello, world\n");
      }
    
    • Aunque las palabras son nuevas, las ideas son exactamente las mismas que los bloques "cuando se hace clic en la bandera verde" y "decir (hola, mundo)" en Scratch:
      bloque etiquetado 'cuando se hace clic en la bandera verde', bloque etiquetado 'decir (hola, mundo)'
  • Aunque parezca críptico, no olvides que 2/3 de los estudiantes de CS50 nunca han tomado CS antes, ¡así que no te asustes! Y aunque al principio, para tomar prestada una frase del MIT, tratar de absorber todos estos conceptos nuevos puede parecer como beber de una manguera contra incendios, ten la seguridad de que para el final del semestre estaremos empoderados y experimentados en aprender y aplicar estos conceptos.

  • Podemos comparar muchas de las construcciones en C con los bloques que ya hemos visto y usado en Scratch. La sintaxis es mucho menos importante que los principios, a los que ya hemos sido introducidos.

Hola, mundo

  • El bloque “cuando se dio clic en la bandera verde” en Scratch inicia el programa principal; al hacer clic en la bandera verde, el conjunto correcto de bloques debajo comienza. En C, la primera línea para lo mismo es int main(void), que aprenderemos más en las próximas semanas, seguido de una llave de apertura {, y una llave de cierre }, envolviendo todo lo que debería estar en nuestro programa.

      int main(void)
      {
    
      }
    
  • El bloque “decir (hola, mundo)” es una función, y corresponde a printf(“hola, mundo”);. En C, la función para imprimir algo en la pantalla es printf, donde f significa “formato”, lo que significa que podemos formatear la cadena impresa de diferentes maneras. Luego, usamos paréntesis para transmitir lo que queremos imprimir. Tenemos que usar comillas dobles para rodear nuestro texto para que se entienda como texto, y finalmente, agregamos un punto y coma ; al final de esta línea de código.

  • Para que nuestro programa funcione, también necesitamos otra línea en la parte superior, una línea de encabezado #include <stdio.h> que define la función printf que queremos usar. En algún lugar hay un archivo en nuestra computadora, stdio.h, que incluye el código que nos permite acceder a la función printf, y la línea de #include le dice a la computadora que incluya ese archivo con nuestro programa.
  • Para escribir nuestro primer programa en Scratch, abrimos el sitio web de Scratch. Del mismo modo, usaremos el CS50 Sandbox para comenzar a escribir y ejecutar código de la misma manera. CS50 Sandbox es un entorno virtual basado en la nube con las bibliotecas y herramientas ya instaladas para escribir programas en varios idiomas. En la parte superior, hay un editor de código simple, donde podemos escribir texto. Debajo, tenemos una ventana de terminal, en la que podemos escribir comandos: dos paneles, arriba con la etiqueta hello.c, abajo con la etiqueta Terminal
  • Escribiremos nuestro código de antes en la parte superior, después de usar el signo + para crear un nuevo archivo llamado hello.c: hola, mundo en editor
  • Finalizamos el archivo de nuestro programa con .c por convención, para indicar que está destinado a ser un programa en C. Observe que nuestro código está coloreado, para que ciertas cosas sean más visibles.

Compiladores

  • Una vez que guardamos el código que escribimos, que se conoce como código fuente, necesitamos convertirlo a código máquina, instrucciones binarias que el computador entiende directamente.
  • Usamos un programa llamado compilador para compilar nuestro código fuente en código de máquina.
  • Para hacer eso, utilizamos el panel de Terminal, que tiene un símbolo del sistema. El $ a la izquierda es un símbolo después del cual podemos escribir comandos.
    • Escribimos clang hello.c (donde clang significa “lenguajes C”, un compilador escrito por un grupo de personas). Pero antes de presionar Enter, hacemos clic en el ícono de la carpeta en la parte superior izquierda del CS50 Sandbox. Vemos nuestro archivo hello.c. Entonces presionamos Enter en la ventana de terminal y vemos que ahora tenemos otro archivo, llamado a.out (abreviatura de “salida de ensamblaje”). Dentro de ese archivo está el código para nuestro programa en binario. Ahora podemos escribir ./a.out en el símbolo del sistema para ejecutar el programa a.out en nuestra carpeta actual. ¡Acabamos de escribir, compilar y ejecutar nuestro primer programa!

String

  • Pero después de ejecutar nuestro programa, vemos hello, world$, con el nuevo indicador en la misma línea que nuestra salida. Resulta que debemos especificar precisamente que necesitamos una nueva línea después de nuestro programa, para que podamos actualizar nuestro código para incluir un carácter de nueva línea especial, \n:

      #include <stdio.h>
    
      int main(void)
      {
          printf("hello, world\n");
      }
    
    • Ahora debemos recordar recompilar nuestro programa con clang hello.c antes de poder ejecutar esta nueva versión.
  • La línea 2 de nuestro programa está intencionalmente en blanco ya que queremos comenzar una nueva sección de código, al igual que comenzar nuevos párrafos en los ensayos. No es estrictamente necesario que nuestro programa se ejecute correctamente, pero ayuda a los humanos a leer programas más largos con mayor facilidad.

  • También podemos cambiar el nombre de nuestro programa de a.out a otra cosa. Podemos pasar argumentos de línea de comandos, u opciones adicionales, a programas en la terminal, dependiendo de lo que el programa esté escrito para entender. Por ejemplo, podemos escribir clang -o hello hello.c, y -o hello le está diciendo al programa clang que guarde la salida compilada como hello. Entonces, podemos simplemente ejecutar ./hello.
  • En nuestro símbolo del sistema, podemos ejecutar otros comandos, como ls (lista), que muestra los archivos en nuestra carpeta actual:

      $ ls
      a.out* hello* hello.c
    
    • El asterisco, *, indica que esos archivos son ejecutables o que pueden ser ejecutados por nuestra computadora.
  • Podemos usar el comando rm (eliminar) para eliminar un archivo:

      $ rm a.out
      rm: ¿quitar archivo regular 'a.out'?
    
    • Podemos escribir y o para confirmar y usar ls nuevamente para ver que efectivamente se fue para siempre.
  • Ahora, intentemos obtener información del usuario, como lo hicimos en Scratch cuando quisimos decir "hola, David":
    captura de pantalla de los bloques "preguntar cuál es tu nombre? y esperar", "decir unir hola, respuesta"

      string answer = get_string("What's your name?\n");
      printf("hello, %s\n", answer);
    
    • Primero, necesitamos una cadena, o fragmento de texto (específicamente, cero o más caracteres en una secuencia entre comillas dobles, como "", "ba", o "bananas"), que podemos solicitarle al usuario, con la función get_string. Pasamos el indicador, o lo que queremos preguntarle al usuario, a la función con "What is your name?\n" dentro de los paréntesis. A la izquierda, queremos crear una variable, answer, cuyo valor será lo que ingrese el usuario. (El signo igual = establece el valor de derecha a izquierda). Finalmente, el tipo de variable que queremos es string, por lo que lo especificamos a la izquierda de answer.
    • A continuación, dentro de la función printf, queremos el valor de answer en lo que imprimimos de nuevo. Usamos un marcador de posición para nuestra variable de cadena, %s, dentro de la frase que queremos imprimir, como "hello, %s\n", y luego le damos a printf otro argumento u opción para decirle que queremos que la variable answer sea sustituido.
  • Si cometimos un error, como escribir printf("hello, world"\n); con el \n fuera de las comillas dobles de nuestra cadena, veremos errores de nuestro compilador:

      $ clang -o hello hello.c
      hello.c:5:26: error: paréntesis esperado
          printf("hello, world"\n);
                               ^
      hello.c:5:11: nota: para que coincida con este '('
          printf("hello, world"\n);
                ^
      1 error generado.
    
    • La primera línea del error nos dice que miremos hello.c, línea 5, columna 26, donde el compilador esperaba un paréntesis de cierre, en lugar de una barra invertida.
  • Para simplificar las cosas (al menos al principio), incluiremos una biblioteca o un conjunto de código de CS50. La biblioteca nos proporciona el tipo de variable string, la función get_string y más. Solo tenemos que escribir una línea en la parte superior para incluir el archivo cs50.h:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          string name = get_string("What's your name?\n");
          printf("hello, name\n");
      }
    
  • Así que creemos un nuevo archivo, string.c, con este código:

      #include <stdio.h>
    
      int main(void)
      {
          string name = get_string("What's your name?\n");
          printf("hello, %s\n", name);
      }
    
  • Ahora, si intentamos compilar ese código, obtenemos muchas líneas de errores. A veces, un error significa que el compilador comienza a interpretar el código correcto incorrectamente, lo que genera más errores de los que realmente hay. Entonces comenzamos con nuestro primer error:

      $ clang -o string string.c
      string.c:5:5: error: uso de identificador no declarado 'string'; ¿querías decir 'stdin'?
        string name = get_string("What's your name?\n");
        ^~~~~~
        stdin
      /usr/include/stdio.h:135:25: nota: 'stdin' declarado aquí
      extern struct _IO_FILE *stdin;          /* Standard input stream.  */
    
    • No quisimos stdin ("entrada estándar") en lugar de string, por lo que ese mensaje de error no fue útil. De hecho, necesitamos importar otro archivo que defina el tipo string (en realidad, una rueda de entrenamiento de CS50, como descubriremos en las próximas semanas).
  • Entonces podemos incluir otro archivo, cs50.h, que también incluye la función get_string, entre otras.

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          string name = get_string("What's your name?\n");
          printf("hello, %s\n", name);
      }
    
  • Ahora, cuando intentamos compilar nuestro programa, solo tenemos un error:

      $ clang -o string string.c
      /tmp/string-aca94d.o: In function `main':
      string.c:(.text+0x19): referencia indefinida a `get_string'
      clang-7: error: el comando del enlazador falló con el código de salida 1 (use -v para ver la invocación)
    
    • Resulta que también tenemos que decirle a nuestro compilador que agregue nuestro archivo de biblioteca especial CS50, con clang -o string string.c -lcs50, con -l para "enlace".
  • Incluso podemos abstraer esto y simplemente escribir make string. Vemos que, por defecto en CS50 Sandbox, make usa clang para compilar nuestro código desde string.c en string, con todos los argumentos o banderas necesarios pasados.

Bloques de Scratch en C

  • El bloque "poner [contador] a (0)" crea una variable y en C escribimos int contador = 0; donde int especifica que el tipo de nuestra variable es un entero: bloque etiquetado como 'poner contador en (0)'
  • "cambiar [contador] por (1)" es contador = contador + 1; en C. (En C, el = no es como un signo igual en una ecuación que dice que contador es igual a contador + 1. En cambio, = es un operador de asignación que significa "copia el valor de la derecha en el valor de la izquierda"). Fíjate que ya no tenemos que decir int porque asumimos que ya lo hemos especificado al decir que contador es int, con cierto valor. También podemos decir contador += 1; o contador++;, que son "azúcar sintáctico" (atajos con menos caracteres). bloque etiquetado como 'cambiar contador por (1)'
  • Una condición se asigna así: bloque etiquetado como 'si < (x) < (y)> entonces', que tiene dentro un bloque etiquetado como 'decir (x es menor que y)'

      if (x < y)
      {
          printf("x es menor que y\n");
      }
    
    • Fíjate que en C, usamos { y } (y también sangrías) para indicar cómo se deben anidar las líneas de código.
    • También podemos usar condiciones if-else: bloque etiquetado como 'si < (x) < (y)> entonces', que tiene dentro un bloque etiquetado como 'decir (x es menor que y)', y el bloque padre también tiene un 'si no' con un bloque etiquetado como 'decir (x no es menor que y)'

      if (x < y)
      {
          printf("x es menor que y\n");
      }
      else
      {
          printf("x no es menor que y\n");
      }
      
    • Fíjate que las líneas de código que no son acciones (if... y las llaves) no terminan en punto y coma.

    • E incluso si no si: bloque etiquetado como 'si < (x) < (y)> entonces', que tiene dentro un bloque etiquetado como 'decir (x es menor que y)', y el bloque padre también tiene un 'si no' que anida un bloque etiquetado como 'si < (x) > (y) > entonces', que tiene dentro un bloque etiquetado como 'decir (x es mayor que y)', y el bloque padre también tiene un 'si no' que anida un bloque etiquetado como 'si < (x) = (y) > entonces', que tiene dentro un bloque etiquetado como 'decir (x es igual a y)'

      if (x < y)
      {
          printf("x es menor que y\n");
      }
      else if (x > y)
      {
          printf("x es mayor que y\n");
      }
      else if (x == y)
      {
          printf("x es igual a y\n");
      }
      
    • Fíjate que para comparar dos valores en C usamos ==, dos signos igual.

    • Y, lógicamente, no necesitamos if (x == y) en la condición final, porque es el único caso que queda y podemos decir simplemente else.
    • Los bucles pueden escribirse así: bloque etiquetado como 'para siempre' con un bloque etiquetado como 'decir (hola mundo)'

      while (true)
      {
          printf("hello, world\n");
      }
      
    • La palabra clave while también necesita una condición, así que usamos true como expresión booleana para garantizar que nuestro bucle se ejecute para siempre. Nuestro programa comprobará si la expresión es true (que siempre lo será en este caso) y luego ejecutará las líneas entre las llaves. Luego lo repetirá hasta que la expresión no sea true (que no cambiará en este caso).

    • Podemos hacer algo un número de veces determinado con while: bloque etiquetado como 'repetir (50)' con un bloque etiquetado como 'decir (hola, mundo)'

      int i = 0;
      while (i < 50)
      {
          printf("hello, world\n");
          i++;
      }
      
    • Creamos una variable, i, y le asignamos el valor 0. Luego, mientras i < 50, ejecutamos unas líneas de código y añadimos 1 a i después de cada ejecución.

    • Las llaves que rodean las dos líneas dentro del bucle while indican que esas líneas se repetirán, y podemos añadir líneas adicionales a nuestro programa después si queremos.
    • Para hacer la misma repetición, normalmente podemos usar la palabra clave for:

      for (int i = 0; i < 50; i++)
      {
          printf("hello, world\n");
      }
      
    • De nuevo, primero creamos una variable llamada i y le asignamos el valor 0. Luego, comprobamos que i < 50 cada vez que llegamos al principio del bucle, antes de ejecutar el código. Si la expresión es true, ejecutamos el código. Finalmente, después de ejecutar el código, añadimos uno a i con i++ y el bucle se repite.

Tipos, formatos, operadores

  • Existen otros tipos que podemos utilizar para nuestras variables:

    • bool, una expresión booleana de verdadero o falso
    • char, un solo carácter como a o 2
    • double, un valor de punto flotante con aún más dígitos
    • float, un valor de punto flotante o número real con un valor decimal
    • int, enteros hasta cierto tamaño o número de bits
    • long, enteros con más bits, por lo que pueden contar más alto
    • string, una cadena de caracteres
  • Y la biblioteca CS50 tiene funciones correspondientes para obtener entrada de varios tipos:

    • get_char
    • get_double
    • get_float
    • get_int
    • get_long
    • get_string
  • Para printf, también, hay diferentes marcadores de posición para cada tipo:

    • %c para caracteres
    • %f para flotantes, dobles
    • %i para enteros
    • %li para largos
    • %s para cadenas
  • Y hay algunos operadores matemáticos que podemos utilizar:

    • + para la adición
    • - para la resta
    • * para la multiplicación
    • / para la división
    • % para el resto

Más ejemplos

  • Para cada uno de estos ejemplos, puedes hacer clic en los enlaces de "sandbox" para ejecutar y editar tus propias copias.
  • En int.c, obtenemos e imprimimos un entero:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int age = get_int("¿Cuál es tu edad?\n");
          int days = age * 365;
          printf("Tienes por lo menos %i días.\n", days);
      }
    
  • Observa que usamos %i para imprimir un entero.

  • Ahora, podemos ejecutar make int y ejecutar nuestro programa con ./int.
  • Podemos combinar líneas y eliminar la variable days con:

        int age = get_int("¿Cuál es tu edad?\n");
        printf("Tienes por lo menos %i días.\n", age * 365);
    
  • O incluso, combinar todo en una línea:

        printf("Tienes por lo menos %i días.\n", get_int("¿Cuál es tu edad?\n") * 365);
    
  • Sin embargo, una vez que una línea es demasiado larga o complicada, puede ser mejor mantener dos o incluso tres líneas para mejorar la legibilidad.

  • En float.c, podemos obtener números decimales (llamados valores de punto flotante en las computadoras, porque la coma decimal puede "flotar" entre los dígitos, según el número):

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          float price = get_float("¿Cuál es el precio?\n");
          printf("Tu total es %f.\n", price * 1.0625);
      }
    
  • Ahora, si compilamos y ejecutamos nuestro programa, veremos un precio impreso con impuestos.

  • Podemos especificar el número de dígitos impresos después del decimal con un marcador de posición como %.2f para dos dígitos después del punto decimal.

  • Con parity.c, podemos comprobar si un número es par o impar:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int n = get_int("n: ");
    
          if (n % 2 == 0)
          {
              printf("par\n");
          }
          else
          {
              printf("impar\n");
          }
      }
    
  • Con el operador % (módulo), podemos obtener el resto de n después de dividirlo por 2. Si el resto es 0, sabemos que n es par. De lo contrario, sabemos que n es impar.

  • Y funciones como get_int de la biblioteca CS50 hacen comprobaciones de errores, donde solo se aceptan las entradas del usuario que coincidan con el tipo que queremos.

  • En conditions.c, convertimos los fragmentos de condición de antes en un programa:

      // Condiciones y operadores relacionales
    
      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          // Solicitar al usuario x
          int x = get_int("x: ");
    
          // Solicitar al usuario y
          int y = get_int("y: ");
    
          // Comparar x e y
          if (x < y)
          {
              printf("x es menor que y\n");
          }
          else if (x > y)
          {
              printf("x es mayor que y\n");
          }
          else
          {
              printf("x es igual a y\n");
          }
      }
    
  • Las líneas que comienzan con // son comentarios, o notas para los humanos que el compilador ignorará.

  • Para que David compile y ejecute este programa en su entorno "sandbox", primero tuvo que ejecutar cd src1 en el terminal. Esto cambia el directorio, o carpeta, al lugar donde guardó todos los archivos de origen de la clase. Luego, pudo ejecutar make conditions y ./conditions. Con pwd, puede ver que se encuentra en una carpeta src1 (dentro de otras carpetas). Y cd por sí solo, sin argumentos, nos llevará de vuelta a nuestra carpeta predeterminada en el entorno "sandbox".

  • En agree.c, podemos pedirle al usuario que confirme o niegue algo:

      // Operadores lógicos
    
      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          // Pedirle al usuario que esté de acuerdo
          char c = get_char("¿Estás de acuerdo?\n");
    
          // Comprobar si está de acuerdo
          if (c == 'Y' || c == 'y')
          {
              printf("De acuerdo.\n");
          }
          else if (c == 'N' || c == 'n')
          {
              printf("No está de acuerdo.\n");
          }
      }
    
  • Usamos dos barras verticales, ||, para indicar un "o" lógico, si cualquiera de las expresiones puede ser verdadera para que se siga la condición.

  • Y si ninguna de las expresiones es verdadera, no ocurrirá nada, ya que nuestro programa no tiene un bucle.

  • Implementemos el programa de tos de la semana 0:

      #include <stdio.h>
    
      int main(void)
      {
          printf("cough\n");
          printf("cough\n");
          printf("cough\n");
      }
    
  • Podríamos usar un bucle for:

      #include <stdio.h>
    
      int main(void)
      {
          for (int i = 0; i < 3; i++)
          {
              printf("cough\n");
          }
      }
    
    • Por convenio, los programadores suelen empezar a contar desde 0, por lo que i tendrá los valores de 0, 1 y 2 antes de detenerse, para un total de tres iteraciones. También podríamos escribir for (int i = 1; i <= 3; i++) para lograr el mismo efecto final.
  • Podemos trasladar la línea de printf a su propia función:

      #include <stdio.h>
    
      void cough(void);
    
      int main(void)
      {
          for (int i = 0; i < 3; i++)
          {
              cough();
          }
      }
    
      void cough(void)
      {
          printf("cough\n");
      }
    
    • Hemos declarado una nueva función con void cough(void);, antes de que nuestra función main la llame. El compilador de C lee nuestro código de arriba abajo, por lo que tenemos que decirle que la función cough existe, antes de usarla. Entonces, después de nuestra función main, podemos implementar la función cough. De esta forma, el compilador sabe que la función existe y podemos mantener nuestra función main cerca de la parte superior.
    • Y nuestra función cough no toma ninguna entrada, por lo que tenemos cough(void).
  • Podemos abstraer cough más a fondo:

      #include <stdio.h>
    
      void cough(int n);
    
      int main(void)
      {
          cough(3);
      }
    
      void cough(int n)
      {
          for (int i = 0; i < n; i++)
          {
              printf("cough\n");
          }
      }
    
    • Ahora, cuando queramos imprimir "cough" cualquier número de veces, podemos simplemente llamar a la misma función. Ten en cuenta que, con void cough(int n), indicamos que la función cough toma como entrada un int, al que nos referimos como n. Y dentro de cough, usamos n en nuestro bucle for para imprimir "cough" el número correcto de veces.
  • Veamos positive.c:

      #include <cs50.h>
      #include <stdio.h>
    
      int get_positive_int(void);
    
      int main(void)
      {
          int i = get_positive_int();
          printf("%i\n", i);
      }
    
      // Solicitar al usuario un entero positivo
      int get_positive_int(void)
      {
          int n;
          do
          {
              n = get_int("%s", "Entero positivo: ");
          }
          while (n < 1);
          return n;
      }
    
    • La biblioteca CS50 no tiene una función get_positive_int, pero podemos escribir una nosotros mismos. Nuestra función int get_positive_int(void) solicitará al usuario un int y devolverá ese int, que nuestra función main almacena como i. En get_positive_int, inicializamos una variable, int n, sin asignarle todavía ningún valor. Entonces, tenemos una nueva construcción, do ... while, que hace algo primero, luego verifica una condición y repite hasta que la condición ya no sea verdadera.
    • Una vez que el bucle finaliza porque tenemos una n que no es < 1, podemos devolverla con la palabra clave return. Y de regreso en nuestra función main, podemos establecer int i en ese valor.

Pantallas

  • Podríamos querer un programa que imprima parte de una pantalla de un videojuego como Super Mario Bros. En mario0.c, tenemos:

      // Imprime una fila de 4 signos de interrogación
    
      #include <stdio.h>
    
      int main(void)
      {
          printf("????\n");
      }
    
  • Podemos pedirle al usuario un número de signos de interrogación y luego imprimirlos, con mario2.c:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int n;
          do
          {
              n = get_int("Ancho: ");
          }
          while (n < 1);
          for (int i = 0; i < n; i++)
          {
              printf("?");
          }
          printf("\n");
      }
    
  • Y podemos imprimir un conjunto bidimensional de bloques con mario8.c:

      // Imprime una cuadrícula de ladrillos de n por n con un bucle
    
      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          int n;
          do
          {
              n = get_int("Tamaño: ");
          }
          while (n < 1);
          for (int i = 0; i < n; i++)
          {
              for (int j = 0; j < n; j++)
              {
                  printf("#");
              }
              printf("\n");
          }
      }
    
    • Tenga en cuenta que tenemos dos bucles anidados, donde el bucle externo usa i para hacer todo dentro n veces, y el bucle interno usa j, una variable diferente, para hacer algo n veces para cada uno de esos tiempos. En otras palabras, el bucle externo imprime n "filas" o líneas, y el bucle interno imprime n "columnas" o caracteres # en cada línea.
  • Otros ejemplos no cubiertos en la conferencia están disponibles en "Código fuente" para Semana 1.

Memoria, imprecisiones y desbordamiento

  • Nuestra computadora tiene memoria, en chips de hardware llamados RAM, memoria de acceso aleatorio. Nuestros programas usan esa RAM para almacenar datos mientras se ejecutan, pero esa memoria es finita. Entonces, con un número finito de bits, no podemos representar todos los números posibles (de los cuales hay un número infinito). Entonces, nuestra computadora tiene una cierta cantidad de bits para cada float e int, y tiene que redondear al valor decimal más cercano en cierto punto.
  • Con floats.c, podemos ver qué sucede cuando usamos flotantes:

      #include <cs50.h>
      #include <stdio.h>
    
      int main(void)
      {
          // Solicita a usuario por x
          float x = get_float("x: ");
    
          // Solicita a usuario por y
          float y = get_float("y: ");
    
          // Realiza division
          printf("x / y = %.50f\n", x / y);
      }
    
  • Con %50f, podemos especificar la cantidad de decimales mostrados.

  • Hmm, ahora obtenemos …

        x: 1
        y: 10
        x / y = 0.10000000149011611938476562500000000000000000000000
    
  • Resulta que esto se llama imprecisión de punto flotante, donde no tenemos suficientes bits para almacenar todos los valores posibles, por lo que la computadora tiene que almacenar el valor más cercano que pueda a 1 dividido por 10.

  • Podemos ver un problema similar en overflow.c:

      #include <stdio.h>
      #include <unistd.h>
    
      int main(void)
      {
          for (int i = 1; ; i *= 2)
          {
              printf("%i\n", i);
              sleep(1);
          }
      }
    
  • En nuestro ciclo para, establecemos i en 1 y lo duplicamos con *= 2. (Y seguiremos haciendo esto para siempre, así que no hay condición que verifiquemos).

  • También usamos la función sleep de unistd.h para permitir que nuestro programa se pause cada vez.
  • Ahora, cuando ejecutamos este programa, vemos que el número se hace cada vez más grande, hasta:

        1073741824
        overflow.c:6:25: error de tiempo de ejecucion: desbordamiento de enteros con signo: 1073741824 * 2 no puede ser representado en tipo 'int'
        -2147483648
        0
        0
        ...
    
  • Resulta que nuestro programa reconoció que un entero con signo (un entero con un signo positivo o negativo) no podía almacenar ese siguiente valor e imprimió un error. Entonces, como intentó duplicarlo de todos modos, i se convirtió en un número negativo y luego en 0.

  • Este problema se llama desbordamiento de enteros, donde un entero solo puede ser tan grande antes de que se quede sin bits y "se desborde". Podemos imaginar sumar 1 a 999 en decimal. El último dígito se convierte en 0, llevamos el 1 asi que el siguiente dígito se convierte en 0, y obtenemos 1000. Pero si solo tuviéramos tres dígitos, ¡terminaríamos con 000 ya que no hay lugar para poner el 1 final!

  • El problema del año 2000 surgió porque muchos programas almacenaban el año calendario con solo dos dígitos, como 98 para 1998 y 99 para 1999. Pero cuando se acercaba el año 2000, los programas habrían almacenado 00, lo que llevaría a confusión entre los años 1900 y 2000.

  • Un avión Boeing 787 también tuvo un error donde un contador en el generador se desborda después de una cierta cantidad de días de funcionamiento continuo, ya que la cantidad de segundos que ha estado funcionando ya no podía almacenarse en ese contador.
  • Entonces, hemos visto algunos problemas que pueden ocurrir, pero ahora entendemos por qué y cómo prevenirlos.
  • Con el conjunto de problemas de esta semana, usaremos el CS50 Lab, construido sobre el CS50 Sandbox, para escribir algunos programas con tutoriales que nos guíen.