Está en la página 1de 29

Estructuras de Datos

2. Apuntadores
2.1. Definición de Apuntador

Llamados también punteros. Un Apuntador es una variable que contiene una dirección
de memoria, la cual corresponderá a un dato o a una variable que contiene el dato. Cada
variable que se utiliza en una aplicación ocupa una o varias posiciones de memoria. Estas
posiciones de memoria se accesan por medio de una dirección.

Cada variable que se utiliza en una aplicación ocupa una o varias posiciones de memoria.
Estas posiciones de memoria se accesan por medio de una dirección.

Representación:
2.2. Operador de dirección de
memoria (&)
Una dirección de memoria contiene un byte de información, una variable dependiendo de su tipo
puede ocupar uno o más bytes. Un apuntador solo almacena la dirección del primer byte de la
variable sin importar que esta sea de más de 1 byte.

Para poder ocupar los apuntadores además de la dirección de memoria que estos almacenan, se
necesita poder manipular los datos a los cuales estos apuntan.

El Operador de Dirección ( &) regresa la dirección de una variable. El operador & devuelve la
dirección de cualquier variable aunque no sea de tipo apuntador. Por ejemplo:

int x = 25;
cout << "La dirección de x es: " << &x << endl;

Este código imprime un valor del estilo “0x4fffd34”. Este valor puede variar durante cada ejecución
del programa, debido a que el programa puede reservar distintos espacios de memoria durante
cada ejecución.
2.3. Operador de indirección de
memoria (*)

El Operador de Indirección ( * ), toma la dirección de una variable y regresa el dato que


contiene esa dirección.

La desreferenciación es la obtención del valor almacenado en el espacio de memoria


donde apunta un apuntador. En C y C++ esto se hace a través del operador ‘*’, aplicado
al apuntador que contiene la dirección del valor. Nótese que se trata de un operador
unario. Ejemplos:

int x = 17, y;
int *p;
p = &x;
cout << "El valor de x es: " << *p << endl; // Imprime 17
y = *p + 3; // A ’y’ se le asigna 20
C++ además provee el operador binario ‘->’, utilizado para obtener campos de un
registro con un apuntador al mismo de una manera más fácil y legible. Muchos
compiladores de C también soportan este operador.

Ejemplo:

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

Data d;
Data *pd = &d;
(*pd).edad = 23; // Acceso al campo ’edad’ utilizando el operador ’.’
pd->edad = 23; // Acceso al campo ’edad’ utilizando el operador ’->’

6
Declaración de apuntadores.

La declaración de un apuntador de manera general es:

Tipo_dato *nombre de apuntador;

Tipo_dato : Especifica el tipo de objeto apuntado y puede ser cualquier tipo (int, float, char, etc).

Nombre de apuntador: Es el identificador (nombre asignado) del apuntador.

Ejemplos de declaración:

int *ptr1; // Apuntador a un dato de tipo entero (int)


char *cad1, *cad2; // Dos apuntadores a datos de tipo carácter (char)
float *ptr2; // Apuntador a un dato de tipo punto-flotante (float)
int *ptr, cont;
float *res;
short *bandera;
char *mensaje;

7
Ejemplos:

Suponer la siguiente declaración:


int a=1,b=2,*p;
Si se ejecutarán cada una de las siguientes instrucciones el resultado sería:
p = &a; //p apunta a la variable a
b = *p; //ahora b es igual a 1
*p = 0; //ahora a es igual a 0

Ejemplo 2.

// Programa que demuestra el uso básico de los apuntadores


#include “stdio.h”
#include “conio.h”
void main ( )
{
int var=1, *apun;
apun = &var; // Inicialización del apuntador
printf(“\n Acceso directo, var= %d”, var);
printf(“\n Acceso indirecto, var= %d”, *apun);
// Se despliega la dirección de la variable de dos maneras
printf(“\n La dirección de var= %d”, &var);
printf(“\n LA dirección de var= %d”, apun);
getch( );
}
8
Aritmética de Apuntadores

Las operaciones que se pueden realizar son:

9
Ejemplo: Probar si las siguientes expresiones son verdaderas o falsas, suponiendo que:

char c[11] =“COMPUTACION”, *p ;


p= &c;

10
Verificación de tipos en apuntadores

