Punteros en c++

Iniciado por Julia13, 26 Mayo 2021, 00:44 AM

0 Miembros y 1 Visitante están viendo este tema.

Julia13

Alguien me pude decir como funcionan los punteros y con un ejemplo por favor

DtxdF

#1
Hola @Julia13

En C, un puntero es una variable que hace referencia a una dirección de memoria, que a su vez hace referencia a un dato.

Teniendo el siguiente código:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
   int x;
   int *y = &x;

   x = 3;

   printf("%d\n", *y);

   return EXIT_SUCCESS;
}


Se crean dos variables. Una de tipo entero y otra un puntero que apunta a un entero.

Primero se asigna el valor 3 en la variable x, y después se asigna el valor de x a y. Se puede notar claramente un operador *, que desreferencia y permite la lectura y la escritura de lo que haya allí en esa variable.

En realidad esta no es la única forma de asignar un valor de una variable a otra.

y = &x;

Lo que hará el operador & será obtener la dirección de x y se la asignará a y, entonces ahora las dos variables apuntarán al mismo dato, pero para acceder a él, es de forma distinta.

Los punteros también son muy útiles para manejar arreglos y que a su vez pueden ser pasados a funciones.

#include <stdio.h>
#include <stdlib.h>

int sum_array(int *array, int length)
{
   int result = 0;

   while (length--)
       result += array[length];

   return result;
}

int main(void)
{
   int n_array[] = { 1, 2, 3, 4 };
   int result = sum_array(n_array, sizeof(n_array)/sizeof(n_array[0]));

   printf("%d\n", result);

   return EXIT_SUCCESS;
}


Lo que sucede en este pequeño programa es que se pasa a la función dos argumentos: el primero es la dirección donde comienza el arreglo y el segundo argumento es la cantidad de elementos que contiene.

En realidad no se ve prácticamente nada de lo que sucede. Veamos este pequeño programa en nasm:

Código (asm) [Seleccionar]
section .data
   n_array: dq 1, 2, 3, 4
   n_array_length: equ ($-n_array)/8

section .text
   global _start
_start:
   mov rsi, n_array
   mov rcx, n_array_length
   xor rax, rax
   xor rbx, rbx

.loop:
   add rax, [rsi+rbx*8]
   inc rbx
   loop .loop


Si mira en la sección de datos se creó n_array, que básicamente es el inicio del arreglo de datos que se ven allí. Si mira la línea 8 se coloca la dirección de inicio en el registro rsi, y mucho después en el bucle .loop, se comienza a obtener los datos para sumarlo en el acumulador rax. Vea [rsi+rbx*8], prácticamente sería como un equivalente al operador * de C, ya que se obtiene ese dato al que apunta el resultado de la operación de sumar el registro rsi con rbx (multiplicado por 8, ya que cada dato del arreglo n_array pesa 8 bytes).

Este programa no se debe ejecutar como uno común, ya que nunca le pide al sistema que salga, pero para estos fines, está perfecto. Hay que mirar desde el depurador, que en mi caso es gdb.

(gdb) break _start
Breakpoint 1 at 0x201160
(gdb) r
Starting program: /tmp/main

Breakpoint 1, 0x0000000000201160 in _start ()
(gdb) disassemble _start
Dump of assembler code for function _start:
=> 0x0000000000201160 <+0>:     movabs $0x202180,%rsi
  0x000000000020116a <+10>:    mov    $0x4,%ecx
  0x000000000020116f <+15>:    xor    %rax,%rax
  0x0000000000201172 <+18>:    xor    %rbx,%rbx
End of assembler dump.
(gdb) disassemble '_start.loop'
Dump of assembler code for function _start.loop:
  0x0000000000201175 <+0>:     add    (%rsi,%rbx,8),%rax
  0x0000000000201179 <+4>:     inc    %rbx
  0x000000000020117c <+7>:     loop   0x201175 <_start.loop>
End of assembler dump.
(gdb) nexti
0x000000000020116a in _start ()
(gdb)
0x000000000020116f in _start ()
(gdb)
0x0000000000201172 in _start ()
(gdb)
0x0000000000201175 in _start.loop ()
(gdb)
0x0000000000201179 in _start.loop ()
(gdb)
0x000000000020117c in _start.loop ()
(gdb)
0x0000000000201175 in _start.loop ()
(gdb)
0x0000000000201179 in _start.loop ()
(gdb)
0x000000000020117c in _start.loop ()
(gdb)
0x0000000000201175 in _start.loop ()
(gdb)
0x0000000000201179 in _start.loop ()
(gdb)
0x000000000020117c in _start.loop ()
(gdb)
0x0000000000201175 in _start.loop ()
(gdb)
0x0000000000201179 in _start.loop ()
(gdb)
0x000000000020117c in _start.loop ()
(gdb)
0x000000000020117e in ?? ()
(gdb) print $rax
$1 = 10
(gdb)


