Page 40
C:\> COPY A:\MIO\PROGRAM.C C:
lo que en realidad hacemos es llamar al programa COPY pasándole una serie de parámetros. Lo
que recibe la funció n main() es la línea de comandos, es decir, el nombre de la funció n seguido
de los parámetros, cada palabra será una de las cadenas del array argv[]. Para el ejemplo, main()
recibirá:
argc = 3
argv[1] = "COPY"
argv[1] = "A:\MIO\PROGRAM.C"
argv[2] = "C:"
Por ú ltimo, para salir de un programa en C++ tenemos dos opciones, retornando un valor al SO
en la funció n main() (o cuando esta termine si no retorna nada) o empleando la funció n exit(),
declarada en la cabecera estándar <stdlib.h>:
void exit (int);
El entero que recibe la funció n exit() será el valor que se retorne al SO. Generalmente la salida
con exit() se utiliza cuando se produce un error.
VARIABLES DINÁMICAS
En la mayoría de los lenguajes de alto nivel actuales existe la posibilidad de trabajar con
variables dinámicas, que son aquellas que se crean en tiempo de ejecució n. Para soportar el
empleo de estas variables aparecen los conceptos de puntero y referencia que están íntimamente
relacionados con el concepto de direcció n física de una variable.
Punteros y direcciones
Ya hemos mencionado que el C++ es un lenguaje que pretende acercarse mucho al nivel de
máquina por razones de eficiencia, tanto temporal como espacial. Por está razó n el C++ nos
permite controlar casi todos los aspectos de la ocupació n y gestió n de memoria de nuestros
programas (sabemos lo que ocupan las variables, podemos trabajar con direcciones, etc.).
Uno de los conceptos fundamentales en este sentido es el de puntero. Un puntero es una
variable que apunta a la direcció n de memoria donde se encuentra otra variable. La clave aquí
está en la idea de que un puntero es una direcció n de memoria.
Pero, ¿ có mo conocemos la direcció n de una variable declarada en nuestro programa? La
solució n a esto está en el uso del operador de referencia (&), ya mencionado al hablar de los
operadores de indirecció n. Para obtener la direcció n de una variable solo hay aplicarle el
operador de referencia (escribiendo el símbolo & seguido de la variable), por ejemplo:
int i = 2;
int *pi = &i; // ahora pi contiene la dirección de la variable i.
El operador de indirecció n só lo se puede ser aplicado a variables y funciones, es decir, a
LValues. Por tanto, sería un error aplicarlo a una expresió n (ya que no tiene direcció n).
Por otro lado, para acceder a la variable apuntada por un puntero se emplea el operador de
indirecció n (*) poniendo el * y despué s el nombre del puntero:
int j = *pi; // j tomaría el valor 2, que es el contenido de la variable i anterior
Para declarar variables puntero ponemos el tipo de variables a las que va a apuntar y el nombre
del puntero precedido de un asterisco. Hay que tener cuidado al definir varios punteros en una
misma línea, ya que el asterisco se asocia al nombre de la variable y no al tipo. Veamos esto con
un ejemplo:
char *c, d, *e; // c y e son punteros a carácter, pero d es una variable carácter
34

Page 41
Como los punteros son variables podemos emplearlos en asignaciones y operaciones, pero hay
que diferenciar claramente entre los punteros como dato (direcciones) y el valor al que apuntan.
Veremos esto con un ejemplo, si tenemos los punteros:
int *i, *j;
La operació n:
i= j;
hace que i y j apunten a la misma direcció n de memoria, pero la operació n:
*i = *j;
hace que lo apuntado por i pase a valer lo mismo que lo apuntado por j, es decir, los punteros
no han cambiado, pero lo que contiene la direcció n a la que apunta i vale lo mismo que lo que
contiene la direcció n a la que apunta j. Es decir, si i y j apuntaran a las variables enteras a y b
respectivamente:
int a, b;
int *i = &a;
int *j = &b;
lo anterior sería equivalente a:
a= b;
Por ú ltimo indicaremos que hay que tener mucho cuidado para no utilizar punteros sin
inicializar, ya que no sabemos cuál puede ser el contenido de la direcció n indefinida que
contiene una variable puntero sin inicializar.
El puntero NULL
Siempre que trabajamos con punteros solemos necesitar un valor que nos indique que el puntero
es nulo (es decir, que no apuntamos a nada). Esto se consigue dándole al puntero el valor 0 o
NULL. NULL no es una palabra reservada, sino que se define como una macro en las
cabeceras estándar <stddef.h> y <stdlib.h>, y por tanto será necesario incluirlas para usarlo. Si
no queremos usar las cabeceras podemos definirlo nosotros de alguna de las siguientes formas:
#define NULL (0)
#define NULL (0L)
#define NULL ((void *) 0)
la primera y segunda formas son válidas porque 0 y 0L tienen conversió n implícita a puntero, y
la tercera es válida porque convertimos explícitamente el 0 a puntero void. Una forma adecuada
de definir NULL es escribiendo:
#ifndef NULL
#define NULL ((void *) 0)
#endif
que define NULL só lo si no está definido (podría darse el caso de que nosotros no
incluyé ramos las cabeceras que definen NULL, pero si se hiciese desde alguna otra cabecera
que si hemos incluido).
Cualquier indirecció n al puntero NULL se transforma en un error de ejecució n.
Punteros void
Ya hemos mencionado que el C++ define un tipo especial denominado void (vacío), que
utilizábamos para indicar que una funció n no retorna nada o no toma parámetros. Además el
tipo void se emplea como base para declarar punteros a variables de tipo desconocido.
35

