여러 C++ 파일에서 네이티브 Mac 실행 파일을 만들려면 필요한 모든 구현 파일을 clang++에 전달하거나, 파일을 객체 파일로 컴파일한 뒤 함께 링크하세요. 이 안내에서는 간단한 Terminal 프로그램을 빌드하고 정의가 누락되었을 때 진단하는 방법을 설명합니다.
Mac 컴파일러 도구 준비
이 절차는 Terminal에서 로컬로 실행할 작은 C++ 프로그램을 위한 것입니다. Apple은 Xcode 또는 별도의 Command Line Tools 패키지를 통해 컴파일러와 macOS SDK를 제공합니다. 이미 Xcode가 설치되어 있다면 별도 패키지는 필요하지 않습니다. 그렇지 않다면 다음 명령을 실행하고 설치 대화상자를 완료하세요. Apple의 명령줄 도구 설치를 참조하세요.
xcode-select --install
설치 후 컴파일러를 사용할 수 있는지 확인하세요.
clang++ --version
최종 링크에는 입력 파일이 이미 컴파일된 객체 파일인 경우에도 clang++를 사용하세요. Clang 문서는 언어 모드에 따라 자동으로 링크되는 런타임 라이브러리가 결정되므로 C++ 프로그램에는 이 드라이버를 사용할 것을 명시적으로 권장합니다. 완전한 툴체인 구성을 참조하세요.
세 파일로 된 완전한 C++ 프로그램 빌드
이 가상의 예제는 책상 위 세 개의 쟁반에 놓인 물품 수를 세고 결과를 서식에 맞게 출력합니다. 진입점 하나와 이를 지원하는 구현 파일 두 개로 구성됩니다. 비어 있는 새 작업 디렉터리를 만든 다음, 다음 네 파일을 그 안에 바로 저장하세요.
desk-demo/
desk.hpp
main.cpp
label.cpp
totals.cpp
공유 선언을 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
진입점을 main.cpp로 저장하세요.
#include "desk.hpp"
#include <iostream>
int main() {
const int count = total_items(3, 4);
std::cout << make_label(count) << '\n';
return 0;
}
서식 지정 구현을 label.cpp로 저장하세요.
#include "desk.hpp"
std::string make_label(int count) {
return "Desk items: " + std::to_string(count);
}
계산 구현을 totals.cpp로 저장하세요.
#include "desk.hpp"
int total_items(int trays, int per_tray) {
return trays * per_tray;
}
각 구현 파일은 같은 헤더를 포함하므로 컴파일러가 함수 정의를 공유 선언과 대조해 검사할 수 있습니다. 헤더 가드는 하나의 번역 단위 안에서 반복 포함되는 것을 방지합니다. 일반적인 비인라인 함수 본문은 구현 파일에 두세요. C++에서는 단일 정의 규칙에 따라 프로그램 전체에서 정의 요건을 적용합니다.
한 명령으로 컴파일 및 링크
네 파일이 있는 디렉터리에서 Terminal을 열고 다음을 실행하세요.
clang++ -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo && ./desk-demo
예상 출력은 다음과 같습니다.
Desk items: 12
이 명령은 세 구현 파일을 모두 지정하고 -o로 출력 파일 이름을 설정합니다. 단계를 중단하는 옵션이 없으므로 Clang은 입력 파일을 컴파일한 뒤 링커를 실행합니다. 명시적인 -std=c++17 옵션은 컴파일러 기본값에 의존하지 않고 언어 버전을 선택합니다. 이 옵션은 Clang 컴파일러 명령 안내서에 문서화되어 있습니다.
명령이 &&로 연결되어 있으므로 빌드가 성공한 경우에만 셸이 실행 파일을 실행합니다. 다시 빌드할 때는 이 점이 중요합니다. 빌드가 실패했는데도 이전 실행 파일이 실수로 실행되어 그 출력을 새 결과로 착각하는 일을 막아야 합니다.
와일드카드는 언제 적절한가요?
이 특정 디렉터리에서는 다음 명령을 명시적 소스 목록 대신 사용할 수 있습니다.
clang++ -std=c++17 ./*.cpp -o desk-demo && ./desk-demo
셸은 ./*.cpp를 현재 디렉터리에서 일치하는 파일 목록으로 확장합니다. 하위 폴더를 검색하지 않으며 .cc 또는 .cxx로 끝나는 파일도 포함하지 않습니다. 경로를 직접 지정할 때 공백이 포함된 개별 경로는 따옴표로 묶으세요.
일치하는 모든 파일이 이 실행 파일에 속할 때만 와일드카드를 사용하세요. 나중에 자체 main가 있는 별도 데모 프로그램이나 플랫폼별 대체 구현을 추가한다면 명시적인 소스 목록을 사용하세요. 여기서 “모든 구현 파일”이란 하나의 대상에 필요한 모든 파일을 뜻하며, 필요한 각 정의가 적절히 제공되어야 합니다.
먼저 객체 파일을 컴파일한 다음 링크
단계를 분리하면 한 소스 파일을 컴파일할 때 실패했는지, 프로그램을 결합할 때 실패했는지 더 쉽게 확인할 수 있습니다. 같은 네 파일과 작업 디렉터리에서 다음을 실행하세요.
(
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
)
-c 옵션은 링크하지 않고 객체 파일을 생성합니다. 마지막 컴파일러 호출은 객체 파일 세 개를 받아 실행 파일로 링크합니다. 이는 Clang에 문서화된 컴파일 단계를 따릅니다. 서브셸의 set -e는 명령 실행 중 하나라도 실패하면 순차 실행을 중단합니다.
여기서도 객체 파일 이름을 명시적으로 지정하세요. 재사용하는 빌드 디렉터리의 모든 객체 파일을 링크하면 이전 버전 프로그램의 오래된 파일이 실수로 포함될 수 있습니다. 서로 다른 폴더에 있는 두 소스 파일의 기본 이름이 같다면, 두 파일을 같은 출력 파일 이름으로 저장하지 말고 객체 파일 경로를 서로 다르게 지정하세요.
label.cpp만 변경했다면 해당 객체 파일을 다시 컴파일한 다음 링크 명령을 반복하면 됩니다. desk.hpp을 변경했다면 이를 포함하는 모든 소스를 다시 빌드하세요. 이 수동 절차에서는 이러한 관계를 직접 추적해야 합니다. 프로젝트 규모가 커지면 빌드 시스템을 사용하는 편이 더 실용적입니다.
빌드 명령을 바꾸기 전에 실패 원인 진단
다음 순서에 따라 이 예제에 컴파일 단계와 C++ 정의 규칙을 적용해 보세요. 정확한 진단 문구는 설치된 툴체인에 따라 달라집니다.
| 증상 | 먼저 확인할 사항 | 다음 조치 |
|---|---|---|
| 헤더를 찾을 수 없다고 표시됨 | include 표기와 헤더 위치 | 제시된 디렉터리 구조를 복원하거나 올바른 헤더 검색 디렉터리를 설정하세요. |
링크 중 make_label 또는 total_items이 정의되지 않았다고 표시됨 |
해당 구현 파일이 입력에 포함되었는지 여부 | 누락된 .cpp 파일이나 컴파일된 객체 파일을 링크에 추가하세요. |
| 해당 파일을 포함했는데도 함수가 정의되지 않았다고 표시됨 | 선언과 정의의 시그니처 | 네임스페이스, 매개변수 유형, 한정자를 비교하고 구현 파일에 공유 헤더를 포함하세요. |
| 중복 기호가 표시됨 | 구현 파일 입력이 반복되었는지 여부 | 중복 객체 파일, 포함된 .cpp 파일 또는 의도치 않은 대체 정의를 제거하세요. |
main이 정의되지 않았거나 중복되었다고 표시됨 |
진입점 파일 | 의도한 main.cpp을 정확히 한 번 포함하고 다른 프로그램의 진입점은 제외하세요. |
| C++ 라이브러리 기호가 정의되지 않았다고 표시됨 | 최종 링크 드라이버 | C++ 런타임을 제공하도록 clang++를 사용하세요. |
예를 들어 main.cpp만 컴파일해도 헤더에 두 함수가 모두 선언되어 있으므로 함수 호출이 허용될 수 있습니다. 하지만 최종 링크에는 여전히 해당 함수의 정의가 필요합니다. 헤더를 한 번 더 포함해도 누락된 구현 객체 파일은 제공되지 않습니다. 관련 요건은 C++ 단일 정의 규칙에 설명되어 있습니다.
누락된 기호를 해결하려고 label.cpp을 main.cpp 안에 포함하는 동시에 label.cpp도 독립적으로 컴파일하지 마세요. 그러면 동일한 일반 함수 정의가 두 번역 단위를 통해 제공됩니다. 공유 헤더와 별도의 구현 파일 입력만으로 의도한 구조를 만들 수 있습니다.
빌드를 실행하지 않고 도구 명령을 살펴보려면 Clang에 문서화된 -### 옵션을 사용하세요. 셸이 옵션을 그대로 전달하도록 따옴표로 묶으세요.
clang++ '-###' -std=c++17 main.cpp label.cpp totals.cpp -o desk-demo
이 옵션은 드라이버가 선택한 컴파일러 및 링커 호출을 보여 주므로, 의도한 명령과 툴체인이 실제로 실행할 명령을 구분하는 데 도움이 됩니다. 완전한 툴체인 구성을 참조하세요.
아키텍처, 플랫폼 및 ABI 호환성 유지
모든 소스 파일을 추가해도 호환되지 않는 바이너리 입력은 해결할 수 없습니다. 로컬 빌드에서는 설치된 Apple 툴체인 하나를 기준으로 시작하고 프로그램의 소스를 함께 다시 빌드하세요. 사전 빌드된 라이브러리를 추가할 때는 소스 목록을 바꾸기 전에 지원 플랫폼과 아키텍처를 확인하세요.
선택 사항인 유니버설 빌드의 경우 Apple은 아키텍처별로 따로 컴파일하고 링크한 다음, 완성된 실행 파일을 lipo으로 병합하는 방법을 안내합니다. 두 대상을 모두 지원하는 툴체인과 SDK를 사용한다면 같은 샘플을 다음과 같이 빌드할 수 있습니다. 아래 macOS 11 배포 대상은 예시입니다.
(
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
)
링크되는 모든 종속성은 해당 아키텍처 빌드에 사용할 수 있는 호환 입력을 제공해야 합니다. 둘 다 arm64을 사용한다고 해서 iOS 바이너리가 macOS 바이너리로 바뀌지는 않습니다. Apple은 유니버설 macOS 바이너리 빌드에서 아키텍처 슬라이스, 플랫폼 구분 및 실행 파일 검사 방법을 설명합니다.
C++ 언어 버전은 바이너리 호환성, 즉 ABI와도 별개입니다. -std=c++17이 일치한다는 사실만으로 별도로 빌드된 라이브러리의 레이아웃과 구성이 호환된다고 볼 수는 없습니다. LLVM은 ABI에 영향을 주는 라이브러리 구성 플래그를 문서화하며, 사용자가 해당 공급업체 플래그를 직접 설정하지 않아야 한다고 안내합니다. 이 작업에서는 툴체인의 기본 라이브러리 구성을 유지하는 것이 실용적입니다. libc++의 ABI 보장을 참조하세요.
Xcode 또는 Swift 혼합 프로젝트에서 같은 소스 목록 적용
기존 Xcode 대상에서는 대상을 선택한 다음 Build Phases를 여세요. Compile Sources에서 해당 실행 파일에 필요한 구현 파일을 확인하세요. 사용자 지정 라이브러리 종속성은 Link Binary with Libraries를 통해 추가하세요. Apple은 대상의 빌드 단계 사용자화에서 이 두 가지 역할을 별도로 설명합니다.
이 샘플의 소스 목록에는 main.cpp, label.cpp, totals.cpp이 포함되어야 합니다. 프로젝트에 헤더를 추가하는 것만으로 구현 파일을 컴파일할 필요가 없어지는 것은 아닙니다.
주변 애플리케이션이 Swift를 사용한다면 위의 순수 C++ 명령은 C++ 프로그램에만 해당합니다. Swift 문서에 따르면 언어 간 API를 노출하려면 C++ 상호 운용성을 활성화해야 하며, 지원되는 언어 기능만 경계를 넘어 사용할 수 있습니다. Swift와 C++ 혼합을 참조하세요.
Swift와 C++를 혼합한 코드는 같은 C++ 표준 라이브러리를 사용해야 합니다. Apple 플랫폼에서 Swift는 libc++를 사용합니다. Swift 문서에는 상호 운용성에 C++14 이상이 필요한 점을 포함해 컴파일러 및 언어 버전 제약도 설명되어 있습니다. 기존 라이브러리를 호환 가능한 종속성으로 간주하기 전에 이 요건을 확인하세요. C++ 상호 운용성의 지원 기능 및 제약을 참조하세요.
로컬 Terminal 작업에서는 명시적인 세 소스 빌드부터 시작하세요. 실패하면 실패한 단계를 파악하고, 누락되거나 중복된 정의를 확인한 다음, 바이너리 입력의 호환성을 검증하세요. 해당 명령으로 예상 결과가 나오면 더 큰 빌드에 적용할 구체적인 소스 목록이 마련된 것입니다.
출처
- 명령줄 도구 설치 — Apple
- clang — Clang C, C++ 및 Objective-C 컴파일러 — LLVM Project
- 완전한 툴체인 구성 — LLVM Project
- 단일 정의 규칙 — C++ Working Draft
- 유니버설 macOS 바이너리 빌드 — Apple
- libc++의 ABI 보장 — LLVM Project
- 대상의 빌드 단계 사용자화 — Apple
- Swift와 C++ 혼합 — Swift.org
- C++ 상호 운용성의 지원 기능 및 제약 — Swift.org