Punteros
Sitio: | Facultad de Ingeniería U.Na.M. |
Curso: | Computación ET-344 |
Libro: | Punteros |
Imprimido por: | Invitado |
Día: | miércoles, 22 de enero de 2025, 13:47 |
Tabla de contenidos
- 1. Introducción.
- 2. Mapa de Memoria
- 3. El heap y la memoria dinámica
- 4. Punteros en C++
- 5. big endian , little endian
- 6. Operadores Nuevos
- 7. Inicializando Punteros
- 8. Aritmética de punteros
- 9. Pasaje por valor.
- 10. Pasaje por referencia.
- 11. Variables Dinámicas
- 12. Variables sin nombres
- 13. Problema con el ámbito de una variable
- 14. Arreglos de punteros.
- 15. Punteros a punteros - Punteros Dobles
- 16. ¿Y si new no consigue espacio?
- 17. Arreglos de Punteros a funciones (informativo)
- 18. Operador Delete
- 19. Argumentos a main
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.
¿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;
}
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{ int x=5000000000;
cout<<x;
return 0;
}
- 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.
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)
- La memoria RAM es una memoria de Acceso Aleatorio.
- 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.
- La memoria es un recurso caro y cuanto mas rápida sea, mas caro será.
- La memoria RAM afecta al rendimiento del equipo dependen de : la cantidad y la velocidad de la misma
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.
- 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;
}
cout<<text;
#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;
}
#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;
}
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;
}
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;
}
#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 1Como 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.
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;
}
#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;
}
13. Problema con el ámbito de una variable
#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;
}
#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;
}
- 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
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.
#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);
Ver 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.