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.8d  extern

§1  Sinopsis

Desafortunadamente la palabra reservada extern tiene en C++ dos significados distintos (aunque emparentados) cada uno con su propia sintaxis, lo que lleva aparejado que en español la palabra externo/a tenga dos significados distintos cuando se refieren a este lenguaje:

  • Especificador de tipo de almacenamiento [1].
  • Especificador de tipo de enlazado

En este capítulo nos centramos principalmente en la utilización como especificador de almacenamiento, permitiendo que ciertas variables puedan ser globales a varias unidades de compilación [2]. Para esto, las variables se definen en el espacio global de uno de los ficheros, generalmente main ( 4.4.4) y después se declaran como externas al comienzo de los demás módulos (generalmente dentro de ficheros de cabecera).

§2  Sintaxis

extern <definición-de-objeto> ;

[extern] <prototipo-de-función> ;

Ejemplos

extern val;
extern int funcX(int, char);
int funcY(int, char);

§3  Comentario

Este especificador acompaña a declaraciones de objetos o funciones hechas fuera de cualquier función, que por tanto, son globales al fichero (no puede aplicarse a miembros de clases ni parámetros de funciones). Significa decirle al compilador algo así: "toma nota de la existencia de una entidad de tipo T con nombre N, cuya definición está en otro módulo".  Observe que T solo puede referirse a un objeto o al prototipo de una función. En el primer caso puede ser un tipo simple (preconstruido en el lenguaje) o abstracto (instancia de una clase).

Nota: el especificador static ( 4.1.8c) permite que una variable global sea conocida únicamente en el fichero en que se declara, de forma que las funciones de otros ficheros no las reconocerán ni podrán alterar su valor (viene a ser lo contrario de extern).

Existe otro especificador export ( 4.12.1b) que, en cierto sentido, viene a ser simétrico de extern y opuesto a static. Se utiliza en el sitio de la definición y viene a decirle al compilador: "Esta definición debe ser conocida también por el resto de los módulos".


§3.1  Cuando está aplicado a una variable, significa que está siendo declarada pero no definida (no se le asigna espacio de almacenamiento), por esta razón no deben añadirse iniciadores como parte de la declaración extern. Por ejemplo:

extern int number = 100;      // incorrecto
extern int mat[3]= {1,2,3};   // incorrecto

Nota:  C++ permite tales definiciones, pero avisa de la duplicidad si la variable también está definida en otro módulo.  Lo correcto en este tipo de variables es que se definan en un módulo, y se declaren extern en todos los demás.


Dentro de una declaración extern no pueden añadirse otros especificadores de clase de almacenamiento tales como auto o register:

extern auto val1;       // Error
extern register val2;   // Error
extern int val3;        // Ok.

Nota: las expresión anterior es una declaración externa, en el sentido que simplemente informan al compilador de un nombre y tipo de objeto o función.  Por el contrario, una definición externa también define el objeto o función. Es decir,  le asigna espacio de almacenamiento.

Si un identificador que ha sido declarado como externo (por ejemplo val3), es utilizado en alguna expresión (que no sea el operando de sizeof), es imprescindible que en alguna parte del programa total (en alguno de sus módulos) exista una definición externa de dicho identificador.


§3.2  La utilización del especificador extern con declaraciones de funciones es opcional. En todo caso indicaría, como siempre, que la definición de la función se encuentra en otro módulo del programa.  Ejemplo:

extern void Factorial(int n);   // Ok.
void Factorial(int n);          // Ok. equivalente


En realidad, y "por definición" ( 4.4), las funciones comparten el ámbito global del fichero y si no existe una definición concordante en el mismo módulo en que aparece la declaración (prototipo) de una función, entonces el compilador supone que el especificador extern acompaña implícitamente a la declaración,  por lo que extern es opcional (y redundante) para los prototipos de funciones ( 4.4.1).

Observe que el hecho de que una función u otro objeto tenga visibilidad global de fichero, y por tanto pueda decirse coloquialmente que "es visible desde cualquier punto del programa", no significa que esta visibilidad sea automática. Como veremos, el comportamiento del compilador está lleno de matices [3] que intentaremos explicar.

