06. Estructura de memoria

Última modificación por editor1 el 2018/09/17 10:11

Objetivos

  • Entender el concepto de dirección de memoria.
  • Conocer el espacio en memoria de los tipos de datos.
  • Entender la relación entre espacio de memoria y capacidad de representación.
  • Entender el concepto de memoria estática.
  • Introducir el tipo de datos puntero en C.

Introducción

Hasta ahora hemos visto cómo escribir un programa y posteriormente compilarlo y enlazarlo para lograr un ejecutable. Los detalles de qué pasa cuando ejecutamos el programa quedan fuera del alcance de esta asignatura, pero en esta unidad se introducirán algunos conceptos básicos (y simplificados) de qué efectos tiene en la memoria.

1. Memoria

Lo primero que hay que saber es que un programa no se ejecuta desde el disco, sino que el primer paso es cargarlo en la memoria del ordenador, lo que conocemos como la RAM (Random Access Memory). Dado que la memoria es un recurso escaso en un ordenador, el sistema operativo se encarga de ir moviendo cosas de memoria a disco constantemente pero, desde el punto de vista del programador, podemos asumir que siempre está cargado en memoria y vemos la memoria como una serie numerada de posiciones donde podemos guardar información. Cada posición está identificada unívocamente con un valor numérico (su dirección) y tiene capacidad para un byte (8 bits).

MemIB.png

Cuando definimos un objeto en nuestro programa, ya sea una constante o una variable, a este se le asigna un rango de direcciones donde pondrá su valor. Podemos pensar en la memoria como un aparcamiento público donde tenemos muchas plazas numeradas del tamaño de una moto y sin ninguna columna de por medio. Si queremos aparcar una moto (asignación), con una plaza tendremos suficiente (1 byte), siendo necesario que recordemos el número de la plaza cuando queramos recuperar la moto (acceso). Si en vez de una moto queremos aparcar un coche, con una sola plaza no tendremos suficiente, sino que acabaremos ocupando seguramente 4 plazas (4 bytes) y, para recuperar el coche, con que recordemos el número de la primera plaza ocupada tendremos suficiente para encontrarlo.

En el resto de esta unidad veremos cuánta memoria ocupa cada tipo de datos y la relación que esto tiene con el rango de valores que puede representar. También veremos que C tiene un conjunto más amplio de tipos de datos, que nos permitirán ajustar mejor la cantidad de memoria a nuestras necesidades.

1.1. Información y espacio

El espacio que ocupa una variable está estrechamente relacionado con la cantidad de información que puede guardar. El bit es la unidad mínima de información y puede guardar solo 21 = 2 valores (0 y 1). Con solo un bit podemos representar por ejemplo el resultado de tirar un moneda al aire, asignando 1 a la cara y 0 a la cruz (o al revés), pero no tenemos suficiente para poder representar el resultado de tirar un dado. Si añadimos un segundo bit, el número de valores que podremos representar será de 22 = 4 valores. Esto nos permitiría, por ejemplo, guardar las estaciones del año, codificándolas de la siguiente forma:

CodificaciónEstación
  00  Primavera
  01  Verano
  10  Otoño
  11  Invierno

Teniendo en cuenta que un byte contiene 8 bits, su capacidad de representación es de 28 = 256 valores diferentes. Por lo tanto, la capacidad de representación está estrechamente ligada a la cantidad de bits que utilizamos. Cuando programamos es necesario que lo tengamos en cuenta, ya que cada tipo de datos tiene una longitud en bytes determinada, y esto nos fija cuántos valores diferentes podemos almacenar. La asignación entre valor y su codificación binaria viene determinada por una serie de estándares, que quedan fuera del alcance de esta asignatura.

En C encontraremos a nuestra disposición diferentes alternativas para cada tipo de datos numéricos del lenguaje algorítmico según el espacio que ocupan y, por tanto, su capacidad de representación. En la siguiente tabla se puede ver un resumen:

TipoTipo CTamaño en bytesRango de valores
 Entero char 1 -128 to 127 o 0 to 255
 Entero unsigned char 1 0 a 255
 Entero signed char 1 -128 a 127
 Entero int 2 o 4 -32,768 a 32,767 o -2,147,483,648 a 2,147,483,647
 Entero unsigned int 2 o 4 0 a 65,535 o 0 a 4,294,967,295
 Entero short 2 -32,768 a 32,767
 Entero unsigned short 2 0 a 65,535
 Entero long 4 -2,147,483,648 a 2,147,483,647
 Entero unsigned long 4 0 a 4,294,967,295
 Real float 4 1.2E-38 a 3.4E+38 -> 6 decimales
 Real double 8 2.3E-308 a 1.7E+308 -> 15 decimales
 Real long double 10 3.4E-4932 a 1.1E+4932 -> 19 decimales

