Punteros

Sitio: Facultad de Ingeniería U.Na.M.
Curso: Computación ET-344
Libro: Punteros
Imprimido por: Invitado
Día: miércoles, 3 de julio de 2024, 06:28

1. Introducción.

Para los ansiosos:

Un puntero es una variable que almacena la dirección de memoria de un objeto, usando la "dirección de memoria" puedo acceder a la variable.

Alguna duda? - Sportis Formación Deportiva. Formación de calidad para  profesionales del deporte

¿Por que necesitaría la dirección de memoria para acceder a una variable, si puedo usar el nombre de la variable?

Rta: Veremos que los punteros permiten manejar de manera muy sencilla:

  • Permite que las funciones cambien el valor de sus argumentos.
  • Permite pasar arreglos de forma eficiente entre funciones, esto por la forma de almacenamiento contiguo de los elementos de un arreglo.
  • Permite reservar memoria en tiempo de ejecución en lugar de en tiempo de compilación.
  • etc.

Conceptos.

Los punteros proporcionan la mayor parte de la potencia al C y C++, y marcan la principal diferencia con otros lenguajes de programación. Una buena comprensión y un buen dominio de los punteros pondrá en tus manos una herramienta de gran potencia.

Un conocimiento mediocre o incompleto te impedirá desarrollar programas eficaces.

Por eso le dedicaremos mucha atención y mucho espacio a los punteros. Es muy importante comprender bien cómo funcionan y cómo se usan. Para entender qué es un puntero veremos primero cómo se almacenan los datos en un ordenador.

La memoria de un ordenador está compuesta por unidades básicas llamadas bits. Cada bit sólo puede tomar dos valores, normalmente denominados alto y bajo, o 1 y 0. Pero trabajar con bits no es práctico, y por eso se agrupan, esto es histórico y se toma como base 8 bits que se conoce como byte u octeto.  

En realidad el microprocesador, y por lo tanto nuestro programa, sólo puede manejar directamente bytes o grupos de dos o cuatro bytes.

Para acceder a cada bits, primero hay que acceder a los a los bytes.

Y aquí llegamos al quid, cada byte tiene una dirección, llamada normalmente dirección de memoria.

La unidad de información básica es la palabra, dependiendo del tipo del microprocesador almacenan sus datos en bloques de dos, cuatro, ocho o dieciséis bytes. Hablaremos en estos casos de plataformas de 16, 32, 64 ó128 bits.

Se habla indistintamente de direcciones de memoria, aunque las palabras sean de distinta longitud.

Cada dirección de memoria contiene (o apunta )siempre un byte.

Lo que sucederá cuando las palabras sean de 32 bits es que accederemos a posiciones de memoria que serán múltiplos de 4. Podemos saber qué tipo de plataforma estamos usando averiguando el tamaño del tipo int, y para ello hay que usar el operador "sizeof()".

En el equipo que estoy usando por ejemplo:

#include <iostream>
using namespace std;

int main(int argc, char *argv[]) 
{ cout<<sizeof(int);
return 0;
}

Para este código la salida, en mi equipo sería 4 bytes 



Es decir que el tipo de dato entero (int) usa 4 bytes 0 32 bits. Esto nos da otro tema. Con 32 bits, puedo lograr : 2^32= 4294967296
Surge la pregunta..¿ Que pasa si quiero almacenar  un nro mayor a 4294967296?. Veamos que sucede.

#include <iostream>
using namespace std;
int main(int argc, char *argv[]) 
{   int x=5000000000;
	cout<<x;
	return 0;
}

La salida sería:
Figura 2


Claramente vemos que hay un error, lo que debería ser 5 mil millones se muestra como: 705.032.704  lo cual refleja el punto que TENEMOS QUE TENER EN CUENTA EL TIPO DE DATO QUE ALMACENAMOS Y QUE TIENE RELACIÓN CON EL VALOR DEL DATO.
Que es un anuncio - Gestion.Org
  • Un Entero, tiene un tamaño en bits distinto de un Char, de un float.
  • Un puntero tiene SIEMPRE el mismo tamaño en bits, el tamaño de una dirección.

2. Mapa de Memoria

Vamos a introducir el concepto de Mapa de Memoria.

Hay varios tipos de memorias en un Equipo. Cada una tiene una ventaja tecnologica y un costo, eso define el uso.

  • Disco Rígido
  • Memoria ROM ( Solo lectura)
  • Memoria RAM
  • Memoria Caché
  • etc.

Un Procesador o Microprocesador , TOMA el contenido del Disco Rígido que es una memoria que almacena la infomración y la pasa a la memoria RAM ( es mas rápida) y este contenido es cargado al procesador por pequeñas unidades de tiempo, donde se le concede la ejecución , luego se intercala con otro proceso, ese es el nombre .

Podemos ver que en una computadora hay cientos de procesos ejecutandose de forma alternada.

pensar

Buscar en Internet 

  • ¿Como puedo ver los procesos que se están ejecutando en la pc ?
  • ¿Que es un mapa de memoria?

