Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Introducción a la programación en C

La sintaxis y programas básicos.

Universidad Nacional de Río Negro

Introducción

Antes de iniciar el estudio de la programación en C, es fundamental comprender que programar no consiste únicamente en aprender un lenguaje, sino en desarrollar una forma de pensar. El lenguaje es la herramienta, pero el verdadero arte reside en la construcción de instrucciones lógicas para resolver problemas.

Programar es el acto de proporcionar instrucciones precisas a una computadora para que realice una tarea específica. Una diferencia clave con la comunicación humana es que la computadora no interpreta ambigüedades. No comprende conceptos como «más o menos». Cada paso debe estar perfectamente definido. En este sentido, C presenta ciertas ambigüedades que pueden conducir a resultados inesperados.

Durante la programación, la omisión de un solo detalle puede provocar que el programa no funcione. Por ello, es necesario aprender a pensar como una máquina, pero también a estructurar el pensamiento como un ser humano inteligente.

El lenguaje C es minimalista y provee acceso directo al hardware. Si bien promueve un estilo de programación explícito y prolijo para evitar bugs, C ofrece numerosos atajos sintácticos (tales como operadores de incremento ++, asignaciones compactas += y el operador condicional ternario ?:). La recomendación de evitarlos al principio responde a una decisión didáctica y no a una limitación técnica del lenguaje.


Desarrollo

¿Por qué aprender C?

El lenguaje C fue creado en 1972 por Dennis Ritchie y Brian Kernighan en los Bell Labs. A pesar de tener más de 50 años de existencia, se sigue utilizando ampliamente debido a sus características fundamentales:

Características Principales de C

Nivel de Abstracción

Aunque C es considerado un lenguaje de nivel medio, en el momento en el que fue creado se lo consideraba de alto nivel en comparación con el lenguaje ensamblador.

Hoy en día, y teniendo en cuenta que pasaron solo 50 años desde que fue creado, se lo sitúa en un nivel superior a los de bajo nivel, pero inferior a los que actualmente son considerados de alto nivel como Python o Java.

Esto le da un balance único:

C es un lenguaje compilado. El código fuente se traduce directamente a código máquina integramente antes de ejecutarse mediante un compilador, a diferencia de los lenguajes interpretados que son traducidos línea por línea en tiempo de ejecución.

Atributos Clave

Fortalezas del Lenguaje C

Acceso a conceptos de bajo nivel

C provee acceso a conceptos directamente relacionados con el hardware. Conceptos como el tamaño de la memoria, punteros y direccionamiento físico de memoria son muy similares a los que la CPU utiliza, de forma que los programas sean lo más rápidos posible.

C es un lenguaje pequeño

El lenguaje C posee un núcleo sintáctico y un conjunto de palabras clave reducido. Todo lo demás provisto por el entorno se cubre mediante la biblioteca estándar de C y funciones auxiliares.

C es un lenguaje permisivo

El lenguaje asume que el programador sabe lo que está haciendo, por lo que permite un control absoluto sobre el sistema, reduciendo las capas de validación del compilador, para bien y para mal.

Debilidades del Lenguaje C

Los programas en C pueden ser propensos a errores

La gran flexibilidad y permisividad de C facilitan la introducción de fallas de lógica o memoria que no siempre son detectadas por el compilador en tiempo de traducción. La mayoría de los errores de memoria (como el acceso fuera de límites o desreferencia de punteros nulos) se detectan recién en tiempo de ejecución.

Los programas en C pueden ser difíciles de entender

Debido a su diseño conciso e histórico (creado en una época donde la entrada de texto por consola era lenta), C utiliza una sintaxis compacta y operadores que pueden combinarse de forma críptica, exigiendo un cuidado extremo para mantener la legibilidad.

Los programas en C pueden ser difíciles de modificar

Los programas grandes escritos en C pueden ser difíciles de mantener si no se diseñan con cuidado. A diferencia de lenguajes orientados a objetos, C carece de conceptos como clases o paquetes integrados, delegando la modularidad a la estructuración de archivos del desarrollador.

