4.8. Estructuras anidadas

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.

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 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.