Zadeklaruj zmienną globalną za pomocą extern w pliku nagłówkowym, zdefiniuj ją dokładnie w jednym pliku .cpp i dołącz plik obiektowy z tą definicją podczas linkowania każdego pliku, który z niej korzysta. Ten przewodnik pokazuje pełny proces budowania w systemie macOS, sprawdzanie adresu oraz wyjaśnia, kiedy warto użyć zmiennej inline z C++17.

Dlaczego zmienna globalna w pliku nagłówkowym powoduje wielokrotne definicje?

W małym natywnym programie dla Maca zadanie polega na tym, by kilka osobno kompilowanych plików źródłowych mogło odczytywać i modyfikować ten sam obiekt. Zacznij od rozróżnienia deklaracji i definicji: extern int itemCount; deklaruje obiekt, a int itemCount = 0; go definiuje. Dodanie inicjalizatora do extern, jak w extern int itemCount = 0;, również tworzy definicję. Projekt roboczy standardu C++ opisuje to rozróżnienie w sekcji Declarations and definitions.

Plik nagłówkowy zawierający zwykłą definicję na poziomie przestrzeni nazw może wprowadzić tę definicję do każdego pliku źródłowego, który go dołącza. W przypadku zmiennej niewbudowanej i nietemplatowej z łącznością zewnętrzną wiele definicji w różnych jednostkach translacji narusza regułę jednej definicji. Konsolidator może zgłosić powielone symbole, ale język nie wymaga, by każde takie naruszenie zostało zdiagnozowane. Zobacz One-definition rule w projekcie roboczym standardu.

Osłona pliku nagłówkowego zapobiega wielokrotnemu dołączeniu go w obrębie jednej jednostki translacji. Nie koordynuje jednak osobno kompilowanych plików źródłowych. Dlatego plik nagłówkowy z osłoną nadal może powodować wielokrotne definicje w plikach obiektowych. Przewodnik Clanga compiler command guide opisuje rozwijanie plików nagłówkowych podczas preprocessingu oraz późniejsze linkowanie plików obiektowych.

Zbuduj na Macu kompletny przykład ze współdzieloną zmienną globalną

Użyj Xcode albo pakietu Command Line Tools for Xcode firmy Apple. Apple opisuje samodzielny pakiet jako alternatywę do programowania poza Xcode; pełna instalacja Xcode zawiera już te narzędzia. Jeśli nie masz zainstalowanego żadnego z nich, uruchom poniższe polecenie i dokończ instalację w wyświetlonym oknie. Zobacz Installing the command-line tools.

xcode-select --install

Przed kompilacją sprawdź wybrany katalog deweloperski i kompilator:

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

Zarchiwizowana dokumentacja Apple Building from the Command Line with Xcode FAQ opisuje zarówno sprawdzanie wybranego katalogu deweloperskiego, jak i używanie xcrun do znajdowania lub uruchamiania narzędzi deweloperskich. W całym przykładzie korzystaj z tego samego wybranego łańcucha narzędzi.

Utwórz nowy, pusty folder o nazwie shared-global-demo, otwórz go w edytorze i zapisz w nim cztery poniższe pliki. To syntetyczny, jednowątkowy program C++: jeden plik jest właścicielem zmiennej globalnej, drugi ją modyfikuje, a punkt wejścia sprawdza zarówno jej wartość, jak i tożsamość.

Zadeklaruj obiekt we współdzielonym pliku nagłówkowym

Zapisz ten plik jako shared_state.hpp:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
extern int itemCount;

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

#endif

Plik nagłówkowy zawiera deklarację używaną przez każdy plik źródłowy. Umieszczenie przestrzeni nazw i typu we wspólnym pliku nagłówkowym pozwala też uniknąć utrzymywania osobnych, ręcznie pisanych deklaracji, które mogłyby się z czasem rozjechać.

Zdefiniuj obiekt tylko raz

Zapisz ten plik jako shared_state.cpp:

#include "shared_state.hpp"

namespace demo {
int itemCount = 0;
}

To jedyna definicja demo::itemCount. Dołączenie pliku nagłówkowego w pliku źródłowym zawierającym definicję pozwala kompilatorowi sprawdzić, czy definicja jest zgodna z deklaracją. Jawny inicjalizator jasno określa wartość początkową.

Zmodyfikuj obiekt z innej jednostki translacji

Zapisz ten plik jako worker.cpp:

#include "shared_state.hpp"

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

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

Zapisz ten plik jako 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;
}

