3.a – Vectores y Array/ Aritmética de Punteros

Antes de iniciar, podes ver la diferencia entre size_t y sizeof.

size_t es un tipo de dato utilizado para representar tamaños de objetos en memoria, como el tamaño de un arreglo, la cantidad de elementos en una estructura de datos, etc. Se trata de un tipo de dato sin signo y se utiliza comúnmente en combinación con el operador sizeof para obtener tamaños de objetos. Algunos puntos clave sobre size_t son:

  • Uso con sizeof: Puedes usar size_t en conjunto con el operador sizeof para obtener tamaños en bytes de objetos en memoria.

Por ejemplo:

#include <stdio.h>

int main() {
    size_t size = sizeof(int);
    printf("El tamaño de int es: %zu bytes\n", size);
    return 0;
}
  • Índices y tamaños: Es común utilizar size_t para representar índices y tamaños en funciones y estructuras de datos. Por ejemplo, muchas funciones de la biblioteca estándar de C, como strlen, devuelven valores de tipo size_t para representar tamaños.
#include <stdio.h>
#include <string.h>

int main() {
    const char *cadena = "Hola, mundo";
    size_t longitud = strlen(cadena);
    printf("La longitud de la cadena es: %zu\n", longitud);
    return 0;
}

  • Asignación de memoria dinámica: size_t también se utiliza con funciones de asignación de memoria dinámica, como malloc, calloc y realloc, para especificar la cantidad de memoria a asignar.
#include <stdio.h>
#include <stdlib.h>

int main() {
    size_t cantidad_elementos = 10;
    int *arreglo = (int *)malloc(cantidad_elementos * sizeof(int));
    
    if (arreglo != NULL) {
        printf("Memoria asignada correctamente.\n");
        free(arreglo); // No olvides liberar la memoria cuando ya no la necesites
    } else {
        printf("No se pudo asignar memoria.\n");
    }
    
    return 0;
}

Usar size_t es una buena práctica cuando trabajas con tamaños de objetos en memoria, ya que se adapta al sistema subyacente y es garantizado que tiene suficiente capacidad para representar tamaños de memoria.

Ahora, dicho todo esto, sizeof se utiliza para determinar el tamaño en bytes de un tipo de dato, una variable o una expresión. Las formas mas comunes para usarlas son:

  1. Tamaño de un tipo de dato:
#include <stdio.h> 
 
int main() { 
    printf("El tamaño de int es: %zu bytes\n", sizeof(int)); 
    printf("El tamaño de char es: %zu bytes\n", sizeof(char)); 
    printf("El tamaño de float es: %zu bytes\n", sizeof(float)); 
    return 0; 
}
  1. Tamaño de una variable:

Puedes usar sizeof para determinar el tamaño en bytes de una variable:

#include <stdio.h>

int main() {
    int numero = 42;
    printf("El tamaño de la variable 'numero' es: %zu bytes\n", sizeof(numero));
    return 0;
}
  1. Tamaño de una estructura:

Puedes usar sizeof para obtener el tamaño en bytes de una estructura:

#include <stdio.h>

struct Persona {
    char nombre[20];
    int edad;
};

int main() {
    printf("El tamaño de la estructura 'Persona' es: %zu bytes\n", sizeof(struct Persona));
    return 0;
}
  1. Tamaño de un arreglo:

También puedes usar sizeof para obtener el tamaño en bytes de un arreglo:

#include <stdio.h>

int main() {
    int numeros[10];
    printf("El tamaño del arreglo 'numeros' es: %zu bytes\n", sizeof(numeros));
    return 0;
}

Es importante tener en cuenta que sizeof devuelve el tamaño en bytes como un valor del tipo size_t. El tamaño puede variar dependiendo de la arquitectura y el compilador que estés utilizando. Además, sizeof es un operador estático, lo que significa que se evalúa en tiempo de compilación, por lo que no puedes usarlo para obtener el tamaño de una asignación dinámica de memoria.


Relación entre Array y Punteros

Como vimos, para ver las direcciones podemos usar %p o %x (hexa).

Para ver la relación podemos crear un vector de enteros llamado v, donde:

v contiene la dirección de inicio del vector por lo que : v es equivalente a &v[0] y a &v como se aprecia a continuación:

int vec[3]={5,3,2};
printf("%p \n", &vec[0]);
printf("%p \n", &vec);
printf("%p \n", vec);

Ahora si yo creo un puntero llamado pe con la dirección contenida por v, resulta que pe también apunta a vec[0], es decir v, es equivalente a pe.

    int vec[3]={5,3,2};

    printf("%p \n", &vec[0]);
    printf("%p \n", &vec);
    printf("%p \n", vec);

    int *p = &vec[0];  // p=v;
    
    printf("%p", p);

