Los punteros son variables que en vez de contener un valor (como los enteros, caracteres o reales), contienen una dirección de memoria. Es decir, un puntero es una variable que apunta a otra variable.
Dado que el puntero contiene una dirección de memoria, el espacio que ocupa en memoria es fijo. Este tamaño depende de la arquitectura del sistema, pero suele ser de 4 bytes en sistemas de 32 bits y de 8 bytes en sistemas de 64 bits.
Al igual que cualquier otra variable, el puntero se debe declarar:
```c
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
type *p;
/* Variable initialization */
p = NULL;
}
```
`type` puede ser cualquier tipo de dato. La constante NULL tiene un valor 0, y se interpreta como que este puntero no ha sido inicializado. Es una buena práctica inicializar los punteros a NULL para evitar errores.
A los punteros se les asigna un tipo para que el compilador pueda saber cuántos bytes tiene que leer o escribir. Por ejemplo, si se declara un puntero a un entero, el compilador sabe que tiene que leer o escribir 4 bytes. El tipo asignado al puntero tiene un gran efecto en el resultado que se obtiene al operar con él.
La dirección de una variable nos permite acceder al contenido almacenado en esa posición de memoria. Aunque la dirección en si misma puede cambiar cada vez que el programa se ejecuta, lo relevante es que nos da acceso al contenido de la variable apuntada. Es decir, si tenemos la dirección guardada en `pb`, podemos recuperar el contenido de `b` con la variable apuntada por `pb`.
Cuando recuperamos el contenido el tipo de 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.
El contenido de un puntero a entero se puede asignar a una variable entera y se le puede aplicar cualquier operación entera. Lo mismo ocurre con cualquier otro tipo.
Una diferencia importante en el comportamiento de estos operadores cuando se aplican a punteros es que trabajan con unidades de datos, no con valores. Por ejemplo, si le sumamos 3 a un puntero entero que contiene la dirección 33, el resultado no será la dirección 36, sino que será 33 + 3 * tamaño(caracter) = 33 + 3 * 1 = 36. Por lo tanto, el puntero determina el resultado final de la operación.
Es importante entender la diferencia entre asignar un valor al puntero o asignarlo al contenido del puntero. Cuando asignamos el valor de `a+b` al contenido de `p`, estamos asignando el valor de `a+b` a la variable `a`, es decir, tendrá el valor de 8.
Al igual que los punteros a enteros, cuando apuntemos a una tupla ocupará lo mismo, lo que ocupe la dirección de memoria.
```c
#include <stdio.h>
/* Type definition */
typedef struct {
float x;
float y;
} tPoint;
int main(int argc, char** argv) {
/* Variables definition */
tPoint a;
tPoint *pPoint;
float *pCoord;
/* Step 1, variables initialization */
a.x = 3;
a.y = 5;
pPoint = NULL;
pCoord = NULL;
/* Step 2 */
pPoint = &a;
/* Step 3 */
pPoint->x = -2;
/* Step 4 */
pCoord = &(a.y);
/* Step 5 */
*pCoord = 6;
return 0;
}
```
Lo que se hace en este código, línea a línea, es:
1.`tPoint a;`: Declara una variable de tipo `tPoint` llamada `a`.
2.`tPoint *pPoint;`: Declara un puntero a una estructura `tPoint` llamado `pPoint`.
3.`float *pCoord;`: Declara un puntero a un float llamado `pCoord`.
4.`a.x = 3;`: Asigna el valor 3 al miembro `x` de la estructura `a`.
5.`a.y = 5;`: Asigna el valor 5 al miembro `y` de la estructura `a`.
6.`pPoint = NULL;`: Inicializa el puntero `pPoint` a `NULL`.
7.`pCoord = NULL;`: Inicializa el puntero `pCoord` a `NULL`.
8.`pPoint = &a;`: Asigna la dirección de memoria de la estructura `a` al puntero `pPoint`.
9.`pPoint->x = -2;`: Accede al miembro `x` de la estructura apuntada por `pPoint` (que es `a`) y le asigna el valor -2.
10.`pCoord = &(a.y);`: Asigna la dirección de memoria del miembro `y` de la estructura `a` al puntero `pCoord`.
11.`*pCoord = 6;`: Accede al valor almacenado en la dirección de memoria apuntada por `pCoord` y le asigna el valor 6, lo que cambia el valor de `a.y`.
Los valores que tienen las variables en cada step:
El caso de los vectores (incluidas las cadenas de caracteres) y las matrices es un caso especial de punteros. Cuando declaramos una variable como vector o matriz, en realidad estamos declarando un puntero a la primera posición de memoria del vector o matriz.
Aunque en el lenguaje algorítmico podemos expresar la asignación sin problemas, cuando lo implementamos en C este hecho provoca dos efectos que hay que tener en cuenta.
*Efecto 1*: No podemos asignar los valores de un vector o matriz a otro vector o matriz directamente. En su lugar, tenemos que recorrer los elementos y asignarlos uno a uno.
```c
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
int a[3];
int b[3];
/* Variable initialization */
a[0] = 1;
a[1] = 2;
a[2] = 3;
b[0] = a[0];
b[1] = a[0];
b[2] = a[0];
b = a; /* Error */
return 0;
}
```
En este código, el tipo de las variables a y b realmente es int*, o sea, punteros a enteros. POr tanto, la línea `b = a;` dará un error de compilación. Estamos asingnando la dirección de la primera posición de a. Si mostramos el contenido de los dos vectores podríamos tener la sensación de que la asignación se ha hecho correctamente, ya que en efecto vemos los mismos valores, pero si modificamos cualquiera de los valores de b también se modificarán los valores de a, ya que a efectos prácticos hemos hecho que los dos punteros apunten al mismo vector.
Tampoco funcionará si intentamos asignar el contenido delvector:
```c
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
int a[3];
int b[3];
/* Variable initialization */
a[0] = 1;
a[1] = 2;
a[2] = 3;
b[0] = a[0];
b[1] = a[0];
b[2] = a[0];
*b = *a; /* Error */
return 0;
}
```
Como a y b apuntan a la primera posición de los vectores, lo que haríamos con este código sería copiar el contenido de la primera posición del vector a, o sea el entero 1 a la primera posición del vector b, el cual pasaría a tener los valores {1,0,0}.
*Efecto 2*: Cuando pasamos un vector o matriz como parámetro a una función o acción, este parámetro siempre será de entrada/salida.
Al pasar un vector o matriz realmente estamos pasando un puntero y, por tanto, como hemos visto en el apartado anterior, los cambios que se hagan en este vector perdurarán al finalizar la acción o función. El lenguaje C nos permite protegernos de este efecto. Si queremos evitar que una acción o función modifique los contenidos de un vector o matriz, podemos declarar el parámetro como const:
```c
#include <stdio.h>
void f(const int *p) {
*p = *p+2; /* Error */
}
int main(int argc, char** argv) {
/* Variable definition */
int a;
/* Variable initialization */
a = 3;
f(&a);
printf("%d\n", a);
return 0;
}
```
Al haber añadido la palabra reservada const, el compilador no dejará que modifiquemos el contenido del puntero.
La forma correcta de asignar los valores de un vector o matriz a otro vector seria recorriendo los elementos y asignarlos uno a uno.
Si queremos pasar un parámetro a una función o acción que sea de salida o entrada/salida, tenemos que pasar un puntero a la variable. De esta forma, la función o acción podrá modificar el contenido de la variable.
Tenemos la acción principal, la cual tiene una variable `a` de tipo entero, la inicializa a 3 y la pasa a una segunda
acción `f`, la que simplemente suma 2 al valor del parámetro `p`.
```alg
action f(in p: integer)
p := p + 2
end action
algorithm example1
var
a: integer
end var
a := 3;
f(a)
writeInteger(a)
end algorithm
```
```c
#include <stdio.h>
void f(int *p) {
/* Step 3 */
*p = *p + 2;
}
int main() {
/* Variable definition */
int a;
/* Variable initialization - Step 1*/
a = 3;
/* Step 2 */
f(a);
/* Step 4 */
printf("%d\n", a);
return 0;
}
```
1. Ha comenzado la ejecución, por lo tanto, la función principal está en memoria, y contiene una variable a de tipo entero con un valor de 3.
2. La acción principal ha llamado la acción `f`, por lo que esta ahora se encuentra en memoria. Vemos que tiene una variable `p` de tipo entero. Como se ha llamado a `f` pasando el parámetro a de la acción principal, la variable `p` contiene el valor de `a`.
3. La acción `f` modifica la variable `p`, sumando 2 a su valor.
4. Al haber terminado la ejecución de la acción `f`, esta ya no está en memoria y vemos que el valor de la variable a de la acción principal no se ha visto alterado.
Los valores que tienen las variables en cada step:
- Step 1 - `a = 3`
- Step 2 - `a = 3``p = 3`
- Step 3 - `a = 3``p = 5`
- Step 4 - `a = 3`
Ahora vamos a cambiar el código anterior para que el parámetro se pase como parámetro de entrada/salida.
```alg
action f(inout p: integer)
p:=p+2;
end action
algorithm example2
var
a: integer
end var
a:=3;
f(a);
writeInteger(a);
end algorithm
```
```c
#include <stdio.h>
void f(int *p) {
/* Step 3 */
*p=*p+2;
}
void main() {
/* Variable definition */
int a;
/* Variable initialization - Step 1*/
a = 3;
/* Step 2 */
f(&a);
/* Step 4 */
printf("%d \n", a);
return 0;
}
```
1. Ha comenzado la ejecución, por lo tanto, la función principal está en memoria, y contiene una variable `a` de tipo entero con un valor de 3.
2. La acción principal ha llamado a la acción `f`, por lo que esta ahora se encuentra en memoria. Tiene una variable `p` de tipo puntero a entero. Como se ha llamado a `f` pasando la dirección de la variable a de la acción principal, la variable `p` contiene la dirección de `a`.
3. La acción `f` modifica el contenido de la variable `p`, sumando 2 a su valor. Al ser un puntero, realmente se está modificando el valor que está guardado en la variable `a` de la acción principal.
4. Al haber terminado la ejecución de la acción `f`, esta ya no está en memoria y vemos que el valor de la variable a de la acción principal se ha visto alterado, y ahora contiene el valor 5.
Los valores que tienen las variables en cada step:
- Step 1 - `a = 3`
- Step 2 - `a = 3``p = &a`
- Step 3 - `a = 5``p = &a`
- Step 4 - `a = 5`
Vemos que en este segundo caso, el hecho de utilizar punteros ha permitido que los cambios efectuados dentro de la acción se propaguen al programa principal. En el primer caso, decimos que se ha pasado la variable `a` a la acción `f` por valor, mientras que en el segundo caso diremos que se ha pasado la variable a a la acción `f` por referencia.
El concepto de modularidad se basa en dividir el código en partes más pequeñas que tangan cierta funcionalidad. Esta división permite:
- Hacer más legible el código.
- Reutilizar partes del código en diferentes aplicaciones.
- Facilitar la depuración del código.
- Facilitar la colaboración entre diferentes programadores.
- Facilitar la actualización de partes del código.
- Facilitar la documentación del código.
- Facilitar la prueba del código.
La modularización más simple es el uso de funciones y acciones, pero a medida que necesitamos trabajar con códigos más grandes, también se hace necesario separar estas acciones y funciones en diferentes ficheros.
Cuando creamos un nuevo proyecto en CodeLite, por ejemplo Modularidad, se genera una distribución concreta de ficheros dentro de la carpeta del proyecto. En la carpeta del proyecto se encuentran los siguientes ficheros:
-`main.c`: Fichero principal del proyecto que contiene el programa HelloWorld.
-`Modularidad.project`: Fichero que contiene la configuración del proyecto.
Si compilamos el programa se crearán una nueva carpeta llamada `Debug` y en la carpeta del proyecto se crearán nuevos ficheros:
-`Debug`: Carpeta que contiene los ficheros generados por el compilador.
-`main.c`: Fichero principal del proyecto que contiene el programa HelloWorld.
-`Modularidad.mk`: Fichero que contiene la configuración del proyecto.
-`Modularidad.project`: Fichero que contiene la configuración del proyecto.
-`Modularidad.txt`: Fichero que contiene la salida de la compilación.
En la carpeta `Debug` se encuentran los siguientes ficheros:
-`main.c.o`: Fichero objeto generado por el compilador a partir del fichero `main.c`.
-`main.c.o.d`: Fichero que contiene la información de las dependencias del fichero `main.c`.
-`Modularidad`: Ejecutable generado por el compilador a partir del fichero `main.c.o`.
Desde un terminal podemos ejecutar el programa `Modularidad` de la siguiente forma:
A medida que nuestro programa crece, es necesario dividirlo en múltiples ficheros. Cuando todos los ficheros se encuentran en un mismo directorio no supone un problema, pero si estos se encuentran en distintas carpetas es necesario indicar a CodeLite dónde tiene que ir a buscarlos.
Si continuamos con el ejemplo anterior, Modularidad, lo que deseamos es organizar el proyecto con la siguiente estrucura:
```bash
.
├── include
│ └── helloWorld.h
└── src
├── helloWorld.c
└── main.c
```
Se trata de modularizar el fichero inicial main.c en tres:
-`main.c`: Fichero principal del proyecto que contiene el programa HelloWorld.
-`helloWorld.c`: Fichero que contiene la implementación de acciones y funciones.
-`helloWorld.h`: (Cabecera) Fichero que contiene la declaración de la acción HelloWorld.
Ahora transformamos el programa HelloWorld en un programa modularizado. Necesitamos la definición de algunas funciones/acciones que nos permitan separar un único archivo en los ficheros mencionados.
Primero eliminaremos desde CodeLite el programa actual; por lo tanto, CodeLite -> proyecto Modularidad -> src -> botón derecho sobre el nombre del fichero main.c -> Remove -> confirmamos la eliminación -> confirmamos la eliminación del fichero main.c de disco.
Creamos el programa principal main.c de la siguiente forma: proyecto Modularidad -> botón derecho sobre la carpeta src -> Add New File -> seleccionamos el tipo C Source File (.c) -> indicamos como Name: main.c, y como Location seleccionamos la carpeta ./src creada anteriormente.
Creamos el segundo fichero, helloWorld.c, de la misma manera: proyecto Modularidad -> botón derecho sobre la carpeta src -> Add New File -> seleccionamos el tipo C Source File (.c) -> indicamos como Name: helloWorld.c, y como Location seleccionem la misma carpeta ./src anterior.
Creamos una carpeta virtual nueva dentro del proyecto: botón derecho sobre el proyecto Modularidad -> New Virtual Folder -> indicamos como nombre include.
Para finalizar, creamos el tercer fichero, helloWorld.h. Los pasos serán: proyecto Modularidad -> botón derecho sobre la carpeta include -> Add New File -> seleccionamos el tipo Header File (.h) -> indicamos como Name: helloWorld.h, y como Location la carpeta ./include que acabamos de crear.
En este punto, nuestro proyecto Modularidad ya tendrá la estructura deseada. Ahora solo precisamos dar contenido a los tres ficheros creados y que inicialmente están vacíos.
- Configurar CodeLite para que sepa dónde buscar el fichero de cabecera .h cuando lo referenciemos. Lo hacemos de la siguiente forma: click botón derecho sobre Modularidad -> Settings... -> Compiler -> dentro de la opción Include Paths añadir el valor .;./include.
- Permitir que los archivos estén enlazados entre ellos, ya que en estos momentos son independientes entre sí. En el archivo main.c se hace referencia a la acción showHelloMessage() de la que no sabe nada. Es preciso importar el fichero de cabecera con un include usando comillas dobles: `#include "helloWorld.h"`.