Prg78_VectoresCadenas
Prg78_VectoresCadenas
Prg78_VectoresCadenas
Cadenas
Programación
Curso 2017/18
Objetivos Contenido
En este documento aprenderás a: 1. ¿Por qué hay tantas estructuras de datos?
● Crear e inicializar vectores 2. Introducción a las estructuras de datos
● Almacenar información en un vector estáticas
● Acceder a los elementos de un vector 3. Vectores estáticos
● Crear e inicializar cadenas 4. Cadenas (de caracteres) estáticas
● Llamar a funciones de biblioteca para traba- 5. Vectores multidimensionales
jar con cadenas 6. Memoria dinámica
● Crear vectores dinámicos
Bibliografía
Joyanes Aguilar, J. “Programación en C++. Algoritmos, estructuras de datos y Objetos”.
Capítulos 7 y 9. Ed. McGraw-Hill.
Pont, M.J. “Software Engineering with C++ and CASE Tools”. Capítulo 7. Pointers and Vectores.
Ed. Addison-Wesley.
Marzal, A., García, I. "Introducción a la Programación con C". Capítulo 2. Estructuras de datos
en C. Dpto. de Lenguajes y Sistemas Informárticos, Universitat Jaume I. ISBN:
978-84-693-0143-2.
VECTORES Y CADENAS
1. ¿Por qué hay tantas estructuras de datos?
Estamos acostumbrados a usar en nuestra vida cotidiana estructuras de datos sin darnos cuenta. Nuestro
cerebro es un complejo sistema de procesamiento de información, y la información se almacena y manipu-
la en estructuras.
Los datos más simples necesitan estructuras simples. "Tengo 38 años" es un enunciado que transmite una
determinada información que todos han comprendido sin esfuerzo. La información "viaja" transportada en
un dato: 38. Es un dato simple de tipo número entero.
Puedo sustituir la frase anterior por "Tengo X años", donde X es cualquier número entero. Entonces deci-
mos que X es una variable de tipo entero, porque puede ser asignada a cualquier número entero. Así no
tengo un solo enunciado, sino toda una colección de enunciados diferentes y válidos que responden al pa-
trón genérico de "Tengo X años".
Cuando manipulamos conjuntos mayores de datos, disponemos de otras estructuras más complejas, como
los vectores o las matrices. Un vector es una colección de elementos del mismo tipo. Cada elemento se
identifica con un número llamado índice.
He aquí un vector de números enteros:
+---+---+---+----+----+----+---+---+---+----+
v = | 5 | 7 | 2 | 23 | 18 | 19 | 7 | 5 | 3 | 19 |
+---+---+---+----+----+----+---+---+---+----+
Una sola variable (v) es capaz de almacenar y manipular muchos números. v[0] será el primero de ellos,
v[1] será el segundo, etc. "Tengo v[4] años" es un enunciado tan válido como "Tengo X años". v[4] es
una variable entera simple. v es una variable compleja: un vector de enteros.
Resolver determinados problemas es muchísimo más sencillo utilizando vectores que utilizando variables
simples. Un ejemplo: un programa que genere una combinación válida para jugar a la lotería primitiva. Es
decir, que genere seis números diferentes entre 1 y 49. He aquí un algoritmo:
1. i = 1
2. v[i] = un número al azar entre 1 y 49
3. Comprobar que ese número no se haya elegido ya,
es decir, que v[i] no sea igual a ningún v[j],
para cualquier valor de j menor que i
4. i = i +1
5. Repetir los pasos 2, 3 y 4 hasta que i > 6
• Estáticas: son aquéllas que ocupan un espacio determinado en la memoria del ordenador. Este espa-
cio es invariable y lo especifica el programador durante la escritura del código fuente.
• Dinámicas: sin aquéllas cuyo espacio ocupado en la memoria puede modificarse durante la ejecución
del programa.
Las estructuras estáticas son mucho más sencillas de manipular que las dinámicas, y son suficientes para
resolver la mayoría de los problemas. Las estructuras dinámicas, de manejo más difícil, permiten aprove-
char mejor el espacio en memoria y tienen aplicaciones más específicas.
Además, se pueden mencionar como una clase de estructura de datos diferente las estructuras externas,
entendiendo como tales aquéllas que no se almacenan en la memoria principal (RAM) del ordenador, sino
en alguna memoria secundaria (típicamente, un disco duro). Las estructuras externas, que también pode-
mos denominar archivos, son en realidad estructuras dinámicas almacenadas en memoria secundaria.
En las siguientes semanas nos dedicaremos a estudiar con detalle los tres tipos de estructuras.
3. Vectores estáticos
Un vector (en inglés, «array») es una secuencia de valores a los que podemos acceder mediante índices que
indican sus respectivas posiciones. Los vectores tienen una limitación fundamental: todos los elementos del
vector han de tener el mismo tipo. Podemos definir vectores de enteros, vectores de flotantes, etc., pero
no podemos definir vectores que, por ejemplo, contengan a la vez enteros y flotantes. El tipo de los ele-
mentos de un vector se indica en la declaración del vector.
C nos permite trabajar con vectores estáticos y dinámicos. En esta sección nos ocuparemos únicamente de
los denominados vectores estáticos, que son aquellos que tienen tamaño fijo y conocido en tiempo de
compilación. Es decir, el número de elementos del vector no puede depender de datos que suministra el
usuario: se debe hacer explícito mediante una expresión que podamos evaluar examinando únicamente el
texto del programa.
En una misma línea puedes declarar más de un vector, siempre que todos compartan el mismo tipo de da-
tos para sus componentes. Por ejemplo, en esta línea se declaran dos vectores de números reales (float),
uno con 20 componentes y otro con 100:
float a[20], b[100];
También es posible mezclar declaraciones de vectores y escalares en una misma línea. En este ejemplo se
declaran las variables a y c como vectores de 80 caracteres y la variable b como escalar de tipo carácter:
char a[80], b, c[80];
Se considera mal estilo declarar la dimensión de los vectores con literales de entero. Es preferible utilizar
algún identificador para la dimensión, pero teniendo en cuenta que éste debe corresponder a una cons-
tante:
#define DIM 80
char a[DIM];
Esta otra declaración es incorrecta, pues usa una variable para definir la dimensión del vector1:
int dim = 80;
...
char a[dim]; /* !No siempre es válido! */
Puede que consideres válida esta otra declaración que prescinde de constantes definidas con define y usa
constantes declaradas con const, pero no es así:
const int dim = 80;
char a[dim]; /* !No siempre es válido! */
Una variable const es una variable en toda regla, aunque de «sólo lectura».
Observa que el acceso a elementos del vector sigue la siguiente notación: usamos el identificador del vec-
tor seguido del índice encerrado entre corchetes. En una ejecución del programa obtuvimos este resultado
en pantalla (es probable que obtengas resultados diferentes si repites el experimento):
2293652
1
Como siempre, hay excepciones: el estándar C99 permite declarar la dimensión de un vector con una expresión cuyo valor sólo se
conoce en tiempo de ejecución, pero sólo si el vector es una variable local a una función. Para evitar confusiones, no haremos
uso de esa característica en este tema y lo consideraremos incorrecto.
3. Vectores estáticos 6
4200718
4200624
5777664
128
Evidentemente, no son cinco ceros. Podemos inicializar todos los valores de un vector a cero con un bucle
for:
Programa 2: inicializados_a_cero.c
1 #include <stdio.h>
2
3 #define DIM 10
4
5 int main(void)
6 {
7 int i, a[DIM];
8
9 for(i=0; i<DIM; i++)
10 a[i] = 0;
11
12 for(i=0; i<DIM; i++)
13 printf("%d\n", a[i]);
14
15 return 0;
16 }
Hay una forma alternativa de inicializar vectores. En este fragmento de código C se definen e inicializan dos
vectores, uno con todos sus elementos a 0 y otro con una secuencia ascendente de números:
#define DIM 5
...
int a[DIM] = {0, 0, 0, 0, 0};
int b[DIM] = {1, 2, 3, 4, 5};
Ten en cuenta que, al declarar e inicializar simultáneamente un vector, debes indicar explícitamente los
valores del vector y, por tanto, esta aproximación sólo es factible para la inicialización de unos pocos valo-
res.
int main(void)
{
int i, a[10], b[10];
return 0;
}
Las dimensiones de a y b aparecen en seis lugares. Imagina que deseas modificar el programa para que
el vector a pase a tener 20 enteros: tendrás que modificar sólo tres de esos dieces. Esto te obliga a leer
el programa detenidamente y, cada vez que encuentres un 10, pensar si ese 10 en particular es o no es la
dimensión de a. Complicado. Estudia esta versión:
include <stdio.h>
#define DIM_A 10
#define DIM_B 10
int main(void)
{
int i, a[DIM_A], b[DIM_B];
El compilador actúa como un ciego que cuenta por pasos la distancia desde una casa. Comienza en la pri-
mera casa, PaseoZorrilla[0]. Cuando le pedimos que vaya a la sexta casa del Paseo Zorrilla, él se dice:
“Debo ir cinco casas más allá. Cada casa mide cuatro pasos largos. Por tanto, debo andar 20 pasos”. Si le
pedimos que vaya a PaseoZorrilla[100] y el Paseo Zorrilla solo tiene 75 casas, él contará 400 pasos, y
con mucha probabilidad, se pare delante… ¡de un autobús! si no se ha caído antes al río. El programa 2
muestra lo que sucede cuando escribimos valores más allá del final del vector.
PRECAUCIÓN: ¡No ejecutes este programa, puede bloquear tu ordenador!
Salida
Test 1:
paseoZorrilla[0] : 0
paseoZorrilla[20]: 20
3. Vectores estáticos 9
Asignando...
Test 2:
paseoZorrilla[0] : 0
paseoZorrilla[20]: 20
paseoZorrilla[21]: 21
calleUno[0]: 20
calleUno[1]: 21
calleUno[2]: -1
calleDos[0]: -2
calleDos[1]: -2
calleDos[2]: -2
Análisis:
Primero se declaran dos vectores de tres enteros que actúan como centinelas alrededor de
paseoZorrilla. Estos vectores centinelas se inicializan con los valores -1 y -2. Si escribimos algo en la
memoria después del final de paseoZorrilla, alguno de los centinelas cambiará probablemente de valor.
Algunos ordenadores asignan memoria de arriba abajo (de direcciones altas a direcciones bajas) mientras
que otros lo hacen de abajo hacia arriba (de direcciones bajas a direcciones altas). Por esta razón, hemos
colocado centinelas a ambos lados de paseoZorrilla.
Después se asignan valores a los miembros de paseoZorrilla, pero el contador cuenta hasta los subín-
dices 20 y 21, que no existen en paseoZorrilla.
Podemos observar que al imprimir paseoZorrilla[20] se escribe sin ningún problema el valor 20. Sin
embargo, cuando se imprimen calleUno y calleDos, observamos que calleUno[0] ha cambiado. Esto
se debe que la zona de memoria de paseoZorrilla[20] coincide con la zona de memoria de
calleUno[0].
d) Realizar alguna operación que implique a todos los elementos. Por ejemplo, sumarlos:
suma = 0;
for (i = 0; i <= 9; i++)
{
suma = suma + v[i];
}
El resultado de las tres declaraciones es, en principio, idéntico, porque todas indican al compilador que se
va a recibir la dirección de un vector de números enteros.
Dentro de la función, el vector puede usarse del mismo modo que en el programa que la llama, es decir, no
es preciso utilizar el operador de indirección, '*'.
Por ejemplo: Un programa que sirve para leer 50 números por teclado, y calcular la suma, la media y la
desviación típica de todos los valores. La desviación es una magnitud estadística que se calcula restando
cada valor del valor medio, y calculando la media de todas esas diferencias.
Observa el siguiente programa de ejemplo detenidamente, prestando sobre todo atención al uso de los
vectores y a cómo se pasan como parámetros.
3. Vectores estáticos 11
Los números de la serie se almacenarán en un vector float de 50 posiciones llamado valores. La in-
troducción de datos en el vector se hace en la función introducir_valores(). No es necesario usar el
símbolo & al llamar a la función, porque los vectores siempre se pasan por dirección. Por lo tanto, al modi-
ficar el vector dentro de la función, también se modificará en la función desde donde se la llama.
Después, se invoca a tres funciones que calculan las tres magnitudes. El vector también se pasa por direc-
ción a estas funciones, ya que en C no hay modo de pasar un vector por valor.
#include <stdio.h>
#include <math.h>
int main(void)
{
float valores[50];
float suma, media, desviacion;
introducir_valores(valores);
suma = calcular_suma(valores);
media = calcular_media(valores, suma);
desviacion = calcular_desviacion(valores, media);
printf("La suma es %f, la media es %f y la desviación es %f", suma, media,
desviacion);
return 0;
}
/* Lee 50 números y los almacena en el vector N pasado por variable */
void introducir_valores(float N[])
{
int i;
for (i=1; i<=49; i++)
{
printf("Introduzca el valor nº %d: ", i);
scanf("%f", &N[i]);
}
}
/* Devuelve la suma todos los elementos del vector N */
float calcular_suma(float N[])
{
int i;
float suma;
suma = 0;
for (i=1; i<=49; i++)
suma = suma + N[i];
return suma;
}
/* Devuelve el valor medio de los elementos del vector N. Necesita conocer la
suma de los elementos para calcular la media */
float calcular_media(float N[50], float suma)
{
int i;
float media;
media = suma / 50;
return media;
}
/* Calcula la desviación típica de los elementos del vector N. Necesita conocer
la media para hacer los cálculos */
float calcular_desviacion(float N[], float media)
{
int i;
3. Vectores estáticos 12
float diferencias;
diferencias = 0;
for (i=1; i<=49; i++)
diferencias = diferencias + abs(N[i] – media) ;
diferencias = diferencias / 50;
return diferencias;
}
El lenguaje C no permite comparar vectores directamente. Si quieres comparar dos vectores, has de hacerlo
elemento a elemento:
Programa 3. compara_vectores.c
1 #define DIM 3
2
3 int main (void)
4 {
5 int original[DIM] = { 1, 2, 3};
6 int copia[DIM] ={1,1+1,3};
7 int i, son_iguales;
8
9 son_iguales = 1; /* Suponemos elementos iguales dos a dos. */
10 i = 0;
11 while (i < DIM && son_iguales) {
12 /* Pero basta con que dos elementos no sean iguales
13 para que los vectores sean distintos. */
14 if (copia[i] != original[i])
15 son_iguales = 0;
16 i++;
17 }
18
19 if (son_iguales)
20 printf ("Son iguales\n");
21 else
22 printf("No son iguales\n");
23
24 return 0;
25 }
4. Cadenas (de caracteres) estáticas 13
Resumen de vectores
Para declarar un vector, escribe el tipo de variable a almacenar, seguido del nombre del vector y
de un índice con el número de elementos que tendrá el vector.
Ejemplo:
int a[90];
int b = a[8];
a esta otra:
Fíjate en que la zona de memoria asignada a a sigue siendo la misma. El «truco» del final de cadena ha
permitido que la cadena decrezca. Podemos conseguir también que crezca a voluntad... pero siempre que
no se rebase la capacidad del vector.
Hemos representado las celdas a la derecha del terminador como cajas vacías, pero no es cierto que lo es-
tén. Lo normal es que contengan valores arbitrarios, aunque eso no importa mucho: el convenio de que la
cadena termina en el primer carácter nulo hace que el resto de caracteres no se tenga en cuenta. Es posible
que, en el ejemplo anterior, la memoria presente realmente este aspecto:
Por comodidad representaremos las celdas a la derecha del final de cadena con cajas vacías, pues no im-
porta en absoluto lo que contienen. ¿Qué ocurre si intentamos inicializar una zona de memoria reservada
para sólo 10 caracteres con una cadena de longitud mayor que 9?
charo[10] = "supercalifragilisticoespialidoso"; /* ¡Mal! */
Estaremos cometiendo un gravísimo error de programación que, posiblemente, no detecte el compilador.
Los caracteres que no caben en a se escriben en la zona de memoria que sigue a la zona ocupada por a.
Ya vimos en un apartado anterior las posibles consecuencias de ocupar memoria que no nos ha sido reser-
vada: puede que modifiques el contenido de otras variables o que trates de escribir en una zona que te está
vetada, con el consiguiente aborto de la ejecución del programa.
4. Cadenas (de caracteres) estáticas 15
Como resulta que en una variable cadena con capacidad para, por ejemplo, 80 caracteres, sólo caben real-
mente 79 caracteres (aparte del nulo), adoptaremos una curiosa práctica al declarar variables de cadena
que nos permitirá almacenar los 80 caracteres (además del nulo) sin crear una constante confusión con
respecto al número de caracteres que caben en ellas:
1 #include <stdio.h>
2
3 #define MAXLOM 80
4 int main (void)
5 {
6 char cadena[MAXLON+1]; /* Reservamos 81 caracteres: 80 caracteres
7 más el final de cadena */
8 return 0;
9 }
salida_cadena_con_modificadores.c
1 #include <stdio.h>
2
3 #define MAXL0N 80
4
5 int main (void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8
9 printf("El valor de cadena es (%s).\n", cadena);
10 printf("El valor de cadena es (%20s).\n", cadena);
11 printf("El valor de cadena es (%-20s).\n", cadena);
12
13 return 0;
14 }
El valor de cadena es (una cadena).
El valor de cadena es ( una cadena).
El valor de cadena es (una cadena ).
¿Y si deseamos mostrar una cadena carácter a carácter? Podemos hacerlo llamando a la función printf()
sobre cada uno de los caracteres, pero recuerda que la marca de formato asociada a un carácter es %c:
Programa 5: salida_caracter_a_caracter.c
1 #include <stdio.h>
2
3 #define MAXL0N 80
4
5 int main (void)
6 {
7 char cadena[MAXLON+1] = "una cadena";
8 int i;
9
10 i = 0;
11 while(cadena[i]!=0) {
12 printf("%c\n", cadena[i]);
13 i++;
14 }
15
16 return 0;
17 }
Resultado de la ejecución:
u
n
a
c
a
d
e
n
a
4. Cadenas (de caracteres) estáticas 17
¡Ojo! ¡No hemos puesto el operador & delante de cadena! ¿Es un error? No. Con las cadenas no hay que
escribir el operador de dirección & delante del identificador al usar la función scanf(). ¿Por qué? Porque
la función scanf() espera una dirección de memoria y el identificador, por la dualidad vector-puntero, ¡es
una dirección de memoria!
Recuerda: cadena[0] es un char, pero cadena, sin más, es la dirección de memoria en la que
empieza el vector de caracteres.
Ejecutemos el programa e introduzcamos una palabra:
una
La cadena leida es una
Cuando la función scanf() recibe el valor asociado a cadena, recibe una dirección de memoria y, a partir
de dicha dirección, escribe los caracteres leídos de teclado. Debes tener en cuenta que si los caracteres
leídos exceden la capacidad de la cadena, se producirá un error de ejecución.
¿Y por qué lafunción printf() no muestra por pantalla una simple dirección de memoria cuando ejecu-
tamos la llamada printf("La cadena leida es %s.\n", cadena)? Si es cierto lo dicho, que lo es,
cadena es una dirección de memoria. La explicación es que la marca %s es interpretada por la función
printf() como «me pasan una dirección de memoria en la que empieza una cadena, así que he de mos-
trar su contenido carácter a carácter hasta encontrar un carácter nulo».
Programa 7: lee_frase_mal.c
1 #include <stdio.h>
2
3 #define MAXL0N 80
4
5 int main (void)
6 {
7 char cadena[MAXLON+1];
8
9 scanf("%s", cadena);
10 printf("La cadena leida es %s\n", cadena);
11
12 return 0;
13 }
¿Qué ha ocurrido con los restantes caracteres tecleados? ¡Están a la espera de ser leídos! La siguiente ca-
dena leída, si hubiera un nueva llamada a la función scanf(), sería "frase". Si es lo que queríamos, per-
fecto, pero si no, el desastre puede ser mayúsculo.
¿Cómo se puede leer, pues, una frase completa? No hay forma sencilla de hacerlo con la función scanf().
Tendremos que recurrir a una función diferente. La función gets() lee todos los caracteres que hay hasta
encontrar un salto de línea. Dichos caracteres, excepto el salto de línea, se almacenan a partir de la direc-
ción de memoria que se indique como argumento y se añade un final de cadena.
Aquí tienes un ejemplo:
1 #include <stdio.h>
2
3 #define MAXL0N 11
4
5 int main (void)
6 {
7 char a[MAXLON+1], b[MAXLON+1];
8
9 printf("Introduce una cadena: "); gets(a);
10 printf("Introduce otra cadena: "); gets(b);
11 printf("La primera es %s y la segunda es %s \n", a, b);
12
13 return 0;
14 }
Ejecutemos el programa:
Introduce una cadena: uno dos
Introduce otra cadena: tres cuatro
La primera es uno dos y la segunda es tres cuatro
4. Cadenas (de caracteres) estáticas 19
La función sscanf() es similar a la función scanf() (fíjate en la «s» inicial), pero no obtiene información
leyéndola del teclado, sino que la extrae de una cadena. Un ejemplo ayudará a entender el procedimiento:
Programa 8: lecturas.c
1 #include <stdio.h>
2
3 #define MAXLINEA 80
4 #define MAXFRASE 40
5
6 int main (void)
7 {
8 int a, b;
9 char frase[MAXFRASE+1];
10 char linea[MAXLINEA+1];
11
12 printf("Dame el valor de un entero: ");
13 gets(linea); sscanf(linea, "%d", &a);
14
15 printf("Introduce ahora una frase: ");
16 gets(frase);
17
18 printf("Y ahora dame el valor de otro entero: ");
19 gets(linea); sscanf(linea, "%d", &b);
20
21 printf("Enteros leidos: %d, %d.\n", a, b);
22 printf("Frase leida: %s.\n", frase);
23
24 return 0;
25 }
En el programa hemos definido una variable auxiliar, linea, que es una cadena con capacidad para 80 ca-
racteres más el final de cadena (puede resultar conveniente reservar más memoria para dicha variable en
según qué aplicación). Cada vez que deseamos leer un valor escalar, almacenamos en la variable linea el
texto que introduce el usuario y obtenemos el valor escalar con la función sscanf(). Dicha función recibe,
como primer argumento, la cadena linea; como segundo, una cadena con marcas de formato; y como ter-
cer parámetro, la dirección de la variable escalar en la que queremos almacenar el resultado de la lectura.
Es un proceso un tanto incómodo, pero al que tenemos que acostumbrarnos... de momento.
4. Cadenas (de caracteres) estáticas 20
Si compilas el programa, obtendrás un error que te impedirá obtener un ejecutable. Recuerda: los identifi-
cadores de vectores estáticos se consideran punteros inmutables y, a fin de cuentas, las cadenas son vec-
tores estáticos (más adelante aprenderemos a usar vectores dinámicos). Para efectuar una copia de una
cadena, has de hacerlo carácter a carácter.
1 #define MAXLON 10
2
3 int main (void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia [MAXLON+1];
7 int i;
8
9 for (i = 0; i <= MAXLON; i++)
10 copia[i] = original[i];
11
12 return 0;
13 }
Fíjate en que el bucle recorre los 10 caracteres que realmente hay en original pero, de hecho, sólo necesi-
tas copiar los caracteres que hay hasta el carácter final de cadena, incluyendo a dicho carácter.
1 #define MAXLON 10
2
3 int main (void)
4 {
5 char original [MAXLON+1] = "cadena";
6 char copia [MAXLON+1];
7 int i;
8
9 for(i=0; i<= MAXLON; i++){
10 copia[i] = original[i];
11 if(copia[i]=='\0')
12 break;
13 }
14
15 return 0;
16 }
4. Cadenas (de caracteres) estáticas 21
Observa que la condición del bucle for controla si hemos llegado al final de cadena o no. Como el finalde
cadena no llega a copiarse, lo añadimos tan pronto finaliza el bucle. Este tipo de bucles, aunque perfecta-
mente lícitos, pueden resultar desconcertantes.
El copiado de cadenas es una acción frecuente, así que hay funciones predefinidas para ello, accesibles
incluyendo la cabecera string.h:
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char original[MAXLON+1] = "cadena";
8 char copia [MAXLON+1];
9
10 strcpy(copia, original); /* Copia el contenido de original
11 en copia.*/
12 return 0;
13 }
Ten cuidado: la función strcpy() (abreviatura de «string copy») no comprueba si el destino de la copia
tiene capacidad suficiente para la cadena, así que puede provocar un desbordamiento. La función strcpy() se
limita a copiar carácter a carácter hasta llegar a un carácter nulo.
Tampoco está permitido asignar un literal de cadena a un vector de caracteres fuera de la zona de declara-
ción de variables. Es decir, este programa es incorrecto:
4. Cadenas (de caracteres) estáticas 22
1 #define MAXLON 10
2
3 int main (void)
4 {
5 char a[MAXLON+1] ;
6
7 a = "cadena"; /* ¡Mal! */
8
9 return 0;
10 }
Si deseas asignar un literal de cadena, tendrás que hacerlo con la ayuda de la función strcpy():
1 #include <string.h>
2
3 #define MAXLON 10
4
5 int main(void)
6 {
7 char a[MAXLON+1];
8
9 strcpy(a, "cadena");
10
11 return 0;
12 }
Calcular la longitud de una cadena es una operación frecuentemente utilizada, así que está predefinida en
la biblioteca de tratamiento de cadenas. Si incluimos la cabecera string.h, podemos usar la función
strlen() (abreviatura de «string length»):
4. Cadenas (de caracteres) estáticas 23
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXL0N+1] ;
9 int lon;
10
11 printf("Introduce una cadena (máx. %d cars.): ", MAXLON);
12 gets(a);
13 lon = strlen(a);
14 printf("Longitud de la cadena: %d\n", lon);
15
16 return 0;
17 }
La función strlen() hace lo mismo que hacía el primer programa, es decir, recorrer la cadena de izquier-
da a derecha incrementando un contador hasta llegar al carácter nulo. Esto implica que empleará más
tiempo cuanto más larga sea la cadena. Has de tener en cuenta la fuente de ineficiencia que puede suponer
utilizar directamente strlen() en lugares críticos como los bucles. Por ejemplo, esta función cuenta las
vocales minúsculas de una cadena leída por teclado:
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main (void)
7 {
8 char a[MAXL0N+1];
9 int i, contador;
10
11 printf("Introduce una cadena (máx. €d ucars.): ", MAXLON);
12 gets(a);
13 contador = 0;
14 for(i=0; i< strlen(a); i++)
15 if(a[i] == 'a' || a[i] == 'e' || a[i] == 'i' ||
16 a[i] == 'o' || a[i] == 'u')
17 contador++;
18 printf ("Vocales minusculas: %d\n", contador);
19
20 return 0;
21 }
Pero tiene un problema de eficiencia. Con cada iteración del bucle for se llama a la función strlen(), y
dicha función emplea un tiempo proporcional a la longitud de la cadena. Si la cadena tiene, por ejemplo, 60
caracteres, se llamará a la función strlen() 60 veces para efectuar la comparación, y para cada llamada,
strlen() tardará unos 60 pasos en devolver lo mismo: el valor 60. Esta nueva versión del mismo progra-
ma no presenta ese inconveniente:
4. Cadenas (de caracteres) estáticas 24
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main (void)
7 {
8 char a[MAXL0N+1];
9 int i, longitud, contador;
10
11 printf("Introduce una cadena (máx. €d ucars.): ", MAXLON);
12 gets(a);
13 longiutd = strlen(cadena);
14 contador = 0;
15 for(i=0; i< longitud; i++)
16 if(a[i] == 'a' || a[i] == 'e' || a[i] == 'i' ||
17 a[i] == 'o' || a[i] == 'u')
18 contador++;
19 printf ("Vocales minusculas: %d\n", contador);
20
21 return 0;
22 }
4.6. Concatenación
C no puede usar el operador + para concatenar cadenas. Una posibilidad es que las concatenes tú mismo «a
mano», con bucles. Este programa, por ejemplo, pide dos cadenas y concatena la segunda a la primera:
1 #include <stdio.h>
2
3 define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXL0N+1], b[MAXLON+1];
8 int longa, longb;
9 int i;
10
11 printf("Introduce untexto(máx. %d cars.): ", MAXLON); gets(a) ;
12 printf("Introduce otro texto(máx. %d cars.): ", MAXLON); gets(b);
13
14 longa = strlen(a);
15 longb = strlen(b);
16 for (i=0; i<longb; i++)
17 a[longa+i] = b[i];
18 a[longalongbl = '\0';
19 printf("Concatenacion de ambos: %s", a);
20
21 return 0;
22 }
1 #include <stdio.h>
2 #include <string.h>
3
4 #define MAXLON 80
5
6 int main(void)
7 {
8 char a[MAXL0N+1], b[MAXLON+1];
9
10 printf("Introduce un texto (máx. %d cars.): ", MAXLON);
11 gets(a);
12 printf ("Introduce otro texto (máx. %d cars.): ", MAXLON);
13 gets(.b);
14 strcat (a, b);
15 printf("Concatenacion de ambos: %s", a);
16
17 return 0;
18 }
Recuerda que es responsabilidad del programador asegurarse de que la cadena que recibe la concatena-
ción dispone de capacidad suficiente para almacenar la cadena resultante.
Un carácter no es una cadena
Un error frecuente es intentar añadir un carácter a una cadena con la función strcat() o
asignárselo como único carácter con la función strcpy():
char linea[10] = "cadena";
char caracter = 's';
strcat(linea, caracter); /* ¡Mal! */
strcpy (linea,’X’); /* ¡Mal! */
Recuerda: los dos datos de strcat() y strcpy() han de ser cadenas y no es aceptable que
uno de ellos sea un carácter.
5. Vectores multidimensionales
Podemos declarar vectores de más de una dimensión muy fácilmente:
int a [10][5]; float b[3][2][4];
En este ejemplo, a es una matriz (vector bidimensional) de 10x5 enteros y b es un vector de tres di-
mensiones con 3x2x4 números en coma flotante.
Puedes acceder a un elemento cualquiera de los vectores a o b utilizando tantos índices como dimensiones
tiene el vector: a[4][2] y b[1][0][3], por ejemplo, son elementos de a y b, respectivamente.
La inicialización de los vectores multidimensionales necesita tantos bucles anidados como dimensiones ten-
gan éstos:
5. Vectores multidimensionales 28
1 int main(void)
2 {
3 int a[10][5];
4 float b[3][2][4];
5 int i, j, k;
6
7 for (i=0; i<10; i++)
8 for (j=0; j<5; j++)
9 a[i][j] = 0;
10
11 for (i=0; i<3; i++)
12 for(j=0; j<2; j++)
13 for(k=0; k<4; k++)
14 b[i][j][k] = 0.0;
15
16 return 0;
17 }
Cuando accedemos a un elemento O[Í] [/], C sabe a qué celda de memoria acceder sumando a la dirección de
a el valor (í*3+y)*4 (el 4 es el tamaño de un int y el 3 es el número de columnas).
Aun siendo conscientes de cómo representa C la memoria, nosotros trabajaremos con una representación de
una matriz de 3 x 3 como ésta:
6. Memoria dinámica 29
Como puedes ver, lo relevante es que a es asimilable a un puntero a la zona de memoria en la que están
dispuestos los elementos de la matriz.
6. Memoria dinámica
Los vectores de C presentan un serio inconveniente: su tamaño debe ser fijo y conocido en tiempo de com-
pilación, es decir, no podemos ampliar o recortar los vectores para que se adapten al tamaño de una serie
de datos durante la ejecución del programa. C permite una gestión dinámica de la memoria, es decir, soli-
citar memoria para albergar el contenido de estructuras de datos cuyo tamaño exacto no conocemos hasta
que se ha iniciado la ejecución del programa. Existen dos formas de superar las limitaciones de tamaño que
impone el C:
• mediante vectores dinámicos, cuyo tamaño se fija en tiempo de ejecución,
• y mediante registros enlazados, también conocidos como listas enlazadas (o, simplemente, listas).
Ambas aproximaciones se basan en el uso de punteros y cada una de ellas presenta diferentes ventajas e
inconvenientes. En esta sección analizaremos únicamente los vectores dinámicos.
Fíjate en cómo se ha definido el vector a (línea 6): como int * a, es decir, como puntero a entero. No te
dejes engañar: no se trata de un puntero a un entero, sino de un puntero a una secuencia de enteros. Am-
bos conceptos son equivalentes en C, pues ambos son meras direcciones de memoria. La variable a es un
vector dinámico de enteros, pues su memoria se obtiene dinámicamente, esto es, en tiempo de ejecución y
según convenga a las necesidades. No sabemos aún cuántos enteros serán apuntados por a, ya que el valor
de dimension no se conocerá hasta que se ejecute el programa y se lea por teclado.
Sigamos. La línea 11 reserva memoria para dimensión enteros y guarda en el puntero a la dirección de
memoria en la que empiezan esos enteros. La función malloc() presenta un prototipo similar a éste:
stdlib.h
...
void * malloc (int bytes);
...
Es una función que devuelve un puntero especial, del tipo de datos void *. ¿Qué significa void *? Signi-
fica «puntero a cualquier tipo de datos», o sea, «dirección de memoria», sin más. La función malloc() no
se usa sólo para reservar vectores dinámicos de enteros: puedes reservar con ella vectores dinámicos de
cualquier tipo de dato. Analicemos ahora el argumento que pasamos a malloc(). La función espera recibir
como argumento un número entero: el número de bytes que queremos reservar. Si deseamos reservar
dimensión valores de tipo int, hemos de solicitar memoria para dimension * sizeof (int) bytes.
Recuerda que sizeof(int) es la ocupación en bytes de un dato de tipo int (y que estamos asumiendo
que es de 4).
Si el usuario decide que dimension valga, por ejemplo, 5, se reservará un total de 20 bytes y la memoria
quedará así tras ejecutar la línea 10:
vector estático desde el punto de vista práctico. Ambos pueden indexarse (línea 13) o pasarse como argu-
mento a funciones que admiten un vector del mismo tipo de dato.
Aritmética de punteros
Una curiosidad: el acceso indexado a[0] es equivalente a la expresión *a. En general, a[i] es
equivalente a *(a+i), es decir, ambas son formas de expresar el concepto «accede al conte-
nido de la dirección a con un desplazamiento de i veces el tamaño del tipo base».
La sentencia de asignación a[i] = i podría haberse escrito como *(a+i) = i. En C es posible
sumar o restar un valor entero a un puntero. El entero se interpreta como un desplazamiento
dado en unidades «tamaño del tipo base» (en el ejemplo, 4 bytes, que es el tamaño de un int).
Es lo que se conoce por aritmética de punteros.
La aritmética de punteros es un punto fuerte de C, aunque también tiene sus detractores: re-
sulta sencillo provocar accesos incorrectos a memoria si se usa mal.
Finalmente, la línea 13 del programa libera la memoria reservada y la línea 15 guarda en a un valor espe-
cial: NULL. La función free() tiene un prototipo similar a éste:
stdlib.h
...
void free (void * puntero);
...
Como puedes ver, free() recibe un puntero a cualquier tipo de datos: la dirección de memoria en la que
empieza un bloque previamente obtenido con una llamada a malloc(). Lo que hace free() es liberar ese
bloque de memoria, es decir, considerar que pasa a estar disponible para otras posibles llamadas a
malloc(). Es como cerrar un archivo: si no necesito un recurso, lo libero para que otros lo puedan apro-
vechar2. Puedes aprovechar así la memoria de forma óptima.
Recuerda: tu programa debe efectuar una llamada a free() por cada llamada a malloc(). Es muy
importante.
Conviene que después de llamar a free() asignes al puntero el valor NULL, especialmente si la variable
sigue «viva» durante bastante tiempo. NULL es una constante definida en stdlib.h. Si un puntero tiene co-
mo valor NULL, se entiende que no apunta a un bloque de memoria. Gráficamente, un puntero que apunta
a NULL se representa así:
2
Y, como en el caso de un archivo, si no lo liberas tú explícitamente, se libera automáticamente al finalizar la ejecución del programa. Aún así, te exigimos dis-
ciplina: oblígate a liberarlo tú mismo tan pronto dejes de necesitarlo.
6. Memoria dinámica 32
La función malloc() puede fallar por diferentes motivos. Podemos saber cuándo ha fallado porque
malloc() lo notifica devolviendo el valor NULL. Imagina que solicitas 2 meqabytes de memoria en un or-
denador que sólo dispone de 1 megabyte. En tal caso, la función malloc() devolverá el valor NULL para
indicar que no pudo efectuar la reserva de memoria solicitada.
Los programas correctamente escritos deben comprobar si se pudo obtener la memoria solicitada y, en
caso contrario, tratar el error.
1 a = malloc (dimension * sizeof(int));
2 if (a == NULL) {
3 printf("Error: no hay memoria suficiente\n");
4 }
5 else {
6 ...
7 }
Es posible (y una forma de expresión idiomática de C) solicitar la memoria y comprobar si se pudo obtener
en una única línea (presta atención al uso de paréntesis, es importante):
Nuestros programas, incluirán esta comprobación. También puedes usar NULL para inicializar punteros y
dejar explícitamente claro que no se les ha reservado memoria.
Programa 11: vector_dinamico.c
1 #include <stdlib.h>
2 #indude <stdio.h>
3
4 int main(void)
5 {
6 int * a = NULL;
7 int dimension, i;
8
9 printf("Número de elementos: "); scanf("%d" , &dimension);
10 a = malloc( dimension * sizeof(int) );
11 for (i=0; i<dimension; i++)
12 a[i] = i;
13 free(a);
14 a = NULL;
15
16 return 0;
17 }
6. Memoria dinámica 33
Fragmentación de la memoria
Ya hemos dicho que malloc() puede fracasar si se solicita más memoria de la disponible en el
ordenador. Parece lógico pensar que en un ordenador con 64 megabytes, de los que el sistema
operativo y los programas en ejecución han consumido, digamos, 16 megabytes, podamos soli-
citar un bloque de hasta 48 megabytes. Pero eso no está garantizado. Imagina que los 16 me-
gabytes ya ocupados no están dispuestos contiguamente en la memoria sino que, por ejemplo,
se alternan con fragmentos de memoria libre de modo que, de cada cuatro megabytes, uno
está ocupado y tres están libres, como muestra esta figura:
En tal caso, el bloque de memoria más grande que podemos obtener con malloc() es de ¡sólo
tres megabytes!
Decimos que la memoria está fragmentada para referirnos a la alternancia de bloques libres y
ocupados que limita su disponibilidad. La fragmentación no sólo limita el máximo tamaño de
bloque que puedes solicitar, además, afecta a la eficiencia con la que se ejecutan las llamadas
a la funciones malloc() y free().
Observa que devolvemos un dato de tipo int *, es decir, un puntero a entero; en realidad se trata de un
puntero a una secuencia de enteros (recuerda que son conceptos equivalentes en C). Es la forma que te-
nemos de devolver vectores desde una función. Este programa, por ejemplo, llama a la función
selecciona_pares():
6. Memoria dinámica 34
3
En realidad, hay una pequeña diferencia. La declaración int a[] hace que a sea un puntero inmutable, mientras que int *
a permite modificar la dirección apuntada por a haciendo, por ejemplo, a++. De todos modos, no haremos uso de esa
diferencia en esta sección.
6. Memoria dinámica 37
int main(void)
{
int * a;
a = primeros();
printf("%d ", a[i]); /* No existe a[i] */
}
Recuerda: si devuelves un puntero, éste no puede apuntar a datos locales.
No resulta muy elegante que una función devuelva valores mediante la sentencia return y, a la vez, me-
diante parámetros pasados por referencia. Una posibilidad es usar únicamente valores pasados por refe-
rencia:
Programa 14: pares.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define DIM 10
6
7 void selecciona_pares(int a[], int dim, int *pares[], int * numpares)
8 {
9 int i, j;
10
11 *numpares = 0;
12 for(i=0; i<dim; i++)
13 if(a[i]%2 == 0)
14 (*numpares)++;
15
16 *pares = malloc(*numpares * sizeof(int) );
17
18 j = 0;
19 for(i=0; i<dim; i++)
20 if(a[i]%2 == 0)
21 (*pares)[j++] = a[i];
22 }
23
24 int main(void)
25 {
26 int vector[DIM], i;
27 int * seleccion , seleccionados;
28
29 srand(time(0));
30 for(i=0; i<DIM; i++)
31 vector[i] =rand();
32
33 selecciona_pares(vector, DIM, &seleccion,&seleccionados);
34
35 for(i=0; i<seleccionados; i++)
36 printf("%d\n", seleccion[i]);
37
38 free(seleccion);
39 seleccion = NULL;
40
41 return 0;
42 }
6. Memoria dinámica 38
Fíjate en la declaración del parámetro pares en la línea 7: es un puntero a un vector de enteros, o sea, un
vector de enteros cuya dirección se suministra a la función. ¿Por qué? Porque, como resultado de llamar a
la función, la dirección apuntada por pares será una «nueva» dirección (la que obtengamos mediante una
llamada a la función malloc() ). La línea 16 asigna un valor a *pares. Resulta interesante que veas cómo
se asignan valores al vector apuntado por *pares en la línea 21 (los paréntesis alrededor de *pares son
obligatorios). Finalmente, observa que la variable seleccion se declara en la línea 27 como un puntero a
entero y que se pasa la dirección en la que se almacena dicho puntero en la llamada a la función
selecciona_pares() desde la línea 33. Hay una forma alternativa de indicar que pasamos la dirección
de memoria de un puntero de enteros. La cabecera de la función selecciona_pares() se podría haber
definido así:
void selecciona _pares(int a[], int dim, int ** pares , int * numpares);
*numpares = 0;
for(i=0; i<dim; i++)
if(a[i]%2 == 0)
(*numpares)++;
*pares = malloc(*numpares * sizeof(int) );
if(*pares == NULL) { /* Algo fue mal: no conseguimos la memoria. */
*numpares = 0; /* Info: el vector tiene capacidad 0 */
return 0; /* advertimos del error */
}
j = 0;
for(i=0; i<talla; i++)
if(a[i]%2 == 0)
(*pares)[j++] = a[i];
return 1; /* Si llegamos hasta aquí todo fue bien, avisamos con valor 1 */
}
/* Todo va bien */
}
else {
/* Algo fue mal. */
}
Hay que decir, no obstante, que esta forma de aviso de errores empieza a quedar obsoleto. Los lenguajes
de programación más modernos, como C++ o Python, suelen basar la detección (y el tratamiento) de
errores en las denominadas «excepciones».
Como puedes ver, tienes muchas soluciones técnicamente diferentes para realizar lo mismo. Deberás elegir
en función de la elegancia de cada solución y de su eficiencia.
16 free(cadena3);
17 cadena 3 = NULL;
18 return 0;
19 }
Como las dos primeras cadenas se leen con la función gets(), hemos de definirlas como cadenas estáti-
cas. La tercera cadena reserva exactamente la misma cantidad de memoria que ocupa.