Como uso extern para compartilhar variáveis ​​entre arquivos de origem?

987

Eu sei que variáveis ​​globais em C às vezes têm a externpalavra - chave. O que é uma externvariável? Como é a declaração? Qual é o seu escopo?

Isso está relacionado ao compartilhamento de variáveis ​​entre arquivos de origem, mas como isso funciona com precisão? Onde eu uso extern?

Lundin
fonte

Respostas:

1751

O uso externé relevante apenas quando o programa que você está construindo consiste em vários arquivos de origem vinculados, onde algumas das variáveis ​​definidas, por exemplo, no arquivo de origem file1.cprecisam ser referenciadas em outros arquivos de origem, como file2.c.

É importante entender a diferença entre definir uma variável e declarar uma variável :

  • Uma variável é declarada quando o compilador é informado de que existe uma variável (e esse é o seu tipo); ele não aloca o armazenamento para a variável nesse ponto.

  • Uma variável é definida quando o compilador aloca o armazenamento para a variável.

Você pode declarar uma variável várias vezes (embora uma vez seja suficiente); você pode defini-lo apenas uma vez dentro de um determinado escopo. Uma definição de variável também é uma declaração, mas nem todas as declarações de variáveis ​​são definições.

Melhor maneira de declarar e definir variáveis ​​globais

A maneira limpa e confiável de declarar e definir variáveis ​​globais é usar um arquivo de cabeçalho para conter uma extern declaração da variável.

O cabeçalho é incluído no arquivo de origem que define a variável e em todos os arquivos de origem que fazem referência à variável. Para cada programa, um arquivo de origem (e apenas um arquivo de origem) define a variável. Da mesma forma, um arquivo de cabeçalho (e apenas um arquivo de cabeçalho) deve declarar a variável. O arquivo de cabeçalho é crucial; ele permite a verificação cruzada entre TUs independentes (unidades de tradução - pense em arquivos de origem) e garante consistência.

Embora existam outras maneiras de fazer isso, esse método é simples e confiável. É demonstrado por file3.h, file1.ce file2.c:

file3.h

extern int global_variable;  /* Declaration of the variable */

file1.c

#include "file3.h"  /* Declaration made available here */
#include "prog1.h"  /* Function declarations */

/* Variable defined here */
int global_variable = 37;    /* Definition checked against declaration */

int increment(void) { return global_variable++; }

file2.c

#include "file3.h"
#include "prog1.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

Essa é a melhor maneira de declarar e definir variáveis ​​globais.


Os próximos dois arquivos completam a fonte para prog1:

Os programas completos mostrados usam funções; portanto, as declarações de função foram introduzidas. Tanto o C99 quanto o C11 exigem que as funções sejam declaradas ou definidas antes de serem usadas (enquanto o C90 não o fez, por boas razões). Eu uso a palavra-chave externna frente das declarações de função nos cabeçalhos para obter consistência - para corresponder à externfrente das declarações de variável nos cabeçalhos. Muitas pessoas preferem não usar externna frente das declarações de função; o compilador não se importa - e, por fim, nem eu, desde que você seja consistente, pelo menos em um arquivo de origem.

prog1.h

extern void use_it(void);
extern int increment(void);

prog1.c

#include "file3.h"
#include "prog1.h"
#include <stdio.h>

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog1usos prog1.c, file1.c, file2.c, file3.he prog1.h.

O arquivo prog1.mké um makefile prog1apenas. Ele funcionará com a maioria das versões makeproduzidas desde a virada do milênio. Não está ligado especificamente ao GNU Make.

prog1.mk

# Minimal makefile for prog1

PROGRAM = prog1
FILES.c = prog1.c file1.c file2.c
FILES.h = prog1.h file3.h
FILES.o = ${FILES.c:.c=.o}

CC      = gcc
SFLAGS  = -std=c11
GFLAGS  = -g
OFLAGS  = -O3
WFLAG1  = -Wall
WFLAG2  = -Wextra
WFLAG3  = -Werror
WFLAG4  = -Wstrict-prototypes
WFLAG5  = -Wmissing-prototypes
WFLAGS  = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}
UFLAGS  = # Set on command line only

CFLAGS  = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}
LDFLAGS =
LDLIBS  =

all:    ${PROGRAM}

${PROGRAM}: ${FILES.o}
    ${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}

prog1.o: ${FILES.h}
file1.o: ${FILES.h}
file2.o: ${FILES.h}

# If it exists, prog1.dSYM is a directory on macOS DEBRIS = a.out core *~ *.dSYM RM_FR = rm -fr 

clean:
    ${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}


Diretrizes

