Enlace interno y externo en C ++

Buen dia a todos!

Le presentamos la traducción de un artículo interesante que ha sido preparado para usted como parte del curso "Desarrollador C ++" . Esperamos que sea útil e interesante para usted, así como para nuestros oyentes.

Vamos

¿Alguna vez ha encontrado los términos comunicación interna y externa? ¿Quiere saber para qué se usa la palabra clave externa o cómo la declaración de algo estático afecta el alcance global? Entonces este artículo es para ti.

En pocas palabras

La unidad de traducción (.c / .cpp) y todos sus archivos de encabezado (.h / .hpp) están incluidos en la unidad de traducción. Si un objeto o función tiene un enlace interno dentro de una unidad de traducción, este símbolo es visible para el enlazador solo dentro de esta unidad de traducción. Si el objeto o función tiene un enlace externo, el enlazador podrá verlo cuando procese otras unidades de traducción. El uso de la palabra clave estática en el espacio de nombres global proporciona el carácter de enlace interno. La palabra clave externa da enlace externo.
El compilador predeterminado proporciona a los personajes los siguientes enlaces:

  • Variables globales no constantes: enlace externo;
  • Const variables globales - enlace interno;
  • Funciones - Enlace externo.



Los fundamentos

Primero, hablemos de dos conceptos simples necesarios para discutir el enlace.

  • La diferencia entre una declaración y una definición;
  • Unidades de difusión.

También preste atención a los nombres: utilizaremos el concepto de "símbolo" cuando se trata de cualquier "entidad de código" con la que trabaje el enlazador, por ejemplo, con una variable o función (o con clases / estructuras, pero no nos centraremos en ellas).

Anuncio VS. Definición

Discutimos brevemente la diferencia entre una declaración y una definición de símbolo: un anuncio (o declaración) le dice al compilador sobre la existencia de un símbolo específico y permite el acceso a este símbolo en casos que no requieren una dirección de memoria exacta o almacenamiento de símbolos. La definición le dice al compilador qué está contenido en el cuerpo de la función o cuánta memoria necesita asignar la variable.

En algunas situaciones, una declaración no es suficiente para el compilador, por ejemplo, cuando un elemento de datos de clase tiene una referencia o un tipo de valor (es decir, no una referencia y no un puntero). Al mismo tiempo, se permite un puntero a un tipo declarado (pero indefinido), ya que necesita una cantidad fija de memoria (por ejemplo, 8 bytes en sistemas de 64 bits), independientemente del tipo al que apunta. Para obtener el valor de este puntero, se requiere una definición. Además, para declarar una función, debe declarar (pero no definir) todos los parámetros (ya sea que se tomen por valor, referencia o puntero) y el tipo de retorno. Determinar el tipo de valor de retorno y los parámetros solo es necesario para definir una función.

Las funciones

La diferencia entre definir y declarar una función es muy obvia.

int f(); //  int f() { return 42; } //  

Variables

Con las variables, es un poco diferente. La declaración y la definición generalmente no se comparten. Lo principal es:

 int x; 

No solo declara x , sino que también lo define. Esto se debe a la llamada al constructor predeterminado int. (En C ++, a diferencia de Java, el constructor de tipos simples (como int) no inicializa el valor a 0. De forma predeterminada. En el ejemplo anterior, x será igual a cualquier basura que se encuentre en la dirección de memoria asignada por el compilador).

Pero puede separar explícitamente la declaración de variable y su definición utilizando la palabra clave extern .

 extern int x; //  int x = 42; //  

Sin embargo, al inicializar y agregar extern a la declaración, la expresión se convierte en una definición y la palabra clave extern vuelve inútil.

 extern int x = 5; //   ,   int x = 5; 

Vista previa del anuncio

En C ++, existe el concepto de predeclarar un personaje. Esto significa que declaramos el tipo y el nombre del símbolo para su uso en situaciones que no requieren su definición. Por lo tanto, no necesitamos incluir la definición completa de un personaje (generalmente un archivo de encabezado) sin una necesidad obvia. Por lo tanto, reducimos la dependencia del archivo que contiene la definición. La principal ventaja es que al cambiar un archivo con una definición, el archivo donde declaramos preliminarmente este símbolo no requiere una nueva compilación (lo que significa que todos los demás archivos lo incluyen).

Ejemplo

Supongamos que tenemos una declaración de función (llamada prototipo) para f que toma un objeto de tipo Class por valor:

 // file.hpp void f(Class object); 

Inmediatamente incluya la definición de Class - ingenua. Pero como acabamos de declarar f , es suficiente para darle al compilador una declaración de Class . Por lo tanto, el compilador puede reconocer la función por su prototipo, y podemos deshacernos de la dependencia de file.hpp en el archivo que contiene la definición de Class , digamos class.hpp:

 // file.hpp class Class; void f(Class object); 