TODOS los dispositivos de Almacenamiento de cualquier tipo y otras  posiciones que no son para almacenamiento (por ejemplo un registro donde están datos de la recepción de un puerto serie) forman parte del mapa de Memoria de un equipo a la que accede el Procesador para leer y/o escribir datos.

Un mapa de memoria es TODO el espacio al que puede acceder para leer y/o escribir datos el procesador o microprocesador.

Cada Notebook, Arduino, PIC, Celular, tiene un mapa de Memoria.

En este espacio LÓGICO llamado Mapa de Memoria que se vé como algo contínuo, en realidad NO LO ES y está compuesto por Almacenamientos de distintos tipo, los cuales podemos dividirlo por el uso y por la tecnología.



Uso:

  • Zonas que son para almacenar datos de usuario y programas
  • Zonas que NO pueden se accedidas por el usuario.

Tecnología

  • Zonas que se borran si no se re-leen con cierta frecuencia (RAM)
  • Zonas que no se borran (ROM)
Lógicamente NO SON INDEPENDIENTES !! pero eso escapa a nuestra materia.
El almacenamiento temporal datos y programas se realiza temporalmente antes de ejecutarse en la Memoria RAM.
Los punteros que vamos a ver acceden a la memoria RAM
Cuesiones de  Memoria RAM.
  1. La memoria RAM  es una memoria de Acceso Aleatorio.
  2. La memoria RAM tiene una Capacidad y una Velocidad. La capacidad de una PC pueden ser por Ejemplo 4G y la de un Arduino 32Kbytes.
  3. La memoria es un recurso caro y cuanto mas rápida sea, mas caro será.
  4. La memoria RAM afecta al rendimiento del equipo  dependen de : la cantidad y la velocidad de la misma
Por todo lo mencionado, el USO de la Memoria RAM o de sus alternativas debe ser cuidado.

¿ Pensar que pasa si un programa accede a una zona de memoria que no le pertenece?

3. El heap y la memoria dinámica

Donde mayor potencia desarrollan los punteros de C++ es cuando se unen al concepto de memoria dinámica.

Cuando se inicia la ejecución de un programa, el sistema operativo carga el código ejecutable en una zona libre de la memoria reservando además varias zonas para almacenar los datos que necesite durante su ejecución.

De toda el Mapa de memoria comentado en el capítulo anterior..hay una parte de la memoria RMA que tiene unas zonas sobre las que vamos a comentar su función .

Estas zonas de memoria son:
  • El área del código.
    • Una zona de datos para almacenar las variables globales y las constantes, reservando el espacio justo ya que se conoce en tiempo de compilación.
  • El área de datos estáticos.
    • Una zona para las llamadas que almacenará los parámetros de las funciones, las variables locales y los valores de retorno de las funciones ó para intercambiar datos entre funciones.
  • La pila de llamadas (call stack , LIFO).
    • Una zona para las llamadas que almacenará los parámetros de las funciones, las variables locales y los valores de retorno de las funciones ó para intercambiar datos entre funciones.
  • El área de datos dinámicos (heap o montón).
    • Una zona para la gestión dinámica de la memoria que se solicita durante la ejecución. Por ejemplo en C++ con el operador “new”. Para liberar esta porción de memoria se usa delete.

De manera gráfica lo podemos ver...


Una vez que el sistema operativo ha reservado las zonas de memoria y ha cargado el código del ejecutable, el programa está listo para ser ejecutado.

El S.O indicará a la CPU que debe apuntar el  "contador" o registro del puntero de instrucciones a la primera instrucción del código (el punto de entrada de las aplicaciones, la función main), y que debe apuntar con su puntero de pila a la primera dirección de memoria reservada para la pila (stack).

INFORMACIÓN A TENER EN CUENTA:

Hay una regla de oro cuando se usa memoria dinámica, toda la memoria que se reserve durante el programa hay que liberarla antes de salir del programa. No seguir esta regla es una actitud muy irresponsable, y en la mayor parte de los casos tiene consecuencias desastrosas. No se fíen de lo que diga el compilador, de que estas variables se liberan solas al terminar el programa, no siempre es verdad.

Veremos con mayor profundidad los operadores "new" y "delete", que justamente permiten reservar y liberar memoria.



4. Punteros en C++

Los punteros se declaran igual que el resto de las variables, pero  precediendo el identificador con el operador de indirección, (*), que leeremos como "puntero a".

Sintaxis:
    <tipo> *<identificador>; 

    <tipo>* <identificador>;

Ambas formas están permitidas ( asterisco junto al tipo o junto al identificador o nombre del puntero)

Ejemplos:

int *entero;  //puntero a un tipo de dato entero

char *caracter; //piuntero a un tipo de dato caracter

float *temperatura; // puntero a un tipo de dato Flotante.

Observación:

Los punteros siempre apuntan a un objeto de un tipo determinado, PERO EL PUNTERO NO ES DE ESE TIPO DE DATOS !!.