Hay que tener en cuenta que en algunos casos existen dos opciones, esto es porque depende del sistema en donde ejecutamos nuestro programa. Para poder saber con exactitud cuánto ocupa un tipo de datos en el sistema donde se ejecuta nuestro programa, disponemos de la instrucción sizeof de C. A continuación se muestra un ejemplo donde se ve cómo utilizar este comando y cómo acceder a las constantes que indican los valores máximos y mínimos en el caso de un real declarado en C como float:

[Ejemplo 06_01]

#include <stdio.h>
#include
<float.h>

int main() {

    printf("Storage size for float : %d \n", sizeof(float));
    printf("Minimum float positive value: %E\n", FLT_MIN );
    printf("Maximum float positive value: %E\n", FLT_MAX );
    printf("Precision value: %d\n", FLT_DIG );
  
   return 0;
}

2. Memoria estática

Ahora que ya sabemos qué ocupa una variable, vamos a ver qué ocupa un programa en memoria. Cuando ejecutamos un programa, el sistema operativo busca una zona en la memoria donde este quepa. Por tanto, la dirección inicial de nuestro programa cambiará cada vez que lo ejecutemos. Lo que no cambia es su estructura en memoria (fijaos que las direcciones van en orden inverso):

MemStruct3_2.png

Podemos ver que hay unas cuantas posiciones de memoria marcadas como args. En estas posiciones es donde se guardan los parámetros que se han pasado al programa en su ejecución. Por ejemplo, cuando compilamos un programa con GCC -o main main.c, al programa GCC le estamos pasando 3 parámetros: -o, main y main.c.

El bloque BSS es donde se guardan todas las variables globales y estáticas que no se han inicializado cuando se han declarado. Todas las variables que llegan a este bloque se inicializan poniendo todos los bits a 0 antes de comenzar a ejecutar el programa.

El bloque DS o Data Segment es donde se guardan todas las variables globales o estáticas que se han inicializado en el momento de declararse y el resto de variables declaradas dentro del programa (no estáticas ni globales).

Finalmente el bloque texto o Code Segment contiene parte de las instrucciones de nuestro programa, que se irán ejecutando una a una.

Las variables que quedan en el DS y que no se hayan inicializado en el momento de ser declaradas pueden tener cualquier valor, por lo que si se usan antes de asignarles ningún valor conocido, el resultado puede ser cualquiera. Es una buena costumbre inicializar todas las variables cuando se declaran.

Por ejemplo, si tenemos el siguiente programa:

[Ejemplo 06_02]

#include <stdio.h>

int a;
int b=2;

int main() {

   static char c;
   const float d=3.14;   
   return 0;
}

Las variables a y c quedarían guardadas en el bloque BBS, mientras que las variables b y d se guardarían en el bloque DS.

La cantidad de memoria que ocupan los segmentos BBS y DS es fija desde el momento en que se ejecuta el programa hasta que este finaliza y no se puede modificar su tamaño durante la ejecución. Por este motivo, esta memoria se conoce como memoria estática.

3. Relación entre objetos y memoria

Cuando declaramos un objeto, ya sea variable o constante, el nombre que le damos queda vinculado con la dirección de la posición de memoria donde este comienza. Por ejemplo, si tenemos el código:

[Ejemplo 06_03]

#include <stdio.h>

int main() {

  int a=0;
  return 0;
}

En alguna zona del bloque DS tendremos un espacio de 4 bytes (o 2 según el sistema) donde se guardará la información de esta variable:

intVar.png

Para saber la dirección donde ha quedado guardada una variable, lo podemos hacer con el operador &. Por ejemplo, el siguiente código declara dos variables en BSS y dos en DS y muestra la dirección inicial y final, y el tamaño en bytes que ocupa cada variable.

[Ejemplo 06_04]

#include <stdio.h>

long b;
int c;

int main() {

   int a=0;
   float d=5.0;
   
    printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'a', sizeof(a), (unsigned long)(&a), (unsigned long)(&a)+(sizeof(a)-1));
    printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'b', sizeof(b), (unsigned long)(&b), (unsigned long)(&b)+sizeof(b)-1);
    printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'c', sizeof(c), (unsigned long)(&c), (unsigned long)(&c)+sizeof(c)-1);
    printf("Var %c (%d bytes) starts at %ld and ends at %ld\n", 'd', sizeof(d), (unsigned long)(&d), (unsigned long)(&d)+sizeof(d)-1);

   return 0;
}

En general, en esta asignatura no utilizaremos ni variables globales ni estáticas, por lo tanto, podemos asumir que todas nuestras variables van al bloque DS y, por lo tanto, nos podemos hacer una idea de cómo quedarían en memoria. Por ejemplo, si queremos hacer la representación gráfica de la memoria para el siguiente programa:

[Ejemplo 06_05]

#include <stdio.h>

long b;
int c;

int main() {

   int a=5;
   short b=7;
   char c='a';    

   return 0;
}

Tendríamos un resultado similar al siguiente (los valores binarios que hay no son los correctos):

MemStrucABC.png

4. El tipo puntero en C

Por su estrecha relación con la memoria, en esta sección se introduce brevemente el tipo de datos de lenguaje C puntero. Los punteros son variables que en vez de contener un valor (como los enteros, caracteres o reales), contienen una dirección de memoria. 

Un puntero es una variable cuyo valor es una dirección de memoria. Dado que un puntero contiene una dirección de memoria, el espacio que ocupa en memoria será siempre el mismo, el tamaño de una dirección. Este tamaño dependerá de si nuestro sistema es de 32 bits o 64 bits, pero será independiente del tipo que le asignamos al puntero.

4.1. Declaración

Al igual que cualquier otro variable, un puntero se declarará para poderlo utilizar:

[Ejemplo 13_01]

#include <stdio.h>

int main() {

   /* Variable definition */
    type *p=NULL;

}

Donde type es cualquier tipo de datos (entero, real, etc.). La constante NULL tiene un valor 0, y se interpreta como que este puntero no ha sido inicializado, o sea, que no apunta a ningún sitio.

4.2. Operaciones

Aunque las direcciones son iguales sea cual sea el tipo, se le asigna un tipo para indicar qué contiene la dirección que se guarda en este puntero. Hay que tener mucho cuidado con los tipos de los punteros, aunque los punteros de todos los tipos ocupan lo mismo, el tipo asignado a un puntero tiene un gran efecto en el resultado que se obtiene al operar con él.

Dado que un puntero contiene una dirección de memoria, que es un valor numérico, podemos realizar las mismas operaciones que con cualquier valor numérico:

4.2.1. Asignación

Como hemos dicho anteriormente, un puntero contiene una dirección de memoria. Si queremos que un puntero apunte a una determinada variable, o sea, asignar al puntero la dirección de una determinada variable, lo haremos de la siguiente manera (fijaos en que los tipos deben coincidir entre variable y puntero):

[Ejemplo 13_02]

#include <stdio.h>

int main() {

   /* Variable definition */
   int a=3;
   float b=4.5;

   int *pa=&a;
   float *pb=&b;

}

En este ejemplo, a la variable pa se le asigna la dirección de la variable a, y a la variable pb la dirección de la variable b. A continuación, se muestra una representación simplificada de cómo quedaría en memoria:

pointerAssigment.png

Observemos que cuando asignamos a un puntero la dirección de una variable, estamos asignando la dirección del primer byte, por tanto, esta operación no se ve afectada por el tamaño en memoria de la variable (dada por su tipo).

4.2.2. Contenido

La dirección de una variable es una información que por sí sola no nos aporta gran cosa, ya que cada vez que ejecutamos nuestro programa este valor cambiará. Lo realmente importante es el hecho de que a partir de esta dirección podemos acceder al contenido que se guarda en esa posición de memoria. Siguiendo con el ejemplo anterior, a partir de la dirección guardada en pb, podemos recuperar el contenido b. Dicho de otro modo, podemos acceder al contenido de la variable apuntada por pb.

El siguiente código muestra el operador contenido, que nos permite recuperar el contenido a partir del puntero.

[Ejemplo 13_03]

#include <stdio.h>

int main() {

   /* Variable definition */
   int a=5;
   int *pa=NULL;

    pa=&a;
   *pa=25;
}

Observad que cuando recuperamos el contenido, el tipo del puntero es muy importante, ya que el puntero solo contiene la dirección inicial y el tipo es lo que nos permitirá saber cuántos bytes hay que utilizar y cómo interpretarlos. A continuación, se muestra una representación de la memoria en diferentes situaciones:

  1. El estado inicial de las variables.
  2. El resultado de asignar la dirección de a al puntero pa.
  3. Cambiar el contenido del puntero pa, asignándole un nuevo valor.

valueAssigment.png

Hay que tener presente que el contenido de un puntero a entero es un entero y, por lo tanto, se puede asignar a una variable de tipo entero y se le puede aplicar cualquier operación entera, como por ejemplo, sumarle otro entero o escribirlo por pantalla. Lo mismo ocurre con cualquier otro tipo, o sea, el contenido de un puntero a real es un real y por lo tanto lo podemos tratar como tal.

Resumen

En esta unidad hemos visto:

  • La representación en memoria de un programa.
  • Los tipos de datos en C.
  • La representación de los datos en memoria y la relación con sus direcciones.
  • El concepto de memoria estática.
  • El tipo puntero en C.
Etiquetas:
Creado por editor1 el 2018/09/17 10:08