Page 42
Debemos recordar que no se pueden declarar variables de tipo void, por lo que estos punteros
tendrán una serie de restricciones de uso.
Un puntero void se declara de la forma normal:
void *ptr;
y se usa só lo en asignaciones de punteros. Para trabajar con los datos apuntados por un puntero
tendremos que realizar conversiones de tipo explícitas (casts):
char a;
char *p = (char *) ptr;
a = *p;
¿ Cuál es la utilidad de estos punteros si só lo se pueden usar en asignaciones? Realmente se
emplean para operaciones en las que no nos importa el contenido de la memoria apuntada, sino
só lo la direcció n, como por ejemplo en las funciones estándar de C para manejo de memoria
dinámica (que tambié n son válidas en C++, aunque este lenguaje ha introducido un operador
que se encarga de lo mismo y que es más có modo de utilizar). La definició n de algunas de estas
funciones es:
void *malloc (size_t N); // reserva N bytes de memoria
void free (void *); // libera la memoria reservada con malloc
/*
size_t es un tipo que se usa para almacenar tamaños de memoria definido en las
cabeceras estándar mediante un typedef, generalmente lo consideraremos un entero sin
más
*/
y para usarlas hacemos:
void *p;
p= malloc (1000); // reservamos 1000 bytes y asignamos a p la dirección del
// primer byte
free(p); // liberamos la memoria asignada con malloc.
Aunque podemos ahorrarnos el puntero void haciendo una conversió n explícita del retorno de la
funció n:
long *c;
c=(long *)malloc(1000); // aunque se transforma en puntero a long sigue reservando
// 1000 bytes, luego si cada long ocupa 4 bytes sólo nos
// cabrán 250 variables de tipo long
Por ú ltimo señalar que en todas las ocasiones en las que hemos hecho conversiones con
punteros hemos usado é l mé todo de C y no el de C++:
c = long *(malloc (1000));
ya que esto ú ltimo da error de compilació n. La solució n a este problema es definir tipos puntero
usando typedef:
typedef long *ptr_long;
c = ptr_long(malloc(1000));
Aritmé tica con punteros
Las variables de tipo puntero contienen direcciones, por lo que todas ellas ocuparan la misma
memoria: tantos bytes como sean necesarios para almacenar una direcció n en el computador
sobre el que trabajemos. De todas formas, no podemos declarar variables puntero sin
especificar a qué tipo apuntan (excepto en el caso de los punteros void, ya mencionados), ya
que no es só lo la direcció n lo que nos interesa, sino que tambié n debemos saber que es lo
apuntado para los chequeos de tipos cuando dereferenciamos (al tomar el valor apuntado para
36

Page 43
usarlo en una expresió n) y para saber que cantidades debemos sumar o restar a un puntero a la
hora de incrementar o decrementar su valor.
Es decir, el incremento o decremento de un puntero en una unidad se traduce en el incremento o
decremento de la direcció n que contiene en tantas unidades como bytes ocupan las variables del
tipo al que apunta.
Además de en sumas y restas los punteros se pueden usar en comparaciones (siempre y cuando
los punteros a comparar apunten al mismo tipo). No podemos multiplicar ni dividir punteros.
Punteros y parámetros de funciones
Lo ú nico que hay que indicar aquí es que los punteros se tratan como las demás variables al ser
empleadas como parámetro en una funció n, pero tienen la ventaja de que podemos poner como
parámetro actual una variable del tipo al que apunta el puntero a la que aplicamos el operador de
referencia. Esta frase tan complicada se comprende mejor con un ejemplo, si tenemos una
funció n declarada como:
void f (int *);
es decir, una funció n que no retorna nada y recibe como parámetro un puntero a entero,
podemos llamarla con:
int i;
f(&i);
Lo que f recibirá será el puntero a la variable i.
Punteros y arrays
La relació n entre punteros y arrays en C++ es tan grande que muchas veces se emplean
indistintamente punteros y vectores. La relació n entre unos y otros se basa en la forma de tratar
los vectores. En realidad, lo que hacemos cuando definimos un vector como:
int v[100];
es reservar espacio para 100 enteros. Para poder acceder a cada uno de los elementos ponemos
el nombre del vector y el índice del elemento al que queremos acceder:
v[8] = 100;
Pero, ¿ có mo gestiona el compilador los vectores?. En realidad, el compilador reserva un
espacio contiguo en memoria de tamaño igual al nú mero de elementos del vector por el nú mero
de bytes que ocupan los elementos del array y guarda en una variable la direcció n del primer
elemento del vector. Para acceder al elemento i lo que hace el compilador es sumarle a la primera
direcció n el nú mero de índice multiplicado por el tamaño de los elementos del vector. Esta es la
razó n de que los vectores comiencen en 0, ya que la primera direcció n más cero es la direcció n
del primer elemento.
Hemos comentado todo esto porque en realidad esa variable que contiene la direcció n del primer
elemento de un vector es en realidad un puntero a los elementos del vector y se puede utilizar
como tal. La variable puntero se usa escribiendo el nombre de la variable array sin el operador
de indexado (los corchetes):
v [0] = 1100;
es igual que:
*v = 1100;
Para acceder al elemento 8 hacemos:
37

