Prg78_VectoresCadenas

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 40

Vectores y

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

2. Introducción a las estructuras de datos estáticas


Los tipos de datos vistos hasta ahora (enteros, reales, caracteres y lógicos) se denominan simples o primi-
tivos porque no pueden descomponerse en otros datos más simples aún.
Los tipos de datos complejos son aquellos que se componen de varios datos simples y, por lo tanto, pueden
dividirse en partes más sencillas. A los tipos de datos complejos se les llama también estructuras de datos.
Las estructuras de datos pueden ser de dos tipos:
3. Vectores estáticos 4

• 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.

3.1. Declaración de vectores


Para declarar un vector escribimos el tipo de dato, seguido del nombre del vector y del subíndice. El subín-
dice es el número de elementos del vector, encerrado entre corchetes. Por ejemplo,
long vectorLargo[25];
declara un vector de 25 enteros largos, llamado vectorLargo. Cuando el compilador encuentra esta de-
claración reserva la memoria necesaria para los 25 elementos. Puesto que cada entero necesita 4 bytes,
esta declaración reserva 100 bytes contiguos de memoria, como se ilustra en la figura 1.

Figura 1. Declaración de un vector.


3. Vectores estáticos 5

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».

3.2. Inicialización de los vectores


Una vez creado un vector, sus elementos presentan valores arbitrarios. Es un error suponer que los valores
del vector son nulos tras su creación. Si no lo crees, fíjate en este programa:
Programa 1: sin inicializar.c
1 #include <stdio.h>
2
3 #define DIM 5
4
5 int main(void)
6 {
7 int i, a[DIM];
8
9 for ( i=0; i<DIM; i++)
10 printf("%d\n", a[i]);
11 return 0;
12 }

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.

Cuestión de estilo: ¿constantes o literales al declarar la dimensión de un vector?


¿Por qué es preferible declarar la dimensión de un vector con una constante? Porque la dimensión del
vector puede aparecer en diferentes puntos del programa y es posible que algún día modifiquemos el
programa para trabajar con una dimensión diferente. En tal caso, deberíamos editar muchas líneas dife-
rentes del programa (quizá decenas o cientos). Bastaría que olvidásemos modificar una o que modificá-
semos una de más para que el programa fuera erróneo. Fíjate en este programa C:
include <stdio.h>

int main(void)
{
int i, a[10], b[10];

for(i=0; i<10; i++) a[i] = 0;


for(i=0; i<10; i++) b[i] = 0;
for(i=0; i<10; i++) printf("%d\n", a[i]);
for(i=0; i<10; i++) printf("%d\n", b[i]);
3. Vectores estáticos 7

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];

for(i=0; i<DIM_A; i++) a[i] = 0;


for(i=0; i<DIM_B; i++) b[i] = 0;
for(i=0; i<DIM_A; i++) printf("%d\n", a[i]);
for(i=0; i<DIM_B; i++) printf("%d\n", b[i]);
return 0;
}
Si ahora has de modificar a para que tenga 20 elementos, basta con editar la línea 3: cambia el 10 por un
20. Más rápido y con mayor garantía de no cometer errores.

§ Omisión de dimensión en declaraciones con inicialización y otro modo de inicializar


También puedes declarar e inicializar vectores así:
int a[] = {0, 0, 0, 0, 0};
int b[] = {1, 2, 3, 4, 5};
El compilador deduce que la dimensión del vector es 5, es decir, el número de valores que aparecen a la
derecha del operador de asignación. Te recomendamos que, ahora que estás aprendiendo, no uses esta
forma de declarar vectores: siempre que puedas, opta por una que haga explícito el tamaño del vector.
En el estándar C99 es posible inicializar sólo algunos valores del vector. La sintaxis es un poco enrevesada.
Aquí tienes un ejemplo en el que sólo inicializamos el primer y último elementos de un vector de dimensión
10:
int a[] = {[0] = 0, [9]= 0};

3.3. Asignación de valores fuera del vector