Si el enlazador encuentra una invocación a una función func (que no sea un miembro de clase) y no encuentra su definición en ese módulo, no busca automáticamente la definición en el resto de módulos o librerías. En su lugar se limita a lanzarnos un mensaje de error: Call to undefined function 'func' in function somefunc()... (aquí somefunc es la función desde la que se realizó la invocación).  Si en vez de la invocación de una función se pretende utilizar cualquier otro objeto, por ejemplo una variable x que no ha sido previamente declarada (digamos que se intenta x++;), la respuesta del compilador sería análoga:  Undefined symbol 'x' in function somefunc().... Lo anterior con independencia de que func y x estén perfectamente definidas en el espacio global de otro fichero.

La razón de estos errores es que para el compilador, los identificadores func y x que hemos pretendido usar, no tienen existencia semántica en esta unidad de compilación (dicho en otras palabras: en este contexto "no sabe de que le estamos hablando").

La forma de darle existencia semántica a ambos objetos es declararlos en cada unidad de compilación en que daban usarse (algo así com "presentarlos" formalmente 4.1.2). Entonces el compilador sabe que x es una variable de tal tipo, y que func es una función de características cuales. Observe que el hecho de que el error señale que func es una función, es una suposición del compilador, motivada en que estábamos aplicando el operador de invocación de función al referido identificador.

Una vez que el compilador sabe "quienes son" func y x,  es necesario que tengan existencia física. Que estén "realmente" en algún sitio, lo que para el compilador supone conocer su localización, y que en ese sitio esté realmente quien se dice que está. Dicho en otras palabras: que ambos objetos estén correctamente definidos.  A partir de aquí el programa puede usarlos.

Nota: si son objetos-dato, para que su utilización no cause problemas se exige una condición adicional: que el contenido sea correcto (no sea basura), lo que formalmente se traduce en decir que estén correctamente iniciados.


Si los objetos son "conocidos" en cuanto a sus características generales, pero de paradero desconocido, el compilador seguirá teniendo problemas y producirá los correspondientes mensajes de error. La forma de darles existencia física es definirlos, lo que puede y debe hacerse en un solo punto del programa (existe una regla C++ en este sentido 4.1.2 según la cual para cada entidad del programa pueden existir múltiples declaraciones, pero una sola definición). Esto obliga a definir func y x solo en uno de los módulos, lo que conduce al problema de qué hacer con el resto de unidades de compilación en las que queramos usar estos objetos.

Es precisamente en el aspecto "localización" donde intervienen los especificadores extern (y static). Como se ha señalado, anteponiendo la palabra extern a una declaración, se está indicando al compilador que las entidades están en otra unidad de compilación.  Es en este contexto en el que se puede utilizar la frase original de que los objetos de ámbito global al fichero "son visibles desde cualquier punto del programa", porque si el compilador encuentra esta indicación (y solo entonces), repasa el espacio global del resto de ficheros en busca de la definición correspondiente.

Nota:  si se desea que, a pesar de estar en el espacio global de un fichero, una entidad no sea incluida en esta búsqueda desde el resto de unidades de compilación, se debe anteponer el declarador static a su definición.


El funcionamiento descrito puede esquematizarse en los ficheros adjuntos de un mismo programa que se compilan separadamente:

// modulo1.cpp
extern int x2;    // Ok. declarada
extern int x3;    // Ok. declarada
extern int a[2];  // Ok. declarada
void f1();        // Ok. declarada
extern void f2(); // Ok. extern superfluo
 
void main() {
   ...
   x1++   // Error!! x1 indefinida
   x2++   // Error!! x2 no visible
   x3++   // Ok.
   cout << a[1];  // -> 1082130432 !?
   f1();  // Ok.
   f2();  // Error!! f2 indefinida
}
// modulo2.cpp
...
static int x2 = 31; // visibilidad local
int x3 = 13;        // visibilidad global
float a[2] = {1, 2} // visibilidad global
void f1 { x3++; }   // declarada & definida
...
void f() {
   x2++        // Ok (visible desde aquí)
}

