Introducción a las Secuencias en C¶
En C, una secuencia es una colección de elementos de datos del mismo tipo, almacenados en ubicaciones de memoria contiguas. Esta organización en memoria es una de las características que definen el rendimiento de C, ya que permite al hardware acceder a los datos con una gran eficiencia.
La forma más general de una secuencia es el arreglo, que puede contener
cualquier tipo de dato, como int
, float
o estructuras más complejas que
veremos más adelante. Aquí, el rol del programador es gestionarlos; definiendo
su tamaño y respetando sus límites, ya que el lenguaje no lo hará por nosotros.
Un caso particular y muy importante es la cadena de caracteres. En C, una
cadena no es un tipo de dato especial, sino una convención: se trata de un
arreglo de caracteres (char
) cuyo final se indica con un carácter especial, el
carácter nulo (\0
). Esta convención es la base sobre la que se construye
toda la manipulación de texto en C y también la de algunos problemas.
Arreglos: Secuencias de datos¶
Definición y Disposición en Memoria¶
Un arreglo es una colección de tamaño fijo de elementos homogéneos. Al declararlo, el compilador reserva un bloque de memoria continuo y suficientemente grande para albergar todos sus elementos.
int mi_arreglo[4];
Esta declaración reserva espacio para 4 enteros. Si un int
ocupa 4 bytes, la disposición en memoria es contigua:
Figure 1:Disposición en memoria de un arreglo de enteros. Los elementos se almacenan de forma contigua, lo que permite calcular la dirección de cualquier elemento mediante aritmética: dirección_base + (índice × tamaño_elemento). Esta organización es fundamental para el acceso eficiente en tiempo constante O(1).
Esta contigüidad es lo que permite el acceso indexado (mi_arreglo[2]
) de forma casi instantánea.
Declaración e Inicialización¶
La declaración de un arreglo sigue la sintaxis tipo identificador[cantidad];
.
La inicialización define los valores iniciales del arreglo. Es importante distinguir entre arreglos locales (dentro de una función) y los globales o estáticos:
Arreglos locales no inicializados: Contienen valores indeterminados (“basura”), lo cual viola la regla de estilo Regla
0x0003h
: Siempre debés inicializar las variables a un valor conocido.Arreglos globales o estáticos no inicializados: Sus elementos se inicializan a cero por defecto.
Formas de inicialización explícita:
Figure 2:Tres formas de inicializar arreglos en C: completa (todos los valores especificados), parcial (valores restantes a cero), y con inicializadores designados C99 (índices específicos). Los elementos no inicializados explícitamente en arreglos locales contienen basura (regla Regla 0x0003h
: Siempre debés inicializar las variables a un valor conocido).
Completa:
int arr[5] = {10, 20, 30, 40, 50};
Parcial:
int arr[5] = {10, 20};
(los elementos restantes,arr[2]
aarr[4]
, se inicializan a0
).Implícita:
int arr[] = {10, 20, 30};
(el compilador infiere el tamaño, en este caso 3).Inicializadores designados (C99): Permite inicializar índices específicos.
int arr[5] = {[0] = 10, [4] = 50};
(los no indicados se inicializan a0
).
El Operador sizeof
¶
Para comprender mejor el manejo de memoria de los arreglos, es necesario
introducir el operador sizeof
.
Este operador es una herramienta de tiempo de compilación que devuelve el
tamaño en bytes de un tipo de dato o una variable. Esto es posible por la forma
en la que C crea las variables en la memoria, con un único tipo, los “tipos
estáticos”. El valor que retorna es de tipo size_t
, un tipo de entero
sin signo.
1 2 3 4 5
int numeros[10]; // sizeof(int) -> Devuelve el tamaño de un entero (4 bytes) // sizeof(numeros[0]) -> Devuelve el tamaño de los elementos del arreglo (4 bytes) // sizeof(numeros) -> Devuelve el tamaño total del arreglo (40 bytes)
Esto nos permite calcular la cantidad de elementos de un arreglo de una forma simple, esto es de suma importancia en C, ya que los arreglos no guardan su tamaño.
1 2 3 4
// Funciona incluso si cambiamos 'numeros' a 'long numeros[]'. size_t cantidad = sizeof(numeros) / sizeof(numeros[0]); // La cuenta es, el tamaño total del arreglo / el tamaño de un elemento. // Qué aplicado al arreglo anterior, nos debiera dar 10.
Teniendo en cuenta que los tamaños de los tipos básicos como int
o long
,
pueden cambiar de tamaño entre compiladores.
1 2 3 4 5 6 7 8 9 10
#include <stdio.h> int main() { printf("Tamaño de char: %zu bytes\n", sizeof(char)); printf("Tamaño de int: %zu bytes\n", sizeof(int)); printf("Tamaño de float: %zu bytes\n", sizeof(float)); printf("Tamaño de double: %zu bytes\n", sizeof(double)); printf("Tamaño de long long: %zu bytes\n", sizeof(long long)); return 0; }
La salida de este código en una computadora de escritorio actual sería:
Tamaño de char: 1 bytes
Tamaño de int: 4 bytes
Tamaño de float: 4 bytes
Tamaño de double: 8 bytes
Tamaño de long long: 8 bytes
Una característica del lenguaje C es que los tamaños de sus tipos de datos
numéricos pueden variar entre diferentes arquitecturas de hardware. El operador
sizeof
permite escribir código portable que no dependa de un tamaño de tipo de
dato fijo, facilitando la adaptación del software a distintas plataformas.
Por ejemplo, históricamente, un int
en sistemas de 16-bits ocupa 2 bytes,
mientras que en sistemas de 32-bits o 64-bits, comúnmente ocupa 4 bytes.
El uso de sizeof
es crucial en la gestión de memoria dinámica, un tema que se
abordará más adelante.
Acceso, Modificación y la Identidad del Arreglo¶
Se accede a los elementos mediante el operador de subíndice []
, donde el
índice va de 0
a cantidad - 1
.
Acceso (lectura)¶
Para leer el valor de un elemento, se utiliza el operador de subíndice []
con
el índice del elemento deseado. Es importante recordar que los índices en C
comienzan en cero. Una expresión de acceso como miArreglo[i]
es un “r-value”,
ya que representa un valor que puede ser leído.
Por ejemplo int valor = mi_arreglo[3];
. Aquí, mi_arreglo[3]
nos permite
acceder al valor de la cuarta posición.
1 2 3 4 5 6 7 8 9 10 11
int calificaciones[5] = {10, 8, 9, 7, 10}; // Obtener el valor del primer elemento (índice 0) int primera = calificaciones[0]; // Obtener el valor del cuarto elemento (índice 3) int cuarta = calificaciones[3]; printf("La primera calificación es: %d\n", primera); printf("La cuarta calificación es: %d\n", cuarta); printf("Acceso directo al segundo elemento: %d\n", calificaciones[1]);
Salida:
La primera calificación es: 10
La cuarta calificación es: 7
Acceso directo al segundo elemento: 8
Modificación (escritura)¶
Para escribir un nuevo valor en un elemento, la expresión de subíndice
mi_arreglo[i]
se coloca en el lado izquierdo de una operación de asignación.
En este contexto, la expresión es un “l-value”, ya que representa una ubicación
de memoria modificable.
Por ejemplo, mi_arreglo[3] = 100;
. Acá, mi_arreglo[3]
nos permite modificar
el cuarto valor de la secuencia.
1 2 3 4 5 6 7
int edades[4] = {20, 25, 22, 28}; // 1. Mostrar el valor original del tercer elemento (índice 2) printf("La edad original en el índice 2 es: %d\n", edades[2]); // 2. Modificar el valor en el índice 2. 'edades[2]' es un l-value. edades[2] = 23; // 3. Mostrar el valor modificado. 'edades[2]' se evalúa como un r-value. printf("La nueva edad en el índice 2 es: %d\n", edades[2]);
Salida:
La edad original en el índice 2 es: 22
La nueva edad en el índice 2 es: 23
Identidad¶
El identificador de un arreglo, como mi_arreglo
, es especial. No es una
variable que contiene el arreglo, sino que está permanentemente asociado con
la dirección de memoria del primer elemento de esa secuencia. Por esta razón,
no se puede reasignar para que se refiera a otra secuencia. La operación
arr1 = arr2;
es ilegal. Para copiar los valores, se debe recorrer y copiar
cada elemento, uno por uno.
El identificador de un arreglo es una constante que representa la dirección de inicio del bloque de memoria asignado. Por esta razón, el nombre de un arreglo es un l-value no modificable.
1 2 3 4 5
int arr1[5] = {1, 2, 3, 4, 5}; int arr2[5] = {10, 20, 30, 40, 50}; // La siguiente línea es ilegal y causará un error de compilación. arr1 = arr2; // Error: expression is not assignable.
Recorrido de Arreglos y Comportamiento Indefinido¶
La estructura de control ideal para iterar sobre un arreglo es el lazo for
.
El uso de size_t
para el índice del lazo es la forma correcta de hacerlo, tal como lo indica la regla de estilo Regla 0x002Eh
: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t
.
1 2 3 4 5 6
int numeros[] = {10, 20, 30, 40, 50}; size_t cantidad = sizeof(numeros) / sizeof(numeros[0]); for (size_t i = 0; i < cantidad; i++) { printf("Elemento %zu: %d\n", i, numeros[i]); }
Arreglos de Longitud Variable (ALV/VLA)¶
Desde el estándar C99, C permite declarar arreglos cuyo tamaño se determina en tiempo de ejecución. Estos se conocen como ALV o VLA (en Inglés).
1 2 3 4 5 6 7 8
int cantidad = 0; printf("Ingrese el tamaño del arreglo:\n"); scanf("%d", &cantidad); int arreglo[cantidad]; // Declaración de un VLA printf("El tamaño del arreglo en bytes es: %zu\n", sizeof(arreglo)); // El resultado será sizeof(int) * cantidad
Estos tienen limitaciones importantes. Por ejemplo, un ALV no puede ser inicializado en su declaración. Intentarlo producirá un error de compilación:
error: variable-sized object may not be initialized
Las implicaciones y el uso correcto de la memoria dinámica, que es la alternativa recomendada a los ALV, se abordarán en un capítulo posterior.
De todas formas y como se imaginarán, hay una regla de estilo Regla 0x000Eh
: Los arreglos estáticos deben ser creados con un tamaño fijo en tiempo de compilación.
El Mecanismo de Paso a Funciones¶
Cuando un arreglo se pasa a una función, no se crea una copia del mismo. En su lugar, la función recibe la dirección de memoria del primer elemento. Es como darle a alguien la dirección de tu casa en lugar de una foto de ella; pueden entrar y redecorar.
Figure 3:Paso de arreglos a funciones por referencia: a diferencia de las variables simples (que se copian), los arreglos se pasan mediante su dirección de memoria. Tanto el arreglo original como el parámetro de la función apuntan a la misma ubicación, permitiendo modificaciones directas del contenido original.
Esto explica dos situaciones clave:
Efectos secundarios en arreglos¶
Al recibir la “dirección de la casa”, los cambios en el arreglo que pasamos a la función, se reflejan en el arreglo original, efectivamente dando un resultado por fuera del retorno explícito de la función.
Esta situación se puede dar cuando utilizamos al arreglo como una variable más.
1 2 3 4 5 6 7 8
int maximo(int *arreglo, size_t size) { for (size_t i = 1; i < size; i++) { if (arreglo[i] > arreglo[0]) { arreglo[0] = arreglo[i]; } } return arreglo[0]; }
Contraejemplo con efectos secundarios destructivos
El problema principal del código es el efecto secundario destructivo. La función no solo calcula el valor máximo, sino que también modifica de manera irreversible el arreglo que se le pasa como argumento.
Este tipo de comportamiento puede llevar a errores sutiles y difíciles de
depurar, especialmente en programas grandes donde el arreglo original podría ser
necesario para operaciones posteriores. Por ejemplo, si el valor original en la
posición 0
fuera crucial para otro cálculo, esa información se perdería
permanentemente. Para evitar modificaciones no deseadas, es una buena práctica usar el calificador const
en los parámetros de arreglo que no deben ser alterados, adhiriendo a la regla de estilo Regla 0x0021h
: Los argumentos de tipo puntero deben ser const
siempre que la función no los modifique.
Resolver este problema solo requiere que agreguemos una variable para reemplazar
arreglo[0]
, pero es un buen contraejemplo de un uso negativo de los efectos
secundarios de los arreglos.
Si la función (o procedimiento) modifica el arreglo, esto debe estar claramente expresado en la documentación.
Y aunque parezca algo negativo, la utilización de efectos secundarios en arreglos es sumamente importante, y se utiliza en código que, por ejemplo, ordene los valores que contiene.
Pérdida de sizeof
¶
Dentro de la función, sizeof(arreglo)
no funciona como se espera. La función
solo conoce la dirección en memoria del arreglo, no el tamaño total de la
secuencia original.
/**
* Esta funcion, ¡no cumple con su objetivo!
* para cualquier arreglo que le pasemos;
* ¡vamos a obtener el mismo valor!
*/
size_t tamanio_arreglo(int arreglo[]) {
printf("Tamaño del arreglo: %zu\n", sizeof(arreglo));
// El arreglo es siempre de tamaño 8 (la dirección)
printf("Tamaño de un valor: %zu\n", sizeof(arreglo[0]));
// El valor apuntado va a ser siempre `int` con 4 bytes.
return sizeof(arreglo) / sizeof(arreglo[0]);
}
int main() {
int arreglo1[] = {10, 20, 30, 40, 50};
int arreglo2[20];
size_t uno = tamanio_arreglo(arreglo1);
size_t dos = tamanio_arreglo(arreglo2);
printf("Tamaño de arreglo1: %zu\n", uno); // obtenemos 2
printf("Tamaño de arreglo2: %zu\n", dos); // obtenemos 2
return 0;
}
Esta situación se explica porque, teniendo en cuenta que sizeof
se resuelve en
tiempo de compilación, el operador no puede saber con qué arreglo vamos a llamar
a la función.
Por lo tanto, para que una función pueda trabajar sobre cualquier arreglo, se
debe pasar el tamaño de forma explícita, como un argumento separado. La firma correcta de la función debe incluir el tamaño del arreglo como parámetro. El uso de size_t
para el tamaño (Regla 0x002Eh
: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t
) y el hecho de pasar el tamaño explícitamente (Regla 0x0027h
: Verificá siempre los límites de los arreglos antes de acceder a sus elementos) son cruciales para la seguridad y portabilidad.
1 2 3 4 5 6 7 8 9 10 11
/** * Imprime los elementos de un arreglo de enteros en la salida estándar. * * @param arreglo de numeros a mostrar. * @param size la cantidad de valores en el arreglo. * * @post * - Los elementos del arreglo `arreglo` se han impreso. * - El `arreglo` no será modificado. */ void imprimir_arreglo(int arreglo[], size_t size);
Retorno de Secuencias desde Funciones¶
Una función no puede retornar un arreglo local. Cuando una función se ejecuta, obtiene un espacio de memoria temporal (un stack frame). Al retornar, ese espacio se considera libre y puede ser sobrescrito en cualquier momento. Devolver la dirección de una variable local es como devolver la llave de una habitación de hotel después de haber hecho el check-out: la llave puede seguir existiendo, pero la habitación ya no te pertenece.
Cadenas: Secuencias de Caracteres¶
Una cadena es un arreglo de char
que sigue una regla: el último carácter de
interés está seguido por un carácter nulo (\0
). Este terminador es
crucial, ya que las funciones de biblioteca como strlen
o printf
con %s
dependen de él para saber dónde termina el texto.
El comportamiento general de una cadena, es el mismo que el de un arreglo.
Por ejemplo, la siguiente cadena:
char cadena[7] = "Hola";
Figure 4:Representación en memoria de una cadena con terminador nulo. La cadena “Hola” ocupa 5 bytes (4 caracteres + ‘\0’), pero el arreglo tiene capacidad para 7. Los bytes no inicializados contienen basura. El terminador ‘\0’ marca el fin lógico de la cadena y es esencial para las funciones de string.h.
En donde los ?
quizás sean cero (\0
), pero como no está inicializado, no
podemos estar seguros del valor que tendrá, a todos los efectos prácticos, es
basura.
También, si aplicamos el cálculo de tamaño usando sizeof
que vimos antes,
vamos a obtener el tamaño en bytes de la cadena.
1
size_t espacio_reservado = sizeof(mi_cadena) / sizeof(mi_cadena[0]);
Que casualmente, coincide con el largo del arreglo, esto es porque char
suele ocupar 1-byte por la estrategia de codificación empleada, el código
ASCII
Por lo tanto, el espacio_reservado
tendría 7
, y no el largo de la cadena que
es 4
.
Inicialización de una cadena¶
Al declarar una cadena usando la sintaxis de arreglo, estás creando una copia
local y mutable del texto basada en el Literal de cadena; el texto entre
comillas dobles ("
), sumando un carácter más para el terminador \0
.
char mi_cadena[] = "Texto inicial";
Es sumamente importante destacar que los literales, que no están asignados a una
variable arreglo (char []
), no pueden ser modificados. Esto significa que
nuestros programas no funcionaran si pasamos como argumento un literal a una
función que modifica dicha cadena.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
void ordena_caracteres(char cadena[]) { size_t n = strlen(cadena); for (size_t i = 0; i < n - 1; i++) { for (size_t j = 0; j < n - i - 1; j++) { if (cadena[j] > cadena[j + 1]) { char temp = cadena[j]; cadena[j] = cadena[j + 1]; cadena[j + 1] = temp; } } } } int main() { char mi_cadena[] = "ejemplo de cadena desordenada"; printf("Cadena original: \"%s\"\n", mi_cadena); // Llama a la función para ordenar la cadena. ordena_caracteres(mi_cadena); printf("Cadena ordenada: \"%s\"\n", mi_cadena); //ademas, ¿en donde modificamos la cadena? ordena_caracteres("ejemplo de cadena desordenada"); return 0; }
Modificando cadenas
Y la salida sería:
1 2 3
Cadena original: "ejemplo de cadena desordenada" Cadena ordenada: " aaaacdddddeeeeeejlmnnooprs" Segmentation fault (core dumped)
Ese Segmentation fault (core dumped)
es intentar modificar algo que no debía
ser modificado, por lo que es importante que utilicen una variable de cadena en
el medio para evitar este tipo de errores.
Más adelante veremos detalles adicionales sobre los literales de cadena y que situaciones nos podemos encontrar si no los utilizamos con cuidado.
Largo de cadenas¶
Para obtener el largo de una cadena, podemos usar strlen
, definido en
<string.h>
.
Esta función está definida de la siguiente forma:
size_t strlen( char str );
Y se encarga de recorrer la cadena hasta encontrarse un carácter nulo (\0
)
Lectura Segura de Cadenas¶
El uso de scanf("%s", buffer)
es una de las fuentes de errores de seguridad
más comunes en C. La alternativa segura es fgets
, como lo recomienda la regla de estilo Regla 0x001Ch
: Preferí fgets
sobre gets
y scanf
para leer cadenas.
1 2 3 4 5 6 7 8
char buffer[100]; fgets(buffer, sizeof(buffer), stdin); // fgets puede incluir el salto de línea ('\n'). Es buena práctica removerlo. size_t len = strlen(buffer); if (len > 0 && buffer[len - 1] == '\n') { buffer[len - 1] = '\0'; }
Biblioteca Estándar <string.h>
: Un Vistazo rápido¶
Las funciones de biblioteca para manipular cadenas más importantes:
strcpy(dest, src)
: Copiasrc
endest
, incluyendo\0
. No comprueba el tamaño dedest
, siendo muy peligrosa.strncpy(dest, src, n)
: Copia hastan
caracteres. Cuidado: si la longitud desrc
esn
o más, no añade el\0
. La terminación nula debe ser asegurada manualmente.strcmp(s1, s2)
: Compara las cadenas carácter por carácter según su valor numérico. Devuelve<0
sis1<s2
,0
si son iguales, y>0
sis1>s2
.strcat(dest, src)
: Busca el\0
endest
y copiasrc
a partir de ahí. También es insegura por no verificar el tamaño.
Más información sobre string.h
y otras funciones disponibles.
Glosario¶
size_t
- Según el estándar ISO C de 1999 (C99), size_t es un tipo de dato entero
sin signo de al menos 16 bits (en las secciones 7.17 y 7.18.3). Este se utiliza
para representar el tamaño de un objeto. Las funciones de la biblioteca que
toman o devuelven tamaños esperan que estos sean de este tipo. Además, el
operador
sizeof
, que es evaluado en tiempo de compilación, debe dar como resultado un valor que debe ser al menos, compatible. Para utilizarlo, es necesario importarstddef.h
ostdlib.h
. Esto también implica que unsize_t
es un tipo garantizado para contener cualquier índice de un arreglo. Para mas información, ver: CPPReference -size_t
- Literal de cadena
- Es el texto que se escribe directamente en el código, encerrado entre comillas
dobles, como “Texto inicial”. Piensa en él como una plantilla de texto
original y constante que el programa crea al compilarse. Este literal se
almacena en una parte de la memoria del programa que se considera fija y no debe
alterarse. Cuando creas un arreglo como
char mi_cadena[] = "Texto inicial";
, lo que realmente sucede es que el contenido de este literal se copia al arreglo, permitiendo su modificación.
Ejercicios¶
Solution to Exercise arreglos-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <stdio.h> #include <stddef.h> int main() { int numeros[] = {10, 20, 30, 40, 50, -10}; int suma = 0; size_t cantidad = sizeof(numeros) / sizeof(numeros[0]); for (size_t i = 0; i < cantidad; i++) { suma += numeros[i]; } printf("El arreglo tiene %zu elementos.\n", cantidad); printf("La suma de los elementos es: %d\n", suma); return 0; }
Solution to Exercise funciones-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
#include <stdio.h> #include <stddef.h> float promedio_arreglo(int arreglo[], size_t cantidad) { int suma = 0; for (size_t i = 0; i < cantidad; i++) { suma = suma + arreglo[i]; } // Hacemos un cast a float para asegurar una división con decimales return (float)suma / cantidad; } int main() { int notas[] = {8, 7, 10, 9, 6}; size_t cantidad_notas = sizeof(notas) / sizeof(notas[0]); float prom = promedio_arreglo(notas, cantidad_notas); printf("El promedio de las notas es: %.2f\n", prom); return 0; }
Solution to Exercise funciones-2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
#include <stdio.h> #include <stddef.h> void invertir_arreglo(int arreglo[], size_t cantidad) { // Si no hay elementos o hay uno solo, no hay nada que hacer if (cantidad < 2) { return; } // Iteramos hasta la mitad del arreglo for (size_t i = 0; i < cantidad / 2; i++) { // Intercambiamos el elemento i con su correspondiente desde el final int temporal = arreglo[i]; arreglo[i] = arreglo[cantidad - 1 - i]; arreglo[cantidad - 1 - i] = temporal; } } void imprimir_arreglo(int arreglo[], size_t cantidad) { printf("[ "); for (size_t i = 0; i < cantidad; i++) { printf("%d ", arreglo[i]); } printf("]\n"); } int main() { int mi_arreglo[] = {1, 2, 3, 4, 5}; size_t n = sizeof(mi_arreglo) / sizeof(mi_arreglo[0]); printf("Arreglo original: "); imprimir_arreglo(mi_arreglo, n); invertir_arreglo(mi_arreglo, n); printf("Arreglo invertido: "); imprimir_arreglo(mi_arreglo, n); return 0; }
Solution to Exercise funciones-3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
#include <stdio.h> #include <string.h> #include <ctype.h> int contar_vocales(char cadena[]) { int contador = 0; size_t largo = strlen(cadena); for (size_t i = 0; i < largo; i++) { // Convertimos el caracter a minúscula para simplificar la comparación char caracter = tolower(cadena[i]); if (caracter == 'a' || caracter == 'e' || caracter == 'i' || caracter == 'o' || caracter == 'u') { contador++; } } return contador; } int main() { char texto[] = "Este Es un Ejemplo de Cadena"; int vocales = contar_vocales(texto); printf("La cadena: "%s"\n", texto); printf("Tiene %d vocales.\n", vocales); return 0; }
Solution to Exercise cadenas-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include <stdio.h> #include <string.h> int main() { char nombre[50]; printf("Por favor, ingresa tu nombre: "); // Leemos de forma segura desde la entrada estándar (stdin) fgets(nombre, sizeof(nombre), stdin); // Buscamos el salto de línea al final de la cadena size_t largo = strlen(nombre); if (largo > 0 && nombre[largo - 1] == '\n') { // Si lo encontramos, lo reemplazamos por el terminador nulo nombre[largo - 1] = '\0'; } printf("¡Hola, %s! Bienvenido.\n", nombre); return 0; }
Conceptos Clave¶
Este apunte introduce las secuencias (arreglos) como la primera estructura de datos para manejar colecciones homogéneas, y las cadenas como caso especial para texto.
Conexión con el Siguiente Tema¶
Los arreglos que estudiamos tienen una limitación crítica: tamaño fijo determinado en compilación. Si declaramos int arr[100]
, consumimos memoria para 100 enteros incluso si solo usamos 10. Si necesitamos 101, el programa no compila. Esta rigidez es problemática para software real que debe adaptarse a cantidades variables de datos.
Además, cuando pasamos arreglos a funciones, vimos que en realidad estamos pasando la dirección del primer elemento. ¿Qué significa “dirección”? ¿Cómo manipulamos estas direcciones directamente?
El próximo tema introduce conceptos que profundizan en cómo se organiza y manipula la memoria:
Los punteros (Punteros) como variables que almacenan direcciones
Las matrices (Matrices) como arreglos multidimensionales
La gestión de memoria (Gestión de Memoria Dinámica en C) para estructuras dinámicas
Los punteros son el concepto más poderoso y peligroso de C. Dominando punteros y arreglos simultáneamente, se comprende la esencia del lenguaje: control directo sobre la memoria con la sintaxis mínima necesaria.
Pregunta puente: Cuando escribimos int arr[5]
, ¿dónde exactamente en la memoria se almacenan estos 5 enteros? ¿Cómo accede la CPU a arr[3]
? La respuesta requiere entender direcciones de memoria, lo que nos lleva naturalmente a los punteros.
Referencias y Lecturas Complementarias¶
Textos Fundamentales¶
Kernighan & Ritchie (2014). Capítulo 5: Pointers and Arrays. La relación fundamental entre punteros y arreglos.
King (2008). Capítulo 8: Arrays y Capítulo 13: Strings. Funciones de biblioteca y manipulación de cadenas.
Weiss (2014). Capítulo 3: Lists, Stacks, and Queues. Algoritmos sobre secuencias.
Algoritmos sobre Arreglos¶
Cormen et al. (2009). Capítulo 2: Getting Started (Insertion Sort), Capítulo 6: Heapsort, Capítulo 9: Medians and Order Statistics.
Sedgewick & Wayne (2011). Capítulo 2: Sorting. Visualizaciones excelentes de algoritmos de ordenamiento.
Cadenas y Procesamiento de Texto¶
POSIX String Handling - https://
pubs .opengroup .org /onlinepubs /9699919799 /basedefs /string .h .html Especificación estándar de funciones de cadenas.
Seacord (2013). Capítulo 2: Strings. Vulnerabilidades comunes con cadenas.
Recursos en Línea¶
C String Handling - https://
en .cppreference .com /w /c /string Referencia completa de
<string.h>
con ejemplos.
Sorting Algorithms Visualizations - https://
visualgo .net /en /sorting Visualizaciones interactivas de algoritmos de ordenamiento.
Big-O Cheat Sheet - https://
www .bigocheatsheet .com/ Complejidades de operaciones comunes sobre arreglos.
Herramientas¶
Valgrind - https://
valgrind .org/ Detección de accesos fuera de límites en arreglos.
AddressSanitizer - https://
github .com /google /sanitizers Instrumentación de compilador para detectar errores de memoria.
Uso:
gcc -fsanitize=address programa.c
Ejercicios y Práctica¶
LeetCode - Array Problems - https://
leetcode .com /tag /array/ Cientos de problemas clasificados por dificultad.
HackerRank - Data Structures - https://
www .hackerrank .com /domains /data -structures Arrays y strings con tests automáticos.
- Kernighan, B. W., & Ritchie, D. M. (2014). C Programming Language, 2nd Edition.
- King, K. N. (2008). C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company.
- Weiss, M. A. (2014). Data Structures and Algorithm Analysis in C (2nd ed.). Pearson.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.). Addison-Wesley. https://algs4.cs.princeton.edu/
- Seacord, R. C. (2013). Secure Coding in C and C++ (2nd ed.). Addison-Wesley.