Cuando asignamos un valor a un elemento de un vector, el compilador calcula la posición de memoria
donde tiene que almacenar dicho valor a partir del subíndice. Supongamos que queremos asignar un valor
a VectorLargo[5] (con el ejemplo anterior en el que VectorLargo tiene 5 elementos. Declaración:
long VectorLargo[5];), que es el sexto elemento del vector. El compilador multiplica el subíndice (5)
por el tamaño del dato long = 4 bytes). Después se desplaza desde el principio del vector tantos bytes co-
mo indique el resultado de la multiplicación anterior (20), y escribe el nuevo valor en esa posición. Si tene-
mos suerte, el programa fallará inmediatamente.
Si queremos asignar un valor a VectorLargo[50], el compilador ignora el hecho de que dicho elemento
no existe. Solamente calcula el desplazamiento a realizar desde el primer elemento (200 bytes) y escribe el
valor en dicha posición. Esta posición puede estar reservada para otra variable, y escribir un valor en dicha
posición puede tener resultados imprevistos. Si no es así, es posible que el programa falle más adelante, y
será difícil determinar el por qué ha fallado.
3. Vectores estáticos 8

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!

Programa 2: Cuidado con el final de un Vector


1: /* Programa 2
2: Ejemplo de lo que ocurre cuando escribimos mas allá
3: del final de un vector */
4:
5: #include <stdio.h>
6: int main()
7: {
8: short calleUno[3];
9: short paseoZorrilla[20]; // vector a rellenar
10: short calleDos[3];
11: int i;
12: for (i=0; i<3; i++)
13: {
14: calleUno[i] = -1;
calleDos[i] = -2;
15: }
16:
17: for (i=0; i<=25; i++)
18: paseoZorrilla[i] = i;
19:
20: printf("Test 1: \n");
21: printf("paseoZorrilla[0] : %d \n", paseoZorrilla[0]);
22: printf("paseoZorrilla[20]: %d \n", paseoZorrilla[20]);
23:
24: printf("\nAsignando...");
25: paseoZorrilla[21] = 21;
26:
27: printf("\nTest 2: \n");
28: printf("paseoZorrilla[0] : %d\n", paseoZorrilla[0] );
29: printf("paseoZorrilla[20]: %d\n", paseoZorrilla[20]);
30: printf("paseoZorrilla[21]: %d\n", paseoZorrilla[21]);
31: for (i = 0; i<3; i++)
32: printf("calleUno[%d]: %d\n", i, calleUno[i]);
33:
34: for (i = 0; i<3; i++)
35: printf("calleDos[%d]: %d\n", i, calleDos[i]);
36:
37: return 0;
38: }

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].

3.4. Recorrido de un vector


Una forma habitual de manipular un vector es accediendo secuencialmente a todos sus elementos, uno tras
otro. Para ello, se utiliza un bucle con contador, de modo que la variable contador nos sirve como índice
para acceder a cada uno de los elementos del vector.
Supongamos, por ejemplo, que tenemos un vector de 10 números enteros declarado como int v[10]; y
una variable entera declarada como int i;. Por medio de un bucle, con ligeras modificaciones, podemos
realizar todas estas operaciones:
a) Inicializar todos los elementos a un valor cualquiera (por ejemplo, 0):

for (i = 0; i <= 9; i++)


{
v[i] = 0;
}

b) Inicializar todos los elementos con valores introducidos por teclado:

for (i = 0; i <= 9; i++)


{
printf("Escriba el valor del elemento nº %i: ",
i);
scanf("%i", &v[i]);
}
3. Vectores estáticos 10

c) Mostrar todos los elementos en la pantalla:

for (i = 0; i <= 9; i++)


{
printf("El elemento nº %i vale %i\n", i, v[i]);
}

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];
}

3.5. Vectores y funciones en C


Para pasar un vector como argumento a una función, en la llamada a la función se escribe simplemente el
nombre del vector, sin índices. Esto sirve para pasar a la función la dirección de memoria donde se alma-
cena el primer elemento del vector. Como C guarda todos los elementos de los vectores en posiciones de
memoria consecutivas, conociendo la dirección del primer elemento es posible acceder a todas las demás.
El hecho de que a la función se le pase la dirección del vector y no sus valores provoca un efecto importan-
te: que los vectores siempre se pasan por dirección o referencia, nunca por valor. Esto incluye a los vecto-
res, que son vectores unidimensionales. Por lo tanto, si algún elemento del vector se modifica en una fun-
ción, también será modificado en la función desde la que fue pasado.
Como siempre se pasan por dirección, no es necesario utilizar el operador dirección de (&) delante del pa-
rámetro. Por ejemplo, supongamos que serie es un vector de 15 números enteros. Para pasarlo como
parámetro a una función llamada funcion1() escribiríamos simplemente esto:
int serie[15];
funcion1(serie); // llamada a la función funcion1

En cuanto a la definición de la función, la declaración de un parámetro que en realidad es un vector se