Se recorre todo el arreglo haciendo la misma operación y al final el resultado estará en el registro rax.

Espero le haya servido.

~ DtxdF

Loretz

Bueno, no, no está bien.

Cita de: DtxdF en 26 Mayo 2021, 01:52 AM
    int x;
    int *y;  // (1) y es un puntero sin inicializar

    x = 3;
    *y = x;  // (2) se usa una dirección de memoria que no fue inicializada. 

(1) debería llamar la atención
(2) es un error.



DtxdF

@Loretz

Supongo que no lo has probado. Porque parece que no.

¿Cómo asignarías un valor a un puntero?


~ DtxdF

Eternal Idol

Cita de: DtxdF en 26 Mayo 2021, 22:47 PMSupongo que no lo has probado. Porque parece que no.

No hace falta probarlo, estas desreferenciando un puntero indefinido como ya te dijeron, puede funcionar DE CASUALIDAD miles de veces.

c:\src\badptr.cpp(10) : warning C4700: uninitialized local variable 'y' used

Cita de: DtxdF en 26 Mayo 2021, 22:47 PM¿Cómo asignarías un valor a un puntero?

Un ejemplo elemental:
Código (cpp) [Seleccionar]
int *y = new int;
La economía nunca ha sido libre: o la controla el Estado en beneficio del Pueblo o lo hacen los grandes consorcios en perjuicio de éste.
Juan Domingo Perón

DtxdF

#5
Comprendo Eternal Idol y Loretz. No me había fijado, gracias por la aclaratoria.

~ DtxdF

temporalEotw

Hola, ya veo que te dieron una explicación muy detallada arriba, tocando temas técnicos y tal. Pero trataré de enseñarte los punteros de esta forma... Un puntero no guarda datos específicamente, sino, direcciones de memoria.

Ejemplo:

Tú tienes una casa y la dirección de la casa en un papel, así que el puntero vendría siendo ese papel. Este no tiene la casa pero si su dirección.

Esto te podría servir para apuntar una variable en constante cambio, solo deberías asignarle la dirección al puntero una sola vez y te ahorrarías el tener que reasignarle el valor a cada rato (en caso de que sea una variable).

Siguiendo el ejemplo de arriba:

Resulta que a la casa le estas cambiando el color cada cierto tiempo, ya que tienes la dirección puedes ir y ver esos cambios sin ningún problema o paso de por medio.

En cambio, si hubieras guardado la casa en una variable, al revisarla te darías cuenta de que la casa no ha cambiado de color (cuando se supone que la original sí) así que te tocaría volver a guardar la casa en esa variable para poder apreciar el cambio.

MAFUS

#7
Puntero a lo easy.

Un puntero es un número (cómo todo dentro de un ordenador) y ese número representa una dirección de memoria.

Por ejemplo:
#include <stdio.h>

int main() {
   printf("%d", *(int*)0x61FE1C);
}


En el ejemplo anterior directamente le digo a C que me muestre qué hay en la dirección de memoria 61FE1C. ¿Se puede? Sí. Por cierto, ese casting (int*) es necesario para convertir el literal en un puntero. El * que hay delante nos muestra el contenido.

* Un poco más avanzado: En este caso he hecho un programa previo para ver dónde residía la memoria de datos del programa y pudiera apuntar ahí con este ejemplo. Los sistemas operativos actuales y los microprocesadores funcionan por capas. No van a dejar que un programa de usuario pueda leer o escribir fuera de su área designada. Antes sí que se podía, o en sistemas sin protección sí puede hacerse.

Podemos usar una variable para alojar esa dirección de memoria del ejemplo anterior:
#include <stdio.h>

int main() {
   int *p = (int*)0x61FE1C;
   printf("%d", *p);
}


Aquí asignamos el literal a una variable puntero. Las variables puntero se denotan por anteponer el asterisco (*) delante del nombre durante su definición o declaración. Para ver la definición y declaración por separado sería así:

#include <stdio.h>

int main() {
   int *p;
   p = (int*)0x61FE1C;
   printf("%d", *p);
}


Nótese que cuándo se hace la definición (se le da valor) no se usa el asterisco (*), pues éste tiene la función, en sus posteriores usos, de conseguir el valor guardado en esa posición de memoria, que en este caso es la posición 0x61FE1C.

Pero cómo hemos dicho el conjunto sistema operativo/microprocesador no te deja apuntar a zonas arbitrarias de la memoria, el sistema te la tiene que dar y ahí es cuando entra C que es quien negocia con el sistema para conseguir memoria libre; nosotros no debemos preocuparnos por nada de eso (afortunadamente). Por tanto cuando haces

