4.8. Estructuras anidadas

La ventaja de sacar los nombres de funciones y datos del espacio de nombres global también 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 struct 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 puntero next es cero, significa que es el último elemento de la lista.

Fíjese que el puntero head está definido justo después de la declaración de la struct Link, en 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 presentadas hasta el momento, para asegurar una inicialización adecuada. Stack tiene tanto una función initialize() 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 peek() 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 contenedora. Stack::Link::initialize() toma dos argumentos y los asigna a sus atributos.

Stack::initialize() asigna 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() de Link para asignar los valores apropiados a los miembros del Link. Fíjese que al puntero next se le asigna el head actual; entonces a head se le asigna el puntero al nuevo Link. De este modo se apila 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 y, finalmente, devuelve el puntero capturado. Cuando pop() elimina 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 se verá 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 éste se apilan líneas de un fichero (como punteros a cadena) en la Stack y después se des-apilan, lo que provoca que el fichero se imprima en orden inverso. Fíjese que pop() devuelve un void* que debe ser moldeado a string* antes de poderse usar. Para imprimir una cadena, se dereferencia el puntero.

A medida que se llena textlines, 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 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 este libro aprenderá más sobre este proceso de «clonación».

El nombre del fichero se toma de la línea de comandos. Para garantizar que hay suficientes argumentos en la línea de comandos, 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.

4.8.1. Resolución de ámbito global

El operador de resolución de ámbito puede ayudar en situaciones en las que el nombre elegido por defecto 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.