puede hacer de tres maneras diferentes:
void funcion1 (int serie[15]); /* Vector delimitado */
void funcion1 (int serie[]); /* Vector no delimitado */
void funcion1 (int *serie); /* Puntero */

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;
}

3.6. Comparación de vectores

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];

Para acceder a los miembros de un vector, se utiliza el operador subíndice, [].


Ejemplo:

int b = a[8];

Los vectores cuentan desde cero. Un vector de n elementos se numera de 0 a n-1.

4. Cadenas (de caracteres) estáticas


Las cadenas de caracteres no son un tipo de dato básico en C. Las cadenas de C son vectores de caracteres
(elementos de tipo char) con una peculiaridad: el texto de la cadena termina siempre en un carácter nulo. El
carácter nulo tiene código ASCII 0 y podemos representarlo tanto con el entero 0 como con el carácter
'\0' (recuerda que '\0' es una forma de escribir el valor entero 0). ¡Ojo! No confundas '\0' con '0': el
primero corresponder al valor 0 y el segundo correspondes al valor 48.
Las cadenas estáticas en C son mutables. Eso significa que puedes modificar el contenido de una cadena du-
rante la ejecución de un programa.

4.1. Declaración de cadenas


Las cadenas se declaran como vectores de caracteres, por lo que debes proporcionar el número máximo de
caracteres que es capaz de almacenar: su capacidad. Esta cadena, por ejemplo, se declara con capacidad
para almacenar 10 caracteres:
char a[10];
Puedes inicializar la cadena con un valor en el momento de su declaración:
char a[10] = "cadena";
Hemos declarado a como un vector de 10 caracteres y lo hemos inicializado asignándole la cadena "cadena".
Fíjate: hemos almacenado en a una cadena de menos de 10 caracteres. No hay problema: la longitud de la
cadena almacenada en a es menor que la capacidad de a.

4.2. Representación de las cadenas en memoria


A simple vista, "cadena" ocupa 6 bytes, pues contamos 6 caracteres, pero no es así. En realidad, "cadena"
ocupa 7 bytes: los 6 que corresponden a los 6 caracteres que ves más uno correspondiente a un carácter
nulo al final, que se denomina final de cadena y es 'invisible'.
Al declarar e inicializar una cadena así:
char a[10] = "cadena";
la memoria queda de este modo:
4. Cadenas (de caracteres) estáticas 14

Es decir, es como si hubiésemos inicializado la cadena de este otro modo equivalente:


char a[10] = {'c', 'a', 'd', 'e', 'n', 'a', '\0' };
Recuerda, pues, que hay dos valores relacionados con el tamaño de una cadena:
• su capacidad, que es la dimensión del vector de caracteres;
• su longitud, que es el número de caracteres que contiene, sin contar el final de de la cadena. La lon-
gitud de la cadena debe ser siempre estrictamente menor que la capacidad del vector para no des-
bordar la memoria reservada.
¿Y por qué toda esta complicación del final de cadena? Lo normal al trabajar con una variable de tipo ca-
dena es que su longitud varíe conforme evoluciona la ejecución del programa, pero el tamaño de un vector
es fijo. Por ejemplo, si ahora tenemos en a el texto "cadena" y más tarde decidimos guardar en ella el
texto "texto", que tiene un carácter menos, estaremos pasando de esta situación:

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 }

4.3. Entrada/salida de cadenas


Las cadenas se muestran con printf() y la adecuada marca de formato sin que se presenten dificultades
especiales. Lo que sí resulta problemático es leer cadenas. La función scanf() presenta una seria limita-
ción: sólo puede leer «palabras», no «frases». Esto nos obligará a presentar una nueva función (gets() )...
que se lleva fatal con scanf().

§ Salida con printf()


Empecemos por considerar la función printf(), que muestra cadenas con la marca de formato %s. Aquí
tienes un ejemplo de uso:
Programa 4: salida_cadena. 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
11 return 0;
12 }

Al ejecutar el programa obtienes en pantalla esto:


El valor de cadena es una cadena.
Puedes alterar la presentación de la cadena con modificadores:
4. Cadenas (de caracteres) estáticas 16

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

§ Entrada con scanf()


Poco más hay que contar acerca de la función printf(). La función scanf() es un reto mayor. He aquí
un ejemplo que pretende leer e imprimir una cadena en la que podemos guardar hasta 80 caracteres (sin
contar el terminador nulo):
Programa 6: lee_una_cadena.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 }