La caja de herramientas

Preparación del entorno

Para los usuarios de Windows, la cátedra ha preparado un programa para simplificar la instalación de las herramientas necesarias para editar, compilar y ejecutar los programas.

INGCOM-UNRN-P1/entorno

Esencialmente, instala gcc, VSCode, git y una terminal, también podés instalar manualmente todo siguiendo la guía: compilador.

En sistemas basados en Debian/Ubuntu:

sudo apt install build-essential

Primer programa: el «Hola Mundo» en C

hola.c
1
2
3
4
5
6
7
#include <stdio.h>              // directiva del preprocesador

int main(void)                  // punto de entrada del programa
{                               // inicio de un bloque de código
    printf("Hola mundo C.\n");  // llamada a función de biblioteca para salida
    return 0;                   // finalización del programa
}                               // fin del bloque de código

Program 1:El indispensable Hola Mundo!

Compilación y Ejecución

Para ejecutar un programa en C, primero hay que compilarlo. Esto se realiza desde la terminal traduciendo el código fuente en un ejecutable de código máquina.

Proceso de compilación en C

Figure 1:El proceso de compilación transforma el código fuente en un ejecutable que la máquina puede ejecutar directamente.

1
2
3
4
5
6
7
8
# Compila el archivo hola.c y crea un ejecutable por defecto llamado a.out (a.exe en Windows)
$> gcc hola.c

(ejecuta-el-programa)=
# Ejecuta el programa
$> ./a.out
Hola mundo C.
$>

Program 2:La salida por la terminal.

En la función printf, el carácter especial \n es una secuencia de control que indica un salto de línea en la consola de salida.

Más adelante, vamos a ver detalles de la compilación para crear programas más complejos.

Pieza por pieza
  1. #include <stdio.h>: Es una directiva del preprocesador. Le indica al preprocesador que inserte el contenido de la cabecera de la biblioteca estándar de entrada/salida (stdio.h), que contiene la declaración de la función printf.

  2. int main(void): Es la definición de la función principal y el punto de entrada del programa. Todo ejecutable en C comienza su ejecución en esta función. int indica que devolverá un valor numérico entero al sistema operativo.

  3. { ... }: Las llaves delimitan el bloque de código del cuerpo de la función.

  4. printf("Hola mundo C.\n");: Es una llamada a función de biblioteca para salida que imprime la cadena en pantalla.

  5. return 0;: Finaliza la ejecución de la función main devolviendo el estado 0 al entorno. Por convención, un retorno de 0 significa finalización exitosa.

De la Idea al Código en C

En el capítulo Fundamentos de Computación y Algoritmos se estudiaron los fundamentos lógicos y el diseño de algoritmos en pseudocódigo. Ahora daremos el paso crucial: traducir ese diseño abstracto a un programa real en C.

La Mentalidad del Programador

  1. Leé el problema, comprendelo y diseñalo.

  2. Dividilo en pasos simples e inequívocos (en papel o pseudocódigo) antes de tipear una sola línea.

  3. Escribí el código en C basándote estrictamente en el algoritmo diseñado, no en la improvisación.

Roll Safe thinking

Pensar es más importante que escribir.

Ejemplo: Sumar dos números enteros

Diseño algorítmico:

  1. Declarar variables para almacenar dos números.

  2. Solicitar y leer los números.

  3. Calcular la suma y asignarla a un destino.

  4. Mostrar el resultado de la suma por pantalla.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void) {
    int a = 0;
    int b = 0;
    int resultado = 0;
    printf("Ingresá dos números: ");
    scanf("%d %d", &a, &b);
    resultado = a + b;
    printf("La suma es: %d\n", resultado);
    return 0;
}

Sobre las reglas de estilo

El uso de reglas de estilo es fundamental para garantizar la consistencia y legibilidad del código. Al adherirse a normas uniformes (como nomenclatura, indentación y posición de llaves), se facilita la colaboración y el mantenimiento del software. Para más detalles, consultá la regla Regla 0x0000h: La claridad y prolijidad son de máxima importancia.

