Daniel Garcia

sábado, julio 17, 2010

ARRAYS Y PUNTEROS EN C

 

Arrays.

Un array es una colección ordenada de objetos, llamados elementos del array, todos del mismo tipo. Un array de 10 elementos se declara de la siguiente forma:

float a[100];

El primer elemento de este array es a[0], y el último a[9].

Un array se puede inicializar así:

float a[3] = {10.1,10.2,10.3};

Si no se indica el tamaño del array, éste será igual al número de elementos que se indiquen:

float x[] = {1.3, 2.4};

crea un vector x de tamaño 2.

Arrays multidimensionales

Los arrays multidimensionales se declaran:

int a[3][3], b[2][3];

En este ejemplo, a es una matriz 3x3, y b una matriz 2x3. Los elementos se almacenan por filas, al contrario de lo que sucedía en FORTRAN. Así, podemos inicializar b de la siguiente forma:

int b[2][3] = { {1,2,3}, {4,5,6} };

Punteros.

El concepto de puntero está unido a la forma en que los tipos de datos son almacenados en la memoria de un ordenador, ya que denotan la dirección de una variable determinada. El nombre de la variable determina el tipo (char, int, float o double) y su dirección determina dónde está almacenada. Conocer la dirección de una variable es importante porque:

Permite que las funciones cambien el valor de sus argumentos, como veremos en el capítulo siguiente.

Permite pasar vectores de forma eficiente entre funciones: en lugar de copiar cada elemento del vector, se copia la dirección del primer elemento.

Permite reservar memoria en tiempo de ejecución en lugar de en tiempo de compilación, lo que significa que el tamaño de un vector puede ser determinado por el usuario en lugar de por el programador.

El nombre de un array es un puntero al array. Por tanto, los punteros y los arrays están íntimamente ligados en C y en C++.

Terminología básica.

Para entender los punteros y los vectores, es necesario conocer primero cómo se almacenan los números en la memoria del ordenador. El valor de cada variable en un programa de ordenador se guarda en una sección de memoria cuyo tamaño está determinado por el tipo de dato de la variable. La localización de esta sección de memoria es almacenada en la dirección de la variable. Por tanto, es como si cada variable estuviera compuesta de dos partes: su valor y su dirección. Cada celda de memoria se puede considerar compuesta de una parte con el contenido, y otra en la que se almacena la dirección. Esto es análogo a una fila de casas: cada casa tiene diferentes contenidos, y para mirarla necesitamos conocer su dirección.

Las direcciones se representan normalmente por un número hexadecimal, pudiendo contener un carácter, un entero o un real (aunque en realidad todos son almacenados como números binarios).

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

#include <iostream.h>

main() {

double d = 2.7183;

cout << 'numero = ' << d << '\tdirección = ' << &d << '\n';

}

El valor de d es 2.7183. El valor de &d es una dirección (donde 2.7183 está almacenado). La dirección es imprimida en formato hexadecimal.

Direcciones y punteros

Un puntero guarda la dirección de un objeto en memoria, y como tal un puntero es también una variable. Puede parecer algo confuso, es como decir que el contenido de una casa es la dirección de otra vivienda. Las direcciones se guardan como números hexadecimales, por lo que no hay ninguna razón por la que no podamos definir otro tipo de dato, similar a un entero, pero a través del cual se puede modificar el valor almacenado en esa dirección. Es importante entender la relación entre punteros y direcciones:

Cada variable tiene su dirección, que puede ser obtenida mediante el operador unario &.

La dirección de una variable puede ser almacenada en un tipo de dato llamado puntero.

Un puntero en C o en C++ se declara anteponiendo un * al nombre de la variable, que es el operador inverso a &. El puntero apunta entonces a una variable del tipo especificado, y no debe ser usado con variables de otros tipos. Un experto en C podría forzar la utilización de un puntero con un tipo distinto del que se ha declarado, pero no es recomendable, ya que podría conducir a un uso erróneo.

Ejemplos de declaración de punteros:

int *puntero1;

int *puntero2, *puntero3;

char variable, *punteroCaracter;

float *punteroReal, real;

En la primera línea, declaramos un puntero a un entero. En la segunda, dos punteros a entero. En la tercera, un carácter ( variable) y un puntero a carácter (punteroCaracter). Por último, punteroReal es un puntero a un real, y real es declarado como un número real.

Ejemplo de uso del operador * y del operador de dirección (&):