¡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».

§ Lectura con gets()


Hay un problema práctico con la función scanf(): sólo lee una «palabra», es decir, una secuencia de ca-
racteres no blancos. Hagamos la prueba:
4. Cadenas (de caracteres) estáticas 18

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 }

Si al ejecutar el programa tecleamos un par de palabras, sólo se muestra la primera:


una frase
La cadena leída es una.

¿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

§ Lectura de cadenas y escalares: gets() y sscanf()


Y ahora, vamos con un problema al que te enfrentarás en más de una ocasión: la lectura alterna de cadenas
y valores escalares. La mezcla de llamadas a las funciones scanf() y gets(), produce efectos curiosos
que se derivan de la combinación de su diferente comportamiento frente a los espacios en blanco. El resul-
tado suele ser una lectura incorrecta de los datos o incluso el bloqueo de la ejecución del programa. Los
detalles son bastante escabrosos..
Presentaremos en esta sección una solución directa que deberás aplicar siempre que tu programa alterne
la lectura de cadenas con espacios en blanco y valores escalares (algo muy frecuente). La solución consiste
en:
• Si vas a leer una cadena, utiliza la función gets().
• Y si vas a leer un valor escalar, procede en dos pasos:
1. lee una línea completa con la función gets() (usa una variable auxiliar para ello),
2. y extrae de dicha variable auxiliar los valores escalares que quieres leer, con ayuda de la función
sscanf().

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

4.4. Asignación y copia de cadenas


Este programa, que pretende copiar una cadena en otra, parece correcto, pero no lo es: i
1 #define MAXLON 10
2
3 int main(void)
4 {
5 char original[MAXLON+1] = "cadena";
6 char copia [MAXLON+1];
7
8 copia = original;
9
10 return 0;
11 }

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

Aún podemos hacerlo «mejor»:


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; original[i] !='\0'; i++)
10 copia[i] = original[i];
11 copia[i]='\0';
12
13 return 0;
14 }

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 }

4.5. Longitud de una cadena


El convenio de terminar una cadena con el carácter nulo permite conocer fácilmente la longitud de una
cadena:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXL0N+1];
8 int i;
9
10 printf("Introduce una cadena (máx. %d cars.) :", MAXLON);
11 gets(a);
12 i = 0;
13 while(a[i]!= '\0')
14 i++;
15 printf("Longitud de la cadena: %d\n", i);
16
17 return 0;
18 }

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 }

Pero es mejor usar la función de biblioteca strcat() (por «string concatenate»):


4. Cadenas (de caracteres) estáticas 25

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.

4.7. Comparación de cadenas


Tampoco los operadores de comparación (==, !=, <, <=, >, >=) funcionan con cadenas. Existe, no obstante,
una función de biblioteca (declarada en string.h) que permite paliar esta carencia de C: strcmp() (abre-
viatura de «string comparison»). La función strcmp() recibe dos cadenas, por ejemplo, a y b, y devuelve
un entero. El entero que resulta de efectuar la llamada strcmp(a, b) codifica el resultado de la compa-
ración:
• es menor que cero si la cadena a es menor que b,
• es 0 si la cadena a es igual que b, y
• es mayor que cero si la cadena a es mayor que b.
Naturalmente, menor significa que va delante en orden alfabético, y mayor que va detrás.

4.8. Funciones útiles para manejar caracteres


No sólo el archivo de cabecera string.h contiene funciones útiles para el tratamiento de cadenas. En el
achivo ctype.h encontrarás unas funciones que permiten hacer cómodamente preguntas acerca de los carac-
teres: si son mayúsculas, minúsculas, dígitos, etc:
• isalnum(carácter): devuelve cierto (un entero cualquiera distinto de cero) si carácter es una le-
tra o dígito, y falso (el valor entero 0) en caso contrario,
• isalpha (carácter): devuelve cierto si carácter es una letra, y falso en caso contrario,
4. Cadenas (de caracteres) estáticas 26

• isblank(carácter): devuelve cierto si carácter es un espacio en blanco o un tabulador,