Al igual que el resto de las variables, los apuntadores se enlazan a tipos de datos
específicos (apuntadores a variables de cierto tipo), de manera que a un apuntador sólo
se le pueden asignar direcciones de variables del tipo especificado en la declaración del
apuntador. Ejemplo:

int *p1;
float *p2;
int x;
p1 = &x; // Esto es válido
p2 = &x; // Esto no es válido (el compilador genera un error)

11
2.3. Operador NULL

Normalmente, un apuntador inicializado adecuadamente apunta a alguna posición específica de la


memoria.

Sin embargo, algunas veces es posible que un apuntador no contenga una dirección válida, en cuyo
caso es incorrecto desreferenciarlo (obtener el valor al que apunta) porque el programa tendrá un
comportamiento impredecible y probablemente erróneo, aunque es posible que funcione bien. Un
apuntador puede contener una dirección inválida debido a dos razones:

1. Cuando un apuntador se declara, al igual que cualquier otra variable, el mismo posee un valor
cualquiera que no se puede conocer con antelación, hasta que se inicialice con algún valor
(dirección). Ejemplo:

float *p;
cout << "El valor apuntado por p es: " << *p << endl; // Incorrecto
*p = 3.5; // Incorrecto
2. Después de que un apuntador ha sido inicializado, la dirección que posee puede dejar de ser
válida si se libera la memoria reservada en esa dirección, ya sea porque la variable asociada termina
su ámbito o porque ese espacio de memoria fue reservado dinámicamente y luego se liberó.
Ejemplo:

int *p, y;

void func()
{
int x = 40;
p = &x;
y = *p; // Correcto
*p = 23; // Correcto
}

void main()
{
func();
y = *p; // Incorrecto
*p = 25; // Incorrecto
}
13
Si se intenta desreferenciar un apuntador que contiene una dirección inválida pueden ocurrir
cosas como las siguientes:

 Se obtiene un valor incorrecto en una o más variables debido a que no fue debidamente
inicializada la zona de memoria que se accede a través de la dirección en cuestión. Esto puede
ocasionar que el programa genere resultados incorrectos.

 Si casualmente la dirección es la misma de otra variable utilizada en el programa, o está


dentro del rango de direcciones de una zona de memoria utilizada, existe el riesgo de
sobrescribir datos de otras variables.

 Existe la posibilidad de que la dirección esté fuera de la zona de memoria utilizada para
almacenar datos y más bien esté, por ejemplo, en la zona donde se almacenan las
instrucciones del programa. Al intentar escribir en dicha zona, fácilmente puede ocurrir que el
programa genere un error de ejecución y el sistema operativo lo detenga, o que el programa
no responda y deje al sistema operativo inestable.

 En muchos casos el sistema operativo detecta el acceso inadecuado a una dirección de


memoria, en cuyo caso detiene abruptamente el programa
14
Cuando no se desea que un apuntador apunte a algo, se le suele asignar el valor NULL,
en cuyo caso se dice que el apuntador es nulo (no apunta a nada). NULL es una macro
típicamente definida en archivos de cabecera como stdef.h y stdlib.h. Normalmente,
en C++ se encuentra disponible sin incluir ningún archivo
de cabecera. NULL se suele definir en estas librerías así:

#define NULL O

Un apuntador nulo se utiliza para proporcionar a un programa un medio de conocer


cuando un apuntador contiene una dirección válida. Se suele utilizar un test
condicional para saber si un apuntador es nulo o no lo es, y tomar las medidas
necesarias. El valor NULL es muy útil para la construcción de estructuras de datos
dinámicas, como las listas enlazadas, matrices esparcidas, etc. Es igualmente
incorrecto desreferenciar el valor NULL por las mismas razones presentadas
previamente.

15
Apuntadores y Matrices

Considerar:

int a[10][10];
int *b[10];

El uso de a y b puede ser parecido, desde el momento en que a[5][5] y b[5][5] son
referencias validas a un int.

El arreglo a es un arreglo verdadero, existen 100 celdas de memoria asignadas y se


efectúa el cálculo de subíndices rectangulares convencional para localizar un
elemento dado.

Sin embargo a b la declaración solo le asigna 10 apuntadores, cada uno de los cuales
deberá de apuntar a un arreglo de enteros.

16
Diferentes declaraciones

La matriz a puede declararse:

Como un arreglo de 10 arreglos de tamaño 20.