Y cuando nos referimos a colaboración, esto incluye al profesor que tiene que leer su código.

Sintaxis y Semántica Básica

La sintaxis es el conjunto de reglas formales que definen cómo debe escribirse el código para que sea válido para el compilador. La semántica determina el significado lógico, la estructura y el comportamiento real que tiene el código durante su ejecución. Un código puede ser sintácticamente correcto pero semánticamente erróneo.

Identificadores y Palabras Reservadas

Identificadores: Son los nombres que asignamos a variables, constantes y funciones. Deben comenzar obligatoriamente con una letra o guion bajo (_) y pueden contener letras, dígitos y guiones bajos. C distingue entre mayúsculas y minúsculas (suma es un identificador distinto de Suma). No pueden coincidir con palabras reservadas del lenguaje.

Palabras Reservadas: Son palabras clave del lenguaje C que poseen un significado sintáctico especial predefinido y no pueden ser utilizadas como identificadores (ej: int, float, char, if, else, while, return).

Nuestros identificadores deben respetar las pautas de estilo (ver Regla 0x0001h: Los identificadores deben ser descriptivos).

Tipos de operadores

Los operadores son símbolos que le indican al compilador realizar manipulaciones matemáticas o lógicas específicas sobre las variables. En C se clasifican según su función y la cantidad de operandos que requieren:

Variables y Tipos de Datos

¿Qué es una variable?

Una variable es un identificador asociado a una dirección física de memoria RAM que almacena un dato de un tipo específico.

Variables y memoria

Figure 3:Las variables abstraen ubicaciones físicas de memoria. Cada una tiene una dirección de memoria, un nombre y un tipo.

Tipos básicos de datos en C

Tipos de datos en C

Figure 4:Especificadores de formato de tipos básicos en C.

Declaración e Inicialización

Toda variable debe declararse antes de ser usada, indicando su tipo y su nombre:

1
2
3
4
5
6
#include <stdbool.h>

int edad = 42;
float pi = 3.14f;
char inicial = 'A';
bool activo = true;

Si declarás una variable sin inicializarla, su contenido inicial en memoria física es indeterminado (“basura”). Siempre inicializá tus variables a un valor conocido antes de utilizarlas (ver regla de estilo Regla 0x0003h: Siempre debés inicializar las variables a un valor conocido).

Secuencia incorrecta de asignaciones

Las variables deben poseer datos válidos antes de ser leídas o manipuladas en expresiones o asignaciones.

Roles de las Variables

En programación, el rol de una variable se refiere al propósito específico que una variable desempeña dentro de un algoritmo. No es solo lo que la variable contiene, sino para qué la usamos. Identificar el rol de una variable nos permite diseñar algoritmos más claros, eficientes y fáciles de depurar. Es como asignar una tarea a cada miembro de un equipo; cada uno tiene su función y sabe qué hacer.

Establecer el rol, nos ayuda a elegir un buen identificador y salir de nombres como a, b y c y pasar a sumador, promedio y bandera. Por poner un ejemplo.

Rol de variable Auxiliar o Temporal

Una variable auxiliar o temporal se usa para almacenar un valor de forma transitoria, generalmente para facilitar una operación que requiere conservar un dato mientras se manipulan otros. El caso más común es el intercambio de valores entre dos variables sin perder ninguno.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(void) {
    int a = 10;
    int b = 20;
    int auxiliar = 0; // Inicialización explícita según la regla {ref}`0x0003h`

    printf("Antes del intercambio: a = %d, b = %d\n", a, b);

    auxiliar = a; // Guarda el valor de 'a'
    a = b;    // 'a' toma el valor de 'b'
    b = auxiliar; // 'b' recupera el valor original de 'a' desde 'auxiliar'

    printf("Despues del intercambio: a = %d, b = %d\n", a, b);
    return 0;
}

Acá, auxiliar es crucial para no perder el valor original de a cuando se lo asignamos a b.