• isdigit(carácter): devuelve cierto si carácter es un dígito, y falso en caso contrario,
• isspace(carácter): devuelve cierto si carácter es un espacio en blanco, un salto de línea, un re-
torno de carro, un tabulador, etc., y falso en caso contrario,
• islower(carácter): devuelve cierto si carácter es una letra minúscula, y falso en caso contrario,
• isupper(carácter): devuelve cierto si carácter es una letra mayúscula, y falso en caso contrario.
También en ctype.h encontrarás un par de funciones útiles para convertir caracteres de minúscula a ma-
yúscula y viceversa:
• toupper(carácter): devuelve la mayúscula asociada a carácter, si la tiene; si no, devuelve el
mismo carácter,
• tolower(carácter): devuelve la minúscula asociada a carácter, si la tiene; si no, devuelve el mis-
mo carácter.

4.9. Escritura en cadenas: sprintf()


Hay una función que puede simplificar notablemente la creación de cadenas cuyo contenido se debe calcu-
lar a partir de uno o más valores: sprintf(), disponible incluyendo la cabecera stdio.h (se trata, en cierto
modo, de la operación complementaria de la función sscanf() ). La función sprintf() se comporta
como printf(), salvo por un «detalle»: no escribe texto en pantalla, sino que lo almacena en una cadena.
Fíjate en este ejemplo:
1 #include <stdio.h>
2
3 #define MAXLON 80
4
5 int main(void)
6 {
7 char a[MAXL0N+1] = "una";
8 char b[MAXL0N+1] = "cadena";
9 char c[MAXL0N+1];
10
11 sprintf(c, "%s %s", a, b);
12 printf(“%s\n", c);
13
14 return 0;
15 }

Si ejecutas el programa aparecerá lo siguiente en pantalla:


una cadena
Como puedes ver, se ha asignado a la cadena c el valor de la cadena a seguido de un espacio en blanco y
de la cadena b. Podríamos haber conseguido el mismo efecto con llamadas a strcpy(c, a), strcat(c,
" ") y strcat(c, b), pero sprintf() resulta más legible y no cuesta mucho aprender a usarla, pues ya
sabemos usar printf(). No olvides que tú eres responsable de que la cadena c tenga suficiente capaci-
dad para almacenar la información.

4.10. Un programa de ejemplo


Vamos a implementar un programa que lee por teclado una línea de texto y muestra por pantalla una ca-
dena en la que las secuencias de blancos de la cadena original (espacios en blanco, tabuladores, etc.) se han
sustituido por un sólo espacio en blanco. Si, por ejemplo, el programa lee la cadena
5. Vectores multidimensionales 27

"una cadena Con blancos "


mostrará por pantalla la cadena «normalizada» "una cadena con blancos ".
Programa 9: normaliza.c
1 #include <stdio.h>
2 #include <string.h>
3 #include <ctype.h>
4
5 #define MAXLON 80
6
7 int main(void)
8 {
9 char a[MAXLON+1], b[MAXL0N+1];
10 int longitud, i, j;
11
12 printf("Introduce una cadena(máx. %d cars.): ", MAXLON);
13 gets(a);
14 longitud = strlen(a);
15 b[0] = a[0];
16 j = 1;
17 for (i=1; i<longitud; i++)
18 if (!isspace(a[i]) || (isspace(a[i]) && !isspace(a[i-1])))
19 b[j++] = a[i];
20 b[j] = ‘\0';
21 printf ("La cadena normalizada es %s\n", b);
22
23 return 0;
24 }

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 }

También puedes inicializar explícitamente un vector multidimensional:


int c[3][3] = { {1, 0, 0},
{0, 1, 0},
{0, 0, 1} };

5.1. Sobre la disposición de los vectores multidimensionales en memoria


Cuando el compilador de C detecta la declaración de un vector multidimensional, reserva tantas posiciones
contiguas de memoria como sea preciso para albergar todas sus celdas. Por ejemplo, ante la declaración
int a[3][3], C reserva 9 celdas de 4 bytes, es decir, 36 bytes. He aquí cómo se disponen las celdas en
memoria, suponiendo que la zona de memoria asignada empieza en la dirección 1000:

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.

6.1. Vectores dinámicos


Sabemos definir vectores indicando su tamaño en tiempo de compilación:
#define DIMENSION 10
int a[DIMENSION];
Pero, ¿y si no sabemos a priori cuántos elementos debe albergar el vector? Por lo estudiado hasta el mo-
mento, podemos definir DIMENSION como el número más grande de elementos posible, el número de
elementos para el peor de los casos. Pero, ¿y si no podemos determinar un número máximo de elementos?
Aunque pudiéramos, ¿y si éste fuera tan grande que, en la práctica, supusiera un despilfarro de memoria
intolerable para situaciones normales? Imagina una aplicación de agenda telefónica personal que, por si
acaso, reserva 100000 entradas en un vector. Lo más probable es que un usuario convencional no gaste
más de un centenar. Estaremos desperdiciando, pues, unas 99900 celdas del vector, cada una de las cuales
puede consistir en un centenar de bytes. Si todas las aplicaciones del ordenador se diseñaran así, la memo-
ria disponible se agotaría rapidísimamente.

