Maak een native Mac-programma uit meerdere C++-bestanden door alle benodigde implementatiebestanden aan clang++ mee te geven, of compileer ze naar objectbestanden en koppel die vervolgens aan elkaar. In deze uitleg bouwen we een klein Terminal-programma en leggen we uit hoe je ontbrekende definities opspoort.
De compilertools voor de Mac voorbereiden
Gebruik deze werkwijze voor een klein C++-programma dat je lokaal in Terminal wilt uitvoeren. Apple levert de compiler en macOS SDK via Xcode of het zelfstandige pakket Command Line Tools. Als Xcode al is geïnstalleerd, heb je het aparte pakket niet nodig. Voer anders de volgende opdracht uit en voltooi de installatiedialoog. Zie Apples De opdrachtregeltools installeren.
xcode-select --install
Controleer na de installatie of de compiler beschikbaar is:
clang++ --version
Gebruik clang++ voor de uiteindelijke koppeling, ook als je invoer al gecompileerde objectbestanden zijn. De documentatie van Clang beveelt deze driver expliciet aan voor C++-programma's, omdat de taalmodus bepaalt welke runtimebibliotheken automatisch worden gekoppeld. Zie Een complete toolchain samenstellen.
Een volledig C++-programma met drie bestanden bouwen
Het fictieve voorbeeld telt items op drie bureaubladen en maakt de opmaak van het resultaat. Het bevat één toegangspunt en twee ondersteunende implementatiebestanden. Maak een nieuwe, lege werkmap en sla deze vier bestanden er rechtstreeks in op:
desk-demo/
desk.hpp
main.cpp
label.cpp
totals.cpp
Sla de gedeelde declaraties op als desk.hpp:
#ifndef DESK_DEMO_DESK_HPP
#define DESK_DEMO_DESK_HPP
#include <string>
std::string make_label(int count);
int total_items(int trays, int per_tray);
#endif
Sla het toegangspunt op als main.cpp:
#include "desk.hpp"
#include <iostream>
int main() {
const int count = total_items(3, 4);
std::cout << make_label(count) << '\n';
return 0;
}
Sla de implementatie voor de opmaak op als label.cpp:
#include "desk.hpp"
std::string make_label(int count) {
return "Desk items: " + std::to_string(count);
}
Sla de implementatie voor het tellen op als totals.cpp:
#include "desk.hpp"
int total_items(int trays, int per_tray) {
return trays * per_tray;
}
Elk implementatiebestand bevat hetzelfde headerbestand, zodat de compiler de functiedefinitie kan controleren aan de hand van de gedeelde declaratie. De header guard voorkomt herhaalde inclusie binnen één translation unit. Houd de gewone, niet-inline functie-implementaties in hun implementatiebestanden; de C++-regels stellen voor het hele programma definitievereisten via de one-definition rule.
In één opdracht compileren en koppelen
Open Terminal in de map met de vier bestanden en voer het volgende uit:
clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo
De verwachte uitvoer is:
Desk items: 12
De opdracht vermeldt alle drie de implementatiebestanden en stelt met -o de naam van het uitvoerbestand in. Omdat er geen optie is die de compilatiefase voortijdig beëindigt, compileert Clang de invoerbestanden en start het de linker. Met -std=c++17 selecteer je expliciet de taalversie, in plaats van te vertrouwen op een standaardinstelling van de compiler. Deze opties zijn beschreven in de Handleiding voor Clang-compileropdrachten.
De shell voert het programma alleen uit als het bouwen slaagt, omdat de opdrachten met && zijn verbonden. Dit is belangrijk bij opnieuw bouwen: na een mislukte build mag niet per ongeluk een oudere executable worden gestart, waarvan de uitvoer dan voor het nieuwe resultaat wordt aangezien.
Wanneer is een jokerteken geschikt?
Voor precies deze map is het volgende een alternatief voor de expliciete lijst met bronbestanden:
clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo
De shell breidt ./*.cpp uit naar bestanden in de huidige map die overeenkomen met het patroon. Er wordt niet gezocht in onderliggende mappen en bestanden die eindigen op .cc of .cxx worden niet meegenomen. Zet afzonderlijke paden tussen aanhalingstekens als de naam spaties bevat.
Gebruik het jokerteken alleen zolang elk overeenkomend bestand bij deze executable hoort. Voeg je later een apart demonstratieprogramma met een eigen main toe, of een alternatieve platformgebonden implementatie, gebruik dan een expliciete lijst met bronbestanden. Hier betekent ‘alle implementatiebestanden’: alle bestanden die nodig zijn voor één doel, waarbij elke vereiste definitie op de juiste manier wordt aangeleverd.
Eerst objectbestanden compileren en ze daarna koppelen
Als je de fasen scheidt, kun je makkelijker zien of een fout optreedt tijdens het compileren van een bronbestand of tijdens het samenvoegen van het programma. Voer met dezelfde vier bestanden en werkmap het volgende uit:
(
set -e
mkdir -p build
clang++ -std=c++17 -c main.cpp -o build/main.o
clang++ -std=c++17 -c label.cpp -o build/label.o
clang++ -std=c++17 -c totals.cpp -o build/totals.o
clang++ build/main.o build/label.o build/totals.o -o build/desk-demo
./build/desk-demo
)
Met de optie -c maak je een objectbestand zonder te koppelen. De laatste compileraanroep krijgt de drie objectbestanden en koppelt ze tot de executable. Dit volgt Clangs gedocumenteerde compilatiefasen. De set -e in de subshell breekt deze opeenvolgende opdrachten af als er een mislukt.
Gebruik ook hier expliciete objectbestandsnamen. Als je alle objectbestanden in een hergebruikte buildmap koppelt, kan daar per ongeluk een verouderd bestand van een eerdere programmaversie tussen zitten. Als bronbestanden in verschillende mappen dezelfde basisnaam hebben, geef hun objectbestanden dan verschillende paden in plaats van beide naar hetzelfde uitvoerbestand te schrijven.
Als je alleen label.cpp wijzigt, kun je dat objectbestand opnieuw compileren en de koppelopdracht herhalen. Wijzig je desk.hpp, bouw dan elk bronbestand opnieuw dat dit bestand bevat. In deze handmatige werkwijze moet je die relaties zelf bijhouden; zodra het project groeit, is een buildsysteem praktischer.
De fout opsporen voordat je de buildopdracht wijzigt
In de volgende stappen worden de compilatiefasen en C++-definitieregels op dit voorbeeld toegepast. De precieze formulering van foutmeldingen verschilt per geïnstalleerde toolchain.
| Symptoom | Wat je eerst controleert | Geschikte volgende stap |
|---|---|---|
| Er wordt gemeld dat een header ontbreekt | De schrijfwijze van de include en de locatie van het headerbestand | Herstel de getoonde mapindeling of stel de juiste map voor headerbestanden in. |
make_label of total_items is niet gedefinieerd tijdens het koppelen |
Of de implementatie als invoer is opgegeven | Voeg het ontbrekende .cpp-bestand of het gecompileerde objectbestand toe aan de koppeling. |
| De functie is nog steeds niet gedefinieerd, ook al is het bestand opgenomen | De declaratie en de signatuur van de definitie | Vergelijk namespaces, parametertypen en qualifiers; neem de gedeelde header op in de implementatie. |
| Er verschijnen dubbele symbolen | Herhaalde implementatie-invoer | Verwijder een herhaald objectbestand, een opgenomen .cpp-bestand of een onbedoelde alternatieve definitie. |
main is niet gedefinieerd of komt dubbel voor |
De bestanden met het toegangspunt | Neem het bedoelde main.cpp precies één keer op en sluit de toegangspunten van andere programma's uit. |
| C++-bibliotheeksymbolen zijn niet gedefinieerd | De driver voor de uiteindelijke koppeling | Gebruik clang++, zodat de C++-runtimebibliotheken worden toegevoegd. |
Als je bijvoorbeeld alleen main.cpp compileert, kunnen de functieaanroepen worden geaccepteerd omdat de header beide functies declareert. De uiteindelijke koppeling heeft hun definities nog steeds nodig. Een extra header opnemen levert het ontbrekende implementatieobject niet aan. De onderliggende vereisten worden toegelicht in de C++ one-definition rule.
Probeer ontbrekende symbolen niet op te lossen door label.cpp op te nemen in main.cpp terwijl je label.cpp ook afzonderlijk compileert. Zo wordt dezelfde gewone functiedefinitie via twee translation units aangeleverd. De gedeelde header en afzonderlijke implementatiebestanden bieden al de bedoelde structuur.
Gebruik Clangs gedocumenteerde optie -### om de toolopdrachten te bekijken zonder de build uit te voeren. Zet de optie tussen aanhalingstekens zodat de shell deze ongewijzigd doorgeeft:
clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo
Hiermee krijg je de compiler- en linkeropdrachten te zien die de driver kiest. Zo kun je het commando dat je bedoelde vergelijken met het commando dat de toolchain zou uitvoeren. Zie Een complete toolchain samenstellen.
Architectuur, platform en ABI compatibel houden
Alle bronbestanden toevoegen kan incompatibele binaire invoer niet herstellen. Begin voor een lokale build met één geïnstalleerde Apple-toolchain en bouw de programmabronbestanden samen opnieuw. Controleer bij het toevoegen van vooraf gebouwde bibliotheken welke platforms en architecturen ze ondersteunen voordat je de lijst met bronbestanden wijzigt.
Voor een optionele universele build documenteert Apple hoe je afzonderlijk voor elke architectuur compileert en koppelt, en daarna de voltooide executables samenvoegt met lipo. Met een toolchain en SDK die beide doelen ondersteunen, kun je hetzelfde voorbeeld als volgt bouwen. De hieronder vermelde implementatiedoelen voor macOS 11 zijn illustratieve keuzes:
(
set -e
mkdir -p build
clang++ -std=c++17 -target arm64-apple-macos11 \
main.cpp label.cpp totals.cpp -o build/desk-arm64
clang++ -std=c++17 -target x86_64-apple-macos11 \
main.cpp label.cpp totals.cpp -o build/desk-x86_64
lipo -create build/desk-arm64 build/desk-x86_64 \
-output build/desk-universal
lipo -archs build/desk-universal
)
Elke gekoppelde afhankelijkheid moet een compatibele invoer leveren voor de betreffende architectuurbouw. Een iOS-binair bestand wordt niet ineens een macOS-binair bestand omdat ze allebei arm64 gebruiken. Apple legt architectuurslices, platformverschillen en het inspecteren van executables uit in Een universeel macOS-binair bestand bouwen.
De C++-taalversie staat ook los van binaire compatibiliteit, oftewel ABI. Alleen dezelfde -std=c++17 gebruiken bewijst niet dat afzonderlijk gebouwde bibliotheken compatibele indelingen en configuraties hebben. LLVM documenteert configuratievlaggen voor bibliotheken die de ABI beïnvloeden en stelt dat gebruikers deze leveranciersvlaggen niet zelf moeten instellen. Voor deze taak is het praktisch om de normale bibliotheekconfiguratie van de toolchain te behouden. Zie ABI-garanties van libc++.
Dezelfde lijst met bronbestanden gebruiken in Xcode of een gemengd Swift-project
Selecteer in een bestaand Xcode-doel het doel en open Build Phases. Controleer bij Compile Sources of alle implementatiebestanden staan die voor de executable nodig zijn. Voeg aangepaste bibliotheekafhankelijkheden toe via Link Binary with Libraries. Apple beschrijft deze afzonderlijke verantwoordelijkheden in De buildfasen van een doel aanpassen.
Voor het voorbeeld moet de lijst met bronbestanden rekening houden met main.cpp, label.cpp en totals.cpp. Alleen het headerbestand aan het project toevoegen vervangt het compileren van de implementaties niet.
Als de omliggende applicatie Swift gebruikt, geldt dat de C++-opdrachten hierboven alleen het C++-programma bouwen. Volgens de documentatie van Swift moet C++-interoperabiliteit worden ingeschakeld om API's tussen de talen beschikbaar te maken; alleen ondersteunde taalfuncties kunnen die grens oversteken. Zie Swift en C++ combineren.
Gemengde Swift- en C++-code moet dezelfde C++-standaardbibliotheek gebruiken; op Apple-platforms gebruikt Swift libc++. Swift documenteert ook beperkingen voor compiler- en taalversies, waaronder C++14 of nieuwer voor interoperabiliteit. Controleer deze vereisten voordat je een bestaande bibliotheek als compatibele afhankelijkheid behandelt. Zie Ondersteunde functies en beperkingen van C++-interoperabiliteit.
Begin voor de lokale Terminal-taak met de expliciete build met drie bronbestanden. Identificeer bij een fout in welke fase deze optreedt, controleer of er een definitie ontbreekt of dubbel voorkomt en controleer of de binaire invoer compatibel is. Zodra die opdracht het verwachte resultaat oplevert, heb je een concrete lijst met bronbestanden die je kunt overnemen in een grotere build.
Bronnen
- De opdrachtregeltools installeren — Apple
- clang — de Clang C-, C++- en Objective-C-compiler — LLVM Project
- Een complete toolchain samenstellen — LLVM Project
- One-definition rule — C++-werkdocument
- Een universeel macOS-binair bestand bouwen — Apple
- ABI-garanties van libc++ — LLVM Project
- De buildfasen van een doel aanpassen — Apple
- Swift en C++ combineren — Swift.org
- Ondersteunde functies en beperkingen van C++-interoperabiliteit — Swift.org