La Importancia de los Roles

Entender estos roles te da una perspectiva más rica sobre cómo funcionan tus programas. No se trata solo de saber qué tipo de dato almacena una variable, sino para qué la estás usando. Reconocer estos patrones te ayudará a:

Vamos a ir viendo más roles a medida que vamos aprendiendo las diferentes cosas que podemos hacer con C.

L-Values y R-Values (Asignación y Expresiones)

Para comprender cómo el compilador evalúa y almacena los datos durante una asignación, tenés que conocer las dos categorías de expresiones en C: L-values y R-values, según lo define formalmente el estándar del lenguaje.

L-Values (locator values / object locators)

Un L-value es una expresión que identifica o localiza un objeto persistente en memoria (es decir, una celda física de memoria direccionable).

R-Values (value of an expression)

Un R-value representa simplemente el valor de una expresión. No posee una ubicación de memoria direccionable de almacenamiento persistente; es un valor transitorio.

Restricciones del compilador

Intentar realizar asignaciones sobre un R-value producirá un error inmediato en tiempo de compilación.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int x = 10;
int y = 20;

x = 50;          // VÁLIDO: 'x' es un L-value (ubicación modificable).
y = x + 5;       // VÁLIDO: 'y' es un L-value, 'x + 5' evalúa a un R-value.

// Asignaciones inválidas que causan ERROR DE COMPILACIÓN:
// 100 = x;      // ERROR: el literal '100' es un R-value, no podés asignarle
nada.
// (x + y) = 15; // ERROR: la expresión 'x + y' es un R-value temporal sin
dirección física.
// &x = &y;      // ERROR: la expresión de la izquierda no es un L-value
asignable.
// &(x + 5);     // ERROR: el operador de dirección (&) requiere un L-value.

Entrada y Salida Básica

printf() - Salida Formateada

Se utiliza para imprimir texto y valores de variables formateados en la salida estándar de consola. Podés consultar el apunte más detallado del tema.

1
printf("Tiene %d años\n", edad);

El par de símbolos \n se usan para indicar el ‘salto de línea’, para que no quede todo junto en una sola. Específicamente, cada vez que se ve una \, se indica que el siguiente carácter tiene un significado diferente del que se ve; esto se llama secuencias de escape.

Especificadores de formato de tipos básicos
Rol de variable de Salida

Las variables de salida se utilizan para almacenar los resultados finales de un cálculo o proceso, los cuales serán mostrados al usuario o guardados en algún medio (como un archivo).

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(void) {
    float radio = 5.0;
    float area = 0.0; // 'area' es una variable de salida (resultado)

    // Calculamos el área del círculo
    area = 3.14159 * radio * radio;

    printf("El area del circulo con radio %.2f es: %.2f\n", radio, area); // Se
    muestra el valor de 'area'
    return 0;
}

Aquí, area guarda el resultado del cálculo y luego se imprime.

scanf() - Entrada Formateada

Permite leer datos de entrada ingresados por teclado en la entrada estándar (stdin). Requiere pasar la dirección de la variable de destino anteponiendo el operador de dirección &.

1
2
3
int edad = 0;
printf("Ingrese su edad: ");
scanf("%d", &edad);
Rol de variable de Entrada

Una variable de entrada es aquella que se utiliza para almacenar datos que provienen de una fuente externa al programa, como la entrada del usuario a través del teclado, la lectura de un archivo o datos recibidos de una red.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void) {
    int edad = -1; // 'edad' es una variable de entrada

    printf("Por favor, ingresa tu edad: ");
    scanf("%d", &edad); // Aquí, el valor ingresado se guarda en 'edad'

    printf("Tu edad es: %d años.\n", edad);
    return 0;
}

En este ejemplo, edad toma el valor que el usuario introduce.

El Buffer de Entrada y la Lectura de Caracteres