int n;

C se encarga de reclamar una zona de memoria donde cabe un entero y el sistema se la otorga. ¿Cuál? La verdad es que no nos importa. Ídem para

int *n = malloc(sizeof(int));

Hace lo mismo: pide al sistema una zona de memoria dónde quepa un entero y éste, si tiene, nos la da.

* Un poco más avanzado: la memoria de un programa en C está dividida en sectores que son la memoria de programa, la memoria de la pila y la memoria del montón (heap). Hay más. La pila es donde caen las variables normales como int, char, double, arrays, structs, etcétera y es C el encargado de manejarlas. En el montón van a parar los malloc, calloc y toda esa família; C no entra ahí y es el propio programador el encargado de comprobar que se ha tomado memoria, de liberarla para futuros usos demás menesteres de administración.

Lo normal de un puntero es recibir la dirección de memoria de otra variable:
int n;
int *p = &n


El ampersand (&) es un operador que (en este caso) dice 'La dirección de'.

También es muy importante de los punteros el tipo de dato al que apuntan. Has visto en todos estos ejemplos cómo siempre han sido a entero (int *), pero pueden ser de cualquier tipo de dato: char *, double *, struct mi_estructura *, etcétera. Esto es debido  a que los diferentes tipos de datos ocupan tamaños en bytes diferentes en memoria y el compilador necesita saber cuánto ocupa para recuperarlo o para guardar algo en él. Por ejemplo un char suele ser de 1 byte y un entero de 4 bytes. Si los punteros no especificaran el tamaño con su tipo podríamos guardar un número muy grande en una posición dónde se guarda un char machacando datos vecinos: intentaríamos guardar 4 bytes dónde sólo cabe 1.

Y bueno, esto es lo básico de los punteros. Hay más cosas: punteros como argumentos en funciones y porqué usarlos; similitudes y diferencias entre punteros y arrays; punteros a funciones (sí, también se pueden apuntar a funciones); punteros a punteros (por loco que parezca muchas veces son necesarios). Pero eso ya para más adelante. Ahora intenta comprender sus bases.

Eternal Idol

Cita de: MAFUS en 29 Mayo 2021, 12:55 PM
* Un poco más avanzado: En este caso he hecho un programa previo para ver dónde residía la memoria de datos del programa y pudiera apuntar ahí con este ejemplo. Los sistemas operativos actuales y los microprocesadores funcionan por capas. No van a dejar que un programa de usuario pueda leer o escribir fuera de su área designada. Antes sí que se podía, o en sistemas sin protección sí puede hacerse.

¿Y si estamos trabajando en macOS (y el ASRL esta involucrado por defecto en cada ejecucion)?  :-\ En Windows si el programa tiene ASLR habilitado (por defecto en proyectos desde Visual Studio 2008) al reiniciar la direccion base tambien cambiara.

int zzz = 5;

int main(int argc, const char * argv[])
{
       printf("%p\n", &zzz);
       return 0;
}


Citarm1-Mac-mini:Debug tester$ ./test
0x10b45fc5c
m1-Mac-mini:Debug tester$ ./test
0x10a7d2c5c
m1-Mac-mini:Debug tester$ ./test
0x10eb4ac5c
m1-Mac-mini:Debug tester$ ./test
0x10fcb8c5c
m1-Mac-mini:Debug tester$ ./test
0x106542c5c
m1-Mac-mini:Debug tester$ ./test
0x10f495c5c
m1-Mac-mini:Debug tester$ ./test
0x1080a5c5c
m1-Mac-mini:Debug tester$ ./test
0x10d075c5c
m1-Mac-mini:Debug tester$ ./test
0x10885ac5c




Mejor dejemos en claro que usar direcciones hardcoded es una mala practica y nadie que este aprendiendo a usar punteros deberia emplearla.
La economía nunca ha sido libre: o la controla el Estado en beneficio del Pueblo o lo hacen los grandes consorcios en perjuicio de éste.
Juan Domingo Perón

MAFUS

#9
Sí, pero para entender una cosa realmente es mejor empezar desde los cimientos y los cimientos, en punteros, son que es un número que representa una dirección de memoria. Y de ahí para arriba. Estarás de acuerdo conmigo que realmente no se puede entender C sin entender el esquema de memoria. Al menos eso me sirvió a mi: entonces todo fue más fácil. Eso y la traducción a ensamblador de la convención de llamadas a funciones, para entender las variables automáticas.




Supongo que no será el caso de mucha gente que visite el foro pero este lenguaje es muy habitual verlo en microcontroladores y aquí se mapean en memoria muchos registros de configuración y los puertos E/S. En PC usar constantes para acceder a memoria no es útil, pero en el mundo del firmware es la norma.