Regras a serem quebradas apenas por especialistas e apenas por um bom motivo:

  • Um arquivo de cabeçalho contém apenas externdeclarações de variáveis ​​- staticdefinições de variáveis ​​nunca ou não qualificadas.

  • Para qualquer variável, apenas um arquivo de cabeçalho a declara (SPOT - Single Point of Truth).

  • Um arquivo de origem nunca contém externdeclarações de variáveis ​​- os arquivos de origem sempre incluem o cabeçalho (único) que os declara.

  • Para qualquer variável, exatamente um arquivo de origem define a variável, de preferência inicializando-a também. (Embora não seja necessário inicializar explicitamente para zero, isso não faz mal e pode fazer algum bem, porque pode haver apenas uma definição inicializada de uma variável global específica em um programa).

  • O arquivo de origem que define a variável também inclui o cabeçalho para garantir que a definição e a declaração sejam consistentes.

  • Uma função nunca deve precisar declarar uma variável usando extern.

  • Evite variáveis ​​globais sempre que possível - use funções.

O código fonte e o texto desta resposta estão disponíveis no meu repositório SOQ (Stack Overflow Questions) no GitHub no subdiretório src / so-0143-3204 .

Se você não é um programador C experiente, pode (e talvez deva) parar de ler aqui.

Maneira não tão boa de definir variáveis ​​globais

Com alguns (de fato, muitos) compiladores C, você também pode se dar bem com o que é chamado de definição "comum" de uma variável. 'Comum', aqui, refere-se a uma técnica usada no Fortran para compartilhar variáveis ​​entre arquivos de origem, usando um bloco COMMON (possivelmente nomeado). O que acontece aqui é que cada um de vários arquivos fornece uma definição provisória da variável. Desde que não mais de um arquivo forneça uma definição inicializada, os vários arquivos acabam compartilhando uma definição única comum da variável:

file10.c

#include "prog2.h"

long l; /* Do not do this in portable code */ 

void inc(void) { l++; }

file11.c

#include "prog2.h"

long l; /* Do not do this in portable code */ 

void dec(void) { l--; }

file12.c

#include "prog2.h"
#include <stdio.h>

long l = 9; /* Do not do this in portable code */ 

void put(void) { printf("l = %ld\n", l); }

Essa técnica não está em conformidade com a letra do padrão C e com a 'regra de uma definição' - é oficialmente um comportamento indefinido:

J.2 Comportamento indefinido

Um identificador com ligação externa é usado, mas no programa não existe exatamente uma definição externa para o identificador, ou o identificador não é usado e existem várias definições externas para o identificador (6.9).

§6.9 Definições externas ¶5

Uma definição externa é uma declaração externa que também é uma definição de uma função (que não seja uma definição embutida) ou de um objeto. Se um identificador declarado com ligação externa for usado em uma expressão (que não seja parte do operando de a sizeofou _Alignofoperador cujo resultado é uma constante inteira), em algum lugar de todo o programa deve haver exatamente uma definição externa para o identificador; caso contrário, não haverá mais que um. 161)

161) Assim, se um identificador declarado com ligação externa não for usado em uma expressão, não será necessário definir uma definição externa.

No entanto, a norma C também a lista no Anexo J informativo como uma das extensões comuns .

J.5.11 Múltiplas definições externas

Pode haver mais de uma definição externa para o identificador de um objeto, com ou sem o uso explícito da palavra-chave extern; se as definições discordarem ou mais de uma for inicializada, o comportamento será indefinido (6.9.2).

Como essa técnica nem sempre é suportada, é melhor evitar usá-la, principalmente se o seu código precisar ser portátil . Usando essa técnica, você também pode acabar com punições não intencionais.

Se um dos arquivos acima declarado lcomo em doublevez de como long, os vinculadores inseguros do tipo C provavelmente não detectariam a incompatibilidade. Se você estiver em uma máquina com 64 bits longe double, nem receberá um aviso; em uma máquina com 32 longe 64 bits double, você provavelmente receberá um aviso sobre os diferentes tamanhos - o vinculador usaria o maior tamanho, exatamente como um programa Fortran teria o maior tamanho de qualquer bloco comum.

Observe que o GCC 10.1.0, lançado em 07/05 2020, altera as opções de compilação padrão a serem usadas -fno-common, o que significa que, por padrão, o código acima não é mais vinculado, a menos que você substitua o padrão por -fcommon(ou use atributos, etc. - veja o link).


Os próximos dois arquivos completam a fonte para prog2:

prog2.h

extern void dec(void);
extern void put(void);
extern void inc(void);

prog2.c

#include "prog2.h"
#include <stdio.h>

int main(void)
{
    inc();
    put();
    dec();
    put();
    dec();
    put();
}
  • prog2usos prog2.c, file10.c, file11.c, file12.c, prog2.h.

Atenção

Conforme observado nos comentários aqui, e conforme declarado na minha resposta a uma pergunta semelhante , o uso de várias definições para uma variável global leva a um comportamento indefinido (J.2; §6.9), que é a maneira do padrão de dizer "qualquer coisa pode acontecer". Uma das coisas que pode acontecer é que o programa se comporte conforme o esperado; e J.5.11 diz, aproximadamente, "você pode ter mais sorte do que merece". Mas um programa que se baseia em várias definições de uma variável externa - com ou sem a palavra-chave explícita 'extern' - não é um programa estritamente conforme e não é garantido que funcione em qualquer lugar. Equivalentemente: contém um bug que pode ou não aparecer.

Violando as diretrizes

É claro que existem muitas maneiras pelas quais essas diretrizes podem ser quebradas. Ocasionalmente, pode haver um bom motivo para quebrar as diretrizes, mas essas ocasiões são extremamente incomuns.

faulty_header.h

c int some_var; /* Do not do this in a header!!! */

Nota 1: se o cabeçalho define a variável sem a externpalavra - chave, cada arquivo que inclui o cabeçalho cria uma definição provisória da variável. Como observado anteriormente, isso geralmente funciona, mas o padrão C não garante que funcione.

broken_header.h

c int some_var = 13; /* Only one source file in a program can use this */

Nota 2: se o cabeçalho define e inicializa a variável, apenas um arquivo de origem em um determinado programa pode usar o cabeçalho. Como os cabeçalhos são principalmente para o compartilhamento de informações, é um pouco tolo criar um que possa ser usado apenas uma vez.

seldom_correct.h

c static int hidden_global = 3; /* Each source file gets its own copy */

Nota 3: se o cabeçalho definir uma variável estática (com ou sem inicialização), cada arquivo de origem terminará com sua própria versão privada da variável 'global'.

Se a variável é realmente uma matriz complexa, por exemplo, isso pode levar à duplicação extrema de código. Ocasionalmente, pode ser uma maneira sensata de obter algum efeito, mas isso é muito incomum.


Sumário

Use a técnica do cabeçalho que mostrei primeiro. Funciona de forma confiável e em qualquer lugar. Observe, em particular, que o cabeçalho que declara o global_variableestá incluído em todos os arquivos que o utilizam - incluindo o que o define. Isso garante que tudo seja auto-consistente.

Preocupações semelhantes surgem com a declaração e a definição de funções - regras análogas se aplicam. Mas a pergunta era sobre variáveis ​​especificamente, então eu mantive a resposta apenas para variáveis.

Fim da resposta original

Se você não é um programador C experiente, provavelmente deve parar de ler aqui.


Adição Principal Tardia

Evitando duplicação de código

Uma preocupação que algumas vezes é (e legitimamente) levantada sobre o mecanismo de 'declarações em cabeçalhos, definições na fonte' descrito aqui é que existem dois arquivos a serem mantidos sincronizados - o cabeçalho e a fonte. Isso geralmente é seguido com uma observação de que uma macro pode ser usada para que o cabeçalho cumpra uma tarefa dupla - normalmente declarando as variáveis, mas quando uma macro específica é definida antes da inclusão do cabeçalho, ela define as variáveis.

Outra preocupação pode ser que as variáveis ​​precisam ser definidas em cada um dos vários 'programas principais'. Isso normalmente é uma preocupação espúria; você pode simplesmente introduzir um arquivo de origem C para definir as variáveis ​​e vincular o arquivo de objeto produzido a cada um dos programas.

Um esquema típico funciona assim, usando a variável global original ilustrada em file3.h:

file3a.h

#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable;

file1a.c

#define DEFINE_VARIABLES
#include "file3a.h"  /* Variable defined - but not initialized */
#include "prog3.h"

int increment(void) { return global_variable++; }

file2a.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

Os próximos dois arquivos completam a fonte para prog3:

prog3.h

extern void use_it(void);
extern int increment(void);

prog3.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog3usos prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Inicialização variável

O problema com esse esquema, como mostrado, é que ele não fornece a inicialização da variável global. Com C99 ou C11 e listas de argumentos variáveis ​​para macros, você também pode definir uma macro para suportar a inicialização. (Com C89 e sem suporte para listas de argumentos variáveis ​​em macros, não há uma maneira fácil de lidar com inicializadores arbitrariamente longos.)

file3b.h

#ifdef DEFINE_VARIABLES
#define EXTERN                  /* nothing */
#define INITIALIZER(...)        = __VA_ARGS__
#else
#define EXTERN                  extern
#define INITIALIZER(...)        /* nothing */
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable INITIALIZER(37);
EXTERN struct { int a; int b; } oddball_struct INITIALIZER({ 41, 43 });

Conteúdo reverso de #ife #elseblocos, corrigindo bug identificado por Denis Kniazhev

file1b.c

#define DEFINE_VARIABLES
#include "file3b.h"  /* Variables now defined and initialized */
#include "prog4.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file2b.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

Claramente, o código para a estrutura ímpar não é o que você normalmente escreveria, mas ilustra o ponto. O primeiro argumento para a segunda invocação de INITIALIZERé { 41e o argumento restante (singular neste exemplo) é 43 }. Sem C99 ou suporte semelhante para listas de argumentos variáveis ​​para macros, os inicializadores que precisam conter vírgulas são muito problemáticos.

Cabeçalho correto file3b.hincluído (em vez de fileba.h) por Denis Kniazhev


Os próximos dois arquivos completam a fonte para prog4:

prog4.h

extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);

prog4.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog4usos prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Protetores de cabeçalho

Qualquer cabeçalho deve ser protegido contra reinclusão, para que as definições de tipo (tipos de enum, struct ou union ou typedefs geralmente) não causem problemas. A técnica padrão é envolver o corpo do cabeçalho em um protetor de cabeçalho, como:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

O cabeçalho pode ser incluído duas vezes indiretamente. Por exemplo, se file4b.hinclui file3b.hpara uma definição de tipo que não é mostrada e file1b.cprecisa usar o cabeçalho file4b.he file3b.h, então você tem alguns problemas mais difíceis de resolver. Claramente, você pode revisar a lista de cabeçalhos para incluir apenas file4b.h. No entanto, você pode não estar ciente das dependências internas - e o código deve, idealmente, continuar funcionando.

Além disso, começa a ficar complicado porque você pode incluir file4b.hantes de incluir file3b.hpara gerar as definições, mas as proteções normais do cabeçalho file3b.himpediriam que o cabeçalho fosse reincluído.

Portanto, você precisa incluir o corpo de, file3b.hno máximo, uma vez para declarações e, no máximo, de definições, mas pode ser necessário em uma única unidade de tradução (TU - uma combinação de um arquivo de origem e os cabeçalhos que ele usa).

Inclusão múltipla com definições de variáveis

No entanto, isso pode ser feito sujeito a uma restrição não muito razoável. Vamos apresentar um novo conjunto de nomes de arquivos:

  • external.h para as definições de macro EXTERN, etc.

  • file1c.hpara definir tipos (principalmente struct oddball, o tipo de oddball_struct).

  • file2c.h para definir ou declarar as variáveis ​​globais.

  • file3c.c que define as variáveis ​​globais.

  • file4c.c que simplesmente usa as variáveis ​​globais.

  • file5c.c que mostra que você pode declarar e depois definir as variáveis ​​globais.

  • file6c.c que mostra que você pode definir e depois (tentar) declarar as variáveis ​​globais.

Nestes exemplos, file5c.ce file6c.cincluem diretamente o cabeçalho file2c.hvárias vezes, mas esta é a maneira mais simples de mostrar que as obras do mecanismo. Isso significa que se o cabeçalho fosse indiretamente incluído duas vezes, também seria seguro.

As restrições para que isso funcione são:

  1. O cabeçalho que define ou declara as variáveis ​​globais pode não definir nenhum tipo.

  2. Imediatamente antes de incluir um cabeçalho que deve definir variáveis, você define a macro DEFINE_VARIABLES.

  3. O cabeçalho que define ou declara as variáveis ​​possui conteúdo estilizado.

external.h


#ifdef DEFINE_VARIABLES
#define EXTERN              /* nothing */
#define INITIALIZE(...)     = __VA_ARGS__
#else
#define EXTERN              extern
#define INITIALIZE(...)     /* nothing */
#endif /* DEFINE_VARIABLES */

file1c.h

#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED

struct oddball
{
    int a;
    int b;
};

extern void use_them(void);
extern int increment(void);
extern int oddball_value(void);

#endif /* FILE1C_H_INCLUDED */

file2c.h


/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
#undef FILE2C_H_INCLUDED
#endif

#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE2C_H_INCLUDED */

file3c.c

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file4c.c

#include "file2c.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

file5c.c


#include "file2c.h"     /* Declare variables */

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file6c.c


#define DEFINE_VARIABLES
#include "file2c.h"     /* Variables now defined and initialized */

#include "file2c.h"     /* Declare variables */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

O arquivo fonte próxima completa a fonte (fornece um programa primária) para o prog5, prog6e prog7:

prog5.c

#include "file2c.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog5usos prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog6usos prog5.c, file5c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog7usos prog5.c, file6c.c, file4c.c, file1c.h, file2c.h, external.h.


Este esquema evita a maioria dos problemas. Você só encontra um problema se um cabeçalho que define variáveis ​​(como file2c.h) for incluído por outro cabeçalho (por exemplo file7c.h) que define variáveis. Não há uma maneira fácil de contornar isso além de "não faça".

Você pode solucionar parcialmente o problema revisando file2c.hpara file2d.h:

file2d.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2D_H_DEFINITIONS)
#undef FILE2D_H_INCLUDED
#endif

#ifndef FILE2D_H_INCLUDED
#define FILE2D_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2D_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2D_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2D_H_DEFINITIONS
#undef DEFINE_VARIABLES
#endif /* DEFINE_VARIABLES */

#endif /* FILE2D_H_INCLUDED */

O problema passa a ser "o cabeçalho deve incluir #undef DEFINE_VARIABLES?" Se você omitir isso do cabeçalho e agrupar qualquer chamada de definição com #definee #undef:

#define DEFINE_VARIABLES
#include "file2c.h"
#undef DEFINE_VARIABLES

no código fonte (para que os cabeçalhos nunca alterem o valor de DEFINE_VARIABLES), você deve estar limpo. É apenas um incômodo ter que lembrar de escrever a linha extra. Uma alternativa pode ser:

#define HEADER_DEFINING_VARIABLES "file2c.h"
#include "externdef.h"

externdef.h


#if defined(HEADER_DEFINING_VARIABLES)
#define DEFINE_VARIABLES
#include HEADER_DEFINING_VARIABLES
#undef DEFINE_VARIABLES
#undef HEADER_DEFINING_VARIABLES
#endif /* HEADER_DEFINING_VARIABLES */

Isso está ficando um pouco complicado, mas parece ser seguro (usando o file2d.h, sem #undef DEFINE_VARIABLESno file2d.h).

file7c.c

/* Declare variables */
#include "file2d.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Declare variables - again */
#include "file2d.h"

/* Define variables - again */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file8c.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
#undef FILE8C_H_INCLUDED
#endif

#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file2d.h"     /* struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN struct oddball another INITIALIZE({ 14, 34 });

#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE8C_H_INCLUDED */

file8c.c

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file8c.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

Os próximos dois arquivos completam a fonte prog8e prog9:

prog8.c

#include "file2d.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}

file9c.c

#include "file2d.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}
  • prog8usos prog8.c, file7c.c, file9c.c.

  • prog9usos prog8.c, file8c.c, file9c.c.


No entanto, é pouco provável que os problemas ocorram na prática, especialmente se você seguir o conselho padrão para

Evite variáveis ​​globais


Esta exposição perde alguma coisa?

Confissão : O esquema 'evitando código duplicado' descrito aqui foi desenvolvido porque o problema afeta algum código no qual trabalho (mas não possuo) e é uma preocupação constante com o esquema descrito na primeira parte da resposta. No entanto, o esquema original deixa você com apenas dois lugares para modificar para manter as definições e declarações de variáveis ​​sincronizadas, o que é um grande passo em frente ao ter declarações de variáveis ​​externas espalhadas por toda a base de código (o que realmente importa quando existem milhares de arquivos no total) . No entanto, o código nos arquivos com os nomes fileNc.[ch](mais external.he externdef.h) mostra que ele pode ser feito para funcionar. Claramente, não seria difícil criar um script gerador de cabeçalho para fornecer o modelo padronizado para um arquivo de cabeçalho de definição e declaração de variável.

NB Estes são programas de brinquedo com código apenas insuficiente para torná-los marginalmente interessantes. Há repetição nos exemplos que poderiam ser removidos, mas não é para simplificar a explicação pedagógica. (Por exemplo: a diferença entre prog5.ce prog8.cé o nome de um dos cabeçalhos incluídos. Seria possível reorganizar o código para que a main()função não fosse repetida, mas ocultaria mais do que revelou.)

Jonathan Leffler
fonte
3
@litb: veja o Anexo J.5.11 para a definição comum - é uma extensão comum.
31411 Jonathan Leffler
3
@ litb: e eu concordo que isso deve ser evitado - é por isso que está na seção 'Não é tão bom definir variáveis ​​globais'.
31416 Jonathan Leffler
3
Na verdade, é uma extensão comum, mas é um comportamento indefinido para um programa confiar nela. Só não estava claro se você estava dizendo que isso é permitido pelas próprias regras de C. Agora vejo que você está dizendo que é apenas uma extensão comum e evitá-la se você precisar que seu código seja portátil. Para que eu possa te votar sem dúvidas. Realmente grande resposta IMHO :)
Johannes Schaub - litb
19
Se você parar no topo, mantém as coisas simples. Conforme você lê mais abaixo, ele lida com mais nuances, complicações e detalhes. Acabei de adicionar dois 'pontos de parada precoce' para programadores C menos experientes - ou programadores C que já conhecem o assunto. Não é necessário ler tudo se você já souber a resposta (mas avise se encontrar uma falha técnica).
precisa saber é o seguinte
4
@ supercat: Ocorre-me que você pode usar literais de matriz C99 para obter um valor de enumeração para o tamanho da matriz, exemplificado por ( foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }para definir o inicializador da matriz, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };obter o tamanho da matriz e extern int foo[];declarar a matriz . Claramente, a definição deve ser justa int foo[FOO_SIZE] = FOO_INITIALIZER;, embora o tamanho realmente não precise ser incluído na definição. Isso gera uma constante inteira FOO_SIZE,.
Jonathan Leffler
125

Uma externvariável é uma declaração (graças a sbi pela correção) de uma variável que é definida em outra unidade de tradução. Isso significa que o armazenamento da variável está alocado em outro arquivo.

Digamos que você tenha dois .carquivos test1.ce test2.c. Se você definir uma variável global int test1_var;em test1.ce quiser acessar essa variável, test2.cprecisará usá extern int test1_var;-lo test2.c.

Amostra completa:

$ cat test1.c 
int test1_var = 5;
$ cat test2.c
#include <stdio.h>

extern int test1_var;

int main(void) {
    printf("test1_var = %d\n", test1_var);
    return 0;
}
$ gcc test1.c test2.c -o test
$ ./test
test1_var = 5
Johannes Weiss
fonte
21
Não há "pseudo-definições". É uma declaração.
sbi 16/09/09
3
No exemplo acima, se eu mudar extern int test1_var;para int test1_var;, o vinculador (gcc 5.4.0) ainda passa. Então, é externrealmente necessário neste caso?
Radiohead #
2
@radiohead: Na minha resposta , você encontrará as informações de que largar o arquivo externé uma extensão comum que geralmente funciona - e funciona especificamente com o GCC (mas o GCC está longe de ser o único compilador que o suporta; é predominante nos sistemas Unix). Você pode procurar por "J.5.11" ou a seção "Não é tão bom" na minha resposta (eu sei - é longa) e o texto ao lado que explica isso (ou tenta fazê-lo).
Jonathan Leffler
Uma declaração externa certamente não precisa ser definida em outra unidade de tradução (e geralmente não é). De fato, declaração e definição podem ser a mesma coisa.
Lembre
40

Extern é a palavra-chave usada para declarar que a própria variável reside em outra unidade de tradução.

Assim, você pode optar por usar uma variável em uma unidade de tradução e acessá-la a partir de outra. Em seguida, na segunda, declara-a como externa e o símbolo será resolvido pelo vinculador.

Se você não declarar como externo, receberá 2 variáveis ​​nomeadas da mesma forma, mas não relacionadas, e um erro de várias definições da variável.

Arkaitz Jimenez
fonte
5
Em outras palavras, a unidade de tradução onde extern é usado sabe sobre essa variável, seu tipo etc. e, portanto, permite que o código-fonte na lógica subjacente a use, mas não aloca a variável, outra unidade de tradução fará isso. Se ambas as unidades de conversão declarassem a variável normalmente, haveria efetivamente dois locais físicos para a variável, com as referências "erradas" associadas no código compilado e com a ambiguidade resultante para o vinculador.
Mjv 16/09/09
26

Eu gosto de pensar em uma variável externa como uma promessa que você faz ao compilador.

Ao encontrar um externo, o compilador só pode descobrir seu tipo, não onde "vive", portanto, não pode resolver a referência.

Você está dizendo: "Confie em mim. No momento do link, essa referência será resolvida".

Buggieboy
fonte
De maneira mais geral, uma declaração é uma promessa de que o nome poderá ser resolvido com exatamente uma definição no momento do link. Um externo declara uma variável sem definir.
Lie Ryan
18

extern diz ao compilador para confiar em você que a memória dessa variável está declarada em outro lugar, portanto, ele não tenta alocar / verificar a memória.

Portanto, você pode compilar um arquivo que tenha referência a um externo, mas não poderá vincular se essa memória não for declarada em algum lugar.

Útil para variáveis ​​e bibliotecas globais, mas perigoso porque o vinculador não digita verificação.

BenB
fonte
A memória não está declarada. Veja as respostas para esta pergunta: stackoverflow.com/questions/1410563 para obter mais detalhes.
Sbi 16/09/09
15

Adicionar um externtransforma uma definição de variável em uma declaração de variável . Veja este tópico sobre qual é a diferença entre uma declaração e uma definição.

sbi
fonte
Qual a diferença entre int fooe extern int foo(escopo do arquivo)? Ambos são declaração, não é?
@ user14284: Ambos são declarações apenas no sentido de que toda definição também é uma declaração. Mas eu liguei para uma explicação disso. ("Veja esta discussão sobre qual é a diferença entre uma declaração e uma definição.") Por que você simplesmente não segue o link e lê?
Sbi 9/11
14
                 declare | define   | initialize |
                ----------------------------------

extern int a;    yes          no           no
-------------
int a = 2019;    yes          yes          yes
-------------
int a;           yes          yes          no
-------------

A declaração não alocará memória (a variável deve ser definida para alocação de memória), mas a definição o fará. Esta é apenas outra visão simples da palavra-chave extern, pois as outras respostas são realmente ótimas.

Lucian Nut
fonte
11

A interpretação correta de extern é que você diz algo ao compilador. Você diz ao compilador que, apesar de não estar presente no momento, a variável declarada será encontrada de alguma forma pelo vinculador (normalmente em outro objeto (arquivo)). O vinculador será o sortudo a encontrar tudo e montar tudo, independentemente de você ter algumas declarações externas ou não.

Alex Lockwood
fonte
8

Em C, uma variável dentro de um arquivo, por exemplo, example.c recebe escopo local. O compilador espera que a variável tenha sua definição dentro do mesmo arquivo exemplo.c e, quando não encontrar o mesmo, gerará um erro. Uma função, por outro lado, tem como escopo global por padrão. Portanto, você não precisa mencionar explicitamente ao compilador "look dude ... você pode encontrar a definição dessa função aqui". Para uma função incluindo o arquivo que contém sua declaração é suficiente (o arquivo que você chama de arquivo de cabeçalho). Por exemplo, considere os 2 arquivos a seguir:
example.c

#include<stdio.h>
extern int a;
main(){
       printf("The value of a is <%d>\n",a);
}

example1.c

int a = 5;

Agora, quando você compila os dois arquivos juntos, usando os seguintes comandos:

passo 1) cc -o ex exemplo.c exemplo1.c passo 2) ./ex

Você obtém a seguinte saída: O valor de a é <5>

Phoenix225
fonte
8

Implementação do GCC ELF Linux

Outras respostas abordaram o lado do uso do idioma, agora vamos ver como ele é implementado nesta implementação.

main.c

#include <stdio.h>

int not_extern_int = 1;
extern int extern_int;

void main() {
    printf("%d\n", not_extern_int);
    printf("%d\n", extern_int);
}

Compilar e descompilar:

gcc -c main.c
readelf -s main.o

A saída contém:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 not_extern_int
12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND extern_int

O capítulo "Tabela de símbolos" da especificação ELF de atualização ABI do System V explica:

SHN_UNDEF Este índice da tabela de seções significa que o símbolo está indefinido. Quando o editor de links combina esse arquivo de objeto com outro que define o símbolo indicado, as referências desse arquivo ao símbolo serão vinculadas à definição real.

que é basicamente o comportamento que o padrão C dá às externvariáveis.

A partir de agora, é tarefa do vinculador fazer o programa final, mas as externinformações já foram extraídas do código-fonte para o arquivo de objeto.

Testado no GCC 4.8.

Variáveis ​​em linha do C ++ 17

No C ++ 17, convém usar variáveis ​​embutidas em vez de externas, pois são simples de usar (podem ser definidas apenas uma vez no cabeçalho) e mais poderosas (suporte constexpr). Veja: O que significa 'const static' em C e C ++?

Ciro Santilli adicionou uma nova foto
fonte
3
Não é meu voto negativo, então não sei. No entanto, eu darei uma opinião. Embora examinar a saída de readelfou nmpossa ser útil, você não explicou os fundamentos de como fazer uso externnem concluiu o primeiro programa com a definição real. Seu código nem usa notExtern. Também existe um problema de nomenclatura: embora notExternseja definido aqui e não declarado extern, é uma variável externa que pode ser acessada por outros arquivos de origem se essas unidades de tradução contiverem uma declaração adequada (o que seria necessário extern int notExtern;!).
Jonathan Leffler
1
@JonathanLeffler obrigado pelo feedback! As recomendações padrão de comportamento e uso já foram feitas em outras respostas, então decidi mostrar a implementação um pouco, pois isso realmente me ajudou a entender o que está acontecendo. Não usar notExternera feio, consertou. Sobre a nomenclatura, deixe-me saber se você tem um nome melhor. É claro que esse não seria um bom nome para um programa real, mas acho que se encaixa bem no papel didático aqui.
Ciro Santilli escreveu
Quanto aos nomes, o que dizer global_defda variável definida aqui e extern_refda variável definida em algum outro módulo? Eles teriam simetria clara? Você ainda acaba com int extern_ref = 57;algo assim no arquivo em que está definido, portanto o nome não é ideal, mas dentro do contexto do arquivo de origem único, é uma escolha razoável. Ter extern int global_def;um cabeçalho não é tanto um problema, parece-me. Totalmente com você, é claro.
Jonathan Leffler
7

A palavra-chave extern é usada com a variável para sua identificação como variável global.

