C++
Arrays y cadenas de estilo C
Arrays de tamaño fijo, cómo se recorren, qué son las cadenas terminadas en cero y por qué son el puente hacia los punteros.
De dónde venimos
Hasta ahora cada dato tenía su propia variable: nota1, nota2. Si quisieras el promedio de 300 notas con lo visto en Variables y tipos, tendrías que declarar 300 variables. Evidentemente no es así como se hace.
Esta página introduce la primera estructura para guardar muchos valores del mismo tipo bajo un solo nombre, y combina directamente dos cosas que ya sabes: el bucle for para recorrerlos y las funciones para pasarlos a otro lado. Ese último punto trae una sorpresa que es, literalmente, la puerta de entrada a los punteros.
Qué es un array
Un array (arreglo) es un bloque de memoria contiguo que contiene un número fijo de elementos del mismo tipo. “Contiguo” significa que los elementos están pegados uno detrás de otro en memoria, sin huecos: el elemento 5 está inmediatamente después del 4.
#include <iostream>
int main() {
int notas[5] = {7, 4, 6, 3, 5}; // 5 enteros, inicializados
std::cout << "Primera nota: " << notas[0] << "\n"; // índice 0
std::cout << "Última nota: " << notas[4] << "\n"; // índice 4
notas[2] = 10; // modificar un elemento
std::cout << "Tercera nota modificada: " << notas[2] << "\n";
return 0;
}g++ -std=c++17 -Wall -o arrays arrays.cpp
./arraysPrimera nota: 7
Última nota: 5
Tercera nota modificada: 10Dos reglas que no se negocian:
- Los índices empiezan en 0. Un array de 5 elementos tiene índices válidos
0, 1, 2, 3, 4. No existenotas[5]. - El tamaño es fijo y se decide al compilar. No puedes agrandar un array después.
int notas[n];connleído por consola no es C++ estándar (aunque GCC lo acepte como extensión).
C++ no comprueba los límites de un array. Escribir notas[5] o notas[100] compila sin queja y accede a memoria que no te pertenece: puedes leer basura, corromper otra variable, o provocar un crash — y las tres cosas pueden pasar en ejecuciones distintas del mismo programa. Esto se llama buffer overflow (desbordamiento de búfer) y es el origen de una parte enorme de las vulnerabilidades de seguridad históricas escritas en C y C++. El compilador no te salvará: la responsabilidad de no salirte del rango es 100% tuya.
Recorrer un array
Con el for clásico, usando el índice:
#include <iostream>
int main() {
const int TOTAL = 5;
int notas[TOTAL] = {7, 4, 6, 3, 5};
int suma = 0;
for (int i = 0; i < TOTAL; ++i) { // ojo: i < TOTAL, no i <= TOTAL
suma += notas[i];
}
std::cout << "Suma: " << suma << "\n";
std::cout << "Promedio: " << static_cast<double>(suma) / TOTAL << "\n";
return 0;
}Suma: 25
Promedio: 5Dos detalles heredados de páginas anteriores: la condición es i < TOTAL (con <= te pasarías un elemento, el error clásico llamado off-by-one), y el static_cast<double> está ahí para evitar la división entera que convertiría 25 / 5 en un entero.
El for basado en rango
Desde C++11 puedes recorrer el array sin índices, que es la forma preferida cuando no necesitas saber en qué posición estás:
#include <iostream>
int main() {
int notas[5] = {7, 4, 6, 3, 5};
for (int nota : notas) { // "para cada nota dentro de notas"
std::cout << nota << " ";
}
std::cout << "\n";
return 0;
}7 4 6 3 5 Al no haber índice, es imposible salirse del rango. Por eso se prefiere siempre que sea posible.
Los arrays de estilo C que ves en esta página no son lo que usarías en C++ moderno para una lista de datos. Lo idiomático hoy es std::vector<int>, de la Standard Library: crece dinámicamente, conoce su propio tamaño (.size()), y libera su memoria sola. Se explican aquí porque siguen apareciendo en todas partes (código C, APIs de sistema, librerías como Dear ImGui) y porque entender su relación con los punteros es imprescindible para entender C++ por dentro. Toma esta página como “cómo funciona la maquinaria”, no como “cómo escribir código nuevo”.
Arrays y funciones: la primera sorpresa
Aquí es donde el modelo mental de la página anterior se rompe. En Funciones quedó establecido que los argumentos se pasan por valor, es decir, se copian. Con los arrays, no:
#include <iostream>
void ponerEnCero(int datos[], int cantidad) {
for (int i = 0; i < cantidad; ++i) {
datos[i] = 0; // modifica el array ORIGINAL
}
}
int main() {
int notas[5] = {7, 4, 6, 3, 5};
ponerEnCero(notas, 5);
std::cout << notas[0] << " " << notas[4] << "\n"; // imprime 0 0
return 0;
}0 0No hicimos nada especial —no hay ningún &— y aun así la función modificó el original. La razón es que un array, al pasarlo a una función, se degrada (decay) a un puntero a su primer elemento. La función no recibe 5 enteros: recibe una dirección de memoria. Y con esa dirección puede tocar los datos reales.
De ahí viene el segundo parámetro int cantidad. Como la función solo recibe una dirección, pierde por completo la información del tamaño: dentro de ponerEnCero, no existe forma de saber cuántos elementos hay. Por eso, en C y en C++ de estilo C, el tamaño siempre viaja como un parámetro aparte. Si te equivocas al pasarlo, la función escribirá fuera del array sin enterarse. Esta es la razón práctica más fuerte para preferir std::vector, que lleva su tamaño consigo.
Dentro de main(), en cambio, el array sí conoce su tamaño, y puedes obtenerlo con sizeof:
int notas[5] = {7, 4, 6, 3, 5};
int cantidad = sizeof(notas) / sizeof(notas[0]); // 20 bytes / 4 bytes = 5
Ese truco de sizeof funciona solo donde el array fue declarado. Si lo usas dentro de una función que recibió el array como parámetro, sizeof(datos) te dará el tamaño de un puntero (típicamente 8 bytes), no el del array — y el cálculo dará un resultado sin sentido, normalmente 2. Es un error muy frecuente y silencioso.
Cadenas de estilo C
Una cadena de estilo C (C-string) es simplemente un array de char que termina con un carácter especial: el carácter nulo '\0'. Ese terminador es la única marca de dónde acaba el texto.
#include <iostream>
#include <cstring> // funciones de C-strings: strlen, strcpy...
#include <string> // std::string
int main() {
const char saludo[] = "Hola"; // ocupa 5 chars: 'H','o','l','a','\0'
std::string moderno = "Hola"; // la versión de la Standard Library
std::cout << "C-string: " << saludo << " (largo " << std::strlen(saludo) << ")\n";
std::cout << "std::string: " << moderno << " (largo " << moderno.size() << ")\n";
return 0;
}g++ -std=c++17 -Wall -o cadenas cadenas.cpp
./cadenasC-string: Hola (largo 4)
std::string: Hola (largo 4)Aunque "Hola" tiene 4 letras, el array saludo ocupa 5 bytes: el '\0' invisible del final cuenta. strlen devuelve 4 porque cuenta caracteres hasta el terminador, sin incluirlo.
Si el '\0' falta o se sobrescribe, funciones como std::strlen o std::cout << saludo siguen leyendo memoria hacia adelante hasta encontrar por casualidad un byte cero. El resultado puede ser texto basura o un crash. Este es el motivo por el que std::string (que guarda su longitud explícitamente y gestiona su propia memoria) es la opción correcta en C++ salvo que estés obligado a interoperar con una API de C.
Cuando una librería en C te pida un const char* y tú tengas un std::string, no reescribas nada: miString.c_str() te devuelve la versión C-string equivalente. Es exactamente lo que harás al pasar textos a funciones de Dear ImGui.
Siguiente paso
Este capítulo dejó dos cabos sueltos deliberados: qué es exactamente eso a lo que “se degrada” un array, y por qué una función puede modificar datos ajenos sin usar &. La respuesta a ambos es la misma, y es el tema de la próxima página: Punteros.