#include <iostream.h>

main() {

double d, *dp;

d = 2.7183;

dp = &d;

cout << 'numero = ' << d << '\tdirección = ' << &d << '\n';

}

Operaciones con punteros.

Un puntero es un tipo de dato similar a un entero, y hay un conjunto de operaciones definidas para punteros:

La suma o resta de un entero produce una nueva localización de memoria.

Se pueden comparar punteros, utilizando expresiones lógicas, para ver si están apuntando o no a la misma dirección de memoria.

La resta de dos punteros da como resultado el número de variables entre las dos direcciones.

Siempre que se realiza una operación aritmética sobre un puntero, sumando o restando un entero, el puntero se incrementa o decrementa un número apropiado de sitios tal que el nuevo valor apunta a la variable que está n elementos (no n bytes) antes o después que el dado. De la misma forma, al restar dos punteros se obtiene el número de objetos entre las dos localizaciones. Finalmente, dos punteros son iguales si y sólo si apuntan a la misma variable (el valor de las direcciones es el mismo). No son necesariamente iguales si sus valores indirectos son los mismos, ya que estas variables podrían estar en diferentes localizaciones de memoria.

La siguiente tabla resume los operadores que manipulan punteros:

RESERVA DINAMICA DE MEMORIA EN C

Los operadores new y delete se utilizan para reservar y liberar memoria dinámicamente. new y delete son parte del lenguaje C++ y no parte de una librería como sucedía con las funciones equivalentes malloc() y free() de C. Ahora los operadores new y delete.

El propósito de new es crear arrays cuyo tamaño pueda ser determinado mientras el programa se ejecuta.

delete funciona igual que free() en C. La memoria a la que apunta el puntero es liberado, pero no el puntero en sí.

A continuación se presenta a modo de ejemplo un programa que reserva memoria de modo

dinámico para un vector de caracteres:

#include <iostream.h>

#include <string.h>

void main() {

char Nombre[50];

cout << 'Introduzca su Nombre:';

cin >> Nombre;

char *CopiaNombre = new char[strlen(Nombre)+1];

strcpy(CopiaNombre, Nombre); //copio Nombre en la variable CopiaNombre

cout << CopiaNombre;

delete [] CopiaNombre; //libero memoria

}

Se puede utilizar el operador new para crear variables de cualquier tipo. New devuelve, en todos los casos, un puntero a la variable creada. También se pueden crear variables de tipos definidos por el usuario.

struct usuario {

..........

};

usuario* Un_Usuario;

Un_Usuario = new usuario;

Cuando una variable ya no es necesaria se destruye con el operador delete para poder utilizar

la memoria que estaba ocupando, mediante una instrucción del tipo:

FUNCIONES EN LENGUAJE C

Las funciones se declaran y se definen exactamente igual que en C, y, al igual que en éste, se puede utilizar prototipo (prototype).

Prototipos

La declaración de una función es el prototipo. El prototipo da un modelo de la interface a la función. Veamos un ejemplo:

# include <iostream.h>

void haz_algo (int alas, float pies, char ojos);

main() {

int ala = 2;

float pie = 1000.0;

char ojo = 2;

haz_algo (3, 12.0, 4);

haz_algo (ala, pie, ojo);

}

void haz_algo (int alas, float pies, char ojos) {

cout << 'Hay ' << alas << 'alas.' << '\n';

cout << 'Hay ' << pies << 'pies. ' << '\n';

cout << 'Hay ' << int(ojos) << 'ojos.' << '\n';

}

La salida de este programa será:

Hay 3 alas.

Hay 12 pies.

Hay 4 ojos.

Hay 2 alas.

Hay 1000 pies.

Hay 2 ojos.

Cada llamada a la función haz_algo() debe verificar:

El número de parámetros debe ser exactamente tres.

Los tipos deben ser compatibles con los de la declaración.

Nótese que cuando llamamos a la función, la comprobación de tipo la hace el compilador basándose en el prototipo (en la declaración) puesto que la función todavía no ha sido definida.

Los nombres de variables que aparecen en el prototipo son opcionales y actúan casi como comentarios al lector del programa, ya que son completamente ignorados por el compilador.

Tipos compatibles

Son compatibles cualquiera de los tipos simples (definidos en C++) que pueden ser convertidos de uno a otro de manera significativa. Por ejemplo, si llamamos con un entero a una función que está esperando un número real como parámetro, el sistema lo convertirá automáticamente, sin mencionarlo al usuario. Esto también es cierto de float a char, o de char a int.

