La conveniencia de coger nombres de funciones y datos fuera del espacio de nombre global es aplicable a las estructuras. Puede anidar una estructura dentro de otra estructura, y por tanto guardar juntos elementos asociados. La sintaxis de declaración es la que podría esperarse, tal como puede ver en la siguiente estructura, que implementa una pila como una lista enlazada simple de modo que «nunca» se queda sin memoria.
//: C04:Stack.h // Nested struct in linked list #ifndef STACK_H #define STACK_H struct Stack { struct Link { void* data; Link* next; void initialize(void* dat, Link* nxt); }* head; void initialize(); void push(void* dat); void* peek(); void* pop(); void cleanup(); }; #endif // STACK_H ///:~
Listado 4.9. C04/Stack.h
La struck
anidada se llama Link
, y
contiene un puntero al siguiente Link
en la lista y
un puntero al dato almacenado en el Link
. Si el
siguiente puntero es cero, significa que es el último elemento
de la lista.
Fíjese que el puntero head
está definido a la
derecha después de la declaración de la struct
Link
, es lugar de una definición separada
Link* head
. Se trata de una sintaxis que viene de
C, pero que hace hincapié en la importancia del punto y coma
después de la declaración de la estructura; el punto y coma
indica el fin de una lista de definiciones separadas por comas
de este tipo de estructura (Normalmente la lista está vacía.)
La estructura anidada tiene su propia función
initialize()
, como todas las estructuras
hasta el momento, para asegurar una inicialización
adecuada. Stack
tiene tanto función
initialice()
como
cleanup()
, además de
push()
, que toma un puntero a los datos que
se desean almacenar (asume que ha sido alojado en el montículo),
y pop()
, que devuelve el puntero
data
de la cima de la
Stack
y elimina el elemento de la
cima. (El que hace pop()
de un elemento se
convierte en responsable de la destrucción del objeto apuntado
por data
.) La función
peak()
también devuelve un puntero
data
a la cima de la pila, pero deja el
elemento en la Stack
.
Aquí se muestran las definiciones de los métodos:
//: C04:Stack.cpp {O} // Linked list with nesting #include "Stack.h" #include "../require.h" using namespace std; void Stack::Link::initialize(void* dat, Link* nxt) { data = dat; next = nxt; } void Stack::initialize() { head = 0; } void Stack::push(void* dat) { Link* newLink = new Link; newLink->initialize(dat, head); head = newLink; } void* Stack::peek() { require(head != 0, "Stack empty"); return head->data; } void* Stack::pop() { if(head == 0) return 0; void* result = head->data; Link* oldHead = head; head = head->next; delete oldHead; return result; } void Stack::cleanup() { require(head == 0, "Stack not empty"); } ///:~
Listado 4.10. C04/Stack.cpp
La primera definición es particularmente interesante porque
muestra cómo se define un miembro de una estructura
anidada. Simplemente se usa un nivel adicional de resolución de
ámbito para especificar el nombre de la struct
interna. Stack::Link::initialize()
toma
dos argumentos y los asigna a sus atributos.
Stack::initialize()
asgina cero a
head
, de modo que el objeto sabe que
tiene una lista vacía.
Stack::push()
toma el argumento, que es
un puntero a la variable a la que se quiere seguir la pista, y la
apila en la Stack
. Primero, usa
new
para pedir alojamiento para el
Link
que se insertará en la cima. Entonces
llama a la función initialize()
para
asignar los valores apropiados a los miembres del
Link
. Fijese que el siguiente puntero se
asigna al head
actual; entonces
head
se asigna al nuevo puntero
Link
. Esto apila eficazmente el
Link
en la cima de la lista.
Stack::pop()
captura el puntero
data
en la cima actual de la
Stack
; entonces mueve el puntero
head
hacia abajo y borra la anterior cima de
la Stack
, finalmente devuelve el puntero
capturado. Cuando pop()
elemina el último
elemento, head
vuelve a ser cero, indicando que
la Stack
está vacía.
Stack::cleanup()
realmente no hace
ninguna limpieza. En su lugar, establece una política firme que
dice «el programador cliente que use este objeto
Stack
es responsable de des-apilar todos
los elementos y borrarlos». require()
se usa para indicar que ha ocurrido un error de programación si la
Stack
no está vacía.
¿Por qué no puede el destructor de Stack
responsabilizarse de todos los objetos que el programador
cliente no des-apiló? El problema es que la
Stack
está usando punteros
void
, y tal como se verá en el Capítulo 13 usar delete
para un
void*
no libera correctamente. El asunto de
«quién es el responsable de la memoria» no siempre
es sencillo, tal como veremos en próximos capítulos.
Un ejemplo para probar la Stack
:
//: C04:StackTest.cpp //{L} Stack //{T} StackTest.cpp // Test of nested linked list #include "Stack.h" #include "../require.h" #include <fstream> #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]); assure(in, argv[1]); Stack textlines; textlines.initialize(); string line; // Read file and store lines in the Stack: while(getline(in, line)) textlines.push(new string(line)); // Pop the lines from the Stack and print them: string* s; while((s = (string*)textlines.pop()) != 0) { cout << *s << endl; delete s; } textlines.cleanup(); } ///:~
Listado 4.11. C04/StackTest.cpp
Es similar al ejemplo anterior, pero en este se apilan líneas
de un fichero (como punteros a cadena) en la
Stack
y después los des-apila, lo que
provoca que el fichero sea imprimido en orden inverso. Fíjese que
pop()
devuelve un void*
que debe ser moldeado a string*
antes de poderse
usar. Para imprimir una cadena, el puntero es dereferenciado.
Como textlines
se llena, el contenido de
line
se «clona» para cada
push()
creando un new
string(line)
. El valor devuelto por la expresión
new
es un puntero al nuevo string
que fue
creado y al que se ha copiado la información de la
line
. Si se hubiera pasado directamente la
dirección de line
a
push()
, la Stack
se llenaría con direcciones idénticas, todas apuntando a
line
. Más adelante en ese libro aprenderá más
sobre este proceso de «clonación».
El nombre del fichero se toma de línea de comando. Para garantizar
que hay suficientes argumentos en la línea de comando, se usa una
segunda función del fichero de cabecera
require.h
: requireArgs()
que compara argc
con el número de argumentos
deseado e imprime un mensaje de error y termina el programa si no
hay suficientes argumentos.
El operador de resolución de ámbito puede ayudar en situaciones
en las que el nombre elegido por el compilador (el nombre
«más cercano») no es el que se quiere. Por ejemplo,
suponga que tiene una estructura con un identificador local
a
, y quiere seleccionar un identificador
global a
desde dentro de un método. El
compilador, por defecto, elegirá el local, de modo que es
necesario decirle que haga otra cosa. Cuando se quiere
especificar un nombre global usando la resolución de ámbito,
debe usar el operador sin poner nada delante de él. A
continuación aparece un ejemplo que muestra la resolución de
ámbito global tanto para una variable como para una función:
//: C04:Scoperes.cpp // Global scope resolution int a; void f() {} struct S { int a; void f(); }; void S::f() { ::f(); // Would be recursive otherwise! ::a++; // Select the global a a--; // The a at struct scope } int main() { S s; f(); } ///:~
Listado 4.12. C04/Scoperes.cpp
Sin resolución de ámbito en S::f()
, el
compilador elegiría por defecto las versiones miembro para
f()
y a
.