Catto / Mapa de Temas · Informática I 1er nivel
Informática I · 120 h · Contenido 4 de 6

Lenguaje de programación estructurada

Tres estructuras de control, funciones que se entienden por separado y un compilador que traduce todo eso a instrucciones de máquina. C es el lenguaje donde esas dos cosas se ven a la vez.

C Compilación Funciones Pila de llamadas Módulos

01De los saltos a los bloques

Los primeros programas se escribían con saltos: «si pasa tal cosa, ir a la línea 340». Funcionaban, pero a partir de cierto tamaño nadie podía seguir el hilo, porque a cualquier línea se podía llegar desde cualquier otra. En 1968 Edsger Dijkstra publicó una carta con un título que se volvió famoso, Go To Statement Considered Harmful: el salto libre hace imposible razonar sobre un programa.

La programación estructurada es la respuesta: escribir todo con las tres estructuras de Böhm y Jacopini, cada una con una sola entrada y una sola salida, y partir el problema en funciones que se pueden entender, probar y reusar por separado. El programa se lee de arriba hacia abajo y cada bloque se puede reemplazar por una caja negra.

Por qué C

C, creado por Dennis Ritchie en los Laboratorios Bell entre 1969 y 1973 para escribir Unix, es un lenguaje estructurado que además deja ver la máquina: direcciones, bits y tamaños. Por eso es el lenguaje de los sistemas operativos y de los microcontroladores, y por eso se enseña en una ingeniería electrónica. Los elementos básicos del lenguaje están desarrollados en elementos básicos del lenguaje C; acá el foco está en cómo pensar un programa estructurado y qué hace la máquina con él.

02Del fuente al ejecutable, con la mano en la masa

El compilador gcc deja detenerse en cada etapa. Vale la pena hacerlo una vez para perderle el miedo a los mensajes de error, que dicen en qué etapa fallaron:

gcc -E hola.c -o hola.i     # sólo preprocesar: ver qué quedó de los #include
gcc -S hola.c -o hola.s     # compilar a ensamblador
gcc -c hola.c -o hola.o     # ensamblar: código objeto
gcc hola.o -o hola          # enlazar con la biblioteca estándar
gcc -Wall -Wextra -std=c11 hola.c -o hola   # todo junto, con advertencias
MensajeEtapaCausa típica
fatal error: stdio.h: No such filePreprocesadorNombre de cabecera mal escrito
expected ';' before …CompiladorFalta un punto y coma en la línea anterior
implicit declaration of functionCompiladorSe usa una función sin prototipo ni #include
undefined reference to 'sqrt'EnlazadorFalta la biblioteca: agregar -lm
Las advertencias son errores que todavía no explotaron

Compilar siempre con -Wall -Wextra y no dejar ninguna advertencia sin entender. Un if (x = 5) compila, pero el compilador avisa: es una asignación, no una comparación.

03Tipos y expresiones: lo que C hace sin avisar

En C cada expresión tiene un tipo, y el tipo decide la operación. 7 / 2 es una división entera y da 3, porque los dos operandos son enteros; 7.0 / 2 da 3,5. El resultado no depende de dónde se guarde: double x = 7 / 2; guarda 3,0, porque la división se hizo antes.

Cuando se mezclan tipos, C aplica las conversiones aritméticas habituales: los tipos más chicos que int se promueven a int, y en una operación entre dos tipos distintos el «menor» se convierte al «mayor». La trampa más conocida:

int a = -1;
unsigned int b = 1;
if (a < b)                      // ¡falso!
    printf("lógico");

Al comparar un int con un unsigned int, el −1 se convierte a sin signo y pasa a valer 4 294 967 295 (con 32 bits), que es mayor que 1. Los bits no cambiaron: cambió la lectura, exactamente como en sistemas de numeración.

Operadores, de mayor a menor precedenciaAsociatividad
() [] -> . y los sufijos ++ --izquierda a derecha
! ~ - * & sizeof, prefijos y conversiones (tipo)derecha a izquierda
* / %izquierda a derecha
+ -izquierda a derecha
<< >>izquierda a derecha
< <= > >=, después == !=izquierda a derecha
&, después ^, después |izquierda a derecha
&&, después ||izquierda a derecha
?: y las asignaciones = += -=derecha a izquierda

