Deklarieren Sie das Global mit extern in einem Header, definieren Sie es genau einmal in einer .cpp-Datei und linken Sie deren Objektdatei mit jeder aufrufenden Datei. Diese Anleitung zeigt den vollständigen macOS-Build, eine Adressprüfung und wann eine C++17-inline-Variable geeignet ist.

Warum verursacht ein Global in einem Header mehrfache Definitionen?

Bei einem kleinen nativen Mac-Programm besteht die Aufgabe darin, mehreren separat kompilierten Quelldateien das Lesen und Ändern desselben Objekts zu ermöglichen. Trennen Sie zunächst Deklaration und Definition: extern int itemCount; deklariert ein Objekt, während int itemCount = 0; es definiert. Auch das Hinzufügen eines Initialisierers zu extern, wie in extern int itemCount = 0;, macht es zu einer Definition. Der C++-Arbeitsentwurf erläutert diese Unterscheidung unter Deklarationen und Definitionen.

Ein Header mit einer gewöhnlichen Definition im Namespace-Scope kann diese Definition in jede Quelldatei einführen, die ihn einbindet. Bei einer Nicht-Inline-Variablen ohne Template mit externer Bindung verletzen mehrere Definitionen über verschiedene Translation Units hinweg die One-Definition-Rule. Der Linker kann doppelte Symbole melden, doch die Sprache verlangt nicht, dass jeder solche Verstoß diagnostiziert wird. Siehe die One-Definition-Rule im Arbeitsentwurf.

Ein Include-Guard verhindert das wiederholte Einbinden innerhalb einer Translation Unit. Er koordiniert keine separat kompilierten Quelldateien. Daher kann ein geschützter Header weiterhin mehrfache Definitionen über mehrere Objektdateien hinweg verursachen. Clangs Anleitung zu Compilerbefehlen beschreibt die Erweiterung von Headern während der Vorverarbeitung und das anschließende Linken der Objektdateien.

Erstellen Sie auf Ihrem Mac ein vollständiges Beispiel mit einem gemeinsam genutzten Global

Verwenden Sie entweder Xcode oder Apples Paket Command Line Tools for Xcode. Apple dokumentiert das eigenständige Paket als Alternative für die Entwicklung außerhalb von Xcode; eine vollständige Xcode-Installation enthält die Tools bereits. Falls keines davon installiert ist, führen Sie den folgenden Befehl aus und schließen Sie den Installationsdialog ab. Siehe Installieren der Befehlszeilen-Tools.

xcode-select --install

Prüfen Sie vor dem Build das ausgewählte Entwicklerverzeichnis und den Compiler:

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

Apples archivierte FAQ zum Erstellen über die Befehlszeile mit Xcode beschreibt sowohl die Prüfung des ausgewählten Entwicklerverzeichnisses als auch die Verwendung von xcrun, um Entwicklertools zu finden oder auszuführen. Verwenden Sie während des gesamten Beispiels dieselbe ausgewählte Toolchain.

Erstellen Sie einen neuen leeren Ordner namens shared-global-demo, öffnen Sie ihn in Ihrem Editor und speichern Sie die folgenden vier Dateien darin. Dies ist ein synthetisches, nebenläufigkeitsfreies C++-Programm: Eine Datei besitzt das Global, eine andere ändert es, und der Programmeinstieg prüft sowohl seinen Wert als auch seine Identität.

Deklarieren Sie das Objekt im gemeinsamen Header

Speichern Sie dies als shared_state.hpp:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
extern int itemCount;

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

#endif

Der Header stellt die Deklaration bereit, die von jeder Quelldatei verwendet wird. Wenn Namespace und Typ in einem gemeinsamen Header stehen, müssen außerdem keine separaten, von Hand gepflegten Deklarationen verwaltet werden, die voneinander abweichen können.

Legen Sie eine einzige Definition des Objekts an

Speichern Sie dies als shared_state.cpp:

#include "shared_state.hpp"

namespace demo {
int itemCount = 0;
}

Dies ist die einzige Definition von demo::itemCount. Wenn der Header in die Quelldatei eingebunden wird, in der die Definition steht, kann der Compiler prüfen, ob sie mit der Deklaration übereinstimmt. Der explizite Initialisierer macht den Anfangswert deutlich.

Ändern Sie es aus einer anderen Translation Unit

Speichern Sie dies als worker.cpp:

#include "shared_state.hpp"

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

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

Speichern Sie dies als 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;
}

Der Adressvergleich ist eine nützliche zusätzliche Prüfung über das erfolgreiche Linken hinaus: worker.cpp gibt die Adresse zurück, die es sieht, und main.cpp vergleicht sie mit seiner eigenen. Der Exit-Status prüft außerdem den geänderten Wert. Keine der beiden Prüfungen hängt davon ab, ob Assertions aktiviert sind.

