Declara la variable global con extern en un encabezado, defínela en exactamente un archivo .cpp y enlaza el archivo objeto de ese archivo con cada consumidor. Esta guía muestra la compilación completa en macOS, una comprobación de direcciones y cuándo es apropiado usar una variable inline de C++17.

¿Por qué una variable global en un encabezado causa definiciones duplicadas?

En un programa nativo pequeño para Mac, la tarea consiste en permitir que varios archivos de código fuente compilados por separado lean y modifiquen el mismo objeto. Empieza por separar una declaración de una definición: extern int itemCount; declara un objeto, mientras que int itemCount = 0; lo define. Añadir un inicializador a extern, como en extern int itemCount = 0;, también lo convierte en una definición. El borrador de trabajo de C++ documenta esta distinción en Declaraciones y definiciones.

Un encabezado que contiene una definición ordinaria en el ámbito de espacio de nombres puede introducir esa definición en cada archivo de código fuente que lo incluya. En el caso de una variable que no sea inline ni una plantilla y que tenga enlace externo, las definiciones múltiples en distintas unidades de traducción infringen la regla de una definición. El enlazador puede informar de símbolos duplicados, pero el lenguaje no exige que se diagnostiquen todas estas infracciones. Consulta la Regla de una definición del borrador de trabajo.

Una protección contra inclusiones repetidas evita la inclusión reiterada dentro de una misma unidad de traducción. No coordina archivos de código fuente compilados por separado. Por tanto, un encabezado protegido aún puede causar definiciones múltiples entre archivos objeto. La guía de comandos del compilador de Clang describe la expansión de encabezados durante el preprocesamiento y el enlazado posterior de archivos objeto.

Compila un ejemplo completo con una variable global compartida en tu Mac

Usa Xcode o el paquete de herramientas de línea de comandos de Apple para Xcode. Apple documenta el paquete independiente como alternativa para desarrollar fuera de Xcode; una instalación completa de Xcode ya incluye las herramientas. Si no tienes ninguno instalado, ejecuta el siguiente comando y completa el cuadro de diálogo de instalación. Consulta Instalar las herramientas de línea de comandos.

xcode-select --install

Antes de compilar, comprueba el directorio de desarrollo y el compilador seleccionados:

xcode-select --print-path
xcrun clang++ --version

Las Preguntas frecuentes sobre cómo compilar desde la línea de comandos con Xcode del archivo de documentación de Apple explican cómo inspeccionar el directorio de desarrollo seleccionado y cómo usar xcrun para localizar o ejecutar herramientas de desarrollo. Mantén la misma cadena de herramientas seleccionada durante todo este ejemplo.

Crea una carpeta vacía llamada shared-global-demo, ábrela en tu editor y guarda allí los siguientes cuatro archivos. Este es un programa sintético de C++ de un solo hilo: un archivo es propietario de la variable global, otro la modifica y el punto de entrada comprueba tanto su valor como su identidad.

Declara el objeto en el encabezado compartido

Guárdalo como shared_state.hpp:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
extern int itemCount;

void addItem();
int* itemCountAddress();
}

#endif

El encabezado proporciona la declaración que usan todos los archivos de código fuente. Mantener el espacio de nombres y el tipo en un mismo encabezado compartido también evita tener que mantener declaraciones manuscritas por separado que podrían divergir.

Define el objeto una sola vez

Guárdalo como shared_state.cpp:

#include "shared_state.hpp"

namespace demo {
int itemCount = 0;
}

Esta es la única definición de demo::itemCount. Incluir el encabezado en el archivo de código fuente que la define permite al compilador comprobar que la definición coincide con la declaración. El inicializador explícito deja claro el valor inicial.

Modifícala desde otra unidad de traducción

Guárdalo como worker.cpp:

#include "shared_state.hpp"

namespace demo {
void addItem() {
    ++itemCount;
}

int* itemCountAddress() {
    return &itemCount;
}
}

Guárdalo como main.cpp:

#include "shared_state.hpp"
#include <iostream>