Al presionar “Enter” para enviar datos en la consola, se agrega un carácter de salto de línea (\n) en el buffer de entrada stdin. Si la siguiente instrucción lee un carácter (scanf("%c")), leerá ese \n residual en lugar de la entrada esperada. Para evitar esto, se debe anteponer un espacio en blanco en el especificador (" %c"), lo cual instruye a scanf a descartar los espacios en blanco y saltos de línea residuales del buffer.

1
2
3
char inicial = ' ';
printf("Ingrese su inicial: ");
scanf(" %c", &inicial); // El espacio antes de %c limpia el buffer de stdin

Ejercicios de Autoevaluación

Historia y Características

Solution to Exercise 1

La diferencia principal radica en el momento de la traducción y en quién ejecuta las instrucciones:

  • C (compilado): El código fuente se traduce por completo y de antemano a código máquina (instrucciones nativas de la CPU) mediante el compilador. La CPU del host ejecuta el binario de forma directa y a máxima velocidad, sin intermediarios en tiempo de ejecución.

  • Python (interpretado): El código fuente se traduce a un código intermedio (bytecode) y una máquina virtual de software (el intérprete) lo procesa línea por línea en tiempo de ejecución. Esto introduce un overhead de traducción constante, haciéndolo significativamente más lento pero más flexible.

Solution to Exercise 2

Un ejemplo claro es el desbordamiento de buffer (buffer overflow) o el acceso fuera de los límites de un arreglo. En C, si declarás un arreglo de tamaño 5 y escribís en la posición 10, el compilador no realiza ninguna verificación automática en tiempo de compilación para impedirlo. El programa intentará escribir en esa dirección física de memoria en tiempo de ejecución, lo que puede sobreescribir otros datos, corromper el estado del programa o provocar un fallo de segmentación (Segmentation Fault).

Hola Mundo y Compilación

Solution to Exercise 3

El programa compilará pero emitirá una advertencia (warning) por declaración implícita de la función printf (warning: implicit declaration of function 'printf'). Esto ocurre porque, sin la cabecera <stdio.h>, el compilador no conoce la firma de la función (los tipos de sus parámetros y su retorno) al momento de procesar la llamada. Es una mala práctica de ingeniería porque impide que el compilador verifique que estás pasando los tipos correctos a la función, lo cual puede derivar en comportamientos indefinidos en tiempo de ejecución.

Solution to Exercise 4

La opción -o (output) le indica al compilador el nombre específico que debe tener el archivo ejecutable binario resultante.

  • Con -o mi_programa: el compilador genera un binario ejecutable llamado mi_programa (o mi_programa.exe en Windows).

  • Si se omite: el compilador genera el ejecutable con el nombre por defecto del sistema: a.out en sistemas de tipo Unix/Linux y a.exe en Windows.

Algoritmo a Código

Solution to Exercise 5

El programa completo en C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(void) {
    float base = 0.0f;
    float altura = 0.0f;
    float area = 0.0f;

    printf("Ingresá la base del rectángulo: ");
    scanf("%f", &base);
    printf("Ingresá la altura del rectángulo: ");
    scanf("%f", &altura);

    area = base * altura;

    printf("El área del rectángulo es: %.2f\n", area);
    return 0;
}
Solution to Exercise 6

Este truco aritmético evita el uso de una variable auxiliar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(void) {
    int x = 15;
    int y = 25;

    printf("Antes del intercambio: x = %d, y = %d\n", x, y);

    x = x + y; // x ahora almacena la suma total (40)
    y = x - y; // y toma el valor original de x (40 - 25 = 15)
    x = x - y; // x toma el valor original de y (40 - 15 = 25)

    printf("Después del intercambio: x = %d, y = %d\n", x, y);
    return 0;
}

Sintaxis y Operadores

Solution to Exercise 7

Se imprimirá: 5 y 5.67 La diferencia semántica radica en el tipo de los operandos:

  • 17 / 3: Ambos operandos son literales enteros (int). El compilador realiza una división entera, descartando la parte decimal y retornando el cociente entero 5.

  • 17.0 / 3: El primer operando es un literal real (double). El compilador realiza una promoción implícita del segundo operando a real y efectúa una división de punto flotante, resultando en 5.66666..., que formateado con %.2f se redondea a 5.67.