Vectores o Array

Un vector es un conjunto de variables del mismo tipo ( array unidimensional ) referenciadas por un nombre común e individualizadas mediante un subíndice numérico. Este conjunto de variables están ubicadas de forma contigua en la memoria. Estas variables no tienen nombre, solo el vector posee uno, por lo que son referidas por medio de su posición dentro de él.

En la figura se observa un vector de variables del tipo int. Prestando atenciòn a las direcciones de memoria, podemos ver que estàn distanciadas en 4. Estamos asumiendo que el programa donde se generó, fue compilado en modo 32 bits, en el que las variables del tipo int ocupan 4 bytes. La dirección de cada variable es en realidad la dirección de su primer bytes ( cada byte tiene su propia dirección). Esto se puede ver mejor en la siguiente imagen:

La forma de declarar un vector o array será:

tipo nombre [tamaño];

El tipo puede ser cualquiera de los ya conocidos y el tamaño indica el número de elementos del vector ( se debe indicar entre corchetes [ ] ) .

 int vec[5]; // declaración de un vector.
 vec[2]=4; // asignación a uno de los elementos del vector.
 vec[5] =3 + vec[2]; // asignación de otro int con una expresión.
 scanf("%d", &vec[2]); // uso con scanf.

El nombre del vector en realidad representa a un puntero CONSTANTE que apunta a la dirección del primer elemento en ese bloque de memoria.

También se puede inicializar sin especificar el tamaño. Por ejemplo:

int vec[] = {1,2,3,5,8,13,21,34};

Como dijimos antes, en este caso, lo que declaramos fue un puntero ( vec), el cual apunta ( su valor es una dirección) a la primera de las 8 variables.

Los arrays son variables estructuradas, donde cada elemento se almacena de forma consecutiva en memoria. Al estar almacenados de forma consecutiva en memoria, podemos acceder a ellos accediendo a una posición errónea de un vector. Comprobando así lo anteriormente dicho.

Ejemplo de como definir vectores:

#define TAM 5

int vec1[TAM]={1,2,3,4,5};  // Inicializa un vector vec1 con 5 elementos y valores asignados
int vec2[TAM] = {10,20}; // Inicializa un vector vec2 con 5 elementos, y cero en las demás posiciones.
int vec3[TAM] = {}; // Inicializa un vector con ceros de tamaño 5
int vec4[TAM]={0};  // Inicializa un vector con ceros de tamaño 5
int vec5[TAM];  // Inicializa un vector con basura de tamaño 5
vec5[TAM]=5; // Fuera de memoria, el ultimo elemento es TAM-1
int vec7[]={30,40,50}; // Inicializa un vector con el tamaño mínimo
int vec8[3]={100,200,300,400,500,600};  // Inicializa un vector de tamaño 3, pero asignando 4 valores adicionales, los cuales no van a enlazarse al vector ya que el vec8 reservo 3 lugares en memoria consecutivos y no 6). Arroja un warning

La forma conocida hasta el momento de acceder a una determinada posición del vector es a través de subíndices. Volviendo al primer ejemplo:

int a = vec[5]; // a: 13

vec[5] = 11; // cambiamos el valor de 13 por el 11

Ahora si yo quisiera recorrer todo el vector como lo hacíamos, solo debería hacer lo siguiente:

int i;

printf("%d \n", sizeof(vec));
printf("%d \n ", sizeof(vec[0]));
for ( i = 0 ;i < sizeof(vec)/sizeof(vec[0]);i++)
     printf("Valor en la pos %d: %d \n", i, vec[i]);

Si cargo el vector sin parametros, o sea sin TAM, me va a calcular la cantidad total de espacios reservados, sino me va a devolver el tamaño de TAM.

Algunas equivalencias:

ip = &v[0] ip = v
x = *ip; x = v[0];
*(v + 1) v[1]
v + i &v[i]


Diferencia entre Aritmética de Punteros y Desplazamiento o Indirección

En C, el término “aritmética de punteros” se refiere a la capacidad de realizar operaciones aritméticas en direcciones de memoria utilizando punteros. Por otro lado, el “desplazamiento” se refiere al número de bytes que un puntero se desplaza en memoria cuando se le suma o resta un valor determinado. Ambos conceptos están relacionados, ya que la aritmética de punteros se basa en el desplazamiento en memoria.