int main() {
    std::cout << "before: " << demo::itemCount << '\n';
    demo::addItem();

    const bool sameObject =
        demo::itemCountAddress() == &demo::itemCount;

    std::cout << "after: " << demo::itemCount << '\n';
    std::cout << std::boolalpha
              << "same object: " << sameObject << '\n';

    return (demo::itemCount == 1 && sameObject) ? 0 : 1;
}

La comparación de direcciones aporta una comprobación útil, además de que el enlazado se complete correctamente: worker.cpp devuelve la dirección que observa, y main.cpp la compara con la suya. El estado de salida también comprueba el valor modificado. Ninguna de las dos comprobaciones depende de que las aserciones estén habilitadas.

Compila por separado y luego enlaza los tres archivos objeto

En Terminal, cambia a la carpeta que contiene esos cuatro archivos. Ejecuta este bloque de compilación completo:

(
    set -e
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c shared_state.cpp -o shared_state.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c worker.cpp -o worker.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c main.cpp -o main.o
    xcrun clang++ shared_state.o worker.o main.o -o shared-demo
    ./shared-demo
)

Clang documenta que -c genera un archivo objeto sin enlazar; si se omite una opción de selección de etapa, el controlador puede enlazar. La opción explícita -std=c++17 selecciona el modo del lenguaje. Consulta la guía de comandos del compilador de Clang. El subproceso se detiene si falla un comando, lo que evita que una compilación fallida continúe ejecutando un ejecutable antiguo.

El resultado esperado, deducido del código fuente y no capturado durante una ejecución en Mac, es:

before: 0
after: 1
same object: true

El entorno de investigación no proporcionaba un compilador de Mac, así que el ejemplo se inspeccionó, pero no se ejecutó. Los tres comandos de compilación muestran deliberadamente el límite entre archivos objeto: shared_state.o proporciona la definición, mientras que worker.o y main.o la usan.

¿Cuándo conviene usar una variable inline de C++17?

Elige una variable inline de C++17 cuando la definición de la variable deba estar en un encabezado compartido. Clang incluye las variables inline entre sus características del lenguaje C++17. El lenguaje especifica que una variable inline con enlace externo puede definirse en varias unidades de traducción y, aun así, seguir siendo una sola entidad con una única dirección. Consulta El especificador inline.

Para el mismo ejemplo, reemplaza todo el contenido de shared_state.hpp por esta versión:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
inline int itemCount = 0;

void addItem();
int* itemCountAddress();
}

#endif

Deja worker.cpp y main.cpp sin cambios. En esta variante de la compilación, elimina shared_state.cpp: ya no hace falta una definición independiente. Vuelve a compilar ambos usuarios y enlaza solo sus archivos objeto:

(
    set -e
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c worker.cpp -o worker.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c main.cpp -o main.o
    xcrun clang++ worker.o main.o -o shared-demo
    ./shared-demo
)

Esta variante produce el mismo resultado esperado. No incluyas el antiguo shared_state.o en su comando de enlazado. Excluir archivos objeto obsoletos de la lista explícita de enlazado hace que el cambio de propiedad quede claro.

Las definiciones inline deben cumplir los requisitos de coherencia de la regla de una definición, incluidos los mismos tokens y la búsqueda de nombres correspondiente. Evita las macros que generen inicializadores distintos en diferentes archivos de código fuente. Que el enlazado se complete correctamente no demuestra por sí solo que se cumpla la regla. Consulta la Regla de una definición.

Requisito Enfoque adecuado Condición clave
Un archivo de implementación es propietario del estado mutable Declaración en un encabezado con extern; definición en un único archivo .cpp Enlaza exactamente una vez el objeto propietario
La definición compartida debe estar en un encabezado Variable inline de C++17 Todas las unidades de traducción consumidoras usan un modo de lenguaje compatible y una definición coherente
Cada archivo de código fuente debe tener su propio estado Variable static en el ámbito de espacio de nombres o un espacio de nombres sin nombre Estos crean enlace interno, por lo que no implementan la tarea de compartir un mismo objeto

La distinción del enlace interno se basa en Programa y enlace del borrador de trabajo. Para este pequeño programa con varios archivos, el enfoque extern facilita especialmente la inspección de la propiedad.

Diagnostica el fallo antes de cambiar las opciones del compilador

