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 palavrasA 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ásicoPrimeiro, 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 deDiscutimos 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çõesA diferença entre definir e declarar uma função é muito óbvia.
int f();
VariáveisCom 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;
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;
Visualização de anúncioEm 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).
ExemploSuponha que tenhamos uma declaração de função (chamada protótipo) para f que recebe um objeto do tipo
Class
por valor:
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:
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 usoUma 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ãoOs 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
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çõesDepois 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 externaQuando 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;
Exemplo ruimVamos declarar uma função
f
com link externo em file.hpp e defini-la lá:
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:
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:
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.
UseUm 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:
(Um programador C ++ moderno desejará usar literais de separação: unsigned int clock_rate = 1'000'000;)
IntercomunicadorSe 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).
ExemploAqui 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:
- file1.cpp
- file2.cpp
- 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ônimosNo 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;
UseEntã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.