Page 44
*(v + 8)
Es decir, la ú nica diferencia entre declarar un vector y un puntero es que la primera opció n hace
que el compilador reserve memoria para almacenar elementos del tipo en tiempo de compilació n,
mientras que al declarar un puntero no reservamos memoria para los datos que va a apuntar y
só lo lo podremos hacer en tiempo de ejecució n (con los operadores new y delete). Excepto por
la reserva de memoria y porque no podemos modificar el valor de la variable de tipo vector (no
podemos hacer que apunte a una distinta a la que se ha reservado para ella), los vectores y
punteros son idé nticos.
Todo el tiempo hemos hablado sobre vectores, pero refirié ndonos a vectores de una dimensió n,
los vectores de más de una dimensió n se acceden sumando el valor del índice más a la derecha
con el segundo índice de la derecha por el nú mero de elementos de la derecha, etc. Veamos
como acceder mediante punteros a elementos de un vector de dos dimensiones:
int mat[4][8];
*(mat + 0*8 + 0) // accedemos a mat[0][0]
*(mat + 1*8 + 3) // accedemos a mat[1][3]
*(mat + 3*8 + 7) // accedemos a mat[3][7] (último elemento de la matriz)
Por ú ltimo mencionar que podemos mezclar vectores con punteros (el operador de vector tiene
más precedencia que el de puntero, para declarar punteros a vectores hacen falta paré ntesis).
Ejemplos:
int (*p)[20]; // puntero a un vector de 20 enteros
int p[][20]; // igual que antes, pero p no se puede modificar
int *p[20]; // vector de 20 punteros a entero
Operadores new y delete
Hemos mencionado que en C se usaban las funciones malloc() y free() para el manejo de
memoria dinámica, pero dijimos que en C++ se suelen emplear los operadores new y delete.
El operador new se encarga de reservar memoria y delete de liberarla. Estos operadores se
emplean con todos los tipos del C++, sobre todo con los tipos definidos por nosotros (las
clases). La ventaja sobre las funciones de C de estos operadores está en que utilizan los tipos
como operandos, por lo que reservan el nú mero de bytes necesarios para cada tipo y cuando
reservamos más de una posició n no lo hacemos en base a un nú mero de bytes, sino en funció n
del nú mero de elementos del tipo que deseemos.
El resultado de un new es un puntero al tipo indicado como operando y el operando de un delete
debe ser un puntero obtenido con new.
Veamos con ejemplos como se usan estos operadores:
int * i = new int; // reservamos espacio para un entero, i apunta a él
delete i; // liberamos el espacio reservado para i
int * v = new int[10]; // reservamos espacio contiguo para 10 enteros, v apunta
// al primero
delete []v; // Liberamos el espacio reservado para v
/*
En las versiones del ANSI C++ 2.0 y anteriores el delete se debía poner como:
delete [10]v; // Libera espacio para 10 elementos del tipo de v
*/
Hay que tener cuidado con el delete, si ponemos:
delete v;
38