Porównanie adresów to przydatny test wykraczający poza samo udane linkowanie: worker.cpp zwraca adres, który widzi, a main.cpp porównuje go z własnym. Kod wyjścia sprawdza również zmodyfikowaną wartość. Żaden z tych testów nie zależy od tego, czy asercje są włączone.

Skompiluj pliki osobno, a następnie połącz wszystkie trzy pliki obiektowe

W Terminalu przejdź do folderu zawierającego te cztery pliki. Uruchom cały poniższy blok budowania:

(
    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 dokumentuje, że -c tworzy plik obiektowy bez linkowania; pominięcie opcji wyboru etapu pozwala sterownikowi wykonać linkowanie. Jawna opcja -std=c++17 wybiera tryb języka. Zobacz Clang compiler command guide. Podpowłoka przerywa działanie, jeśli któreś polecenie zakończy się błędem, dzięki czemu po nieudanym budowaniu nie uruchomi się starszy plik wykonywalny.

Oczekiwany wynik, wywnioskowany z kodu źródłowego, a nie uzyskany podczas uruchomienia na Macu, to:

before: 0
after: 1
same object: true

Środowisko badawcze nie udostępniało kompilatora dla Maca, dlatego przykład został przeanalizowany, a nie uruchomiony. Trzy polecenia kompilacji celowo pokazują granicę między plikami obiektowymi: shared_state.o dostarcza definicję, natomiast worker.o i main.o z niej korzystają.

Kiedy używać zmiennej inline z C++17?

Wybierz zmienną inline z C++17, gdy jej definicja powinna znajdować się we wspólnym pliku nagłówkowym. Clang wymienia zmienne inline wśród swoich C++17 language features. Język określa, że zmienna inline z łącznością zewnętrzną może być definiowana w wielu jednostkach translacji, a mimo to pozostaje jednym obiektem z jednym adresem. Zobacz The inline specifier.

W tym samym przykładzie zastąp całą zawartość pliku shared_state.hpp poniższą wersją:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
inline int itemCount = 0;

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

#endif

Pozostaw pliki worker.cpp i main.cpp bez zmian. Usuń shared_state.cpp z procesu budowania tego wariantu: osobna definicja nie jest już potrzebna. Skompiluj ponownie oba pliki korzystające ze zmiennej i połącz tylko ich pliki obiektowe:

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

Ten wariant daje taki sam oczekiwany wynik. Nie dołączaj starego pliku shared_state.o do polecenia linkowania. Pominięcie nieaktualnych plików obiektowych na jawnej liście plików do linkowania jasno wskazuje zmianę właściciela definicji.

Definicje inline muszą spełniać wymagania zgodności wynikające z reguły jednej definicji, w tym mieć zgodne sekwencje tokenów i odpowiadające sobie wyszukiwanie nazw. Unikaj makr, które w różnych plikach źródłowych tworzą różne inicjalizatory. Samo udane linkowanie nie potwierdza zgodności z tymi wymaganiami. Zobacz One-definition rule.

Wymaganie Odpowiednie podejście Główny warunek
Jeden plik implementacji jest właścicielem zmiennego stanu Deklaracja w pliku nagłówkowym z extern; jedna definicja w pliku .cpp Dołącz plik obiektowy właściciela dokładnie raz podczas linkowania
Wspólna definicja powinna znajdować się w pliku nagłówkowym Zmienna inline z C++17 Każda korzystająca jednostka translacji używa zgodnego trybu języka i spójnej definicji
Każdy plik źródłowy celowo ma własny stan Zmienna static na poziomie przestrzeni nazw albo nienazwana przestrzeń nazw Tworzą one łączność wewnętrzną, więc nie służą do współdzielenia jednego obiektu

Rozróżnienie łączności wewnętrznej wynika z sekcji Program and linkage projektu roboczego standardu. W tym małym programie wieloplikowym podejście extern ułatwia sprawdzenie, kto jest właścicielem zmiennej.

Zdiagnozuj problem, zanim zmienisz flagi kompilatora

Zastosuj poniższą kolejność, aby odróżnić problem z właścicielem definicji od brakującego pliku w procesie budowania. Kontrole opierają się na opisanych wyżej regułach deklaracji i etapach kompilacji; brzmienie komunikatów może zależeć od łańcucha narzędzi.

  1. Jeśli kompilacja się nie powiedzie, najpierw sprawdź deklaracje. Upewnij się, że każdy plik źródłowy dołącza shared_state.hpp i używa tej samej przestrzeni nazw, typu oraz pisowni. Linkowanie nie naprawi niezadeklarowanej nazwy ani sprzecznych deklaracji.
  2. Jeśli konsolidator zgłasza powielony symbol, policz definicje i pliki wejściowe do linkowania. Poszukaj inicjalizatora w pliku nagłówkowym, drugiej definicji w pliku .cpp albo dwukrotnego podania tego samego pliku obiektowego zawierającego definicję. W wariancie inline upewnij się, że stara definicja niewbudowana została usunięta.
  3. Jeśli konsolidator zgłasza niezdefiniowany symbol, znajdź brakujący plik z definicją. W wariancie extern sprawdź, czy shared_state.cpp skompilowano pomyślnie i czy shared_state.o znajduje się w końcowym poleceniu linkowania. Sama deklaracja nie dostarcza definicji.
  4. Jeśli linkowanie się powiedzie, ale test tożsamości zawiedzie, sprawdź łączność. Poszukaj zmiennych static na poziomie przestrzeni nazw albo definicji w nienazwanych przestrzeniach nazw. Tworzą one osobny obiekt dla każdej jednostki translacji.
  5. Po zmianie pliku nagłówkowego lub strategii określania właściciela ponownie zbuduj wszystkie powiązane pliki obiektowe. Powtórz cały odpowiedni blok budowania, aby pliki korzystające ze zmiennej i plik z jej definicją używały aktualnych deklaracji.

Dwa skróty wymagają szczególnej uwagi. Po pierwsze, zastąpienie zmiennej globalnej zmienną static int itemCount = 0; na poziomie przestrzeni nazw w pliku nagłówkowym tworzy osobne obiekty dla każdej jednostki translacji. Po drugie, zwykła zmienna const na poziomie przestrzeni nazw ma zazwyczaj łączność wewnętrzną, chyba że zachodzi wyjątek, na przykład jawna deklaracja extern lub inline. Świadomie wybieraj odpowiednią łączność; właściwe wyjątki opisano w Program and linkage.

Zachowaj również łączność językową. extern \"C\" określa łączność języka C; nie dostarcza brakującej definicji ani nie zezwala na powielone definicje. W tym przykładzie, który w całości używa C++, zachowaj domyślną łączność C++. Projekt roboczy standardu wyjaśnia łączność językową oraz zależne od implementacji właściwości nazw i konwencji wywołań w sekcji Linkage specifications.

Co się zmienia, gdy aplikacja na Maca zawiera też kod Swift?

Sposób przechowywania zmiennej wciąż należy zaimplementować w C++. Zaimportowanie pliku nagłówkowego do Swift udostępnia interfejs; proces budowania musi też zawierać implementację C++, która jest właścicielem niewbudowanej zmiennej globalnej. Przewodnik Swift dotyczący mixed-language project setup guide opisuje udostępnianie obsługiwanych interfejsów API C++ za pomocą publicznych plików nagłówkowych frameworka albo pliku nagłówkowego mostkującego celu aplikacji.

W celu Xcode, który umożliwia Swift wywoływanie C++, udokumentowane ustawienie to C++ and Objective-C interoperability, z wartością C++ / Objective-C++. Włącz je dla korzystającego z niego celu i udostępnij odpowiedni plik nagłówkowy. To osobny wymóg, niezwiązany z zapewnieniem zmiennej jednej definicji. Zobacz Setting Up Mixed-Language Swift and C++ Projects.

Przestrzegaj udokumentowanych ograniczeń zgodności: interoperacyjność Swift z C++ wymaga Swift 5.9 lub nowszego, kod Swift i C++ w projekcie mieszanym musi korzystać z tej samej biblioteki standardowej C++, a platformy Apple używają libc++. Zaimportowanie pliku nagłówkowego zawierającego zmienną inline z tego artykułu wymaga wybrania C++17 lub nowszego zarówno dla importera, jak i źródeł C++. Swift opisuje ustawienia standardu języka i ograniczenia kompilatora w sekcji Supported Features and Constraints of C++ Interoperability.

Przykłady dotyczą osobno kompilowanych plików C++, które są linkowane do jednego natywnego pliku wykonywalnego. Stosując test adresu, pamiętaj o tym zakresie; osobno ładowane frameworki i biblioteki wymagają własnego projektu eksportu symboli i linkowania. W opisanym zadaniu wskaż definicję będącą właścicielem zmiennej, dołącz ją raz podczas linkowania i sprawdź, czy oba pliki korzystające ze zmiennej widzą ten sam adres i zaktualizowaną wartość.

Źródła