헤더에서 extern로 전역 변수를 선언하고, 정확히 하나의 .cpp 파일에서 정의한 다음, 해당 파일의 객체 파일을 모든 사용 파일과 함께 링크하세요. 이 가이드에서는 완전한 macOS 빌드, 주소 확인, 그리고 C++17 inline 변수가 적합한 경우를 보여 줍니다.
헤더의 전역 변수는 왜 중복 정의를 일으킬까요?
작은 Mac 네이티브 프로그램에서 해야 할 일은 여러 개별 컴파일 소스 파일이 같은 객체를 읽고 수정하게 하는 것입니다. 먼저 선언과 정의를 구분하세요. extern int itemCount;는 객체를 선언하고, int itemCount = 0;은 객체를 정의합니다. extern에 초기값을 추가하는 것도 extern int itemCount = 0;의 경우처럼 정의가 됩니다. C++ 작업 초안의 선언과 정의에서 이 구분을 설명합니다.
일반적인 네임스페이스 범위 정의가 들어 있는 헤더를 포함하면, 그 정의가 해당 헤더를 포함하는 모든 소스 파일에 들어갈 수 있습니다. 외부 연결을 가진 비인라인, 비템플릿 변수의 경우, 번역 단위 간에 정의가 여러 개 있으면 단일 정의 규칙을 위반합니다. 링커가 중복 심볼을 보고할 수 있지만, 언어 규격은 이런 위반이 모두 진단되도록 요구하지는 않습니다. 작업 초안의 단일 정의 규칙을 참조하세요.
포함 가드는 하나의 번역 단위 안에서 반복 포함을 막습니다. 별도로 컴파일되는 소스 파일을 조정해 주지는 않습니다. 따라서 가드가 있는 헤더도 객체 파일 간 중복 정의를 일으킬 수 있습니다. Clang의 컴파일러 명령 가이드에서는 전처리 중 헤더가 확장되는 과정과 이후 객체 파일을 링크하는 과정을 설명합니다.
Mac에서 공유 전역 변수 예제를 완전히 빌드하기
Xcode 또는 Apple의 Xcode용 Command Line Tools 패키지 중 하나를 사용하세요. Apple은 Xcode 외부에서 개발할 때 독립형 패키지를 대안으로 안내합니다. 전체 Xcode를 설치하면 도구가 이미 포함되어 있습니다. 둘 다 설치되어 있지 않다면 다음 명령을 실행하고 설치 대화상자의 안내를 따르세요. 명령줄 도구 설치하기를 참조하세요.
xcode-select --install
빌드하기 전에 선택된 개발자 디렉터리와 컴파일러를 확인하세요.
xcode-select --print-path
xcrun clang++ --version
Apple의 보관된 Xcode FAQ를 사용한 명령줄 빌드에서는 선택된 개발자 디렉터리를 확인하는 방법과 xcrun을 사용해 개발자 도구를 찾거나 실행하는 방법을 설명합니다. 이 예제 전체에서 같은 선택 도구 체인을 사용하세요.
shared-global-demo이라는 이름의 빈 새 폴더를 만들고 편집기에서 연 다음, 아래 네 파일을 저장하세요. 이 프로그램은 단일 스레드로 실행되는 예제입니다. 한 파일이 전역 변수를 소유하고, 다른 파일이 이를 수정하며, 진입점에서 값과 동일성을 모두 확인합니다.
공유 헤더에서 객체 선언하기
다음 내용을 shared_state.hpp로 저장하세요.
#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP
namespace demo {
extern int itemCount;
void addItem();
int* itemCountAddress();
}
#endif
헤더는 모든 소스 파일이 사용하는 선언을 제공합니다. 네임스페이스와 타입을 하나의 공유 헤더에 두면 서로 어긋날 수 있는 별도의 수기 선언을 관리하지 않아도 됩니다.
객체를 한 번만 정의하기
다음 내용을 shared_state.cpp로 저장하세요.
#include "shared_state.hpp"
namespace demo {
int itemCount = 0;
}
이것은 demo::itemCount의 유일한 정의입니다. 정의하는 소스 파일에서 헤더를 포함하면 컴파일러가 정의와 선언의 일치 여부를 확인할 수 있습니다. 명시적으로 초기값을 지정해 초기 값도 분명하게 나타냅니다.
다른 번역 단위에서 수정하기
다음 내용을 worker.cpp로 저장하세요.
#include "shared_state.hpp"
namespace demo {
void addItem() {
++itemCount;
}
int* itemCountAddress() {
return &itemCount;
}
}
다음 내용을 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;
}
주소 비교는 링크 성공 여부를 넘어 유용한 확인 방법을 제공합니다. worker.cpp은 자신이 보는 주소를 반환하고, main.cpp은 그 주소를 자신의 주소와 비교합니다. 종료 상태로 수정된 값도 확인합니다. 두 검사는 모두 어설션 활성화 여부에 의존하지 않습니다.
각 파일을 따로 컴파일한 다음 객체 파일 세 개를 모두 링크하기
Terminal에서 네 파일이 있는 폴더로 이동하세요. 다음 빌드 블록 전체를 실행하세요.
(
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 문서에 따르면 -c는 링크하지 않고 객체 파일을 생성합니다. 단계를 선택하는 옵션을 생략하면 드라이버가 링크를 수행합니다. 명시적인 -std=c++17은 언어 모드를 선택합니다. Clang 컴파일러 명령 가이드를 참조하세요. 서브셸은 명령 하나라도 실패하면 중단되므로, 빌드 실패 후 오래된 실행 파일이 실행되는 일을 막습니다.
소스에서 도출한 예상 출력은 다음과 같습니다. Mac에서 직접 실행해 캡처한 결과는 아닙니다.
before: 0
after: 1
same object: true
조사 환경에는 Mac 컴파일러가 제공되지 않아 이 예제는 실행하지 않고 검토했습니다. 세 가지 컴파일 명령은 객체 파일 경계를 명확히 보여 줍니다. shared_state.o은 정의를 제공하고, worker.o과 main.o은 이를 사용합니다.
C++17 인라인 변수는 언제 사용해야 할까요?
변수 정의를 공유 헤더에 두어야 할 때 C++17 인라인 변수를 선택하세요. Clang은 C++17 언어 기능 중 하나로 인라인 변수를 소개합니다. 언어 규격에 따르면 외부 연결을 가진 인라인 변수는 여러 번역 단위에서 정의할 수 있으며, 하나의 주소를 가진 단일 개체로 유지됩니다. 인라인 지정자를 참조하세요.
같은 예제에서 shared_state.hpp의 내용을 전부 다음 버전으로 바꾸세요.
#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP
namespace demo {
inline int itemCount = 0;
void addItem();
int* itemCountAddress();
}
#endif
worker.cpp와 main.cpp은 그대로 두세요. 이 변형의 빌드에서는 shared_state.cpp을 제거하세요. 별도 정의가 더는 필요하지 않습니다. 두 사용 파일을 다시 컴파일하고 해당 객체 파일만 링크하세요.
(
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
)
이 변형의 예상 출력도 동일합니다. 링크 명령에 기존 shared_state.o을 포함하지 마세요. 명시적인 링크 목록에서 사용하지 않는 객체 파일을 빼면 소유권의 변경을 명확히 확인할 수 있습니다.
인라인 정의는 토큰 순서와 대응하는 이름 조회를 포함해 단일 정의 규칙의 일관성 요건을 충족해야 합니다. 소스 파일마다 서로 다른 초기값을 만들 수 있는 매크로는 피하세요. 링크가 성공했다고 해서 규칙을 준수한다는 뜻은 아닙니다. 단일 정의 규칙을 참조하세요.
| 요건 | 적합한 방법 | 핵심 조건 |
|---|---|---|
| 하나의 구현 파일이 변경 가능한 상태를 소유함 | extern을 사용한 헤더 선언과 하나의 .cpp 정의 |
소유 객체를 정확히 한 번 링크 |
| 공유 정의를 헤더에 둬야 함 | C++17 인라인 변수 | 사용하는 모든 번역 단위에서 호환되는 언어 모드와 일관된 정의를 사용 |
| 각 소스 파일이 의도적으로 별도의 상태를 소유함 | 네임스페이스 범위의 static 또는 이름 없는 네임스페이스 |
내부 연결을 만들므로 공유 객체 작업에는 적합하지 않음 |
내부 연결에 관한 구분은 작업 초안의 프로그램과 연결을 따릅니다. 이 작은 다중 파일 프로그램에서는 extern 방식으로 소유권을 특히 쉽게 확인할 수 있습니다.
컴파일러 플래그를 바꾸기 전에 오류 진단하기
이 순서를 사용하면 소유권 문제와 빌드 입력 누락을 구분할 수 있습니다. 확인 방법은 위에서 설명한 선언 규칙과 컴파일 단계를 따릅니다. 진단 문구는 도구 체인에 따라 다를 수 있습니다.
- 컴파일에 실패하면 먼저 선언을 확인하세요. 각 소스가
shared_state.hpp을 포함하고, 같은 네임스페이스, 타입, 철자를 사용하는지 확인하세요. 링크로는 선언되지 않은 이름이나 서로 충돌하는 선언을 바로잡을 수 없습니다. - 링커가 중복 심볼을 보고하면 정의와 링크 입력의 개수를 확인하세요. 헤더의 초기값, 두 번째
.cpp정의, 또는 동일한 소유 객체가 두 번 지정되었는지 살펴보세요. 인라인 변형에서는 기존 비인라인 정의가 없는지 확인하세요. - 링커가 정의되지 않은 심볼을 보고하면 소유 파일이 빠졌는지 확인하세요.
extern버전에서shared_state.cpp이 성공적으로 컴파일되었고shared_state.o가 최종 링크 명령에 포함되었는지 확인하세요. 선언만으로는 정의를 제공하지 않습니다. - 링크는 성공했지만 동일성 검사가 실패하면 연결을 확인하세요. 네임스페이스 범위의
static변수나 이름 없는 네임스페이스 안의 정의를 찾아보세요. 이들은 각 번역 단위마다 별개의 객체를 만듭니다. - 헤더나 소유권 방식을 변경한 뒤에는 영향을 받는 객체 파일을 모두 다시 빌드하세요. 적용 가능한 빌드 블록 전체를 다시 실행해 호출 파일과 정의 파일이 최신 선언을 사용하도록 하세요.
두 가지 간편한 방법은 특히 주의해야 합니다. 첫째, 헤더에서 전역 변수를 네임스페이스 범위의 static int itemCount = 0;으로 바꾸면 번역 단위마다 별도의 객체가 생깁니다. 둘째, 일반적인 네임스페이스 범위 const 변수는 명시적인 extern 선언이나 inline와 같은 예외가 적용되지 않는 한 대개 내부 연결을 가집니다. 의도한 연결을 신중히 선택하세요. 관련 예외는 프로그램과 연결에 나와 있습니다.
언어 연결도 유지하세요. extern "C"는 C 언어 연결을 지정할 뿐, 누락된 정의를 제공하거나 중복 정의를 허용하지 않습니다. 이 C++ 전용 예제에서는 기본 C++ 연결을 유지하세요. 작업 초안의 연결 지정에서는 언어 연결과 구현별 이름 지정 및 호출 규약 속성을 설명합니다.
Mac 앱에 Swift도 포함되면 무엇이 달라질까요?
C++ 저장 방식은 여전히 C++ 구현에 속합니다. 헤더를 Swift로 가져오면 인터페이스가 제공되지만, 비인라인 전역 변수를 소유하는 C++ 구현도 빌드에 포함해야 합니다. Swift의 혼합 언어 프로젝트 설정 가이드에서는 프레임워크 공개 헤더나 앱 타깃의 브리징 헤더를 통해 지원되는 C++ API를 노출하는 방법을 설명합니다.
Swift에서 C++를 호출해야 하는 Xcode 타깃에는 문서에 나온 설정인 C++ and Objective-C interoperability를 C++ / Objective-C++로 지정해야 합니다. 이를 호출하는 타깃에서 활성화하고 적절한 헤더를 노출하세요. 이는 전역 변수를 하나만 정의하는 것과 별개의 요건입니다. Swift와 C++ 혼합 언어 프로젝트 설정하기를 참조하세요.
문서에 기재된 호환성 제한을 지키세요. Swift C++ 상호 운용성에는 Swift 5.9 이상이 필요하고, 혼합 Swift 및 C++ 코드는 같은 C++ 표준 라이브러리를 사용해야 하며, Apple 플랫폼은 libc++를 사용합니다. 이 문서의 인라인 변수가 들어 있는 헤더를 가져오려면 C++ 소스뿐 아니라 가져오는 쪽에서도 C++17 이상을 선택해야 합니다. Swift는 C++ 상호 운용성의 지원 기능과 제약 사항에서 언어 표준 제어 및 컴파일러 제약을 설명합니다.
여기서 다룬 예제는 하나의 네이티브 실행 파일에 링크되는 별도 컴파일 C++ 파일을 대상으로 합니다. 주소 확인을 적용할 때도 이 범위를 유지하세요. 별도로 로드되는 프레임워크와 라이브러리에는 각각의 내보내기 및 링크 설계가 필요합니다. 여기서 보여 준 작업에서는 정의를 소유하는 파일을 확인하고, 그 정의를 링크에 한 번 포함하며, 두 호출자가 같은 주소와 수정된 값을 보는지 확인하세요.
출처
- 명령줄 도구 설치하기 — Apple.
- Xcode FAQ를 사용한 명령줄 빌드 — Apple 문서 아카이브.
- clang — Clang C, C++, Objective-C 컴파일러 — Clang/LLVM.
- Clang의 C++ 지원 — Clang/LLVM.
- 선언과 정의 — eel.is에서 호스팅하는 C++ 작업 초안.
- 단일 정의 규칙 — eel.is에서 호스팅하는 C++ 작업 초안.
- 인라인 지정자 — eel.is에서 호스팅하는 C++ 작업 초안.
- 프로그램과 연결 — eel.is에서 호스팅하는 C++ 작업 초안.
- 연결 지정 — eel.is에서 호스팅하는 C++ 작업 초안.
- Swift와 C++ 혼합 언어 프로젝트 설정하기 — Swift.org.
- C++ 상호 운용성의 지원 기능과 제약 사항 — Swift.org.