Hay una sorpresa en la tabla: & tiene menos precedencia que ==. Por eso if (x & 0x08 == 0) no hace lo que parece; se escribe if ((x & 0x08) == 0). Ante la duda, paréntesis.

04Las estructuras de control en C

EstructuraPseudocódigoC
Selección simple o dobleSi … entonces … si noif (c) { … } else { … }
Selección múltipleSegúnswitch (x) { case 1: … break; default: … }
Iteración con condición previaMientraswhile (c) { … }
Iteración con condición posteriorRepetir … hastado { … } while (c); (ojo: la condición es de permanencia)
Iteración con contadorParafor (i = 0; i < n; i++) { … }

En C cualquier valor distinto de cero es verdadero. break sale del ciclo o del switch y continue salta a la vuelta siguiente: rompen la regla de una sola salida, pero de manera controlada, y se aceptan cuando simplifican. goto existe; en código estructurado sólo se tolera para salir de varios niveles ante un error.

El switch se cae

Si un case no termina con break, la ejecución sigue en el siguiente. A veces se hace a propósito para agrupar casos; casi siempre es un olvido.

05Funciones y pasaje de parámetros

Una función es un algoritmo con nombre, entradas (parámetros) y una salida (valor de retorno). En C todos los parámetros se pasan por valor: la función recibe una copia y no puede modificar la variable original. Para que pueda, se le pasa la dirección de la variable, un puntero:

void intercambiar(int *p, int *q)
{
    int aux = *p;       // lo que hay en la dirección p
    *p = *q;
    *q = aux;
}
...
intercambiar(&x, &y);    // se pasan las direcciones de x e y

Por la misma razón scanf("%d", &n) lleva &: necesita saber dónde escribir. Los punteros se desarrollan en contenedores de datos complejos.

VariableAlcance (dónde se ve)Duración (cuánto vive)
LocalEl bloque donde se declaraMientras se ejecuta el bloque: nace y muere en cada llamada
Local staticEl bloque donde se declaraTodo el programa: conserva el valor entre llamadas
GlobalTodo el archivo (y otros, con extern)Todo el programa
Global staticSólo su archivoTodo el programa

06La pila de llamadas: dónde viven las variables locales

¿Cómo puede una función tener «su» n si se llama a sí misma? Cada llamada crea en la memoria un marco (stack frame) con sus parámetros, sus variables locales y la dirección a la que hay que volver. Los marcos se apilan: el último que entra es el primero que sale. Al volver, el marco se descarta y sus variables dejan de existir. Seguí la ejecución de un factorial recursivo:

Laboratorio · la pila de llamadas, en vivo
Pila (el marco de arriba es el que se está ejecutando)
Salida en pantalla
No devolver la dirección de una local

Como el marco se descarta al volver, una función que devuelve &local entrega la dirección de algo que ya no existe. El programa puede andar por casualidad y fallar después, lejos del error. Y una recursión sin caso base apila marcos hasta agotar la pila: es el stack overflow. En un microcontrolador, con 2 KiB de RAM, la pila se agota mucho antes.

07Programas en varios archivos

Un programa estructurado crece partido en módulos: un archivo .c con el código y un .h con lo que el resto del programa necesita saber de él, que son los prototipos, los tipos y las constantes.

/* resistores.h — la interfaz: qué ofrece el módulo */
#ifndef RESISTORES_H        // guarda contra la doble inclusión
#define RESISTORES_H
double paralelo(const double r[], int n);
double serie(const double r[], int n);
#endif

/* resistores.c — la implementación: cómo lo hace */
#include "resistores.h"
static int hay_corto(const double r[], int n);   // privada del módulo
...

Se compila con gcc main.c resistores.c -o calc. Separar interfaz e implementación permite cambiar el cómo sin tocar a quien usa el módulo: es la misma idea de la abstracción por capas, a escala de un programa.

08Entrada y salida estándar

