För att bygga ett inbyggt Mac-program från flera C++-filer skickar du alla nödvändiga implementationsfiler till clang++, eller kompilerar dem till objektfiler och länkar ihop dem. Den här genomgången bygger ett litet Terminal-program och förklarar hur du diagnostiserar saknade definitioner.

Förbered kompilatorverktygen för Mac

Använd det här arbetsflödet för ett litet C++-program som du vill köra lokalt i Terminal. Apple tillhandahåller kompilatorn och macOS SDK via antingen Xcode eller det fristående paketet Command Line Tools. Om du redan har Xcode installerat behöver du inte det separata paketet. Kör annars följande kommando och slutför installationsdialogen. Se Apples Installera kommandoradsverktygen.

xcode-select --install

Kontrollera efter installationen att kompilatorn är tillgänglig:

clang++ --version

Använd clang++ för den slutliga länkningen, även när indata redan är kompilerade objektfiler. Clangs dokumentation rekommenderar uttryckligen den här drivrutinen för C++-program eftersom språkläget avgör vilka körtidsbibliotek den länkar automatiskt. Se Sammanställ en komplett verktygskedja.

Bygg ett komplett C++-program med tre filer

Det syntetiska exemplet räknar saker på tre skrivbordsbrickor och formaterar resultatet. Det har en startpunkt och två stödjande implementationsfiler. Skapa en ny, tom arbetskatalog och spara sedan de här fyra filerna direkt i den:

desk-demo/
  desk.hpp
  main.cpp
  label.cpp
  totals.cpp

Spara de gemensamma deklarationerna som 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

Spara startpunkten som main.cpp:

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

int main() {
    const int count = total_items(3, 4);
    std::cout << make_label(count) << '\n';
    return 0;
}

Spara formateringsimplementationen som label.cpp:

#include "desk.hpp"

std::string make_label(int count) {
    return "Desk items: " + std::to_string(count);
}

Spara räkningsimplementationen som totals.cpp:

#include "desk.hpp"

int total_items(int trays, int per_tray) {
    return trays * per_tray;
}

Varje implementation inkluderar samma header, vilket gör att kompilatorn kan kontrollera funktionsdefinitionen mot den gemensamma deklarationen. Header-skyddet förhindrar upprepad inkludering inom en översättningsenhet. Behåll de här vanliga, icke-inline-funktionsdefinitionerna i sina implementationsfiler; C++ ställer definitionskrav för hela programmet genom sin regel om en definition.

Kompilera och länka med ett enda kommando

Öppna Terminal i katalogen som innehåller de fyra filerna och kör:

clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo

Förväntad utdata:

Desk items: 12

Kommandot anger alla tre implementationsfiler och ställer in utdatafilens namn med -o. Eftersom det inte har något alternativ som stoppar ett steg kompilerar Clang indata och anropar länken. Det uttryckliga -std=c++17 väljer språkversionen i stället för att förlita sig på kompilatorns standardvärde. Alternativen finns dokumenterade i Clangs guide till kompilatorkommandon.

Skalet kör den körbara filen endast om bygget lyckas, eftersom kommandona är sammanfogade med &&. Det är viktigt vid ombyggnad: ett misslyckat bygge ska inte följas av att en äldre körbar fil råkar köras och att dess utdata misstas för det nya resultatet.

När passar ett jokertecken?

För just den här katalogen är följande ett alternativ till den uttryckliga källistan:

clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo

Skalet expanderar ./*.cpp till matchande filer i den aktuella katalogen. Det söker inte i underkataloger och tar inte med filer som slutar på .cc eller .cxx. Sätt citattecken runt enskilda sökvägar som innehåller mellanslag när du anger dem uttryckligen.

Använd jokertecknet bara så länge varje matchande fil hör till det här programmet. Om du senare lägger till ett separat demonstrationsprogram med en egen main, eller en alternativ implementation för en viss plattform, ska du behålla en uttrycklig källista. Här betyder ”alla implementationsfiler” alla filer som behövs för ett mål, med varje nödvändig definition tillhandahållen på lämpligt sätt.

Kompilera objektfiler först och länka dem sedan

Om du separerar stegen blir det lättare att se om ett fel uppstår vid kompileringen av en källfil eller när programmet sätts ihop. Kör följande med samma fyra filer och arbetskatalog:

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

Alternativet -c skapar en objektfil utan att länka. Det sista kompilatoranropet tar emot de tre objekten och länkar det körbara programmet. Det följer Clangs dokumenterade kompileringssteg. Underskalets set -e avbryter den här sekvensen om ett kommando misslyckas.

Använd även här uttryckliga objektnamn. Om du länkar varje objekt i en återanvänd byggkatalog kan en föråldrad fil från en tidigare programversion råka komma med. Om två källfiler i olika mappar har samma basnamn ska du ge deras objekt olika sökvägar i stället för att skriva båda till samma utdatafil.

Om du bara ändrar label.cpp kan du kompilera om det objektet och upprepa länkningskommandot. Om du ändrar desk.hpp ska du bygga om alla källfiler som inkluderar den. Med det här manuella arbetsflödet måste du hålla reda på relationerna; när projektet växer är ett byggsystem mer praktiskt.

Diagnostisera felet innan du ändrar byggkommandot

Följande sekvens tillämpar kompileringsstegen och C++-reglerna för definitioner på det här exemplet. Den exakta formuleringen av diagnostikmeddelanden varierar beroende på vilken verktygskedja som är installerad.

Symptom Vad du bör kontrollera först Lämpligt nästa steg
En header rapporteras saknas Inkluderingssyntaxen och headerns plats Återställ den visade katalogstrukturen eller konfigurera rätt sökkatalog för headers.
make_label eller total_items är odefinierad vid länkningen Om dess implementation ingår som indata Lägg till den saknade .cpp-filen eller dess kompilerade objekt vid länkningen.
Funktionen är fortfarande odefinierad trots att dess fil inkluderats Deklarationens och definitionens signaturer Jämför namnrymder, parametertyper och kvalificerare; inkludera den gemensamma headern i implementationen.
Dubbla symboler visas Upprepade implementationsindata Ta bort ett upprepat objekt, en inkluderad .cpp-fil eller en oavsiktlig alternativ definition.
main är odefinierad eller duplicerad Startpunktsfilerna Ta med den avsedda main.cpp exakt en gång och uteslut andra programs startpunkter.
C++-bibliotekssymboler är odefinierade Den slutliga länkningsdrivrutinen Använd clang++ så att C++-körtidsbiblioteken tillhandahålls.

Om du till exempel bara kompilerar main.cpp kan anropen godtas eftersom headern deklarerar båda funktionerna. Den slutliga länkningen behöver fortfarande deras definitioner. Att lägga till ytterligare en header-inkludering tillhandahåller inte det saknade implementationsobjektet. De underliggande kraven förklaras av C++-regeln om en definition.

Försök inte åtgärda saknade symboler genom att inkludera label.cpp inuti main.cpp samtidigt som du kompilerar label.cpp separat. Då tillhandahålls samma vanliga funktionsdefinition via två översättningsenheter. Den gemensamma headern och separata implementationsindata ger redan den avsedda strukturen.

Om du vill granska verktygskommandona utan att köra bygget använder du Clangs dokumenterade alternativ -###. Sätt citattecken runt det så att skalet skickar det oförändrat:

clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo

Det här visar kompilator- och länkningsanropen som drivrutinen väljer, vilket hjälper dig att skilja kommandot du avsåg från kommandot som verktygskedjan skulle köra. Se Sammanställ en komplett verktygskedja.

Håll arkitektur, plattform och ABI kompatibla

Att lägga till alla källfiler kan inte reparera inkompatibla binära indata. Börja med en installerad Apple-verktygskedja och bygg om programmets källfiler tillsammans för en lokal kompilering. Kontrollera vilka plattformar och arkitekturer som stöds innan du ändrar källistorna för att lägga till förkompilerade bibliotek.

För ett valfritt universalbygge dokumenterar Apple hur man kompilerar och länkar separat för varje arkitektur och sedan slår ihop de färdiga körbara filerna med lipo. Med en verktygskedja och SDK som stöder båda målen kan samma exempel byggas så här. Distributionsmålen macOS 11 nedan är illustrativa val:

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

Varje länkat beroende måste tillhandahålla kompatibel indata för motsvarande arkitekturbygge. En iOS-binär blir inte en macOS-binär bara för att båda använder arm64. Apple förklarar arkitektursnitt, plattformsskillnader och granskning av körbara filer i Bygg en universell macOS-binär.

C++-språkversionen skiljer sig också från binärkompatibilitet, eller ABI. Att enbart matcha -std=c++17 säkerställer inte att separat byggda bibliotek har kompatibla layouter och konfigurationer. LLVM dokumenterar konfigurationsflaggor för bibliotek som påverkar ABI och säger att användare inte själva bör ställa in dessa leverantörsflaggor. För den här uppgiften är det praktiskt att behålla verktygskedjans normala bibliotekskonfiguration. Se libc++s ABI-garantier.

Använd samma källista i Xcode eller ett blandat Swift-projekt

I ett befintligt Xcode-mål väljer du målet och öppnar Build Phases. Kontrollera Compile Sources så att implementationsfilerna som behövs för det körbara programmet finns med. Lägg till anpassade biblioteksberoenden via Link Binary with Libraries. Apple dokumenterar de här separata ansvarsområdena i Anpassa byggfaserna för ett mål.

För exemplet ska källistan innehålla main.cpp, label.cpp och totals.cpp. Att lägga till headern i projektet ersätter inte kompilering av implementationerna.

Om det omgivande programmet använder Swift omfattar C++-kommandona ovan endast C++-programmet. Swifts dokumenterade metod kräver att C++-interoperabilitet aktiveras för att API:er ska kunna exponeras mellan språken; endast språkfunktioner som stöds kan passera den gränsen. Se Blanda Swift och C++.

Kod med både Swift och C++ måste använda samma C++-standardbibliotek; på Apple-plattformar använder Swift libc++. Swift dokumenterar också begränsningar för kompilator och språkversion, däribland C++14 eller senare för interoperabilitet. Kontrollera kraven innan du betraktar ett befintligt bibliotek som ett kompatibelt beroende. Se Funktioner och begränsningar som stöds för C++-interoperabilitet.

För den lokala Terminal-uppgiften börjar du med bygget som uttryckligen anger de tre källfilerna. Om det misslyckas identifierar du steget där felet uppstår, kontrollerar den saknade eller upprepade definitionen och verifierar att de binära indatafilerna är kompatibla. När kommandot har gett förväntat resultat har du en konkret källista att ta med in i ett större bygge.

Källor