Kompilieren Sie die Dateien getrennt und linken Sie anschließend alle drei Objektdateien

Wechseln Sie im Terminal in den Ordner mit den vier Dateien. Führen Sie diesen vollständigen Build-Block aus:

(
    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 dokumentiert, dass -c eine Objektdatei ohne Linken erzeugt; wenn keine Stufenauswahl angegeben wird, kann der Treiber linken. Mit -std=c++17 wird der Sprachmodus explizit ausgewählt. Siehe die Anleitung zu Compilerbefehlen von Clang. Die Subshell wird bei einem fehlgeschlagenen Befehl beendet, sodass ein fehlgeschlagener Build nicht dazu führt, dass eine ältere ausführbare Datei gestartet wird.

Die erwartete Ausgabe, aus dem Quellcode abgeleitet und nicht auf einem Mac aufgezeichnet, lautet:

before: 0
after: 1
same object: true

In der Rechercheumgebung war kein Mac-Compiler verfügbar, daher wurde dieses Beispiel geprüft, aber nicht ausgeführt. Die drei Kompilierbefehle zeigen bewusst die Grenze zwischen den Objektdateien: shared_state.o liefert die Definition, während worker.o und main.o sie verwenden.

Wann sollten Sie eine C++17-Inline-Variable verwenden?

Wählen Sie eine C++17-Inline-Variable, wenn die Definition der Variablen in einen gemeinsamen Header gehört. Clang führt Inline-Variablen unter seinen C++17-Sprachfunktionen auf. Laut Sprachspezifikation darf eine Inline-Variable mit externer Bindung in mehreren Translation Units definiert werden und bleibt dabei ein einziges Objekt mit einer einzigen Adresse. Siehe Der inline-Spezifizierer.

Ersetzen Sie für dasselbe Beispiel den gesamten Inhalt von shared_state.hpp durch diese Version:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
inline int itemCount = 0;

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

#endif

Lassen Sie worker.cpp und main.cpp unverändert. Entfernen Sie shared_state.cpp aus dem Build dieser Variante: Eine separate Definition ist nicht mehr erforderlich. Kompilieren Sie beide Verwender neu und linken Sie nur deren Objektdateien:

(
    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
)

Diese Variante hat dieselbe erwartete Ausgabe. Nehmen Sie das alte shared_state.o nicht in den Link-Befehl auf. Wenn veraltete Objektdateien aus der expliziten Link-Liste herausgehalten werden, ist die geänderte Zuständigkeit eindeutig.

Die Inline-Definitionen müssen die Anforderungen der One-Definition-Rule an Konsistenz erfüllen, einschließlich übereinstimmender Tokenfolgen und entsprechender Namensauflösung. Vermeiden Sie Makros, die in verschiedenen Quelldateien unterschiedliche Initialisierer erzeugen. Ein erfolgreicher Link allein belegt keine Regelkonformität. Siehe One-Definition-Rule.

Anforderung Geeigneter Ansatz Zentrale Bedingung
Eine Implementierungsdatei besitzt den veränderbaren Zustand Header-Deklaration mit extern; eine Definition in einer .cpp-Datei Die Objektdatei des Besitzers genau einmal linken
Die gemeinsame Definition gehört in einen Header C++17-Inline-Variable Alle verwendenden Translation Units nutzen einen kompatiblen Sprachmodus und eine konsistente Definition
Jede Quelldatei soll absichtlich einen eigenen Zustand besitzen static im Namespace-Scope oder ein unbenannter Namespace Dadurch entsteht interne Bindung, was die Aufgabe eines gemeinsam genutzten Objekts nicht erfüllt

Die Unterscheidung der internen Bindung folgt dem Abschnitt Programm und Bindung des Arbeitsentwurfs. Bei diesem kleinen Programm aus mehreren Dateien lässt sich die Zuständigkeit mit dem Ansatz extern besonders leicht nachvollziehen.

Diagnostizieren Sie den Fehler, bevor Sie Compiler-Flags ändern

Gehen Sie diese Schritte durch, um ein Zuständigkeitsproblem von einer fehlenden Build-Eingabe zu unterscheiden. Die Prüfungen folgen den Deklarationsregeln und Kompilierstufen von oben; der genaue Wortlaut der Diagnose kann je nach Toolchain variieren.

  1. Wenn die Kompilierung fehlschlägt, prüfen Sie zuerst die Deklarationen. Vergewissern Sie sich, dass jede Quelldatei shared_state.hpp einbindet und denselben Namespace, Typ und dieselbe Schreibweise verwendet. Linken kann einen nicht deklarierten Namen oder widersprüchliche Deklarationen nicht beheben.
  2. Wenn der Linker ein doppeltes Symbol meldet, zählen Sie Definitionen und Link-Eingaben. Suchen Sie nach einem Initialisierer im Header, einer zweiten Definition in einer .cpp-Datei oder derselben Besitzer-Objektdatei, die zweimal angegeben wurde. Prüfen Sie bei der Inline-Variante, ob die alte Nicht-Inline-Definition fehlt.
  3. Wenn der Linker ein undefiniertes Symbol meldet, suchen Sie nach dem fehlenden Besitzer. Prüfen Sie in der Version extern, ob shared_state.cpp erfolgreich kompiliert wurde und ob shared_state.o im abschließenden Link-Befehl vorkommt. Eine Deklaration allein stellt die Definition nicht bereit.
  4. Wenn das Linken erfolgreich ist, aber die Identitätsprüfung fehlschlägt, untersuchen Sie die Bindung. Suchen Sie nach Variablen static im Namespace-Scope oder Definitionen innerhalb unbenannter Namespaces. Sie geben jeder Translation Unit ein eigenes Objekt.
  5. Erstellen Sie nach Änderungen an einem Header oder der Zuständigkeitsstrategie alle betroffenen Objektdateien neu. Wiederholen Sie den vollständigen passenden Build-Block, damit die aufrufenden Dateien und die Definitionsdatei die aktuellen Deklarationen verwenden.

Zwei Abkürzungen erfordern besondere Vorsicht. Erstens erzeugt das Ersetzen des Globals durch static int itemCount = 0; im Namespace-Scope des Headers separate Objekte für jede Translation Unit. Zweitens hat eine gewöhnliche Variable const im Namespace-Scope normalerweise interne Bindung, sofern keine Ausnahme greift, etwa eine explizite extern-Deklaration oder inline. Verwenden Sie die gewünschte Bindung bewusst; die relevanten Ausnahmen stehen unter Programm und Bindung.

Behalten Sie außerdem die Sprachbindung bei. extern "C" legt die C-Sprachbindung fest; es stellt weder die fehlende Definition bereit noch erlaubt es doppelte Definitionen. Behalten Sie für dieses vollständig in C++ geschriebene Beispiel die standardmäßige C++-Bindung bei. Der Arbeitsentwurf erläutert die Sprachbindung und ihre implementierungsabhängigen Eigenschaften bei Namensgebung und Aufrufkonvention unter Bindungsspezifikationen.

Was ändert sich, wenn die Mac-App auch Swift enthält?

Die Speicheranordnung des C++-Globals gehört weiterhin in die C++-Implementierung. Das Importieren eines Headers in Swift stellt eine Schnittstelle bereit; der Build muss außerdem die C++-Implementierung einbeziehen, der ein Nicht-Inline-Global gehört. Swifts Anleitung zur Einrichtung gemischter Sprachprojekte beschreibt, wie unterstützte C++-APIs über öffentliche Framework-Header oder den Bridging-Header eines App-Targets bereitgestellt werden.

Für ein Xcode-Target, in dem Swift C++ aufrufen muss, lautet die dokumentierte Einstellung C++ and Objective-C interoperability; sie wird auf C++ / Objective-C++ gesetzt. Aktivieren Sie sie für das aufrufende Target und stellen Sie den passenden Header bereit. Dies ist eine zusätzliche Anforderung, unabhängig davon, ob das Global genau eine Definition hat. Siehe Einrichten gemischtsprachiger Swift- und C++-Projekte.

Beachten Sie die dokumentierten Kompatibilitätsgrenzen: Die C++-Interoperabilität mit Swift erfordert Swift 5.9 oder neuer, gemischter Swift- und C++-Code muss dieselbe C++-Standardbibliothek verwenden, und Apple-Plattformen nutzen libc++. Wenn ein Header mit der Inline-Variable aus diesem Artikel importiert wird, muss auch für den Importer C++17 oder neuer ausgewählt sein, ebenso wie für die C++-Quelldateien. Swift dokumentiert die Einstellungen für den Sprachstandard und die Compilerbeschränkungen unter Unterstützte Funktionen und Einschränkungen der C++-Interoperabilität.

Die Beispiele hier behandeln separat kompilierte C++-Dateien, die zu einer nativen ausführbaren Datei gelinkt werden. Berücksichtigen Sie diesen Umfang, wenn Sie die Adressprüfung anwenden; separat geladene Frameworks und Bibliotheken erfordern ein eigenes Design für Export und Linken. Bestimmen Sie für die gezeigte Aufgabe, wo die Definition liegt, nehmen Sie sie genau einmal in den Link auf und prüfen Sie, ob beide aufrufenden Dateien dieselbe Adresse und den aktualisierten Wert sehen.

Quellen