Digamos que file.hpp está contenido en otros 100 archivos. Y digamos que cambiamos la definición de Clase en class.hpp. Si agrega class.hpp a file.hpp, file.hpp y los 100 archivos que lo contengan deberán volver a compilarse. Gracias a la declaración preliminar de Clase, los únicos archivos que requieren recompilación serán class.hpp y file.hpp (suponiendo que f esté definido allí).

Frecuencia de uso

Una diferencia importante entre una declaración y una definición es que un símbolo se puede declarar muchas veces, pero solo se puede definir una vez. Por lo tanto, puede declarar previamente una función o clase tantas veces como desee, pero solo puede haber una definición. Esto se llama la regla de una definición . En C ++, lo siguiente funciona:

 int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; } 

Y esto no funciona:

 int f() { return 6; } int f() { return 9; } 

Unidades de difusión

Los programadores suelen trabajar con archivos de encabezado y archivos de implementación. Pero no compiladores: trabajan con unidades de traducción (unidades de traducción, para abreviar, TU), que a veces se denominan unidades de compilación. La definición de tal unidad es bastante simple: cualquier archivo transferido al compilador después de su procesamiento preliminar. Para ser precisos, este es un archivo resultante del trabajo de un preprocesador de macro de extensión que incluye código fuente, que depende de las expresiones #ifdef y #ifndef , y copia y pega todos los archivos #include .

Los siguientes archivos están disponibles:

header.hpp:

 #ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */ 

program.cpp:

 #include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; } 

El preprocesador producirá la siguiente unidad de traducción, que luego se pasa al compilador:

 int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; } 

Comunicaciones

Después de discutir los conceptos básicos, puede comenzar la relación. En general, la comunicación es la visibilidad de los caracteres para el enlazador al procesar archivos. La comunicación puede ser externa o interna.

Comunicación externa

Cuando un símbolo (variable o función) tiene una conexión externa, se hace visible para los vinculadores de otros archivos, es decir, "globalmente" visible, accesible para todas las unidades de traducción. Esto significa que debe definir dicho símbolo en un lugar específico de una unidad de traducción, generalmente en el archivo de implementación (.c / .cpp), de modo que solo tenga una definición visible. Si intenta definir simultáneamente el símbolo al mismo tiempo que se declara el símbolo, o si coloca la definición en un archivo para la declaración, corre el riesgo de enojar al vinculador. Intentar agregar un archivo a más de un archivo de implementación lleva a agregar una definición a más de una unidad de traducción; su enlazador llorará.

La palabra clave externa en C y C ++ (explícitamente) declara que un personaje tiene una conexión externa.

 extern int x; extern void f(const std::string& argument); 

Ambos personajes tienen una conexión externa. Se señaló anteriormente que las variables globales const tienen enlace interno por defecto, las variables globales sin const tienen enlace externo. Esto significa que int x; - igual que extern int x; ¿verdad? En realidad no int x; en realidad análogo a extern int x {}; (usando la sintaxis de inicialización universal / de paréntesis para evitar el análisis más desagradable (el análisis más irritante)), ya que int x; no solo declara, sino que también define x. Por lo tanto, no agregue extern a int x; globalmente es tan malo como definir una variable al declararla externa:

 int x; //   ,   extern int x{}; //      . extern int x; //      ,   

Mal ejemplo

Declaremos una función f con enlace externo en file.hpp y definamos allí:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* ... */ int f(int) { return x + 1; } /* ... */ #endif /* FILE_HPP */ 

Tenga en cuenta que no necesita agregar extern aquí, ya que todas las funciones son explícitamente externas. La separación de la declaración y la definición tampoco es necesaria. Así que vamos a reescribirlo así:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */ 

Dicho código podría escribirse antes de leer este artículo, o después de leerlo bajo la influencia del alcohol o sustancias pesadas (por ejemplo, rollos de canela).

Veamos por qué esto no vale la pena. Ahora tenemos dos archivos de implementación: a.cpp y b.cpp, ambos incluidos en file.hpp:

 // a.cpp #include "file.hpp" /* ... */ 


 // b.cpp #include "file.hpp" /* ... */ 