Como pasa con todas las variables en C++, cuando se declaran sólo se reserva espacio para almacenarlas, pero no se asigna ningún valor inicial, el contenido de la variable (contiene BASURA) permanecerá sin cambios, de modo que el valor inicial del puntero será aleatorio e indeterminado. Debemos suponer que si NO se inicializa  contiene una dirección no válida.
Si "entero" apunta a una variable de tipo "int", "*entero" será el contenido de esa variable, pero no olvides que "*entero" es un operador aplicado a una variable de tipo "puntero a int", es decir "*entero" es una expresión, no una variable, que significa: ”el contenido a donde apunta el puntero” ó “ contenido de la dirección a la que apunta”.

En este link podemos tener otro material de lectura:  Punteros en Cplusplus

Vamos a presentar algo mas gráfico para tratar de explicar la idea.

Ejemplo:

int A;
int *pA;
pA = &A;


Podemos ver lo que hace cada línea y entender claramente.

5. big endian , little endian

¿Se deja al alumno investigar que significan esos términos?




6. Operadores Nuevos

En esta sección veremos 2 nuevos operadores:

  • Operador de Indirección:   *  (asterisco)

    • Este operador aplicado sobre una variable, indica que esta variable almacena una DIRECCIÓN de memoria.
    • Notar que una dirección de memoria no tiene tipo en si mismo, pero apunta a un datos que SI tiene tipo.
    • float *p,a=3;//el puntero p apuntará en el futuro a una varaible real


  • Operador de Dirección: & (ampersand)


    • Este operador aplicado sobre una variable regresa o "saca" la dirección de la variable.
    • float *p,a=3;
    • p=&a //"saco" la dirección de a y la guardo en una variable puntero.


  • Operador new

    • El operador new sirve para reservar memoria dinámica, esto es durante la ejecución del programa.

    • El operador new retorna una dirección de memoria que es la memoria reservada.

    • Si la reserva de memoria no tuvo éxito, new devuelve un puntero nulo,

    • Se pueden solicitar VARIOS espacios de memoria utilizando la sintaxis:
      • Nombre_de_puntero = new type  //para un solo espacio
      • Nombre_de_puntero = new type [N] // para N espacios
  • Ejemplos de sintaxis:

	int *p; //Declaro puntero a un entero
	float *x;  //Declaro puntero a un real
	char *text;  //Declaro puntero a un char
	p=new int[4];// reserva 4 espacios para enteros de manera contigua.
	x=new float[4];// reserva 4 espacios para reales de manera contigua.
	text=new char[11];// reserva 11 espacios para caracteres de manera contigua.

Operador Delete: del

  • En la mayoría de los casos, la memoria asignada dinámicamente (con new) solo es necesaria durante períodos específicos de tiempo dentro de un programa; una vez que ya no se necesita, se debe liberar para que la memoria vuelva a estar disponible para otras solicitudes de memoria dinámica.
  • Sintaxis es:
    • delete pointer;// para borrar UN solo puntero
      delete[] pointer;// para borrar arreglo de punteros


Aclaración

si se solicitó memoria como arreglo con new[ ] la misma no se puede liberar de a una posición con delete, se debe liberar el conjunto completo con delete[ ].

https://stackoverflow.com/questions/18016295/deleting-elements-of-a-dynamic-array-one-by-one


7. Inicializando Punteros

El puntero y la variable a la que apunta DEBEN ser compatibles.

Un puntero almacena una DIRECCIÓN , no un tipo de dato int. float, bool, etc.

Un puntero almacena una DIRECCION que apunta a un tipo de dato int. float, bool, etc.

Para obtener la dirección de una variable se usa el operador : &

Este operador "OBTIENE" la dirección de la variable y permite INICIALIZAR el puntero.

Ejemplo:

#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
   int b; // variable sin inicializar
   int *puntero; // puntero sin inicializar<
   puntero=&b; //ahora el puntero está inicializarlo
   //a partir de la línea anterior el puntero apunta a variable b
   b=3;
   cout<<*puntero<<endl; //muestro elvalor de la varaible usando el puntero
   cout<<puntero<<endl;//muestro el contenido del puntero: dirección.
   return 0;
}


El nombre de un Arreglo es un puntero!!

Veamos un ejemplo:


#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
float vector[5],*p;
p=vector;//puntero=nombre del arreglo!! equivalente a p=&vector[0];
for(int i=0;i<5;i++)cin>>*(p+i);
for(int i=0;i<5;i++)cout<<*(p+i)<<"\t"<<(p+i)<<endl;
return 0;}
Vemos que en la línea 6 se iguala el nombre del arreglo a un puntero..esto indica que C++ toma el nombre del arreglo como un puntero.!
si podría hacer p++ , pero NO puedo hace vector++!!


#include <iostream>
using namespace std;
 