Também representa que você pode usar a variável declarada usando a palavra-chave externa em qualquer arquivo, embora ela seja declarada / definida em outro arquivo.

Anup
fonte
5

extern permite que um módulo do seu programa acesse uma variável ou função global declarada em outro módulo do seu programa. Você geralmente tem variáveis ​​externas declaradas nos arquivos de cabeçalho.

Se você não deseja que um programa acesse suas variáveis ​​ou funções, use o staticque informa ao compilador que essa variável ou função não pode ser usada fora deste módulo.

loganaayahee
fonte
5

extern significa simplesmente que uma variável é definida em outro lugar (por exemplo, em outro arquivo).

Geremia
fonte
4

Primeiro, a externpalavra-chave não é usada para definir uma variável; ao contrário, é usado para declarar uma variável. Posso dizer que externé uma classe de armazenamento, não um tipo de dados.

externé usado para permitir que outros arquivos C ou componentes externos saibam que essa variável já está definida em algum lugar. Exemplo: se você estiver construindo uma biblioteca, não é necessário definir a variável global obrigatoriamente em algum lugar da própria biblioteca. A biblioteca será compilada diretamente, mas ao vincular o arquivo, ela verifica a definição.

user1270846
fonte
3

externé usado para que um first.carquivo possa ter acesso total a um parâmetro global em outro second.carquivo.

O externpode ser declarado no first.carquivo ou em qualquer um dos arquivos de cabeçalho first.cincluídos.

Shoham
fonte
3
Observe que a externdeclaração deve estar em um cabeçalho, não em first.c, portanto, se o tipo for alterado, a declaração também será alterada. Além disso, o cabeçalho que declara a variável deve ser incluído second.cpara garantir que a definição seja consistente com a declaração. A declaração no cabeçalho é a cola que mantém tudo junto; Ele permite que os arquivos sejam compilados separadamente, mas garante que eles tenham uma visão consistente do tipo da variável global.
Jonathan Leffler 02/09
2

Com o xc8, você deve ter cuidado ao declarar uma variável do mesmo tipo em cada arquivo, pois você pode, erroneamente, declarar algo como um intem um arquivo e chardizer outro. Isso pode levar à corrupção de variáveis.

Esse problema foi resolvido de maneira elegante em um fórum de microchip há 15 anos / * Consulte "http: www.htsoft.com" / / "forum / all / showflat.php / Cat / 0 / Number / 18766 / an / 0 / page / 0 # 18766 "

Mas esse link parece não funcionar mais ...

Então, eu tentarei explicar rapidamente; crie um arquivo chamado global.h.

Declare o seguinte

#ifdef MAIN_C
#define GLOBAL
 /* #warning COMPILING MAIN.C */
#else
#define GLOBAL extern
#endif
GLOBAL unsigned char testing_mode; // example var used in several C files

Agora no arquivo main.c

#define MAIN_C 1
#include "global.h"
#undef MAIN_C

Isso significa que em main.c a variável será declarada como um unsigned char.

Agora, em outros arquivos, incluindo simplesmente global.h, ele será declarado como externo para esse arquivo .

extern unsigned char testing_mode;

Mas será declarado corretamente como um unsigned char.

O post antigo do fórum provavelmente explicou isso um pouco mais claramente. Mas esse é um potencial real gotchaao usar um compilador que permite declarar uma variável em um arquivo e depois a declarar externamente como um tipo diferente em outro. Os problemas associados a isso são: se você declarar declarando testing_mode como um int em outro arquivo, ele pensaria que era uma var de 16 bits e substituiu alguma outra parte do ram, potencialmente corrompendo outra variável. Difícil de depurar!

user50619
fonte
0

Uma solução muito curta que utilizo para permitir que um arquivo de cabeçalho contenha a referência externa ou a implementação real de um objeto. O arquivo que realmente contém o objeto apenas contém #define GLOBAL_FOO_IMPLEMENTATION. Então, quando adiciono um novo objeto a esse arquivo, ele também aparece nesse arquivo sem que eu precise copiar e colar a definição.

Eu uso esse padrão em vários arquivos. Portanto, para manter as coisas o mais independentes possível, apenas reutilizo a única macro GLOBAL em cada cabeçalho. Meu cabeçalho fica assim:

//file foo_globals.h
#pragma once  
#include "foo.h"  //contains definition of foo

#ifdef GLOBAL  
#undef GLOBAL  
#endif  

#ifdef GLOBAL_FOO_IMPLEMENTATION  
#define GLOBAL  
#else  
#define GLOBAL extern  
#endif  

GLOBAL Foo foo1;  
GLOBAL Foo foo2;


//file main.cpp
#define GLOBAL_FOO_IMPLEMENTATION
#include "foo_globals.h"

//file uses_extern_foo.cpp
#include "foo_globals.h
muusbolla
fonte