Diferencia entre aritmética de punteros y desplazamiento:

  1. Aritmética de punteros: La aritmética de punteros permite realizar operaciones como la suma y resta de punteros, lo que puede resultar útil para acceder a elementos en matrices, cadenas de caracteres y otras estructuras de datos. Cuando se realiza una operación aritmética en un puntero, el resultado será otro puntero, pero ajustado según el tipo de datos apuntado por el puntero. Esto significa que al sumar o restar un número entero a un puntero, en realidad, se está moviendo a través de la memoria en un número de bytes equivalente al tamaño del tipo de datos apuntado.
  2. Desplazamiento: El desplazamiento en memoria se refiere a la cantidad de bytes que un puntero se desplaza hacia adelante o hacia atrás cuando se le suma o resta un valor determinado. El desplazamiento se calcula multiplicando el número de elementos que se desean avanzar o retroceder por el tamaño del tipo de datos al que apunta el puntero.

Ejemplo de aritmética de punteros:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr; // El puntero apunta al primer elemento del array (10)

    printf("Valor apuntado por ptr: %d\n", *ptr); // Salida: Valor apuntado por ptr: 10

    ptr++; // Se mueve al siguiente elemento en el array (20)

    printf("Valor apuntado por ptr: %d\n", *ptr); // Salida: Valor apuntado por ptr: 20

    return 0;
}

Ejemplo de desplazamiento en C:

#include <stdio.h>

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr; // El puntero apunta al primer elemento del array (10)

    printf("Valor apuntado por ptr: %d\n", *ptr); // Salida: Valor apuntado por ptr: 10

    int desplazamiento = 2;
    ptr = ptr + desplazamiento; // Se desplaza dos elementos en el array (30)

    printf("Valor apuntado por ptr: %d\n", *ptr); // Salida: Valor apuntado por ptr: 30

    return 0;
}

En resumen, la aritmética de punteros en C permite manipular punteros para acceder a diferentes ubicaciones de memoria, mientras que el desplazamiento en memoria se logra mediante operaciones aritméticas en punteros y depende del tamaño del tipo de datos apuntado.

TENER EN CUENTA : *(vec+i) ES INDIRECCIÓN

Si yo hiciera:

vec[1] = 10; // Esto es manejar arrays con subindices.

Si yo hiciera:

*(vec+19) = 18; // Esto es manejar arrays con indirecciones o desplazamiento. Aca estamos haciendo que el puntero se mueva.

Cuando uno realiza desplazamiento como vimos anteriormente todos los desplazamientos son desde el mismo elemento o la misma posición. O sea estan relacionados a la misma posición.


Ejemplo de MANEJO DE VECTORES CON PUNTEROS

ACLARACIÓN: recordar que VEC un puntero constante que no se puede modificar donde está definido. Lo que se debe hacer es crear un puntero que contiene la dirección de vec. En el ejemplo, cuando se pasa como parámetro a vec, lo que se está pasando es un puntero donde se copió la dirección de vec y que no es constante, por eso se puede usar aritmética de punteros en la función y NO donde se definió el puntero.


TENER EN CUENTA:

– La función imprimir solo debe imprimir, no debe hacer otras cosas. La idea de la programación modular es poder reutilizar código.

– Trabajar los arrays con aritmética de punteros NO con subindices 

– Trabajar los arrays con aritmética de punteros NO con indirección 

Los vectores los vamos a manejar con aritmética de punteros. Esto significa que la función recibe como parámetro un puntero al vector y recuerden que el NOMBRE DE UN VECTOR ES UN PUNTERO CONSTANTE A LA DIRECCIÓN DEL PRIMER ELEMENTO, por lo tanto, ya es un puntero y lo pasamos directamente. 

– Las variables definirlas al inicio del bloque, NO dentro del for, no todos los compiladores lo aceptan. 

– No es necesario utilizar la llave si es una sola línea de código.

– Las banderas se usan cuando son necesarias.

– Si se pide recorrer solo una vez el vector, esto DEBE respetarse.

Siempre identar el código. Ayuda mucho a ver si cometimos errores y ayuda a que nuestro código sea más fácil de leer, lo que vuelve a ayudar a cometer menos errores.

– Utilizar nombres significativos. Los nombres significativos tanto en variables como en funciones ayudan a comprender el funcionamiento del código más fácilmente.

Ejercicios de Vectores con Aritmética de Punteros


Aritmética de Punteros

A un puntero puede sumársele o restársele el tipo de dato del puntero. En el caso particular que este puntero sea del tipo entero, el puntero apuntaría al primer entero seguido del entero que apunta o al anterior.

Ejemplo: 

p = p + 3;
p = p – 3;
p++;
p–;

Tiene especial importancia el tipo base apuntado.

La unidad de incremento o decremento de punteros no es el byte, sino la cantidad de bytes determinados por el tamaño del tipo base.