int main(int argc, char *argv[])
{
	int *p; //Declaro puntero a un entero
	float *x;  //Declaro puntero a un real
	char *text;  //Declaro puntero a un char
	p=new int[4];// reserva 4 espacios para enteros de manera contigua.
	x=new float[4];// reserva 4 espacios para reales de manera contigua.
	text=new char[11];// reserva 11 espacios para caracteres de manera contigua.

	for(int i=0;i<4;i++)
		{
		cout<<"Ingrese un entero: "<<endl;
		cin>>*(p+i);
		}
	cout<<endl;
	cout<<"Ud. ingreso: "<<endl;
	for(int i=0;i<4;i++)cout<<*(p+i)<<" ";
	cout<<endl;
	for(int i=0;i<4;i++)
		{
		cout<<"Ingrese un real: "<<endl;
		cin>>*(x+i);
		}
	cout<<"Ud. ingreso: "<<endl;
	for(int i=0;i<4;i++)cout<<*(x+i)<<" ";
	cout<<endl;
	for(int i=0;i<10;i++)// de 0 a 9 hay 10 caracteres.
		{
		cout<<"Ingrese un caracter: "<<endl;
		cin>>*(text+i);
		}
	cout<<"Ud. ingreso: "<<endl;
	cout<<text; // ver que aqui NO hay for!!
delete[] x;
delete[] p ;
delete[] text;
	
return 0;
}

Notar que : cout<<text;

NUNCA se cargó como último caracter el '\0' para que sea considerado un string. Veamo como forzar eso:

#include 
using namespace std;

int main(int argc, char *argv[])
{
	char *text;  //Declaro puntero a un char	
	text=new char[5];// reserva 6 espacios para caracteres de manera contigua.
	//antes de inicializar el arreglo..
	for(int i=0;i<5;i++) //de 0 a 4 hay 5 Caracteres!!
	//ver que NO cargo la posición 5 con NULL,entonces por que es un srting?
	{
	cout<<"Ingrese un caracter: "<<*(text+i);
	}
	*(text+4)='\0';//aqui estoy forzando a que sea un string
	//cargando en la ultima posición un caracter NULL.
	cout<<"Ud. ingreso: "<

8. Aritmética de punteros

Un puntero apunta a una dirección de memoria.
El lenguaje C++ permite sumar o restar cantidades enteras al puntero, para que apunte a una dirección diferente: aritmética de punteros .

Como son punteros, el hecho de sumar por ejemplo 2, si es un puntero a un float que necesita 4 bytes, hará que se sumen en memoria 8 bytes!!. Veamos el ejemplo.


Podemos observar que la línea 12 le suma 2 a p1 para obtener p2, en realidad como el float tiene 4 bytes, en direcciones van a se 8 bytes.


Eso se puede ver en la última linea de salida del programa.

9. Pasaje por valor.

Contexto de las variables.

Recordemos que las variables tienen un contexto:

  • Global
  • Local
  • Bloque

En el caso de una variable local definida por ejemplo en la definición de una función se crear una copia local, que puede o no llamarse igual, pero NUNCA serán lo mismo

Ejemplo:

#include <iostream>
using namespace std;
void funcion1(float); //función prototipo
int main(){
    float a=10;//a variable local de main
    funcion1(a);
    cout<<"Valor de a="<<a<<" en main";//muestro la variable local a de main
}

void funcion1(float a){  //defino a como variable local
    a=20;//la varaible local a toma valor 20
    cout<<"Valor de a="<<a<<" en funcion1"<<endl;
}
La salida de este programa será:

Figura 1
Lo que confirma la idea planteada que se crea una copia. En realidad podríamos haber llamado a la variable dentro de la función con "b", pero se le dió el mismo nombre en main y en funcion1, para resaltar el hecho que ES UNA COPIA LOCAL lo que usa funcion1.

Lo visto es válido para punteros. Vemos un ejemplo.

#include <iostream>
using namespace std;
void por_valor(float*,float);
int main(){
    float b=10;//b variable local de main
    float* p1 = NULL;//p1 puntero local de main inicializado a NULL
    cout<<"Antes de llamar a la funcion:"<<endl;
    cout<<"valor de b:"<<b<<endl;
    cout<<"p1 apunta a: "<<p1<<endl;
    cout<<"Ahora llamo a la funcion:"<<endl;
    por_valor(p1,b); //paso el valor del puntero p1 y b a la función
    cout<<"---Nuevamente dentro de de main --"<<endl;
    cout<<"valor de b:"<<b<<endl;
    cout<<"p1 apunta a: "<<p1<<endl;
    cout<<"la direccion de b es: "<<&b<<endl;
    return 0;
}
void por_valor(float* p,float b){//se crea una copia local de los valores recibidos
    p = &b;  //inicializo puntero local p de la función con el valor de la variable local b
    //ahora p apunta a la variable local
    cout<<"---Dentro de la funcion----"<<endl;
    cout<<"valor de b:"<<b<<endl;
    cout<<"la direccion de b es: "<<&b<<endl;
    cout<<"valor del puntero: "<<*p<<endl;
}
La salida de este código será:



Pese a que la función por_valor recibe un puntero crea una copia local dentro de la función, este es inicializado para que apunte a b  en la línea de la función: p=&b , pero esta inicialización es SOLO válida dentro de la función.

Esto es una muestra clara que el valor decibido de main ( float* p1 = NULL; ) dentro de por_valor la COPIA (p = &b;) ES MODIFICADA lo que de manera clara muestra que se recibe un valor pero que NO tiene alcance mas allá de la función ya que es una copia local.

10. Pasaje por referencia.

En el capítulo anterior vimos que las copias locales de los argumentos que recibe una función NO pueden tener alcance fuera de la función.

En otras palabras si quiero que las modificaciones hechas variables de la función se vean reflejadas fuera de la función  sin tener que necesariamente retornar algo y asignar ,no lo puedo hacer pasando argumento por valor.

Para cubrir este punto existe la opción de pasaje por Referencia que vermos a continuación.

#include <iostream>
using namespace std;
void por_referencia(int*& p, int a);//prototipo VER QUE ESTA EL & LUEGO DE *

int main(){    int b=10;int* p2 = NULL;//p2 local inicializado en NULL
    cout<<"Antes de llamar a la funcion:"<<endl;
    cout<<"valor de b="<<b<<endl;
    cout<<"la direccion de b: "<<&b<<endl;
    cout<<"la dierccion del puntero p2: "<<p2<<endl;
    cout<<"Ahora llamo a la funcion:"<<endl;
    cout<<"Paso por referencia el puntero y por valor la variable"<<endl<<endl;
    por_referencia(p2,b); //p2 esta pasado por referencia por que el prototipo tiene &, paso LA DIRECCIÓN!!
cout<<"---Nuevamente en main----"<<endl; cout<<"direccion de puntero p2: "<<p2<<endl;
//notar que la dirección de b: &b, NO PERTENCE A MAIN, era local de por_referencia!! cout<<"la direccion de b es: "<<&b;return 0;
// esta línea anterior estaría MAL! } void por_referencia(int*& p, int a) { // puntero paso por referencia, VER QUE ESTÁ &, toma la dirección! // variable paso por valor! p = &a;//inicializo puntero local cout<<"---Dentro de la funcion----"<<endl; cout<<"valor de a="<<a<<" pasado por valor.."<<endl; cout<<"la direccion del puntero es: "<<&a<<" pasado por referencia"<<endl; cout<<"contenido del puntero: "<<*p<<endl<<endl; }
La salida de este código es:

11. Variables Dinámicas

Memoria dinámica
En los programas vistos en capítulos anteriores, todas las necesidades de memoria se determinaron antes de la ejecución del programa mediante la definición de las variables de tamaño necesario , pero para el caso de los arreglos, suele ser mas difícil.. a priori s saber cuantos elementos necesitamos cargar?

Así que hay casos en los que las necesidades de memoria de un programa solo se puedan determinar durante el tiempo de ejecución.

Por ejemplo, cuando la memoria necesaria depende de la entrada del usuario. En estos casos, los programas necesitan asignar memoria dinámicamente, para lo cual el lenguaje C ++ integra los operadores new y delete que mencionamos anteriormente.

Solemos escribir

#include <iostream>
using namespace std;
#define N 5 //Constante simbolica

int main(int argc, char *argv[]) {
    float vec[N];
    for(int i=0;i<5;i++)cin>>vec[i];
    for(int i=0;i<5;i++)cout<<vec[i]<<"\t""'";
    return 0;
}
En este caso el tamaño del Arreglo que es N se define con una constante al momento de COMPILAR, pero luego NO SE PUEDE CAMBIAR.

Que sucedería que queremos cambiar el tamaño durante la ejecución? Por ejemplo cargar X temperaturas donde X se determina al momento de ejecutar el programa?

Para hacerlo correctamente  se utiliza el operador new que veremos a continuación, algo que se suele hacer y que está MAL es lo siguiente como un intento de definir la cantidad de memoria en tiempo de ejecución, cosa que NO es cierta, solo se asigan un tamaño mágicamente grande y luego se utiliza seguramente algo menor.

Esto está mal!!!! NO HAY QUE HACER!!
#include <iostream>
#define N 10000 using namespace std; int main(int argc, char *argv[]){ float vec[N]; int cantidad; cin>>cantidad;//MAL!! en tiempo de ejecución!!! for(int i=0;i<cantidad;i++)cin>>vec[i]; for(int i=0;i<cantidad;i++)cout<<vec[i]<<"\t"; return 0; }

Esto está PEOR!!! NO HAY QUE HACER!!

#include <iostream>
using namespace std;

int main(int argc, char *argv[]){
    int cantidad;
    cin>>cantidad;
    float vec[cantidad];//PEOR!! MUY MAL!! // en tiempo de ejecución defino el tamaño, que pasa si no hay lugar?
    for(int i=0;i<cantidad;i++)cin>>vec[i];
    for(int i=0;i<cantidad;i++)cout<<vec[i]<<"\t";   
    return 0;
}

Montón o Heap

Cuando se inicia la ejecución de un programa, el sistema operativo carga el código ejecutable en una zona libre de la memoria reservando además varias zonas para almacenar los datos que necesite durante su ejecución.

Estas zonas de memoria son:


  • El área del código.

  • El área de datos estáticos.

  • La pila de llamadas (call stack , LIFO).

  • El área de datos dinámicos (heap o montón).


De manera gráfica lo podemos ver...

Figura 1

Como se puede observar en la Figura 1 esto es una parte del Mapa de Memoria.

Hay una zona de datos para almacenar las variables globales y las constantes ( Estática) , reservando el espacio justo ya que se conoce en tiempo de compilación.

Otra zona para las llamadas que almacenará los parámetros de las funciones, las variables locales y los valores de retorno de las funciones ó para intercambiar datos entre funciones.(Pïla/Stack/Lifo)

Una zona para la gestión dinámica  Heap o Montón  de la memoria que se solicita durante la ejecución. Por ejemplo en C++ con el operador “new”.  C++ dispone de otro operador para acceder a la memoria dinámica que es "delete".

Una vez que el sistema operativo ha reservado las zonas de memoria y ha cargado el código del ejecutable en la Memoria RAM, el programa está listo para ser ejecutado.
El S.O indicará a la CPU que debe apuntar con el registro del puntero de instrucciones a la primera instrucción del código (el punto de entrada de las aplicaciones, la función main), y que debe apuntar con su puntero de pila a la primera dirección de memoria reservada para la pila (stack). Esto último escapa a los alcances de nuestra materia.

Veamos un ejemplo:
#include <iostream>
using namespace std;

int main(int argc, char *argv[]) {
    float *vec;
    int n;
    cout<<" Ingrese la dimensión del arreglo: ";
    cin>>n;//Tamaño del Arreglo
    vec=new float[n];//Durante la ejecución determino el tamaño del arreglo!!
    for(int i=0;i<5;i++)cin>>*(vec+i);
    for(int i=0;i<5;i++)cout<<*(vec+i)<<"\t"<<"almacenado en: "<<vec+i)<<"\t"<<endl;
    delete []vec;
    return 0;
}

12. Variables sin nombres

Visto punteros, podemos ir un poco mas allá y utilizar varaibles, pero sin que necesariamente esas varaibles tengan nombres, esto es posible por que un puntero  TIENE un nombre.. y se puede usar el nombre del puntero, para acceder al contenido de donde apunta un puntero.

Veamos un ejemplo;

#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
float *pa;//declaro puntero pero no hay espacio reservado en memoria.
*pa=20.0; // busco que el puntero apunte al valor 20.0
cout<<*pa;
return 0;
}
Este código tira una advertencia al compilar:


Veamos que hace cada línea para entender el por que del error.
Linea 5 : declaro un puntero, pero la declaración NO usa lugar en memoria.
Linea 6 : trato de lograr que el puntero apunte  a 20, pero el puntero NO tiene lugar asignado en memoria al momento de compilar  por lo que NO puedo asegurar que se pueda inicializar, por lo que tira WARNING!!
Si se ejecuta el programa a pesar de la advertencia al  finalizar el programa tira:



SIGSEGV es una señal (signal) en Unix-Linux e implica que el programa ha direccionado memoria fuera de los límites que tenía asignado para su ejecución o segmento de memoria

Veamos el operador new, que permite RESERVAR un espacio en memoria que SI PODEMOS inicializar.

#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
float *pa;//declaro puntero
pa=new float;//con new reservo un espacio
*pa=20.0; // busco que el puntero apunte al valor 20.0
cout<<*pa;
return 0;
}
En este caso el programa funciona correctamente, ya que se reserva un espacio con el operador new  de la línea 6.

Para fijar conceptos vamos a mostrar de manera gráfica.




13. Problema con el ámbito de una variable

El siguiente código compila sin problemas pero está mal.


#include<iostaream>
using namespace std;
double* asignar(void);//Prototipo int main(){ double* pMain; pMain=asignar(); *pMain=1.0; return 0; } double* asignar(void){ double variableLocal; //esta variable solo existe dentro de función asignar. return &variableLocal; //regreso esta variableLocal a main PASANDO LA DIRECCIÓN (VER QUE ESTA &) //pero al abandonar esta funcion esa DIRECCION ES LIBERADA!! }

Si compilamos lo va a hacer con éxito, pero obtendríamos Warning:

¿Donde está el problema?

El problema aquí es que variableLocal está definida solo en el ámbito de la función asignar().

Por ello, en el momento en que la dirección de variableLocal es retornada a main(), la misma apunta a una variable que ya no existe. La memoria que variableLocal solía ocupar probablemente ya esté siendo usada por otro proceso.

Esto es un error muy común. Desafortunadamente, este error no causa que el programa se detenga. De hecho, el programa funcionará la mayoría de las veces. El programa seguirá funcionando mientras la memoria que ocupaba variableLocal no sea reutilizada.

Este tipo de problemas intermitentes son los más difíciles de detectar y solucionar.

Solución: utilizar el montón o Heap (new)

El problema del ámbito es debido a que C++ devuelve la memoria local antes de que el programa finalice. Lo que se necesita es un bloque de memoria controlado por el programador.

Utilizar memoria mientras se considere necesario y no  devolverla porque C++ se pensó que era una buena idea.

Estos bloques de memoria se denomina heap (montón o pila). La memoria del montón se solicita con el comando new seguido del tipo de objeto que se desea guardar en esa memoria. El caso anterior se podría rescribir de la siguiente manera:

double* asignar(void);//Prototipo
    int main(){
    double* asignar();
    *pMain=1.0;
    delete pMain;//Libero la memoria con delete
    pMain=0;
    return 0;
}

double* asignar(void){
    double* pLocal = new double; //solicito memoria con new
    return pLocal;
}

A pesar de que pLocal deja de existir cuando se retorna a main(), la memoria a la que apuntaba sigue perteneciendo al proceso, esto por que se solicitó la memoria con new.

La memoria solicitada con new no vuelve al montón hasta que se lo haga de manera explícita con el comando delete.

En el ejemplo anterior se almacena una valor double en el puntero devuelto por la función asignar(). Cuando ya no se utiliza la memoria se la devuelve al montón en main() a pesar de haber sido solicitada en otro ámbito. Por último se "anula" el puntero. Esto no es necesario pero puede resultar útil ya que acusaría error si accidentalmente queremos volver a usar *pMain después del delete.

Hay que destacar que delete devuelve la dirección de memoria, sin importar con que puntero se apunte a ella o en qué ámbito nos encontremos. 

La memoria solicitada con new es válida y accesible en todo el programa/proceso hasta que termine el programa o proceso o que se libere con delete.

14. Arreglos de punteros.

NO es lo mismo un puntero a un arreglo, que un arreglo de punteros!!

Un arreglo de punteros, no es otra cosa que una variable que tiene espacios consecutivos asignados  en la memoria ( por ser arreglo) y que almacena direcciones de memoria ( por ser puntero)

La sintaxis es la esperada:

tipo  puntero [cantidad_de_valores];

Veamos un ejemplo:

#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
    float *pa;//declaro puntero a float
    cout<<sizeof(float)<<endl;
    pa=new float[5];//con new reservo 5 espacios consecutivos
    // pero regreso un puntero al primero!!
    for(int i=0;i<5;i++)cin>>*pa[i];
    for(int i=0;i<5;i++)cout<<pa[i]<<"\t"<<&pa[i]<<endl;
    return 0;
}
Se puede ver en la salida claramente que los espacios son consecutivos, separados por 4 bytes ( tammaño de float) y que en este caso NO apuntan a nada, lógicamente.



Veamos el siguiente código:
#include <iostream>
using namespace std;
int main(int argc, char *argv[]){
    float *px;
    px=new float[5];//Declaro arreglo de punteros CONSECUTIVOS
    float *pa[5];//Declaro arreglo de punteros <-Esto es nuevo!!
    cout<<sizeof(float)<<endl;
    //Espacios NO consecutivos ( cada vez que ejecuto new) .
    for(int i=0;i<5;i++){
        pa[i]=new float;//dinámicamente resevo UN espacio en memoria.
        cin>>*pa[i];//cada puntero ahora apunta al dato ingresado:inicializo
    }
    for(int i=0;i<5;i++)cout<<*pa[i]<<" "<<pa[i]<<endl;//Espacios consecutivos.
    for(int i=0;i<5;i++)
        cin>>*(px+i);//cada puntero ahora apunta al dato ingresado
    for(int i=0;i<5;i++)cout<<*(px+i)<<" "<<(px+i)<<endl;
    return 0;
}
Algo como :
  • espacio solicitado con new, como new float[5] => asigna 5 espacios consecutivos!!!
  • float *pa[5] no tiene new!! NO reserva espacio!! solo DECLARA UN ARREGLO DE PUNTEROS

La salida de este código sería para entradas 1,2,3,4,5,6,7,8,9,10


No hay NOMBRES de variables !!, Es un arreglo de punteros!!

15. Punteros a punteros - Punteros Dobles

Un puntero a puntero es una variable que contiene la dirección de memoria de un puntero, el cual a su vez contiene la dirección de memoria de un tipo de dato. 

Analicemos este código:


El mismo tiene la siguiente salida:


Gráficamente sería:


Recuerden que un puntero sigue siendo un espacio en memoria, pero en vez de almacenar un valor almacena una dirección.


16. ¿Y si new no consigue espacio?

¿Que pasaría si el operador new no pudiera dar cabida a la solicitud de espacio?

Hasta ahora asumimos que el operador new regresa una dirección de memoria donde comienza el espacio solicitado, pero esto en algún caso puede ser que no sea posible, dijimos que si no hay espacio el puntero que retorna sería NULL, esto no parcialmente cierto.

