Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.1.3 Ámbito

§1 Sinopsis

Aunque los iremos tratando con más detalle, permitidme una breve puesta en escena de tres conceptos que son claves para entender estas cuestiones: ámbito, visibilidad y vida.

Cada identificador es introducido en el código mediante una declaración. A partir de este punto de declaración , es conocido por el compilador en una región que llamaremos ámbito; es la zona en que la declaración tiene efecto. Dentro de este ámbito no puede existir otra declaración con el mismo identificador [3].

Nota: el ámbito corresponde con una zona del fuente englobada entre llaves { },  una lista de parámetros en una función o plantilla, o el espacio de una unidad de compilación no incluido en cualquier otro ámbito.


Dentro del ámbito existen zonas en las que el identificador es visible, es decir, puede ser utilizado para designar a la misma entidad sin necesidad de un cualificador . En la práctica ocurre que cada identificador solo es visible en algunas regiones de su ámbito (que pueden ser discontinuas). El conjunto de estas regiones es su área de visibilidad ("scope").

La razón por la que un identificador deja de ser visible dentro de su ámbito es que sea eclipsado por otra declaración explícita que utiliza el mismo nombre. La nueva declaración puede ocurrir en un bloque de código anidado (en el mismo no es posible la nueva declaración), o en una clase derivada.

Nota: para determinar el "scope" de un identificador es usual referirse al ámbito potencial de su declaración. En principio su "scope" es el de su potencial, a menos que este contenga otra declaración del mismo nombre, en cuyo caso, el ámbito potencial de la nueva declaración oculta o "eclipsa" parte del potencial del primero. En ocasiones el identificador es totalmente inaccesible en estas zonas "de sombra". En otras puede ser accedido mediante un cualificador adecuado.


Como se deduce de lo anterior, las propiedades ámbito, scope y visibilidad, son atributos de un identificador en el código [2]. Observe que las dos primeras se refieren a una zona del código (un conjunto de sentencias), mientras que la visibilidad es una propiedad puntual; el estado visible/invisible del objeto puede cambiar en cada línea dentro del ámbito. El conjunto de todas en las que está visible constituye su área de visibilidad o "scope".

Ejemplo:

int x;              // declaración de x
...                 // punto de declaración de x
void main () {
   x = 10;          // Ok x está en "scope"     (1 variable x)
   int x = 11;      // nueva declaración de x   (2 variables x)
   int& x1 = x;     // referencia a la x anterior
   cout << x;       // -> 11
   cout << ::x;     // -> 10 observe el nombre cualificado
   {
     x = 12;        // nueva declaración de x   (3 variables x)
     cout << x;     // -> 12
     cout << ::x;   // -> 10
     cout << x1;    // -> 11 (único acceso a este x)
   }
   cout << x;       // -> 11                    (2 variables x)
}                   //                          (0 variables)


La vida ("Lifetime") es un atributo de tiempo de ejecución ("runtime"). Es el tiempo en que una entidad se mantiene en memoria. Es decir, desde que es creado hasta que es destruido ( 4.1.5).

Conviene recapitular que en el programa existen dos entidades distintas: un identificador, o lo que es lo mismo, un nombre conocido por el compilador (visible o invisible momentáneamente) y una zona de memoria donde está la entidad que referencia la etiqueta (el Rvalue 2.1.5); que el identificador tiene su propio ámbito y visibilidad, y que la única forma que tiene el compilador para acceder al objeto es mediante su identificador (o mediante el identificador de un objeto que lo señale -un puntero-). En estas circunstancias, al menos teóricamente, pueden suponerse diversas situaciones:


§1.1
  El identificador está en ámbito (vivo) y en "scope" (visible); la zona de memoria contiene los datos correctos. El objeto es accesible por el programa y las cosas funcionan correctamente. Por ejemplo:

int x = 3, j = 1;

x = j +10

cout << "x = " << x << endl;


§1.2
El identificador está en ámbito (existe y está vivo) pero fuera de "scope"; el almacenamiento sigue intacto. Para todos los efectos es como si los datos no existieran; puede que más tarde vuelva a estar en ámbito (vuelva a ser visible). Es la típica situación en que un identificador es ocultado (eclipsado o tapado) momentáneamente por otro del mismo nombre en un bloque más profundo. Por ejemplo:

int x = 3, j;

for (j = 0; j>10; j++) {

   int x = 0;       // oculta x anterior mientras dure el bucle

   ...

}

cout << x << endl;  // la x original vuelve a ser visible


§1.3 El identificador está en ámbito, vivo y visible, pero su zona de memoria está ocupada por otros valores no esperados. El nombre sigue siendo utilizable por el programa, pero al acceder al objeto recibimos basura. Es el caso de identificadores, generalmente punteros, descolgados ("dangling pointers"). Esta es una situación anómala, pero puede presentarse por múltiples causas. Por ejemplo, un objeto ha sido eliminado de memoria mientras que existen referencias válidas al mismo (punteros). También porque no hemos inicializado adecuadamente la variable, o porque algún puntero descontrolado ha metido datos en el sitio inadecuado. Los resultados son impredecibles. Por ejemplo, al realizar una operación con ese objeto recibimos un error de "runtime".


§1.4 El identificador está fuera de ámbito (muerto y por supuesto invisible), el compilador no puede hacer más uso de él, pero el programa no ha rehusado la zona de memoria correspondiente, no la ha vuelto a declarar zona libre. Por ejemplo, porque el programador ha olvidándose el destructor de una clase o usar el operador delete antes de salir de una función. El resultado es que la memoria sigue conservando los datos inútilmente. Es la típica situación de pérdida de memoria por el programa. Es un error de programación típico en sistemas que no disponen de un recolector automático de basura, como es el caso de C++. También la causa de que aparezcan lenguajes como Java, que sí disponen de esta característica.

§2 Clases de ámbito

En C++ hay siete categorías de ámbitos:  De sentencia; de bloque (o local); de función; de prototipo de función; de fichero; de clase y de espacio de nombres.  El ámbito depende de como y donde es declarado el identificador.

Aparte del ámbito de Clase (que no existe en C) las reglas de ámbito para C++ son las mismas que en C, con la salvedad que, a diferencia de este, C++ permite que la declaración de datos y funciones aparezca en cualquier sitio en que pueda aparecer una sentencia. Esta especial flexibilidad implica que deba prestarse especial atención cuando se interpreten cuestiones tales como "punto de declaración" y “enclosing scope”. Por ejemplo, las siguientes declaraciones son correctas en C++ pero no en C.

void main(void) {

   int i = 100;

   cout << "Es el numero " << i << endl;

   char ch = 'A';

   cout << "Es la letra " << ch << endl;

}

Para ser compilado como C tendrían que haberse declarado las variables antes que ninguna ejecución de función. Por ejemplo:

void main(void) {

   int i = 100;

   char ch = 'A';

   cout << "Es el numero " << i << endl;

   cout << "Es la letra " << ch << endl;

}

§2.1  Ámbito de Sentencia

C++ soporta declaraciones en expresiones condicionales; pueden declararse variables dentro de las expresiones de las sentencias for, if, while y switch; entonces el ámbito de las variables es el de la sentencia. En el caso de if el ámbito incluye también el bloque else. Ejemplo:

...

for (j = 0; j>10; j++) { // comienza el ámbito de j

  int x = 0;             // comienza el ámbito de x

   ...

}                         // termina el ámbito de j x (ver nota)

    ...

Nota: C++Builder incluye la opción de la opción -Vd de compilación que permite modificar el ámbito de las variables declaradas dentro de las sentencias for ( 4.10.3).

§2.2  Ámbito de Bloque

El ámbito de un identificador con ámbito local (o de bloque), empieza en el punto de declaración y termina al final del bloque que contiene la declaración (el denominado bloque contenedor). Ejemplo:

...

{

  ...

  char c = 'c';   // comienza el ámbito de c

  int x = 0;      // comienza el ámbito de x

  ...

}                 // termina el ámbito de c x

...


El ámbito de los parámetros declarados en la definición de una función es el del bloque que define dicha función. Ejemplo:

...

