Update PEC5

This commit is contained in:
Manuel Vergara 2024-05-08 21:31:40 +02:00
parent b819f47fea
commit bf13edc69e

View File

@ -104,6 +104,11 @@ int main (int argc, char** argv) {
}
```
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.
@ -198,53 +203,463 @@ int main(int argc, char** argv) {
}
```
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.
```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:
- 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.
```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 del vector:
```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.
```c
#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`.
```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.
## 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 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:
```bash
$ ./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:
```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.
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:
```c
#include <stdio.h>
int main(int argc, char** argv) {
printf("Hello World!\n");
return 0;
}
```
Al siguiente código modularizado:
```c
#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.
```c
#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.
```c
#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.
```c
/* 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.
[Volver arriba](#pec-3)