Page 45
só lo liberamos la memoria ocupada por el primer elemento del vector, no la de los 10
elementos.
Con el operador new tambié n podemos inicializar la variable a la vez que reservamos la memoria:
int *i = new int (5); // reserva espacio para un entero y le asigna el valor
En caso de que se produzca un error al asignar memoria con new, se genera una llamada a la
funció n apuntada por
void (* _new_handler)(); // puntero a función que no retorna nada y no tiene
// parámetros
Este puntero se define en la cabecera <new.h> y podemos modificarlo para que apunte a una
funció n nuestra que trate el error. Por ejemplo:
#include <new.h>
void f() {
...
cout << "Error asignando memoria" << endl;
...
}
void main () {
...
_new_handler = f;
...
}
Punteros y estructuras
Podemos realizar todas las combinaciones que queramos con punteros y estructuras: podemos
definir punteros a estructuras, campos de estructuras pueden ser punteros, campos de
estructuras pueden ser punteros a la misma estructura, etc.
La particularidad de los punteros a estructuras está en que el C++ define un operador que a la
vez que indirecciona el puntero a la estructura accede a un campo de la misma. Este operador es
el signo menos seguido de un mayor que (->). Veamos un ejemplo:
struct dos_enteros {
int i1;
int i2;
};
dos_enteros *p;
(*p).i1 = 10; // asignamos 10 al campo i1 de la estructura apuntada por p
p->i2 = 20; // asignamos 20 al campo i2 de la estructura apuntada por p
A la hora de usar el operador -> lo ú nico que hay que tener en cuenta es la precedencia de
operadores. Ejemplo:
++p->i1; // preincremento del campo i1, es como poner ++ (p->i1)
(++p)->i1; // preincremento de p, luego acceso a i1 del nuevo p.
Por ú ltimo diremos que la posibilidad de definir campos de una estructura como punteros a
elementos de esa misma estructura es la que nos permite definir los tipos recursivos como los
nodos de colas, listas, árboles, etc.
Punteros a punteros
Además de definir punteros a tipos de datos elementales o compuestos tambié n podemos definir
punteros a punteros. La forma de hacerlo es poner el tipo y luego tantos asteriscos como niveles
de indirecció n:
39

Page 46
int *p1; // puntero a entero
int **p2; // puntero a puntero a entero
char *c[]; // vector de punteros a carácter
Para usar las variables puntero a puntero hacemos lo mismo que en la declaració n, es decir,
poner tantos asteriscos como niveles queramos acceder:
int ***p3; // puntero a puntero a puntero a entero
p3 = &p2; // trabajamos a nivel de puntero a puntero a puntero a entero
// no hay indirecciones, a p3 se le asigna un valor de su mismo tipo
*p3 = &p1; // el contenido de p2 (puntero a puntero a entero) toma la dirección de
// p1 (puntero a entero). Hay una indirección, accedemos a lo apuntado
// por p3
p1 = **p3; // p1 pasa a valer lo apuntado por lo apuntado por p3 (es decir, lo
// apuntado por p2). En nuestro caso, no cambia su valor, ya que p2
// apuntaba a p1 desde la operación anterior
***p3 = 5 // El entero apuntado por p1 toma el valor 5 (ya que p3 apunta a p2 que
// apunta a p1)
Como se ve, el uso de punteros a punteros puede llegar a hacerse muy complicado, sobre todo
teniendo en cuenta que en el ejemplo só lo hemos hecho asignaciones y no incrementos o
decrementos (para eso hay que mirar la precedencia de los operadores).
PROGRAMACIÓN EFICIENTE
En este punto veremos una serie de mecanismos del C++ ú tiles para hacer que nuestros
programas sean más eficientes. Comenzaremos viendo como se organizan y compilan los
programas, y luego veremos que construcciones nos permiten optimizar los programas.
Estructura de los programas
El có digo de los programas se almacena en ficheros, pero el papel de los ficheros no se limita al
de mero almacé n, tambié n tienen un papel en el lenguaje: son un ámbito para determinadas
funciones (estáticas y en línea) y variables (estáticas y constantes) siempre que se declaren en el
fichero fuera de una funció n.
Además de definir un ámbito, los ficheros nos permiten la compilació n independiente de los
archivos del programa, aunque para ello es necesario proporcionar declaraciones con la
informació n necesaria para analizar el archivo de forma aislada.
Una vez compilados los distintos ficheros fuente (que son los que terminan en .c, .cpp, etc.), es
el linker el que se encarga de enlazarlos para generar un só lo fichero fuente ejecutable.
En general, los nombres que no son locales a funciones o a clases se deben referir al mismo
tipo, valor, funció n u objeto en cada parte de un programa.
Si en un fichero queremos declarar una variable que está definida en otro fichero podemos
hacerlo declarándola en nuestro fichero precedida de la palabra extern.
Si queremos que una variable o funció n só lo pertenezca a nuestro fichero la declaramos static.
Si declaramos funciones o variables con los mismos nombres en distintos ficheros producimos
un error (para las funciones el error só lo se produce cuando la declaració n es igual, incluyendo
los tipos de los parámetros).
Las funciones y variables cuyo ámbito es el fichero tienen enlazado interno (es decir, el linker
no las tiene en cuenta).
Los ficheros cabecera
40