6.1. Funciones malloc(), free() y constante NULL


Afortunadamente, durante la ejecución del programa podemos definir vectores cuyo tamaño es exacta-
mente el que el usuario necesita. Utilizaremos para ello dos funciones de la biblioteca estándar (disponibles
incluyendo la cabecera stdlib.h):
• malloc() (abreviatura de «memory allocate», que podemos traducir por «reservar memoria»): soli-
cita un bloque de memoria del tamaño que se indique (en bytes);
• free() (que en inglés significa «liberar»): libera memoria obtenida con malloc(), es decir, la mar-
ca como disponible para futuras llamadas a malloc().
Para hacernos una idea de cómo funciona, estudiemos un ejemplo:
6. Memoria dinámica 30

Programa 10: vector_dinamico . c


1 #include <stdlib.h>
2 #include <stdio.h>
3
4 int main(void)
5 {
6 int * a;
7 int dimension, i;
8
9 printf("Número de elementos: ");
10 scanf("%d", &dimension);
11 a = malloc( dimension * sizeof(int) );
12 for (i=0; i<dimension; i++)
13 a[i] = i;
14 free(a);
15 a = NULL;
16 return 0;
17 }

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:

Es decir, se reserva suficiente memoria para albergar 5 enteros.


Como puedes ver, la línea 14 trata al identificador a como si fuera un vector de enteros cualquiera. Una vez
que has reservado memoria para un vector dinámico, no hay diferencia alguna entre dicho vector y un
6. Memoria dinámica 31

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í:

Liberar memoria no cambia el valor del puntero


La llamada a free() libera la memoria apuntada por un puntero, pero no modifica el valor de la
variable que se le pasa. Imagina que un bloque de memoria de 10 enteros que empieza en la
dirección 1000 es apuntado por una variable a de tipo int*, es decir, Imagina que a vale 1000.
Cuando ejecutamos free(a), este bloque se libera y pasa a estar disponible para eventuales
llamadas a malloc(), pero ¡a sigue valiendo 1000! ¿Por qué? Porque a se ha pasado a free() por
valor, no por dirección, así que free() no tiene forma de modificar el valor de a. Es recomenda-
ble que asignes al puntero a el valor NULL después de una llamada a free(), pues así haces
explícito que la variable a no apunta a nada.
Recuerda, pues, que es responsabilidad tuya y que conviene hacerlo: asigna explícitamente el
valor NULL a todo puntero que no apunte a memoria reservada.

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):

1 if ( (a = malloc(dimension * sizeof(int))) == NULL) {


2 printf("Error: no hay memoria suficiente\n");
3 }
4 else {
5 ...
6 }

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().

4.1.2. Algunos ejemplos


Es hora de poner en práctica lo aprendido desarrollando un par de ejemplos.

§ Creación de un nuevo vector con una selección, de dimensión desconocida, de elementos de


otro vector
Empezaremos por diseñar una función que recibe un vector de enteros, selecciona aquellos cuyo valor es
par y los devuelve en un nuevo vector cuya memoria se solicita dinámicamente.
1 int * selecciona_pares(int a[] , int dim)
2 {
3 int i, j, numpares = 0;
4 int * pares;
5
6 /* Primero hemos de averiguar cuántos elementos pares hay en a. */
7 for(i=0; i<dim; i++)
8 if (a[i]%2 == 0)
9 numpares++;
10
11 /* Ahora podemos pedir memoria para ellos. */
12 pares = malloc( numpares * sizeof(int) );
13
14 /* Y finalmente, copiar los elementos pares en la zona de memoria
15 solicitada. */
16 j = 0;
17 for(i=0; i<dim; i++)
18 if (a[i]%2 == 0)
19 pares[j++] = a[i];
20
21 return pares;
22 }

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

Programa 12: pares.c


1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define DIM 10
6