Observe que la búsqueda en otras unidades de compilación de la definición correspondiente a una entidad declarada extern, no solo exige una concordancia en la etiqueta, también en el tipo del objeto. Es el caso de a[2] del módulo1 (matriz de int) que no concuerda con la definición proporcionada en el módulo2 de a[2] (matriz de float).  Observe que en este caso no se ha producido un error de compilación porque, en ausencia de otra definición, el compilador dispone de suficiente información para construir el objeto con la declaración proporcionada (en el módulo1), aunque no para iniciarlo adecuadamente (el espacio asignado está lleno de basura 4.1.2).

§4  extern con tipos abstractos

Como se indicó al principio, extern también puede aparecer en una declaración, precediendo al nombre de un objeto abstracto (instancia de una clase). El significado para el compilador es el de siempre: "toma nota de la existencia de una entidad de tipo C con nombre n, cuya definición está en otro módulo". Sin embargo, las especiales características de los objetos abstractos hacen que en estos casos, el comportamiento del compilador presente diferencias sutiles respecto a las declaraciones extern de tipos simples.

extern int x1;    // L.1 Ok. int es un tipo simple
extern C c1;      // L.2 Ok. C es un tipo abstracto (clase)

Estas diferencias pueden ser fuente de errores y desconcierto en el principiante que intentaremos aclarar aquí. Para entenderlas, hay que tener en cuenta que los compiladores C/C++ actuales realizan su análisis exclusivamente en base a la información contenida en el módulo que se compila (una vez completadas las modificaciones derivadas de las directivas de preproceso), y que la labor de resolver las dependencias entre los distintos módulos del ejecutable se realiza en la fase de enlazado. Con esto en mente, recordar que en ausencia de más información, ante una sentencia como L.1, el compilador conoce toda la información necesaria sobre el tipo int y en consecuencia, sobre el objeto x1, a excepción de su valor y la dirección de su almacenamiento (el primero se obtendrá en run-time, el segundo durante el enlazado).  Por contra, ante la expresión L.2 no dispone de absolutamente ninguna información, por lo que al llegar a ella generará un error:

'C' does not name a type     // GNU gcc-g++
Declaration syntax error     // Borland BC++ 5.5

Para salvar el escollo podríamos incluir una sentencia que indicara al compilador que C es una clase (un tipo particular):

extern int x1;    // L.1 Ok.
class C;          // declaración adelantada
extern C c1;      // L.2 Ok.

Lo anterior puede bastar en algunos casos, pero si existe alguna referencia posterior al objeto c1. se producirá un nuevo error:

extern int x1;    // L.1 Ok.
class C;
extern C c1;      // L.2 Ok.