Solution to Exercise 8

La evaluación por cortocircuito de && establece que si el operando izquierdo es falso, el resultado de la conjunción es necesariamente falso, por lo que el operando derecho no es evaluado. Si denominador es igual a 0, la primera subexpresión denominador != 0 evalúa a false. El compilador aborta la evaluación del && y no ejecuta la expresión del lado derecho (numerador / denominador) > 2, evitando así un error fatal de división por cero en tiempo de ejecución.

Variables y Tipos

Solution to Exercise 9
  1. a: Es un L-value, porque identifica una ubicación de memoria direccionable de almacenamiento persistente.

  2. a + b: Es un R-value, porque representa el valor numérico temporal resultante de la suma, el cual no tiene una dirección de memoria física propia asignada. Intentar hacer (a + b) = 15; arroja un error de compilación.

  3. a = b: Es un L-value en C, ya que la asignación misma devuelve la ubicación del operando izquierdo tras almacenar el valor.

  4. ++a: Es un L-value en C++ pero un R-value en C estándar (C11). En C, el operador de incremento devuelve un valor temporal modificado, por lo que no es posible aplicarle el operador de dirección (&(++a) es inválido en C).

Solution to Exercise 10

En un sistema estándar de complemento a dos, el valor pasará a ser el entero mínimo representable (INT_MIN, que típicamente es -2.147.483.648 para 32 bits). Este fenómeno se denomina desbordamiento de enteros con signo (signed integer overflow). Desde el punto de vista del estándar de C, el desbordamiento de enteros con signo produce un comportamiento indefinido (undefined behavior), aunque físicamente en la mayoría de las arquitecturas de CPU se comporta mediante un ciclo de desbordamiento circular hacia los números negativos.

Entrada y Salida

Solution to Exercise 11
  1. Al ingresar 25 y presionar Enter, se deposita en el buffer de entrada stdin la secuencia de caracteres 2, 5 y el salto de línea \n.

  2. El primer scanf("%d", &edad) consume los caracteres numéricos 2 y 5 para asignarle 25 a la variable edad. El salto de línea \n permanece en el buffer de entrada.

  3. El segundo scanf("%c", &opcion) se ejecuta y, dado que %c no descarta espacios en blanco automáticamente, lee el primer carácter disponible en el buffer, que es el salto de línea residual \n.

  4. La variable opcion termina almacenando el valor '\n' (salto de línea), y la ejecución prosigue sin permitirle al usuario ingresar la letra S.

Solution to Exercise 12

Para especificar tanto el ancho de campo mínimo como la precisión decimal en printf, debés usar el especificador %[ancho].[precision]f. La instrucción correspondiente es:

float promedio = 8.567f;
printf("Valor: %8.2f\n", promedio);

Esto imprimirá el texto Valor: seguido de cuatro espacios en blanco y los caracteres 8.57 (el redondeo de 8.567), sumando un ancho total de 8 caracteres.


Glosario

Compilador
Herramienta de software que traduce un programa escrito en código fuente de alto nivel a instrucciones de código máquina nativo.
Sintaxis
Conjunto de reglas formales y gramaticales que definen la validez estructural de un programa para el compilador.
Semántica
Significado lógico y comportamiento operacional de las instrucciones de un programa en tiempo de ejecución.
L-value
Expresión que localiza un objeto persistente en memoria física, cuya dirección puede ser obtenida.
R-value
Expresión transitoria que representa únicamente un valor y carece de dirección física de almacenamiento direccionable.
Buffer de entrada
Área de almacenamiento temporal de memoria donde se retienen las entradas del usuario hasta ser consumidas por funciones como scanf.

Síntesis y Resumen

En esta unidad introductoria de C analizaste los siguientes fundamentos:

Para continuar con el control de flujo condicional y los lazos iterativos, consultá Introducción al Control de Flujo.


Referencias y Lecturas Complementarias