27 }
28
29 int main(void)
30 {
31 int vector[DIM], i;
32 int * selección;
33
34 /* Rellenamos el vector con valores aleatorios. */
35 srand(time(0));
36 for (i=0; i<DIM; i++)
37 vector[i] = rand();
38
39 //Se efectúa ahora la selección de pares.*/
40 selección = selecciona _pares(vector, DIM);
41 /* La variable seleccion apunta ahora a la zona de memoria
42 con los elementos pares. */
43
44 /* Sí, pero, ¿cuántos elementos pares hay? */
45 for (i=0; i<????; i++)
46 printf ("%d\n" , seleccion[i]);
47
48 free (seleccion);
49 selección = NULL;
50
51 return 0;
52 }

Tenemos un problema al usar selecciona_pares(): no sabemos cuántos valores ha seleccionado. Po-


demos modificar la función para que modifique el valor de un parámetro que pasamos por dirección:
6. Memoria dinámica 35

1 int * selecciona_pares(int a[], int dim, int * numpares)


2 {
3 int i, j;
4 int * pares;
5
6 /* Contamos los elementos pares en el parámetro numpares,
7 pasado por dirección */
8 *numpares = 0;
9 for (i=0; i<dim; i++)
10 if(a[i]%2 == 0)
11 (*numpares)++;
12
13 pares = malloc ( *numpares * sizeof(int) );
14
15 j = 0;
16 for(i=0; i<dim; i++)
17 if(a[i]%2 == 0)
18 pares[j++] = a[i];
19
20 return pares;
21 }

Ahora podemos resolver el problema:


Programa 13: pares.c (II)
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4
5 #define DIM 10
6
7 int * selecciona_pares(int a[], int dim, int * numpares)
8 {
9 int i, j;
10 int * pares;
11
12 /* Contamos los elementos pares en el parámetro numpares,
13 pasado por dirección */
14 *numpares = 0;
15 for (i=0; i<dim; i++)
16 if(a[i]%2 == 0)
17 (*numpares)++;
18
19 pares = malloc ( *numpares * sizeof(int) );
20
21 j = 0;
22 for(i=0; i<dim; i++)
23 if(a[i]%2 == 0)
24 pares[j++] = a[i];
25
26 return pares;
27 }
28
29 int main(void)
30 {
31 int vector[DIM], i;
6. Memoria dinámica 36

32 int * seleccion, seleccionados;


33
34 /* Rellenamos el vector con valores aleatorios. */
35 srand(time(0));
36 for (i=0; i<DIM; i++)
37 vector[i] = rand();
38
39 //Se efectúa ahora la selección de pares.*/
40 seleccion = selecciona _pares(vector, DIM, &seleccionados);
41 /* La variable seleccion apunta ahora a la zona de memoria
42 con los elementos pares. Además, la variable seleccionados
43 contiene el número de pares */
44
45 /* Ahora los mostramos en pantalla */
46 for (i=0; i<seleccionados; i++)
47 printf ("%d\n" , seleccion[i]);
48
49 free (seleccion);
50 selección = NULL;
51
52 return 0;
53 }

Por cierto, el prototipo de la función, que es éste:


int * selecciona_pares( int a[] , int dim, int * seleccionados);
puede cambiarse por este otro:
int * selecciona_pares( int *a, int dimn, int * seleccionados);
Conceptualmente, es lo mismo un parámetro declarado como int a[] que como int* a: ambos son, en
realidad, punteros a enteros3. No obstante, es preferible utilizar la primera forma cuando un parámetro es
un vector de enteros, ya que así lo distinguimos fácilmente de un entero pasado por referencia. Si observas
el último prototipo, no hay nada que te permita saber si a es un vector o un entero pasado por referencia
(como seleccionados). Es más legible, pues, la primera forma.
No se pueden devolver punteros a datos locales
Como un vector de enteros y un puntero a una secuencia de enteros son, en cierto modo, equivalentes,
puede que esta función te parezca correcta:
int * primeros(void)
{
int i, v[10];
for(i=0; i<10; i++)
v[i] = i + 1 ;
return v;
}
La función devuelve, a fin de cuentas, una dirección de memoria en la que empieza una secuencia de en-
teros. Y es verdad: eso es lo que hace. El problema radica en que la memoria a la que apunta ¡no «existe»
fuera de la función! La memoria que ocupa el vector v se libera tan pronto finaliza la ejecución de la fun-
ción. Este intento de uso de la función, por ejemplo, trata de acceder ilegalmente a memoria:

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);

Valores de retorno como aviso de errores