int func (int x, int y) {  // comienza el ámbito de x y

   ...

   int y = 12;             // Error!! declaración duplicada

   return (x + y);

}                          // termina el ámbito de x y

...

§2.3 Ámbito de Función

Los únicos identificadores que tienen ámbito de función son las etiquetas de goto ( 4.10.1), razón por la cual sus nombres deben ser únicos en la función. Su ámbito es el de la función que las contiene, de forma que pueden ser utilizados por las sentencias goto en cualquier punto de la función en que se han declarado.

Los identificadores de función tienen enlazado externo ( 1.4.4), lo que significa que pertenecen al ámbito global (el mismo para todas). Es decir, pueden ser referenciadas desde cualquier punto del fichero, incluso desde otras funciones, incluyendo main(), o desde ellas mismas (recursión), pero el bloque de código que engloba el cuerpo de cada función, incluyendo sus variables, es un espacio oculto, no puede ser accedido directamente desde su exterior. Por esta razón no es posible, por ejemplo, realizar un salto goto a una etiqueta en otra función. La única manera de acceder a una función es mediante una llamada a la misma siguiendo el formato específico definido en su prototipo. El único valor que se puede manejar directamente es el que devuelve y aún así, no es el valor original, sino una copia modelada de este (ver la sentencia return 4.4.7).

Los nombres contenidos en la lista de parámetros formales de una función pertenecen al ámbito del bloque más externo de la función (el que define el cuerpo de la función).

  Una consecuencia de que todas las funciones comparten el mismo ámbito global es que no puedan declararse funciones dentro de funciones.

Nota: las cosas eran como se han descrito hasta la introducción en el lenguaje del mecanismo de espacio de nombres ( 4.1.11), momento desde el cual, C++ permite la existencia de funciones fuera del espacio global [1]. Además, las clases funcionan como auténticos subespacios de nombres ( 4.1.11c1), por lo que también pueden declararse funciones dentro de ellas (las funciones-miembro) que no pertenecen por tanto al espacio global.

§2.4  Ámbito de Prototipo

Los nombres declarados en la lista de parámetros de un prototipo de función (que no sea parte de una declaración) tienen ámbito reducido al prototipo. En realidad estos nombres solo son utilizados para el posible anuncio por el compilador de errores o advertencias sobre el prototipo que se declara.

§2.5  Ámbito de Fichero