Ahora deje que el compilador funcione y genere dos unidades de traducción para los dos archivos de implementación anteriores (recuerde que #include literalmente significa copiar / pegar):

 // TU A, from a.cpp int f(int) { return x + 1; } /* ... */ 

 // TU B, from b.cpp int f(int) { return x + 1; } /* ... */ 

En este punto, el vinculador interviene (la unión se produce después de la compilación). El enlazador toma el carácter f y busca una definición. ¡Hoy tiene suerte, encuentra hasta dos! Uno en la unidad de traducción A, el otro en B. El enlazador se congela de felicidad y te dice algo como esto:

 duplicate symbol __Z1fv in: /path/to/ao /path/to/bo 

El enlazador encuentra dos definiciones para un carácter f . Como f tiene un enlace externo, es visible para el enlazador cuando procesa tanto A como B. Obviamente, esto viola la Regla de una definición y causa un error. Más precisamente, esto causa un error de símbolo duplicado, que recibirá no menos que un error de símbolo indefinido que ocurre cuando declara un símbolo, pero se olvidó de definirlo.

Uso

Un ejemplo estándar de declarar variables externas son las variables globales. Supongamos que está trabajando en un pastel de cocción automática. Seguramente hay variables globales asociadas con el pastel que deberían estar disponibles en diferentes partes de su programa. Digamos la frecuencia de reloj de un circuito comestible dentro de su pastel. Este valor se requiere naturalmente en diferentes partes para el funcionamiento sincrónico de todos los productos electrónicos de chocolate. La forma C (malvada) de declarar dicha variable global es como una macro:

 #define CLK 1000000 

Un programador de C ++ que está disgustado con las macros escribirá mejor el código real. Por ejemplo, esto:

 // global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; } 

(Un programador moderno de C ++ querrá usar literales de separación: unsigned int clock_rate = 1'000'000;)

Intercomunicador

Si el símbolo tiene una conexión interna, será visible solo dentro de la unidad de traducción actual. No confunda visibilidad con derechos de acceso, como privado. La visibilidad significa que el enlazador podrá usar este símbolo solo cuando procese la unidad de traducción en la que se declaró el símbolo, y no más tarde (como en el caso de símbolos con comunicación externa). En la práctica, esto significa que al declarar un símbolo con un enlace interno en el archivo de encabezado, cada unidad de transmisión que incluye este archivo recibirá una copia única de este símbolo. Como si hubiera predeterminado cada uno de esos símbolos en cada unidad de traducción. Para los objetos, esto significa que el compilador literalmente asignará una copia completamente nueva y única para cada unidad de traducción, lo que, obviamente, puede generar altos costos de memoria.

Para declarar un símbolo interconectado, la palabra clave estática existe en C y C ++. Este uso es diferente del uso de static en clases y funciones (o, en general, en cualquier bloque).

Ejemplo

Aquí hay un ejemplo:

header.hpp:

 static int variable = 42; 

file1.hpp:

 void function1(); 

file2.hpp:

 void function2(); 

file1.cpp:

 #include "header.hpp" void function1() { variable = 10; } 


file2.cpp:

 #include "header.hpp" void function2() { variable = 123; } 

main.cpp:

 #include "header.hpp" #include "file1.hpp" #include "file2.hpp" #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; } 

Cada unidad de traducción, incluido header.hpp, obtiene una copia única de la variable, debido a su conexión interna. Hay tres unidades de traducción:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

Cuando se llama a function1, una copia de la variable file1.cpp obtiene el valor 10. Cuando se llama a function2, una copia de la variable file2.cpp obtiene el valor 123. Sin embargo, el valor que se devuelve en main.cpp no ​​cambia y permanece igual a 42.

Espacios de nombres anónimos

En C ++, hay otra forma de declarar uno o más caracteres vinculados internamente: espacios de nombres anónimos. Tal espacio asegura que los caracteres declarados dentro de él sean visibles solo en la unidad de traducción actual. Esencialmente, esta es solo una forma de declarar múltiples caracteres estáticos. Durante un tiempo, se abandonó el uso de la palabra clave estática para declarar un carácter vinculado internamente a favor de espacios de nombres anónimos. Sin embargo, nuevamente comenzaron a usarlo debido a la conveniencia de declarar una variable o función con comunicación interna. Hay algunas otras diferencias menores en las que no me detendré.

En cualquier caso, esto es:

 namespace { int variable = 0; } 

Hace (casi) lo mismo que:

 static int variable = 0; 

Uso

Entonces, ¿en qué casos usar conexiones internas? Usarlos para objetos es una mala idea. El consumo de memoria de objetos grandes puede ser muy alto debido a la copia para cada unidad de traducción. Pero básicamente, solo causa un comportamiento extraño e impredecible. Imagine que tiene un singleton (una clase en la que crea una instancia de solo una instancia) y de repente aparecen varias instancias de su "singleton" (una para cada unidad de traducción).

Sin embargo, la comunicación interna se puede utilizar para ocultar la unidad de traducción del área global de las funciones auxiliares locales. Supongamos que hay una función auxiliar foo en file1.hpp que usa en file1.cpp. Al mismo tiempo, tiene la función foo en file2.hpp utilizada en file2.cpp. El primer y el segundo foo son diferentes entre sí, pero no se te ocurren otros nombres. Por lo tanto, puede declararlos estáticos. Si no agrega tanto file1.hpp como file2.hpp a la misma unidad de traducción, esto ocultará foo el uno del otro. Si esto no se hace, entonces implícitamente tendrán una conexión externa y la definición del primer foo encontrará la definición del segundo, causando que el enlazador viole la regla de una definición.

El fin

Siempre puede dejar sus comentarios y / o preguntas aquí o visitarnos en un día abierto.

Source: https://habr.com/ru/post/es432834/


All Articles