Page 47
Una forma fácil y có moda de que todas las declaraciones de un objeto sean consistentes es
emplear los denominados ficheros cabecera, que contienen có digo ejecutable y/o definiciones de
datos. Estas definiciones o có digo se corresponderán con la parte que queremos utilizar en
distintos archivos.
Para incluir la informació n de estos ficheros en nuestro fichero .c empleamos la directiva
include, que le servirá al preprocesador para leer el fichero cabecera cuando compile nuestro
có digo.
Un fichero cabecera debe contener:
Definició n de tipos
struct punto { int x, y; };
__rendered_path__22
Templates
template <class T> class V { … }
__rendered_path__22
Declaració n de funciones
extern int strlen (const char *);
__rendered_path__23
Definició n de funciones inline
inline char get { return *p++ ;}
__rendered_path__22
Declaració n de variables
extern int a;
__rendered_path__23
Definiciones constantes
const float pi = 3.141593;
__rendered_path__22
Enumeraciones
enum bool { false, true };
__rendered_path__22
Declaració n de nombres
class Matriz;
__rendered_path__24
Directivas include
#include <iostream.h>
__rendered_path__24
Definició n de macros
#define Case break;case
__rendered_path__24
Comentarios
/* cabecera de mi_prog.c */
__rendered_path__24
Y no debe contener:
__rendered_path__24
Definició n de funciones ordinarias
char get () { return *p++}
__rendered_path__24
Definició n de variables
int a;
__rendered_path__24
Definició n de agregados constantes
const tabla[] = { … }
__rendered_path__24
Si nuestro programa es corto, lo más usual es crear un solo fichero cabecera que contenga los
__rendered_path__24
tipos que necesitan los diferentes ficheros para comunicarse y poner en estos ficheros só lo las
__rendered_path__24
funciones y definiciones de datos que necesiten e incluir la cabecera global.
__rendered_path__24
Si el programa es largo o usamos ficheros que pueden ser reutilizados lo más ló gico es crear
__rendered_path__24
varios ficheros cabecera e incluirlos cuando sean necesarios.
__rendered_path__24
Por ú ltimo indicaremos que las funciones de biblioteca suelen estar declaradas en ficheros
__rendered_path__24
cabecera que incluimos en nuestro programa para que luego el linker las enlace con nuestro
__rendered_path__24
programa. Las bibliotecas estándar son:
__rendered_path__24
Bibliotecas de C:
__rendered_path__24
assert.h Define la macro assert()
__rendered_path__24
ctype.h Manejo de caracteres
__rendered_path__24
errno.h Tratamiento de errores
__rendered_path__24
float.h Define valores en coma flotante dependientes de la implementación
__rendered_path__24
limits.h Define los límites de los tipos dependientes de la implementación
__rendered_path__22__rendered_path__24
locale.h Define la función setlocale()
__rendered_path__22
math.h
Definiciones y funciones matemáticas
__rendered_path__23
setjmp.h Permite saltos no locales
__rendered_path__22
signal.h Manejo de señales
__rendered_path__23
stdarg.h Manejo de listas de argumentos de longitud variable
__rendered_path__22
stddef.h Algunas constantes de uso común
__rendered_path__22
stdio.h Soporte de E/S
__rendered_path__22
sdlib.h Algunas declaraciones estándar
__rendered_path__22
string.h Funciones de manipulación de cadenas
__rendered_path__23
time.h
Funciones de tiempo del sistema
__rendered_path__22
Bibliotecas de C++:
__rendered_path__23
fstream.h
Streams fichero
__rendered_path__22
iostream.h
Soporte de E/S orientada a objetos (streams)
__rendered_path__22
new.h
Definición de _new_handler
__rendered_path__24
strstream.hDefinición de streams cadena
__rendered_path__24__rendered_path__24__rendered_path__24__rendered_path__24__rendered_path__22__rendered_path__22__rendered_path__23__rendered_path__22__rendered_path__23__rendered_path__24__rendered_path__22__rendered_path__22
41