Los identificadores con ámbito de fichero, son llamados también globales o externos. Son declarados fuera de cualquier bloque, clase o función. Su ámbito abarca desde el punto de declaración hasta el final del fichero (por esta razón se suelen declarar al principio del fichero, justo después de las directivas # de preproceso).

§2.6 Ámbito de Clase

Una clase ( 4.11) es una colección de elementos (miembros) junto con las operaciones que se realizan con ellos. El término ámbito de clase se aplica a los nombres de los miembros de una clase particular. Las clases y sus miembros tienen reglas de acceso y de ámbito muy especiales.


El nombre N de un miembro de una clase C tiene ámbito “local a C”, y puede ser utilizado solo en las siguientes situaciones:

  • En funciones miembro (métodos) de C.
  • En expresiones tales como c.N, donde c es un objeto de C (Selector directo de miembro 4.9.16)
  • En expresiones tales como cptr->N, donde cptr es un puntero a una instancia de C (Selector indirecto de miembro 4.9.16)
  • En expresiones tales como C::N o D::N, donde D es una clase derivada de C ().
  • En referencias anticipadas de miembros dentro de la clase.

Recuerde que los nombres de funciones declaradas amigas (friend 4.11.2a) de C no son miembros de C; sus nombres simplemente tienen ámbito de la clase C.

§2.7 Ámbito de espacio de nombres

El espacio de nombre es el ámbito en el que un identificador debe ser único. A este respecto, C usa cuatro clases distintas de identificadores:

  • Nombres de etiquetas goto. Deben ser únicas dentro de la función en que se han declarado (el goto tiene ámbito de función).
  • Nombres estructuras, uniones y enumeraciones. Deben ser únicas dentro del bloque en que se han definido. Las etiquetas definidas fuera de cualquier función deben ser únicas (ya que son globales al fichero).
  • Nombres de miembros de estructuras y uniones. Deben ser únicos dentro de la estructura o unión en que se han definido. No existe restricción en el tipo de miembros del mismo nombre en diferentes estructuras.
  • Variables, funciones, typedef y enumeradores. Deben ser únicos dentro del ámbito en que han sido definidos. Los identificadores declarados externos deben ser únicos entre las variables declaradas externas.

C++ tiene una palabra clave: namespace ( 4.1.11), que es en realidad un recurso para manejar los identificadores. Permite dividir el espacio total de nombres en regiones distintas e independientes respecto a los identificadores.

Los objetos definidos en el subespacio raíz tienen ámbito de todo el programa (de la aplicación) siempre que se hayan definido como extern en el resto de los módulos. A su vez los compiladores utilizan una serie de variables y tipos globales a la aplicación cuyos nombres predefinidos que son incluidas automáticamente en cualquier programa C++ para usos varios, como fechas, horas, etc ( 4.1.3a).

§3 Ocultación

Un nombre puede ser ocultado por una declaración explícita del mimo nombre en un bloque más profundo o en una clase. Ejemplo:

int x = 3, j;

for (j = 0; j>10; j++) {

  int x = 0;        // oculta al anterior

  ...

}

cout << x << endl;  // la x original vuelve a ser visible


Los parámetros formales de las funciones ocultan cualquier otra variable o función externas del mismo nombre. Por ejemplo:

int x, y;           // espacio global

func(double x, double y) {

   ...              // x e y globales no son visibles aquí

}

§3.1 Acceso cualificado:

El miembro oculto m de una clase CL es todavía accesible utilizando el operador de acceso a ámbito :: ( 4.9.19) con un nombre de clase: CL::m.

Un nombre de ámbito global (de fichero) oculto, puede ser todavía referenciado utilizando el operador ::. Ejemplo:

#include <iostream>
using namespace std;

int x = 1;               // x-global
int main() {             // ==============
   cout << "1. x = " << x << endl;
   x = 2;                // se refiere a x-global
   cout << "2. x = " << x << endl;
   int x = 4;            // Nueva x (x-de-main) oculta a la anterior
   for (int j = 0; j<1; j++) {
      int x = 3;         // Nueva x (x-de-for) oculta a la anterior
      cout << "3. x = " << x << endl;
      ::x = 5;           // se refiere a x-global
   }
   cout << "4. x = " << x << endl;
   cout << "5. x = " << ::x << endl;
}

Salida:

1. x = 1
2. x = 2
3. x = 3
4. x = 4
5. x = 5


§3.1a Un nombre de clase puede ser ocultado por el nombre de un objeto, función o enumerador declarado dentro de su ámbito, con independencia del orden en que se hubiesen declarado los nombres. Aunque la clase oculta puede ser todavía accesible precediendo su identificador con la palabra clave apropiada: class, estruct o union.

Ejemplo:

class C { .... };

int main() {    // =============
   int C;
   C c;         // Error clase C no definida (oculta por int C)

   class C c;   // Ok. compila sin dificultad
}

§4 Punto de declaración

A todos estos efectos, el punto de declaración de un nombre x es inmediatamente después de su declaración completa, pero antes de su inicializador si es que existe alguno.

§5 Acceso a entidades

Cuando el compilador encuentra en el código la utilización de un identificador, intenta relacionarlo con alguna declaración previa de dicho nombre. Este proceso es conocido como búsqueda de nombre ("Name-lookup").  El proceso puede asociar más de una declaración con un nombre si este corresponde a una función (funciones sobrecargadas); en este caso, la selección de la definición adecuada sigue al "name-lookup" en un proceso conocido como resolución de sobrecarga. Ver en la hoja adjunta una somera descripción del proceso ( Name-lookup).

  Inicio.


[1] El sistema de subespacios de nombres es una adición posterior a las primeras versiones del lenguaje.

[2] El ámbito también es denominado ámbito léxico en relación a que es una propiedad del código y no de runtime.

[3] En caso de funciones se permite que existan dos declaraciones con el mismo identificador (nombre), pero todas deben tener distinta firma ("Signature").