New tiene la posibilidad de recibir un "argumento" , es una constante llamada nothrow, con el único propósito de activar una versión sobrecargada de la función operator new (o operator new []) que toma un argumento de este tipo..

Ver documentación de referencia:https://cplusplus.com/reference/new/nothrow/

La sobrecarga de operadores es uno de los mecanismos que nos permite ampliar o reinterpretar las capacidades de operadores o funciones. Pensamos que en Python donde "sumaba" dos strings, pero en realidad está mal decir que suma.. sería mas apropiado decir concatena, en ese caso el + (suma) se puede pensar como un operador sobrecargado.


En C ++, la función new del operador se puede sobrecargar para tomar más de un parámetro: el primer parámetro que se pasa a la función nueva del operador es siempre el tamaño del almacenamiento que se asignará, pero se pueden pasar argumentos adicionales ( por ejemplo nothrow) a esta función encerrándolos en paréntesis en la nueva expresión.

Controlar la dirección que regresa new que no sea NULL!!


// Ejemplo de nothrow
#include <iostream>
#include <new>
using namespace std;
int main () {
cout << "Tratando de obtener 1 MB de memoria.... ";
char* p = new (nothrow) char [1048576];
if (!p) {             // Si el puntero es NULO no hay espacio en memoria
cout << "Fallo, no puedo obtener espacio en memoria!\n";
}
else {
cout << "Exitoso!!\n";
delete[] p; } return 0; }

17. Arreglos de Punteros a funciones (informativo)

Arreglos de Punteros a funciones

Para hablar de punteros a funciones, previamente hay que establecer que las funciones tengan "dirección", es decir al momento de compilar el código va a para a un lugra de la memoria, esa dirección o lugar es donde vamos a apuntar el puntero.

Al igual que el nombre de un arreglo representa un puntero al primer elemento de un arreglo, el nombre de la función sería el puntero a donde comienza el código de la función.

 Técnicamente un puntero-a-función es una variable que guarda la dirección de comienzo de la función.

Veamos un ejemplo: de un arreglo de punteros que apuntan a funciones.

#include <iostream>
using namespace std;
int suma(int,int);
int producto(int,int);
int main()
{
int (*pf[2])(int,int);  // Array de punteros a funcion con arg. int 
int e1,e2;
int opc;
pf[0] = suma; //Inicializo el 1er elemento del arreglo ahora apunta
// a funcion suma
pf[1] = producto; //Inicializo el 2do elemento del arreglo ahora apunta
// a funcion a product
cout<<"Por favor Ingrese un entero : ";
cin>>e1
cout<<"Por favor Ingrese otro entero : ";
cin>>e2;
cout<<"Ingrese 0 para sumar o 1 para multiplicar : "<<endl;
cin>>opc;
if((opc==0)||(opc==1)) cout<<pf[opc](e1,e2);
else cout<<"opcion Incorrecta"
return 0;
}
//Funciones
int suma(int x, int y){return (x+y);}
int producto(int x, int y){return (x*y);


Que es un anuncio - Gestion.OrgVer que se tiene el mismo beneficio que cuando se usa arreglos, no tenemos que usar el NOMBRE de  la función para hacer referencia, solo un indice:
pf[opc]

18. Operador Delete

 Libera el bloque de memoria previamente reservado con new.

Sintaxis:

    delete punero ; // operador para liberar espacio

    delete [] puntero; //operador para liberar espacio de arreglo.


Una sintaxis como delete , libera el espacio señalado por puntero (si no es nulo), liberando el espacio de almacenamiento previamente asignado por una llamada al operador new.

Por otro lado una sintaxis delete[] puntero permite liberar espacio de un arreglo de punteros.

19. Argumentos a main

Recodemos que main es una función y por lo tanto puede recibir argumentos.
De hecho cuando Uds. abren con el Zinjai un archivo nuevo aparece:


#include <iostream> using namespace std; int main(int argc, char *argv[]) { return 0; }

Podemos ver que seguido de main hay valores que hasta el momento no habíamos prestado atención. Vamos ver un como cada uno de ellos.

  • argc: cantidad de argumentos pasados al momento de ejecutar el programa, es un entero.
  • *arg[]: es un puntero a un arreglo que contiene como elementos los argumentos, cada uno de los elementos es un chat.

Veamos un ejemplo este archivo se llama argumentos_a_main :


#include <iostream>
using namespace std;

/*
argc es un  entero y  contiene el numero de argumentos que se han introducido.
argv array de punteros a caracteres y almacena los valores pasados como argumentos.
*/

int main(int argc, char *argv[]) {
	cout<<"cantidad de argumentos pasados:"<<argc<<endl;
	for(int i=0;i<argc;i++)
		cout<<"Argumento :"<<i<<" es : "<<argv[i]<<endl;
	return 0;
}
La salida de ejecutar este archivo será:

Esta línea que ejecuta el archivo con los argumentos pepe es un poco loco será:

Así que  podemos ver que lo que va luego del nombre del programa puede ser pasado como argumento a main.