int a[10][20];

Como un arreglo de tamaño 20 de vectores de longitud variable

int *a[10];

Como un apuntador de apuntadores a enteros.

int **a;

Como un apuntador a un arreglo de enteros de tamaño 20.

int (* a)[20];
17
Apuntadores y cadenas

Sea la declaración:

char * mensaje[4] = {''Hola'',''Adios'',''Bye'',''Saludos''} ;

Cada cadena está almacenada en memoria como una cadena de caracteres terminada en NULL (\0).

En el arreglo no están colocadas las cadenas, tan solo están almacenados los apuntadores.

Aunque el arreglo es de tamaño fijo, permite el acceso a cadenas de caracteres de cualquier longitud ( por
ejemplo la longitud de Bye es más corta que la de saludos).

Ejemplo con cadenas.

void main()
{
char *esp[10] = { ''uno'', ''dos'', ''tres'‘ };
char frances[5][10] = { ''un'',''deux'', ''trois'' };
printf(''Elemento 3 entrada 2 esp: %c \n'',esp[2][3]);
printf(''Elemento 4 entrada 3 frances: %c \n'',frances[3][4]);
printf(''Elemento 7 entrada 2 esp: %c \n'',esp[2][7]);
frances[3][4]='A';
printf(''Elemento 4 entrada 3 frances: %c \n'',frances[3][4]);
esp[2][3]='A';
printf(''Elemento 3 entrada 2 esp: %c \n'',esp[2][3]);
printf(“Cadena esp %s \n” ,esp);
printf(“Cadena frances %s \n”,frances[1]);
getch();
}

18
2.5. Apuntador de apuntadores

Dado que un apuntador es una variable que apunta a otra, fácilmente se puede
deducir que pueden existir apuntadores a apuntadores, y a su vez los segundos
pueden apuntar a apuntadores, y así sucesivamente.

Estos 4 apuntadores se declaran colocando tantos asteriscos (‘*’) como sea necesario.
Ejemplo:

char c = ’z’;
char *pc = &c;
char **ppc = &pc;
char ***pppc = &ppc;
***pppc = ’m’; // Cambia el valor de c a ’m’
Apuntadores constantes y apuntadores a constantes

Es posible declarar apuntadores constantes. De esta manera, no se permite la


modificación de la dirección almacenada en el apuntador, pero sí se permite la
modificación del valor al que apunta. Ejemplo:

int x = 5, y = 7;
int *const p = &x; // Declaración e inicialización del apuntador constante
*p = 3; // Esto es válido
p = &y; // Esto no es válido (el compilador genera un error)

También es posible declarar apuntadores a datos constantes. Esto hace que no sea
posible modificar el valor al que apunta el apuntador. Ejemplo:

int x = 5, y = 7;
const int *p = &x; // Declaración e inicialización del apuntador a constante
p = &y; // Esto es válido
*p = 3; // Esto no es válido (el compilador genera un error)
y = 3; // Esto es válido
20
Asignación Dinámica de Memoria.

La asignación dinámica permite obtener la memoria para variables que se precisen en


la ejecución del programa.

Para la asignación dinámica de memoria existe una función llamada malloc que se
encuentra en la librería stdlib.h.

void *malloc (tamaño)

Donde tamaño es el tamaño en bytes de la memoria que se requiere asignar o


reservar.

21
Asignación de memoria a valores tipo char

22
Asignación de memoria a valores numéricos.

Si se requiere reservar memoria para un tipo de dato que no es char se realiza de la siguiente manera:

tamaño = (número de elementos) * (tamaño del tipo)

El tamaño del tipo se obtiene con la función sizeof.

Ejemplos:

//Reserva de memoria para 35 enteros

int *apun;

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

//Reserva de memoria para 50 flotantes

float *apun;

apun = (float *) malloc (sizeof(float));

23
2.6. Enviar y recibir apuntadores en
una función

Cuando pasamos un apuntador como parámetro de una función por valor pasa lo
mismo que con cualquier otro objeto.

Dentro de la función trabajamos con una copia del parámetro, que en este caso es un
apuntador. Por lo tanto, igual que pasaba con el ejemplo anterior, las modificaciones
en el valor del parámetro serán locales a la función y no se mantendrán después de
retornar.

Sin embargo, no sucede lo mismo con el objeto apuntado por el apuntador, puesto
que en ambos casos será el mismo, ya que tanto el apuntador como el parámetro
tienen como valor la misma dirección de memoria. Por lo tanto, los cambios que
hagamos en los objetos apuntados por el apuntador se conservarán al abandonar la
función.
Ejemplo:

#include <iostream>
using namespace std;
void funcion(int *q);
int main()
{
int a;
int *p;
a = 100;
p = &a;
// Llamamos a función con un apuntador
funcion(p); // (1)
cout << "Variable a: " << a << endl;
cout << "Variable *p: " << *p << endl; // Llamada a función con la dirección de "a" (constante)
funcion(&a); // (2)
cout << "Variable a: " << a << endl;
cout << "Variable *p: " << *p << endl;
return 0;
}
void funcion(int *q)
{
// Cambiamos el valor de la variable apuntada por
// el apuntador
*q += 50;
q++;
}
25
Dentro de la función se modifica el valor apuntado por el apuntador, y los cambios
permanecen al abandonar la función. Sin embargo, los cambios en el propio
apuntador son locales, y no se conservan al regresar.

Análogamente a como lo hicimos antes al pasar una constante literal, podemos pasar
apuntador variables o constantes como parámetro a la función. En (1) usamos un
variable de tipo apuntador, en (2) usamos un apuntador constante.

De modo que con este tipo de declaración de parámetro para función estamos
pasando el apuntador por valor. ¿Y cómo haríamos para pasar un apuntador por
referencia?:

void funcion(int* &q);

El operador de referencia siempre se pone junto al nombre de la variable. En esta


versión de la función, las modificaciones que se hagan para el valor del apuntador
pasado como parámetro, se mantendrán al regresar al punto de llamada.

26
Nota: En C no existen referencias de este tipo, y la forma de pasar parámetros por
referencia es usar un apuntador. Por supuesto, para pasar una referencia a un
apuntador se usa un apuntador a apuntador, etc.

La idea original de la implementación de referencias en C++ no es la de crear


parámetros variables (algo que existe, por ejemplo, en PASCAL), sino ahorrar recursos
a la hora de pasar como parámetros objetos de gran tamaño.

Por ejemplo, supongamos que necesitamos pasar como parámetro a una función un
objeto que ocupe varios miles de bytes. Si se pasa por valor, en el momento de la
llamada se debe copiar en la pila todo el objeto, y la función recupera ese objeto de la
pila y se lo asigna al parámetro. Sin embargo, si se usa una referencia, este paso se
limita a copiar una dirección de memoria.

En general, se considera mala práctica usar referencias en parámetros con el fin de


modificar su valor en la función. La explicación es que es muy difícil, durante el análisis
y depuración, encontrar errores si no estamos seguros del valor de los parámetros
después de una llamada a función. Del mismo modo, se complica la actualización si los
valores de ciertas variables pueden ser diferentes, dependiendo dónde se inserte
nuevo código.
27
Apuntadores para paso de parámetros por referencia.

El lenguaje C no provee una manera de pasar parámetros por referencia. Sin embargo, es posible
hacerlo a través del uso de apuntadores. A continuación se muestra un ejemplo del paso de un
parámetro por referencia en C++, y luego un código equivalente en C o C++ utilizando un
apuntador:

void suma(int a, int b, int& r)


{
r = a + b;
}

void main()
{
int x;
suma(7, 5, x);
cout << "7 + 5 = " << x;
}

Nótese que en ambos casos se utiliza el operador ‘&’ para cosas distintas. El operador ‘&’ tiene
dos significados como operador unario: señalación de parámetro por referencia y operador de
referenciación.
28
Bibliografía

 “Programación en C++”, del autor Luis Joyanes Aguilar, editorial Mc Graw Hill.

 http://aprendeenlinea.udea.edu.co/lms/men_udea/pluginfile.php/25669/mod_reso
urce/content/0/documentos/Apuntadores.pdf

 http://www.utm.mx/~mgarcia/PE7(Apuntadores).pdf

 http://profesores.fi-b.unam.mx/m3615m/Documentos/apuntadores%20en%20C.pdf

 http://c.conclase.net/curso/?cap=015b

 https://www.fing.edu.uy/tecnoinf/mvd/cursos/eda/material/teo/EDA-teorico7.pdf

También podría gustarte