EspecificadorTipoEjemplo
%d / %uint / unsignedprintf("%d", -5)
%ldlongprintf("%ld", 100000L)
%f en printf, %lf en scanfdoubleprintf("%.2f", 3.14159) → 3.14
%e / %gdouble, científica o automáticaprintf("%e", 4.7e-9)
%x / %oentero en hexadecimal / octalprintf("0x%02X", 12) → 0x0C
%c / %scarácter / cadenaprintf("%s", "hola")

scanf devuelve cuántos datos pudo leer. Un programa robusto lo comprueba: if (scanf("%lf", &r) != 1) significa que el usuario escribió algo que no es un número.

09En el laboratorio

Práctica 1 · Una calculadora de resistores modular

Programar el ejemplo de la resistencia equivalente del tema de algoritmos en tres archivos (main.c, resistores.c, resistores.h), con validación de la entrada y los casos raros. Comprobar los resultados con la calculadora de resistores del sitio para leer los valores de los componentes reales.

Práctica 2 · Ver la pila con el depurador

Compilar el factorial con gcc -g y ejecutarlo en gdb (o en el depurador de VS Code). Poner un punto de parada en el caso base y usar bt (backtrace) para ver todos los marcos apilados, con el valor de n en cada uno.

Práctica 3 · Leer el ensamblador

Compilar una función que sume dos enteros con gcc -S -O1 y ubicar en el .s la instrucción de suma y el retorno. Repetir con -O0 y comparar el largo. Los entornos de desarrollo se ven en entornos de desarrollo C y C++.

10Errores frecuentes

  • = en lugar de == dentro de un if.
  • Punto y coma después del for: for (i = 0; i < n; i++); ejecuta un ciclo vacío.
  • Olvidar el & en scanf: el programa escribe en una dirección cualquiera.
  • División entera sin querer: 1/2 * x vale siempre 0.
  • Variables locales sin inicializar: no valen cero, valen lo que había en la pila.
  • Usar %d para un double o %f para un int: imprime basura sin avisar.

11Autoevaluación

¿Qué vale double x = 5 / 2;?

2,0. La división entre enteros se hace primero y da 2; recién después se convierte a double.

¿Por qué scanf necesita &n y printf sólo n?

Porque C pasa todo por valor. printf sólo necesita el valor; scanf tiene que modificar la variable y para eso necesita su dirección.

¿Cuántos marcos hay en la pila, como máximo, al calcular fact(5) llamándolo desde main?

Seis: el de main y cinco de fact, con n = 5, 4, 3, 2 y 1.

¿Qué diferencia hay entre una variable local común y una static?

Las dos se ven sólo en su bloque, pero la static vive todo el programa y conserva su valor entre llamadas; la común se crea y se destruye en cada llamada.

¿Qué etapa da el error undefined reference y qué significa?

El enlazador: la función fue declarada, pero no encuentra su código en ningún objeto ni biblioteca.

¿Qué imprime if (-1 < 1u) printf("a"); else printf("b");?

b. El −1 se convierte a unsigned y queda en el valor máximo.

12Para ampliar

  • Brian W. Kernighan y Dennis M. Ritchie. El lenguaje de programación C. 2.ª ed., Prentice Hall Hispanoamericana, 1991. De los creadores del lenguaje. Breve, denso y con ejercicios que enseñan a pensar en C.
  • Harvey M. Deitel y Paul J. Deitel. Cómo programar en C/C++ y Java. 4.ª ed., Pearson Educación, 2004. Paso a paso, con muchísimos ejemplos completos y buenas prácticas.
  • Luis Joyanes Aguilar e Ignacio Zahonero Martínez. Programación en C. Metodología, algoritmos y estructura de datos. 2.ª ed., McGraw-Hill, 2005. Une el pseudocódigo del tema anterior con el lenguaje, en castellano.
  • K. N. King. C Programming: A Modern Approach. 2.ª ed., W. W. Norton, 2008 (en inglés). El mejor libro de texto moderno de C: riguroso con el estándar y muy claro.
  • Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM, 11(3), 1968 (en inglés). Una página y media que cambió la manera de programar.
Desarrollo del contenido «Lenguaje de programación estructurada» de Informática I (primer nivel), según el diseño curricular de Ingeniería Electrónica, Plan 2023 — Ordenanza N° 1849 del Consejo Superior de la UTN. Volver al Mapa de Temas · catto.ar