포스트

빌드 ⑧ 컴파일·링킹과 CMake

문법을 넘어 프로젝트를 빌드하는 관점. 소스가 실행 파일이 되는 컴파일·링킹 과정, undefined reference 링커 에러의 정체, 그리고 Make와 CMake로 여러 파일 프로젝트를 빌드하는 법까지 정리하며 로드맵을 마무리한다.

빌드 ⑧ 컴파일·링킹과 CMake

모던 C++ 학습 로드맵마지막(빌드) 단계입니다. 앞 글: ⑦ auto·constexpr와 표준 라이브러리

파일 하나짜리 예제는 g++ main.cpp로 끝납니다. 하지만 실제 프로젝트는 파일이 수십 개고, 여기서 컴파일·링킹빌드 시스템을 이해해야 합니다. 링커 에러 앞에서 멈추지 않으려면 이 단계가 필요합니다.

소스가 실행 파일이 되기까지

g++ 한 번처럼 보이지만, 내부는 네 단계입니다.

단계하는 일결과물
전처리#include·#define 치환확장된 소스
컴파일.cpp를 기계어로 번역목적 파일 .o
어셈블(컴파일에 포함) 
링킹여러 .o와 라이브러리를 하나로 연결실행 파일

핵심은 컴파일은 파일 단위로 따로, 링킹은 그걸 한데 모으는 단계라는 것. 그래서 컴파일 에러와 링커 에러는 원인이 다릅니다.

선언 vs 정의 — 링커 에러의 뿌리

.cpp 하나를 컴파일할 때, 컴파일러는 다른 파일의 함수 본체를 모릅니다. 선언(시그니처)만 있으면 컴파일은 통과시키고, “실제 본체는 링커가 찾겠지” 하고 넘어갑니다. 링킹 때 그 본체를 못 찾으면 나는 게 그 유명한 에러입니다.

1
undefined reference to `foo()'

이건 문법 오류가 아니라, 정의를 못 찾았다는 뜻입니다. 원인은 보통 셋 중 하나:

  • 정의한 .cpp를 빌드에 안 넣었다
  • 라이브러리를 링크 안 했다
  • 선언과 정의의 시그니처가 미묘하게 다르다

앞서 ⑤ 템플릿에서 “템플릿을 .cpp에 분리하면 링크 에러”라 한 것도 같은 뿌리입니다.

헤더는 선언을, .cpp정의를 담습니다. 그리고 한 정의는 프로그램 전체에서 한 번만 존재해야 합니다(ODR, One Definition Rule). 헤더에 함수 정의를 넣고 여러 곳에서 include하면 중복 정의로 터지는 이유입니다.

Make — 무엇을 다시 빌드할지 관리

파일이 많아지면 매번 전체를 컴파일하기 아깝습니다. make는 “바뀐 파일과 그에 의존하는 것만” 다시 빌드합니다.

1
2
3
4
5
app: main.o util.o
	g++ -o app main.o util.o

main.o: main.cpp util.h
	g++ -c main.cpp

동작 원리는 훌륭하지만, 의존 관계를 손으로 적어야 해서 프로젝트가 커지면 관리가 고됩니다. 그래서 오늘날엔 대부분 CMake를 씁니다.

CMake — 오늘날의 표준

CMake는 Makefile을 직접 쓰는 대신 생성해 주는 상위 도구입니다. 플랫폼·컴파일러 차이를 흡수해서, 같은 CMakeLists.txt로 macOS·Linux·Windows에서 빌드됩니다. 현대 C++ 프로젝트의 사실상 표준입니다.

1
2
3
4
5
6
7
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(myapp)

set(CMAKE_CXX_STANDARD 17)

add_executable(app main.cpp util.cpp)   # 소스만 나열하면 의존 관계는 자동

빌드는 두 명령입니다.

1
2
cmake -B build              # build/ 에 빌드 파일 생성
cmake --build build         # 실제 컴파일

의존 관계를 손으로 관리하던 Make의 부담이 사라집니다. 외부 라이브러리 연결(target_link_libraries), 컴파일 옵션(target_compile_options)도 선언적으로 붙입니다.

에디터·도구와 연결

CMake는 compile_commands.json(각 파일을 어떤 옵션으로 컴파일하는지 기록)을 뽑을 수 있습니다.

1
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

이 파일이 있으면 clangd 같은 언어 서버가 프로젝트를 정확히 이해해 자동완성·진단을 제공합니다. 여기에 cpplint·clang-format까지 붙이면 빌드-린트-포맷이 한 줄로 이어집니다.

자주 막히는 지점

  • undefined reference — 문법이 아니라 링킹 문제. 해당 .cpp·라이브러리가 빌드에 포함됐는지 확인.
  • 헤더에 함수 정의 — 여러 곳에서 include되면 중복 정의(ODR 위반). 헤더엔 선언만, 정의는 .cpp로. (인라인·템플릿은 예외)
  • include guard 누락 — 같은 헤더가 두 번 포함돼 재정의 에러. #pragma once를 헤더 맨 위에.

통과 기준 — 그리고 로드맵 완주

  • 컴파일 에러와 링커 에러의 차이를 원인으로 설명할 수 있다.
  • undefined reference를 보고 어디를 봐야 할지 안다.
  • 여러 파일 프로젝트를 CMakeLists.txt로 빌드할 수 있다.

여기까지 왔다면 로드맵의 필수 경로를 완주한 것입니다. 참조자에서 시작해 자원 관리·이동 시맨틱을 지나 프로젝트 빌드까지 — 이제 [나중]·[선택]으로 미뤄둔 주제들을 실무에서 필요할 때 하나씩 채우면 됩니다.

Reference

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.