Vinculação interna e externa em C ++

Bom dia a todos!

Apresentamos a você a tradução de um artigo interessante que foi preparado para você como parte do curso "Desenvolvedor C ++" . Esperamos que seja útil e interessante para você e para nossos ouvintes.

Vamos lá

Você já encontrou os termos comunicação interna e externa? Deseja saber para que serve a palavra-chave extern ou como a declaração de algo estático afeta o escopo global? Então este artigo é para você.

Em poucas palavras

A unidade de conversão (.c / .cpp) e todos os seus arquivos de cabeçalho (.h / .hpp) estão incluídos na unidade de conversão. Se um objeto ou função tiver uma ligação interna dentro de uma unidade de tradução, esse símbolo será visível para o vinculador somente dentro dessa unidade de tradução. Se o objeto ou função tiver um link externo, o vinculador poderá vê-lo ao processar outras unidades de conversão. O uso da palavra-chave estática no espaço para nome global fornece a ligação interna do caractere. A palavra-chave extern fornece ligação externa.
O compilador padrão fornece aos caracteres as seguintes ligações:

  • Variáveis ​​globais não constantes - ligação externa;
  • Const variáveis ​​globais - ligação interna;
  • Funções - Link externo.



O básico

Primeiro, vamos falar sobre dois conceitos simples necessários para discutir a ligação.

  • A diferença entre uma declaração e uma definição;
  • Unidades de transmissão.

Também preste atenção aos nomes: usaremos o conceito de “símbolo” quando se trata de qualquer “entidade de código” com a qual o vinculador trabalha, por exemplo, com uma variável ou função (ou com classes / estruturas, mas não iremos focar neles).

Anúncio VS. Definição de

Discutimos brevemente a diferença entre uma declaração e uma definição de símbolo: um anúncio (ou declaração) informa o compilador sobre a existência de um símbolo específico e permite o acesso a esse símbolo nos casos que não exigem um endereço de memória exato ou armazenamento de símbolos. A definição informa ao compilador o que está contido no corpo da função ou quanta memória a variável precisa alocar.

Em algumas situações, a declaração não é suficiente para o compilador, por exemplo, quando o elemento de dados da classe possui um link ou tipo de valor (ou seja, não é um link e não um ponteiro). Ao mesmo tempo, é permitido um ponteiro para um tipo declarado (mas indefinido), pois ele precisa de uma quantidade fixa de memória (por exemplo, 8 bytes em sistemas de 64 bits), independentemente do tipo para o qual aponta. Para obter o valor desse ponteiro, é necessária uma definição. Além disso, para declarar uma função, você precisa declarar (mas não definir) todos os parâmetros (sejam eles tomados por valor, referência ou ponteiro) e o tipo de retorno. Determinar o tipo de valor de retorno e parâmetros é necessário apenas para definir uma função.

Funções

A diferença entre definir e declarar uma função é muito óbvia.

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

Variáveis

Com variáveis, é um pouco diferente. Declaração e definição geralmente não são compartilhadas. O principal é:

 int x; 

Não apenas declara x , mas também o define. Isso ocorre devido à chamada para o construtor padrão int. (Em C ++, diferentemente de Java, o construtor de tipos simples (como int) por padrão não inicializa o valor como 0. No exemplo acima, x será igual a qualquer lixo localizado no endereço de memória alocado pelo compilador).

Mas você pode separar explicitamente a declaração da variável e sua definição usando a palavra-chave extern .

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

No entanto, ao inicializar e adicionar extern à declaração, a expressão se transforma em uma definição e a palavra-chave extern se torna inútil.

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

Visualização de anúncio

Em C ++, existe o conceito de pré-declarar um caractere. Isso significa que declaramos o tipo e o nome do símbolo para uso em situações que não exigem sua definição. Portanto, não precisamos incluir a definição completa de um caractere (geralmente um arquivo de cabeçalho) sem uma necessidade óbvia. Assim, reduzimos a dependência do arquivo que contém a definição. A principal vantagem é que, ao alterar um arquivo com uma definição, o arquivo em que declaramos preliminarmente esse símbolo não requer recompilação (o que significa que todos os outros arquivos, incluindo ele).

Exemplo

Suponha que tenhamos uma declaração de função (chamada protótipo) para f que recebe um objeto do tipo Class por valor:

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

Inclua imediatamente a definição de ingênuo de Class . Mas como acabamos de declarar f , basta dar uma declaração de Class ao compilador. Assim, o compilador poderá reconhecer a função por seu protótipo e poderemos nos livrar da dependência de file.hpp no ​​arquivo que contém a definição de Class , digamos class.hpp:

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

Digamos que file.hpp esteja contido em 100 outros arquivos. E digamos que alteramos a definição de Class em class.hpp. Se você adicionar class.hpp ao arquivo.hpp, o arquivo.hpp e todos os 100 arquivos que o contêm precisarão ser recompilados. Graças à declaração preliminar de Class, os únicos arquivos que requerem recompilação serão class.hpp e file.hpp (assumindo que f está definido lá).

Frequência de uso

Uma diferença importante entre uma declaração e uma definição é que um símbolo pode ser declarado muitas vezes, mas definido apenas uma vez. Portanto, você pode pré-declarar uma função ou classe quantas vezes quiser, mas só pode haver uma definição. Isso é chamado de regra de uma definição . No C ++, o seguinte funciona:

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

E isso não funciona:

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

Unidades de transmissão

Os programadores geralmente trabalham com arquivos de cabeçalho e arquivos de implementação. Mas não compiladores - eles trabalham com unidades de tradução (unidades de tradução, para abreviar - TU), que às vezes são chamadas de unidades de compilação. A definição de uma unidade desse tipo é bastante simples - qualquer arquivo transferido para o compilador após seu processamento preliminar. Para ser preciso, esse é o arquivo obtido como resultado do trabalho do pré-processador de macro de extensão, incluindo o código-fonte, que depende das expressões #ifdef e #ifndef , e copiar e colar todos os arquivos #include .

Os seguintes arquivos estão disponíveis:

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; } 

O pré-processador produzirá a seguinte unidade de conversão, que é então passada para o compilador:

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

Comunicações

Depois de discutir o básico, você pode iniciar o relacionamento. Em geral, a comunicação é a visibilidade dos caracteres para o vinculador ao processar arquivos. A comunicação pode ser externa ou interna.

Comunicação externa

Quando um símbolo (variável ou função) possui uma conexão externa, ele fica visível para os vinculadores de outros arquivos, ou seja, “globalmente” visível, acessível a todas as unidades de tradução. Isso significa que você deve definir esse símbolo em um local específico de uma unidade de conversão, geralmente no arquivo de implementação (.c / .cpp), para que ele tenha apenas uma definição visível. Se você tentar definir simultaneamente o símbolo ao mesmo tempo que o símbolo for declarado, ou se você colocar a definição em um arquivo para a declaração, corre o risco de irritar o vinculador. Tentar adicionar um arquivo a mais de um arquivo de implementação leva a adicionar uma definição a mais de uma unidade de tradução - seu vinculador irá chorar.

A palavra-chave externa em C e C ++ (explicitamente) declara que um caractere tem uma conexão externa.

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

Ambos os caracteres têm uma conexão externa. Observou-se acima que variáveis ​​globais const têm ligação interna por padrão, variáveis ​​globais não const têm ligação externa. Isso significa que int x; - o mesmo que extern int x; certo? Na verdade não. int x; realmente análogo ao extern int x {}; (usando a sintaxe de inicialização universal / colchete para evitar a análise mais desagradável (a análise mais irritante)), desde int x; não apenas declara, mas também define x. Portanto, não adicione extern ao int x; globalmente é tão ruim quanto definir uma variável ao declará-la externamente:

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

Exemplo ruim

Vamos declarar uma função f com link externo em file.hpp e defini-la lá:

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

Observe que você não precisa adicionar extern aqui, pois todas as funções são explicitamente externas. A separação de declaração e definição também não é necessária. Então, vamos reescrevê-lo assim:

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

Esse código pode ser escrito antes da leitura deste artigo ou após a leitura sob a influência de álcool ou substâncias pesadas (por exemplo, rolos de canela).

Vamos ver por que isso não vale a pena. Agora temos dois arquivos de implementação: a.cpp e b.cpp, ambos incluídos no arquivo.hpp:

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


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