Es habitual que aquellas funciones C que pueden dar lugar a errores nos adviertan de ellos mediante el
valor de retorno. La función malloc(), por ejemplo, devuelve el valor NULL cuando no consigue reservar
la memoria solicitada y un valor diferente cuando sí lo consigue. La función scanf(), que hemos estu-
diado como si no devolviese valor alguno, sí lo hace: devuelve el número de elementos cuyo valor ha sido
leído correctamente. Si, por ejemplo, llamamos a scanf("%d %d", &a, &b), la función devuelve el valor
2 si todo fue bien (se leyó el contenido de dos variables). Si devuelve el valor 1, es porque sólo consiguió
leer el valor de a, y si devuelve el valor 0, indica que no consiguió leer ninguna de las dos variables. Un
programa robusto debe comprobar el valor devuelto siempre que se efectúe una llamada a la función
scanf(); así:
1 if ( scanf("%d %d", &a, &b) !=2)
2 printf("Error! No conseguí leer los valores de a y b.\n");
3 else {
4 /* Situación normal. */
5 ...
6 }
Las rutinas que nosotros diseñamos deberían presentar un comportamiento similar. La función
selecciona_pares, por ejemplo, podría implantarse así:
int selecciona_pares(int a[], int dim, int * pares[], int * numpares)
{
int i, j;

*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 */
}

Aquí tienes un ejempo de uso de la nueva función:


if(selecciona_paers(vector, DIM, &seleccion, &seleccionados) ) {
6. Memoria dinámica 39

/* 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.

§ Reserva con inicialización automática


La función calloc() es similar a la función malloc(), pero presenta un prototipo diferente y hace algo
más que reservar memoria: la inicializa a cero. He aquí un prototipo (similar al) de calloc():
void * calloc (int nmemb, int size);
Con la función calloc(), puedes pedir memoria para un vector de dimensión enteros así:
a = calloc (dimension, sizeof (int));
El primer parámetro es el número de elementos y el segundo, el número de bytes que ocupa cada elemen-
to. No hay que multiplicar una cantidad por otra, como hacíamos con la función malloc().
Todos los enteros del vector se inicializan a cero. Es como si ejecutásemos este fragmento de código:
a = malloc(dimension * sizeof (int) );
for(i=0; i<dimensión; i++)
a[i] = 0;
¿Por qué no usar siempre la función calloc(), si parece mejor que la función malloc()? Por eficiencia.
En ocasiones no desearás que se pierda tiempo de ejecución inicializando la memoria a cero, ya que tú
mismo querrás inicializarla a otros valores inmediatamente. Recuerda que garantizar la mayor eficiencia de
los programas es uno de los objetivos del lenguaje de programación C.

6.2. Cadenas dinámicas


Las cadenas son un caso particular de vector. Podemos usar cadenas de cualquier longitud gracias a la ges-
tión de memoria dinámica. Este programa, por ejemplo, lee dos cadenas y construye una nueva que resulta
de concatenar a éstas.
Programa 15: cadenas_dinamicas.c
1 #include <stdio.h>
2 #indude <string.h>
3 #indude <stdlib.h>
4 #define CAPACIDAD 80
5 int main (void)
6 {
7 char cadena1 [CAPACIDAD+1], cadena2[CAPACIDAD+1];
8 char * cadena3;
9 printf ("Dame un texto: "); gets(cadena1);
10 printf ("Dame otro texto: "); gets(cadena2);
11 cadena3 = malloc ( (strlen(cadena1) + strlen(cadena2) + 1) * sizeof (char)
12 );
13 strcpy(cadena3, cadena1);
14 strcat(cadena3, cadena2);
15 printf ("Resultado de concatenar ambos : "%s\n" , cadena3);
6. Memoria dinámica 40

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.

§ Sobre la mutabilidad de las cadenas


Es posible inicializar un puntero a cadena de modo que apunte a un literal de cadena:
char * p = "cadena";
Pero, ¡ojo!, la cadena apuntada por p es, en ese caso, inmutable: si intentas asignar un valor de tipo char a
p[t], el programa puede abortar su ejecución. ¿Por qué? Porque los literales de cadena «residen» en una
zona de memoria especial (la denominada «zona de texto») que está protegida contra escritura. Y hay una
razón para ello: en esa zona reside, también, el código de máquina correspondiente al programa. Que un
programa modifique su propio código de máquina es una pésima práctica (que era relativamente frecuente
en los tiempos en que predominaba la programación en ensamblador), hasta el punto de que su zona de
memoria se marca como de sólo lectura.

También podría gustarte