void show() {
   std::cout << x1 << std::endl;     // L.3 Ok.
   std::cout << c1.a << std::endl;   // L.4 Error!!

Suponiendo que las definiciones correctas se encuentren en otros módulos, la sentencia L.3 funciona sin problema; en cambio L.4 produce un error de compilación:

invalid use of undefined type `struct C'     // GNU gcc-g++
'a' is not a member of 'C', because the type is not yet defined in function foo() // Borland BC++ 5.5

El mensaje de Borland explica de forma casi perfecta la causa del error.  Decimos "casi" porque en realidad, el compilador no necesita la definición completa de la clase, solo la estrictamente necesaria para conocer los componentes del objeto c1. En este caso, conocer que a es un miembro y su tipo.  En consecuencia, para sortear el nuevo escollo debemos incluir en el módulo la información pertinente.  Por ejemplo:

extern int x1;    // L.1 Ok.

class C {         // Definicion (parcial) de la clase
   public:
   int a;
   ...
   void foo();
};
extern C c1;      // L.2 Ok.

void show() {
   std::cout << x1 << std::endl;     // L.3 Ok.
   std::cout << c1.a << std::endl;   // L.4 Ok.


Observe que hemos sustituido la declaración adelantada (indicación escueta de que C es una clase) por una información más detallada, pero que esta no es completa. Se refiere exclusivamente a los miembros de instancia (que están presentes en cada instancia de la clase).  Las definiciones de los miembros de clase, como los métodos, no son realmente necesarios, solo sus prototipos [4]. En el ejemplo, es el caso del método foo, del que solo se incluye su declaración. Suponemos que su definición (off-line), se encuentra en otro módulo junto con la definición completa de la clase.

Nota: como veremos en el ejemplo práctico que sigue, este detalle es de suma importancia. Si una clase se utiliza en distintos módulos y se desea incluir su definición mediante un fichero de cabecera común, este no debe contener las definiciones de los métodos "off-line".  De lo contrario con algunos compiladores podrían obtenerse errores del tipo "Multiple definition of ...".


§4.1  Para ilustrar lo anterior con un ejemplo práctico, considere la siguiente aplicación distribuida en dos módulos: main.cpp y modulo1.cpp.  En ambos se hace referencia a una misma clase C , por lo que decidimos incluir su definición en un fichero de cabecera defines.h, que será incluido en ambos:

// main.cpp

#include <iostream>
#include "defines.h"

C c1(32);      // definicion
int x1 = 23;   // definicion
void show();   // declaracion (prototipo)

int main(int argc, char *argv[]) {
  show();      // Ok. en modulo1.cpp
  std::cout << "x1: = " << x1 << std::endl;
  std::cout << "c1.a = " << c1.a << std::endl;

  return EXIT_SUCCESS;
}
// defines.h

class C {
   public:
   int a;
   C(int);     // constructor off-line
   void foo(); // método off-line
};

C::C(int x=0) { a = x; }

void C::foo() {
   std::cout << "Hola mundo" << std::endl;
}
// modulo1.cpp
#include <iostream>
#include "defines.h"

extern int x1;
extern C c1;

void show() {    // definición
   std::cout << "x1: = " << x1 << std::endl;
   std::cout << "c1.a = " << c1.a << std::endl;
}
 

Aunque el mencionado conjunto compila sin dificultad con Borland BC++ 5.5, la versión gcc-g++ de MinGW produce sendos errores:

multiple definition of `C::C(int)' 
multiple definition of `C::foo()'

la razón es que las definiciones del constructor C() y del método foo() está presente en ambos módulos por la acción de la directiva include (recuerde la regla ODR de una sola definición 4.1.2) y que el mencionado compilador parece no reconocer la regla de redefinición benigna [5].

Una posible solución sería desglosar la definición de la clase entre dos ficheros de cabecera; el primero defines1.h contendría la información suficiente para las declaraciones extern de las instancias de C. El segundo, defines2.h, contendría las definiciones de todos sus métodos off-line.  Es decir, separamos la declaración de la clase de su implementación ( 4.1.2).

// main.cpp

#include <iostream>
#include "defines1.h"
#include "defines2.h"

C c1(32);      // definicion
int x1 = 23;   // definicion
void show();   // declaracion

int main(int argc, char *argv[]) {
  show();      // Ok. en modulo1.cpp
  std::cout << "x1: = " << x1 << std::endl;
  std::cout << "c1.a = " << c1.a << std::endl;

  return EXIT_SUCCESS;
}
// defines1.h
// declaración de la clase

class C {
   public:
   int a;
   C(int);     // constructor off-line
   void foo(); // método off-line
};
// modulo1.cpp
#include <iostream>
#include "defines1.h"

extern int x1;
extern C c1;

void show() {
   std::cout << "x1: = " << x1 << std::endl;
   std::cout << "c1.a = " << c1.a << std::endl;
}
// defines2.h
// implementación de la clase

C::C(int x=0) { a = x; }

void C::foo() {
   std::cout << "Hola mundo" << std::endl;
}

El fichero defines1.h sería incluido en todos los módulos que utilizaran objetos externos de tipo C (es el caso de modulo1.cpp), mientras que en el módulo que contiene la definición, se incluirían ambos (es el caso de main.cpp).

Si de todos modos se desea mantener en un solo fichero de cabecera la declaración y la implementación de la clase, cabe una segunda solución, consistente en incluir un fichero fuente auxiliar, defines.cpp y dejar que el preprocesador evite la duplicidad introduciendo una nueva directiva de guarda en el fichero defines.h:

// main.cpp

#include <iostream>
#include "defines.h"

C c1(32);      // definicion
int x1 = 23;   // definicion
void show();   // declaracion

int main(int argc, char *argv[]) {
  show();      // Ok. en modulo1.cpp
  std::cout << "x1: = " << x1 << std::endl;
  std::cout << "c1.a = " << c1.a << std::endl;

  return EXIT_SUCCESS;
}
// defines.h
// declaración & implementación

class C {
   public:
   int a;
   C(int);     // constructor off-line
   void foo(); // método off-line
};

#ifdef ALGO_QUE_NO_SEA_CONFUNDIDO
C::C(int x=0) { a = x; }

void C::foo() {
   std::cout << "Hola mundo" << std::endl;
}
#endif
// modulo1.cpp
#include <iostream>
#include "defines.h"

extern int x1;
extern C c1;

void show() {
   std::cout << "x1: = " << x1 << std::endl;
   std::cout << "c1.a = " << c1.a << std::endl;
}
// defines.cpp

#define ALGO_QUE_NO_SEA_CONFUNDIDO
#include defines.h

En estos casos, el macro identificador utilizado en el define debe elegirse de forma que no exista posibilidad de colisión con cualquier otro utilizado en algún otro módulo del proyecto; en sus librerías de apoyo, o en las que acompañan al compilador.

§4.2  extern con plantillas

La mecánica de funcionamiento expuesta para los tipos abstractos también es aplicable cuando estos son especializaciones de clases genéricas (plantillas). A continuación se muestra la sintaxis utilizada si en el ejemplo anterior C fuese una clase genérica.

// main.cpp

#include <iostream>
#include "defines.h"

C<int> c1(32); // definicion
int x1 = 23;   // definicion
void show();   // declaracion

int main(int argc, char *argv[]) {
  show();      // Ok. en modulo1.cpp
  std::cout << "x1: = " << x1 << std::endl;
  std::cout << "c1.a = " << c1.a << std::endl;

  return EXIT_SUCCESS;
}
// defines.h

template<class T> class C {
   public:
   int a;
   C(int);     // constructor off-line
   void foo(); // método off-line
};

template<class T> C<T>::C(int x=0) {
  a = x;
}

template<class T> void C<T>::foo() {
   std::cout << "Hola mundo" << std::endl;
}
// modulo1.cpp
#include <iostream>
#include "defines.h"

extern int x1;
extern C<int> c1;

void show() {    // definición
   std::cout << "x1: = " << x1 << std::endl;
   std::cout << "c1.a = " << c1.a << std::endl;
}
 

La única precaución es utilizar la declaración adecuada, que incluye el tipo de parámetro <int>, utilizado para la especialización de la clase. Como dato curioso señalaremos que en este caso, el compilador gnu-g++ no produce el error de definiciones duplicadas anteriormente señalado.


§5  El compilador C++Builder permite declaraciones posteriores de variables externas, tales como matrices, estructuras y uniones, de forma que se añada información a la contenida en la declaración previa. Ejemplo:

extern int a[];       // no especifica tamaño (decl. externa)
struct mystruct;      // no especifica miembros
...
int a[3] = {1, 2, 3}; // se especifica tamaño y se inicializa
struct mystruct {
   int i, j;
};                    // se añade declaración de miembros



§6  Existe otro uso de extern distinto del que hemos comentado (directiva de tipo de almacenamiento). A este respecto recordar que enlazado externo ( 1.4.4) y enlazado "C" tienen un sentido distinto.  Esta última, que se indica mediante extern "c", señala al "linker" que debe realizar un tipo especial de enlazado conocido como enlazado "C" ( 1.4.4), que tiene entre otras misiones prevenir que los nombres de funciones sean planchados ( 1.4.2). Ejemplo:

extern "c" void cfunc(int);
extern "C" __declspec(dllexport) double changeValue(double, bool);

  Inicio.


[1]  Recordemos que C++ dispone de los siguientes especificadores de almacenamiento: auto, register, static, extern y mutable.

[2]  Ser global a varias unidades de compilación equivale a decir que las variables deben ser globales a varios ficheros que se compilan separadamente.

[3]  En mi opinión uno de los asuntos complejos del lenguaje, que no suele estar demasiado bien explicado, y que al principio resulta más difícil de captar.

[4] El "tipo" de las funciones depende exclusivamente del tipo del valor devuelto y de sus argumentos (en su caso).  Sin embargo, al referirse a funciones, no suele hablarse de "tipo", sino de "firma" ("signature").

[5]  En descargo de este comportamiento anómalo debemos recordar que, al referirse a la mencionada regla ODR, el propio Stroustrup reconoce: "Checking against inconsistent class definitions in separate translation units is beyond the ability of most C++ implementations" [TC++PL-00] §9.2.3.