Agora deixe o compilador trabalhar e gere duas unidades de conversão para os dois arquivos de implementação acima (lembre-se de que #include significa literalmente copiar / colar):

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

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

Nesse ponto, o vinculador intervém (a ligação ocorre após a compilação). O vinculador pega o caractere f e procura uma definição. Hoje ele tem sorte, ele encontra até dois! Um na unidade de tradução A e o outro em B. O vinculador congela de felicidade e diz algo assim:

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

O vinculador encontra duas definições para um caractere f . Como f possui uma ligação externa, é visível para o vinculador ao processar A e B. Obviamente, isso viola a regra de uma definição e causa um erro. Mais precisamente, isso causa um erro de símbolo duplicado, que você receberá não menos que um erro de símbolo indefinido que ocorre quando você declara um símbolo, mas esqueceu de defini-lo.

Use

Um exemplo padrão de declaração de variáveis ​​externas são as variáveis ​​globais. Suponha que você esteja trabalhando em um bolo auto-assado. Certamente, existem variáveis ​​globais associadas ao bolo que devem estar disponíveis em diferentes partes do seu programa. Digamos a frequência do relógio de um circuito comestível dentro do seu bolo. Esse valor é naturalmente exigido em diferentes partes para a operação síncrona de todos os eletrônicos de chocolate. A maneira C (má) de declarar uma variável global é como uma macro:

 #define CLK 1000000 

Um programador de C ++ que tenha nojo de macros escreverá melhor o código real. Por exemplo, isto:

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

(Um programador C ++ moderno desejará usar literais de separação: unsigned int clock_rate = 1'000'000;)

Intercomunicador

Se o símbolo tiver uma conexão interna, será visível apenas dentro da unidade de tradução atual. Não confunda visibilidade com direitos de acesso, como privado. Visibilidade significa que o vinculador poderá usar esse símbolo apenas ao processar a unidade de conversão na qual o símbolo foi declarado e não posteriormente (como no caso de símbolos com comunicação externa). Na prática, isso significa que, ao declarar um símbolo com um link interno no arquivo de cabeçalho, cada unidade de transmissão que inclui esse arquivo receberá uma cópia exclusiva desse símbolo. Como se você tivesse predeterminado cada um desses símbolos em cada unidade de tradução. Para objetos, isso significa que o compilador literalmente alocará uma cópia completamente nova e exclusiva para cada unidade de tradução, o que, obviamente, pode levar a altos custos de memória.

Para declarar um símbolo interconectado, a palavra-chave estática existe em C e C ++. Esse uso é diferente de usar estático em classes e funções (ou, em geral, em qualquer bloco).

Exemplo

Aqui está um exemplo:

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 unidade de tradução, incluindo header.hpp, obtém uma cópia exclusiva da variável, devido à sua conexão interna. Existem três unidades de tradução:

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

Quando a função1 é chamada, uma cópia da variável file1.cpp obtém o valor 10. Quando a função2 é chamada, uma cópia da variável file2.cpp obtém o valor 123. No entanto, o valor retornado no main.cpp não muda e permanece igual a 42.

Namespaces anônimos

No C ++, há outra maneira de declarar um ou mais caracteres vinculados internamente: namespaces anônimos. Esse espaço garante que os caracteres declarados dentro dele sejam visíveis apenas na unidade de tradução atual. Essencialmente, essa é apenas uma maneira de declarar vários caracteres estáticos. Por um tempo, o uso da palavra-chave estática para declarar um caractere vinculado interno foi abandonado em favor de espaços para nome anônimos. No entanto, eles novamente começaram a usá-lo devido à conveniência de declarar uma variável ou função com comunicação interna. Existem algumas outras pequenas diferenças nas quais não vou me debruçar.

De qualquer forma, é isso:

 namespace { int variable = 0; } 

Faz (quase) o mesmo que:

 static int variable = 0; 

Use

Então, em que casos usar conexões internas? Usá-los para objetos é uma má idéia. O consumo de memória de objetos grandes pode ser muito alto devido à cópia de cada unidade de tradução. Mas, basicamente, isso apenas causa um comportamento estranho e imprevisível. Imagine que você tenha um singleton (uma classe na qual você cria uma instância de apenas uma instância) e, de repente, várias instâncias do seu “singleton” aparecem (uma para cada unidade de tradução).

No entanto, a comunicação interna pode ser usada para ocultar a unidade de conversão da área global de funções auxiliares locais. Suponha que exista uma função foo helper no arquivo1.hpp que você usa no arquivo1.cpp. Ao mesmo tempo, você tem a função foo em file2.hpp usada em file2.cpp. O primeiro e o segundo foo são diferentes um do outro, mas você não pode criar outros nomes. Portanto, você pode declará-los estáticos. Se você não adicionar file1.hpp e file2.hpp à mesma unidade de tradução, isso ocultará um o outro. Se isso não for feito, eles terão implicitamente uma conexão externa e a definição do primeiro foo encontrará a definição do segundo, causando um erro de vinculador ao violar a regra de uma definição.

O FIM

Você sempre pode deixar seus comentários e / ou perguntas aqui ou nos visitar em um dia aberto.

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


All Articles