Sigue esta secuencia para distinguir un problema de propiedad de una entrada de compilación ausente. Las comprobaciones se basan en las reglas de declaración y las etapas de compilación usadas antes; el texto de los diagnósticos puede variar según la cadena de herramientas.

  1. Si falla la compilación, comprueba primero las declaraciones. Confirma que cada archivo de código fuente incluya shared_state.hpp y use el mismo espacio de nombres, tipo y escritura del nombre. El enlazado no puede corregir un nombre no declarado ni declaraciones en conflicto.
  2. Si el enlazado informa de un símbolo duplicado, cuenta las definiciones y las entradas de enlazado. Busca un inicializador en el encabezado, una segunda definición en un archivo .cpp o el mismo objeto propietario incluido dos veces. Para la variante inline, confirma que no esté la antigua definición no inline.
  3. Si el enlazado informa de un símbolo sin definir, localiza el propietario que falta. En la versión extern, confirma que shared_state.cpp se haya compilado correctamente y que shared_state.o aparezca en el comando de enlazado final. Una declaración por sí sola no aporta la definición.
  4. Si el enlazado se completa, pero falla la comprobación de identidad, inspecciona el enlace. Busca variables static en el ámbito de espacio de nombres o definiciones dentro de espacios de nombres sin nombre. Estas dan a cada unidad de traducción su propio objeto.
  5. Después de cambiar un encabezado o la estrategia de propiedad, vuelve a compilar todos los objetos afectados. Repite el bloque de compilación completo que corresponda para que los archivos que usan la variable y el archivo que la define utilicen las declaraciones actuales.

Dos atajos requieren especial cuidado. Primero, sustituir la variable global por static int itemCount = 0; en el ámbito de espacio de nombres dentro del encabezado crea objetos independientes por unidad de traducción. Segundo, una variable ordinaria const en el ámbito de espacio de nombres generalmente tiene enlace interno, salvo que se aplique una excepción, como una declaración explícita extern o inline. Usa deliberadamente el enlace previsto; las excepciones pertinentes aparecen en Programa y enlace.

Conserva también el enlace del lenguaje. extern "C" especifica el enlace del lenguaje C; no proporciona la definición que falta ni permite definiciones duplicadas. En este ejemplo íntegramente en C++, conserva el enlace C++ predeterminado. El borrador de trabajo explica el enlace del lenguaje y sus propiedades de nombres y convenciones de llamada, que dependen de la implementación, en Especificaciones de enlace.

¿Qué cambia cuando la app de Mac también incluye Swift?

La organización del almacenamiento de C++ sigue correspondiendo a la implementación de C++. Importar un encabezado en Swift proporciona una interfaz; la compilación también debe incluir la implementación de C++ que es propietaria de una variable global no inline. La guía de configuración de proyectos con varios lenguajes de Swift explica cómo exponer las API de C++ compatibles mediante encabezados públicos de un framework o el encabezado puente de un destino de app.

Para que Swift pueda llamar a C++ desde un destino de Xcode, la opción documentada es C++ and Objective-C interoperability, configurada como C++ / Objective-C++. Actívala en el destino consumidor y expón el encabezado correspondiente. Este requisito es independiente de que la variable tenga una sola definición. Consulta Configuración de proyectos Swift y C++ con varios lenguajes.

Respeta los límites de compatibilidad documentados: la interoperabilidad de Swift con C++ requiere Swift 5.9 o posterior, el código que combina Swift y C++ debe usar la misma biblioteca estándar de C++, y las plataformas de Apple usan libc++. Para importar un encabezado que contiene la variable inline de este artículo, el modo de C++ del importador también debe ser C++17 o posterior, al igual que el de los archivos fuente de C++. Swift documenta los controles del estándar del lenguaje y las restricciones del compilador en Características admitidas y restricciones de la interoperabilidad con C++.

Los ejemplos de aquí abarcan archivos de C++ compilados por separado y enlazados en un único ejecutable nativo. Mantén ese alcance al aplicar la comprobación de direcciones; los frameworks y las bibliotecas que se cargan por separado requieren su propio diseño de exportación y enlazado. Para la tarea demostrada, identifica la definición propietaria, inclúyela una vez en el enlazado y comprueba que ambos consumidores observen la misma dirección y el valor actualizado.

Fuentes