22 KiB
PEC 5
Volver a la página principal de "Fundamentos de la Programación"
Índice
13. Punteros en C
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.
13.1. Uso de punteros
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.
13.1.1. Declaración
Al igual que cualquier otra variable, el puntero se debe declarar:
#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.
13.1.2. Operaciones
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.
Con un puntero podemos realizar las mismas operaciones que con cualquier valor numérico: Asignación, contenido, operaciones lógicas y aritméticas.
13.1.2.1. Asignación
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
int a;
float b;
int *pa;
float *pb;
/* Variable initialization */
a = 3;
b = 4.5;
pa = &a;
pb = &b;
}
En este caso, pa
apunta a la dirección de memoria de a
, y pb
apunta a la dirección de memoria de b
.
13.1.2.2 Contenido
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
.
#include <stdio.h>
int main (int argc, char** argv) {
/* Variable definition */
int a;
int *pa;
/* Step 1, Variable initialization */
a = 5;
pa = NULL;
/* Step 2, Assign the address of a to pa */
pa = &a;
/* Step 3, change the content of pa, new value */
*pa = 25;
}
El valora que tienen las variables en cada step:
- Step 1 -
a = 5
,pa = NULL
- Step 2 -
a = 5
,pa = &a
- Step 3 -
a = 25
,pa = &a
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.
13.1.2.3. Operaciones lógicas
Cuando comparamos dos punteros como el siguiente ejemplo:
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
int a;
int b;
int *pa;
int *pb;
/* Variable initialization */
a = 3;
b = 5;
pa = &a;
pb = &b;
/* Comparing memory addresses */
if (pa < pb) {
printf("pa is smaller than pb\n");
}
return 0;
}
Comparamos las direcciones que guardan, no los valores de las variables a las que apuntan.
Para comparar el contenido de la dirección a la que apunta se tiene que utilizar el operador de indirección *
.
#include <stdio.h>
int main(int argc, char** argv) {
/* Variables definition */
int a;
int b;
int *pa;
int *pb;
/* Variables initialization */
a = 3;
b = 5;
pa = &a;
pb = &b;
/* Comparing the values pointed by pa and pb, not the memory addresses */
if (*pa < *pb) {
printf("the value pointed by pa is smaller than the value pointed by pb");
}
return 0;
}
13.1.2.4. Operaciones aritméticas
Entonces, las direcciones son valores numéricos y les podemos aplicar operadores aritméticos.
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.
13.1.3. Ejemplo de puntero
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
int a;
int b;
int *p;
/* Step 1, Variable initialization */
a = 3;
b = 5;
p = NULL;
/* Step 2, Assign the address of a to p */
p = &a;
/* Step 3, assign the sum of a and b to the content of p */
*p = a+b;
return 0;
}
El valor que tienen las variables en cada step:
- Step 1 -
a = 3
,b = 5
,p = NULL
- Step 2 -
a = 3
,b = 5
,p = &a
- Step 3 -
a = 8
,b = 5
,p = &a
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.
13.2. Punteros a tuplas
Al igual que los punteros a enteros, cuando apuntemos a una tupla ocupará lo mismo, lo que ocupe la dirección de memoria.
#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:
tPoint a;
: Declara una variable de tipotPoint
llamadaa
.tPoint *pPoint;
: Declara un puntero a una estructuratPoint
llamadopPoint
.float *pCoord;
: Declara un puntero a un float llamadopCoord
.a.x = 3;
: Asigna el valor 3 al miembrox
de la estructuraa
.a.y = 5;
: Asigna el valor 5 al miembroy
de la estructuraa
.pPoint = NULL;
: Inicializa el punteropPoint
aNULL
.pCoord = NULL;
: Inicializa el punteropCoord
aNULL
.pPoint = &a;
: Asigna la dirección de memoria de la estructuraa
al punteropPoint
.pPoint->x = -2;
: Accede al miembrox
de la estructura apuntada porpPoint
(que esa
) y le asigna el valor -2.pCoord = &(a.y);
: Asigna la dirección de memoria del miembroy
de la estructuraa
al punteropCoord
.*pCoord = 6;
: Accede al valor almacenado en la dirección de memoria apuntada porpCoord
y le asigna el valor 6, lo que cambia el valor dea.y
.
Los valores que tienen las variables en cada step:
- Step 1 -
a.x = 3
,a.y = 5
,pPoint = NULL
,pCoord = NULL
- Step 2 -
a.x = 3
,a.y = 5
,pPoint = &a
,pCoord = NULL
- Step 3 -
a.x = -2
,a.y = 5
,pPoint = &a
,pCoord = NULL
- Step 4 -
a.x = -2
,a.y = 5
,pPoint = &a
,pCoord = &(a.y)
- Step 5 -
a.x = -2
,a.y = 6
,pPoint = &a
,pCoord = &(a.y)
13.3. Punteros y vectores/matrices
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.
#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 del vector:
#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:
#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.
#include <stdio.h>
int main(int argc, char** argv) {
/* Variable definition */
int a[3];
int b[3];
int i;
/* Variable initialization */
a[0] = 1;
a[1] = 2;
a[2] = 3;
for (i = 0; i < 3; i++) {
b[i] = a[i];
}
return 0;
}
14. Parámetros de entrada/salida en C
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.
14.1. Ejemplo de parámetros de salida y entrada/salida en C
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
.
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
#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;
}
- 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.
- La acción principal ha llamado la acción
f
, por lo que esta ahora se encuentra en memoria. Vemos que tiene una variablep
de tipo entero. Como se ha llamado af
pasando el parámetro a de la acción principal, la variablep
contiene el valor dea
. - La acción
f
modifica la variablep
, sumando 2 a su valor. - 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.
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
#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;
}
- 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. - La acción principal ha llamado a la acción
f
, por lo que esta ahora se encuentra en memoria. Tiene una variablep
de tipo puntero a entero. Como se ha llamado af
pasando la dirección de la variable a de la acción principal, la variablep
contiene la dirección dea
. - La acción
f
modifica el contenido de la variablep
, sumando 2 a su valor. Al ser un puntero, realmente se está modificando el valor que está guardado en la variablea
de la acción principal. - 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.
15. Modularidad en Codeline
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.
15.1. Configuración inicial
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 ficheromain.c
.main.c.o.d
: Fichero que contiene la información de las dependencias del ficheromain.c
.Modularidad
: Ejecutable generado por el compilador a partir del ficheromain.c.o
.
Desde un terminal podemos ejecutar el programa Modularidad
de la siguiente forma:
$ ./Modularidad
Hello World!
15.2. Múltiples ficheros
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:
.
├── 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.
Debemos crear las carpetas ./src
y ./include
que requerirá el proyecto modularizado.
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.
Es decir, pasaremos el siguiente programa:
#include <stdio.h>
int main(int argc, char** argv) {
printf("Hello World!\n");
return 0;
}
Al siguiente código modularizado:
#include <stdio.h>
* Predeclaración de las funciones/acciones */
void showHelloMessage();
/* Código principal */
int main(int argc, char **argv) {
showHelloMessage();
return 0;
}
/* Implementación de las funciones/acciones */
void showHelloMessage() {
printf("hello world\n");
}
Si ejecutamos el programa, obtendremos el mismo resultado que en el programa anterior. Ahora ya podemos llevarlo a los tres ficheros:
main.c
: Fichero principal del proyecto que contiene el programa HelloWorld.
#include <stdio.h>
#include "helloWorld.h"
/* Código principal */
int main(int argc, char **argv) {
showHelloMessage();
return 0;
}
helloWorld.c
: Fichero que contiene la implementación de acciones y funciones.
#include <stdio.h>
/* Implementación de las funciones/acciones */
void showHelloMessage() {
printf("hello world\n");
}
helloWorld.h
: (Cabecera) Fichero que contiene la declaración de la acción HelloWorld.
/* Predeclaración de las funciones/acciones */
void showHelloMessage();
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.
En este punto, dentro del proyecto Modularidad únicamente tenemos la carpeta virtual src.
Atención: en estos momentos la carpeta virtual src del proyecto no tiene ninguna relación con la carpeta física ./src creada anteriormente.
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.
Aquí ya tendremos el programa modularizado, pero nos quedan dos cuestiones:
- 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"
.
Y ya podremos compilar y ejecutar el programa. Si todo ha ido bien, obtendremos el mismo resultado que en los programas anteriores.