En cambio, si pasamos un puntero a un entero a una función que estaba esperando un entero, no habrá conversión de tipo, ya que son dos variables completamente distintas. De la misma forma, un tipo definido por el usuario (estructura o clase) no puede ser convertido automáticamente a un long float, a un array o incluso a otra estructura o clase diferente, porque son tipos incompatibles y no puede realizarse la conversión de manera significativa.

Sin embargo, el tipo devuelto por la función, void en el ejemplo anterior, debe ser compatible con el tipo que se espera que devuelva en la función de llamada, o el compilador dará un warning.

El uso de prototipos no supone coste alguno en tiempo ni en velocidad de ejecución. El prototipo se verifica durante la compilación.

Funciones con void como argumento.

Una función sin lista de argumentos como

void func ();

significa en C que no se ha declarado el tipo de la lista de argumentos que recibe la función, por lo que el compilador no producirá errores respecto al uso impropio de los argumentos. Cuando en C se declara una función que no tiene argumentos se utiliza el tipo void:

void func (void);

En C++, ambas expresiones son equivalentes

Pasar punteros a funciones.(paso por valor y por referencia)

Cuando se llama a una función, todos los parámetros con los que la llamamos son copiados y pasados a la función (paso por valor). Esto significa que si la función cambia el valor de los parámetros, sólo lo hace dentro del ámbito de la función. Por ejemplo:

#include <iostream.h>

void change_values(int a,int b) {

a=4;

b=5;

}

main() {

int a, b;

a=1;

b=2;

change_values(a,b);

cout << 'A is ' << a <<',B is' << b <<'\n';

}

La salida de programa es: A is 1, B is 2

La llamada a la función no ha cambiado el valor de las variables que se le han pasado. La función cambia las copias de lo que se le ha pasado.

Si queremos pasar parámetros por referencia hay que pasar punteros a los datos. Para hacer esto, utilizamos el operador &, que da la dirección de una variable:

#include <iostream.h>

void change_values(int *a,int *b) {

*a=4;

*b=5;

}

main() {

int a, b;

a=1;

b=2;

change_values(&a,&b);

cout << 'A is ' << a <<',B is' << b <<'\n';

}

Ahora la salida del programa es:

A is 4, B is 5

La función main pasa la dirección de a y b, por lo que a la función change_values se le pasa una copia de las direcciones. Utilizando las direcciones de a y b, la función puede acceder a los datos directamente.

Polimorfismo.

En C++ es posible declarar dos funciones diferentes que tengan el mismo nombre. Las funciones deben diferir en la lista de argumentos, bien en el número de variables que se pasan a la función, bien en el tipo de argumentos que recibe. Así, por ejemplo, se puede definir una función que trabaje, bien con enteros, bien con strings; sólo hay que definir dos funciones separadas con el mismo nombre:

#include <iostream.h>

void show(int val) {

cout <<' Es un entero :'<< val << '\n';

}

void show(char *val) {

cout <<'Es un carácter: '<< val << '\n';

}

main() {

show (42);

show ('A');

show (452.2);

}

En la primera llamada a la función show, se le pasa un entero, por tanto se llama a la primera copia de la función show. La segunda vez, el argumento es un carácter, por tanto se utiliza la segunda definición, aquella que utiliza un carácter. Ahora bien, la tercera llamada utiliza un número real, y no existe una definición de la función para este caso. El compilador utiliza la primer definición. La salida del programa es:

Es un entero :42

Es un carácter: A

Es un entero :452

Comentarios sobre la sobrecarga de funciones:

· El uso de más de una función con el mismo nombre pero acciones diferentes debe ser evitado. En el ejemplo anterior, las funciones show() están relacionadas: imprimen información en la pantalla.

· C++ no permite que varias funciones difieran sólo en su valor devuelto. Dos funciones de este tipo no podrían ser distinguidas por el compilador.

Parámetros por defecto

Es una forma de indicar qué valor debe ser pasado a una función en el caso en que en la llamada no se pase nada, o se pasen menos argumentos de los definidos. Un ejemplo de definición de una función que tiene parámetros por defecto en su lista de argumentos es:

void funcion (int y = 2)

En este caso, estamos definiendo un valor, 2, que tomará la variable y en caso de que no se pase nada en la llamada a la función:

funcion ();

0 comentarios: