La memoria para los programas
Punteros
Los punteros y las referencias son herramientas especialmente interesantes. El lenguaje C no conoce las referencias, pero puede trabajar con punteros siguiendo las reglas aplicables a las referencias. Lenguajes más recientes, como Java, han eliminado los punteros de su vocabulario. No porque tengan mala reputación entre los programadores, sino porque actúan a un nivel inferior que las referencias, lo que perturba el uso de herramientas de alto nivel como el recolector de basura (garbage collector).
Los punteros y las referencias son variables que se utilizan para llegar a otras variables. Para ello, el puntero (o la referencia) utiliza la dirección de la variable de destino, es decir, el número de la celda de memoria donde está almacenado el valor de esta variable. Como la memoria se cuenta en bytes y una variable puede repartir la representación de su valor en varios bytes, los punteros y las referencias son variables definidas para trabajar con un tipo determinado, con el fin de limitar los errores de direccionamiento.
1. Punteros a variables
Empecemos por ver la representación de un fragmento de la memoria del ordenador. Este fragmento contiene una variable x de tipo char, previamente inicializada al valor 3. Las direcciones se suelen escribir en hexadecimal para distinguirlas más claramente de los valores almacenados en memoria, pero también porque las direcciones de 16 o 32 bits se escriben fácilmente en hexadecimal.
Para asignar el valor 10 a la variable x, por ejemplo, podemos utilizar el siguiente fragmento de código:
char x=3;
x=10;
Si pudiéramos obtener la dirección de la variable x, 0x1003 en nuestro caso, podríamos modificar esta variable sin utilizar x directamente. Para ello, definiremos un puntero de tipo char, denotado char*. Esta variable especial, p, recibirá la dirección de la variable x, determinada mediante una sintaxis especial. Podemos entonces modificar el valor situado en esta posición de memoria, incluso si la variable x ya no está en nuestro ámbito.
char* p; // declara un puntero de tipo char
p=&x; // obtiene la dirección de la variable x
printf("p=%x",p); // muestra 1003 en hexa
*p=10; // asigna...
Referencias
Aunque realizan la misma función que los punteros, las referencias ofrecen una sintaxis más sencilla y, al mismo tiempo, limitan el riesgo de accesos erróneos a la memoria.
Una referencia siempre está asociada a una variable, mientras que un puntero se puede modificar utilizando la aritmética de punteros.
La sintaxis para definir un tipo de referencia utiliza el prefijo &, que sustituye a la estrella. Sin embargo, este prefijo no se debe confundir con el operador &, que extrae la dirección de una variable o función.
char c; // un carácter
char*p; // un puntero de char
p=&c; // p es c, y &c es la dirección de c
char & refc=c; // refc es una referencia a char, refc es c
En este fragmento de código, hemos definido una referencia char, refc, designando la variable c. Esta referencia se ha convertido en un alias de la variable c, por lo que cualquier modificación realizada por refc tendrá realmente un impacto en c, incluso fuera de su ámbito:
refc++; // en realidad incrementa c
Como ocurre con los punteros, las referencias sólo son realmente útiles para evitar los efectos secundarios. Para ilustrar esto, tomemos la función may() que estudiamos antes.
Versión de puntero |
Versión de referencia |
|
Constantes
1. Constantes simbólicas
El preprocesador, una utilidad de preprocesamiento de texto, cumple tres tareas importantes:
-
Incluye los archivos designados por la directiva #include.
-
Evalúa la presencia de macros utilizando la directiva #ifdef.
-
Evalúa las macros definidas por la directiva #define.
Este preprocesador trabaja además del compilador. Desde que los nuevos lenguajes de programación han abandonado su uso (Java por ejemplo), es mejor limitar el número de macros definidas con #define.
Sin embargo, esta última directiva se puede utilizar para definir constantes simbólicas:
#define PI 3.14159265358
En los archivos a los que se haya dado esta definición, el preprocesador sustituirá la cadena PI por su valor textual, en todas sus apariciones que no aparezcan en una cadena de caracteres.
#define PI 3.14
double x=PI;
char*a="El científico griego Pitágoras descubrió PI
sin calculadora";
El compilador recibe una versión modificada del último fragmento:
double x=3.14;
char*a="El científico griego Pitágoras descubrió PI
sin calculadora";
2. El tipo void
Ninguna variable se puede tipar como void, pero este tipo se incluye en la clasificación de tipos elementales, como int, short, etc.
Esta palabra clave se utiliza para indicar que una función no devuelve nada y que...
Ejercicios prácticos
Aquí tiene tres variantes del algoritmo para encontrar un valor máximo en un conjunto de números representados por una matriz, lo que le dará muchas oportunidades de practicar el uso de punteros y referencias.
1. Encontrar el valor máximo en una matriz
a. Declaración de la matriz
En el cuerpo de la función main(), se define un matriz de enteros y se inicializa sin asignación de memoria mediante new o malloc. Una variable de tipo entero indica el número de valores de la matriz.
int matriz[] = {1, 10, -2, 0, 4, 11, -6};
int numero_valor = 7;
b. Implementación del algoritmo como una función
La función C++ look_for_biggest recibe como argumentos la matriz y el número de valores a buscar. Observe la diferencia de tipado entre la función main(), donde el matriz es int[] y la función de búsqueda, que acepta como argumento un int*.
La palabra clave throw es nueva: provoca una excepción si la matriz no contiene al menos un valor.
El algoritmo utiliza el valor centinela, inicializado con el primer valor de la matriz, que se compara, a su vez, con todos los valores de la matriz.
// buscar el valor más grande de una matriz
int look_for_biggest(int* tab,int nb)
{
if (num < 1)
throw...