CompileTime - 전처리 단계
Compile
컴파일 타임
컴파일 타임(Compile time) 은 개발자가 작성한 소스코드를 기계어로 변환하는 편집 과정을 뜻한다.
컴파일 타임은 전처리 단계 > 컴파일 단계 > 어셈블리 단계 > 링킹 단계 로 구성되어 있다.
컴파일 타임 중 전처리 단계에 대해 공부한 내용을 정리해봤다.
전처리 단계
전처리 단계는 컴파일러가 컴파일을 수행하기 전, 소스 코드를 처리하는 단계다.
전처리 단계에서 처리하는 작업들은 대표적으로 파일 포함, 매크로 치환, 조건부 컴파일 작업 등이 있다.
전처리 단계를 마치고 처리된 코드는 expanded code 라고 불리며 “.i” 확장자를 가지고 있다.
.i 파일을 열어보면 전처리 결과를 확인할 수 있는데, 컴파일러의 옵션을 통해 .i 파일을 출력할 수 있다.
Visual Studio가 사용하는 cl 컴파일러는 /P
명령어로 소스코드를 전처리까지만 수행하고 .i 파일을 출력할 수 있다.
프로젝트 속성->C/C++->명령줄 추가 옵션에 /P
를 적용
빌드해보면 링크 에러가 발생하는데, /P를 적용해 전처리 단계에서 끝내 .obj 파일을 찾을 수 없기 때문에 그렇다.
main.i
를 열어보면 iostream이 include 된 것과 cpp에 작성한 코드를 볼 수 있다.
#pragma external_header(push)
#pragma external_header(push) 이 코드가 굉장히 많이 보여서 찾아봤다.
Microsoft Visual Studio에서 사용되는 전처리 지시문으로, 컴파일러의 상태를 수정하는 전처리 지시문이라고 한다.
컴파일러의 상태를 수정해 특정 헤더 파일이나 코드 블록을 처리하는 방식을 변경하거나 경고를 제어하는 데 사용된다고 한다.
예시 코드
1
2
3
4
#pragma external_header(push) // 컴파일러의 현 상태 저장
#pragma external_header("/external:*") // 지시문 이후 포함되는 헤더 파일은 외부 코드로 간주
#include <iostream> // 표준 라이브러리 헤더 포함 후 외부 코드로 처리
#pragma external_header(pop) // 저장했던 컴파일러의 상태로 복원
컴파일러의 상태는 컴파일 단계에서 공부할 예정이다.
전처리 지시문
’#’ 심볼을 사용하여 컴파일 전에, 전처리기에게 소스코드를 수정하라고 지시할 수 있다.
자주 사용하는 지시문들
지시문 | 목적 |
---|---|
#include | 소스 코드의 헤더 파일을 연결 |
#define | 매크로 상수를 생성 |
#undef | 이미 정의된 매크로 삭제 |
#ifdef / #ifndef | 매크로 존재 여부에 따라 달라지는 컴파일 |
#if / #elif / #else / #endif | 표현식에 따라 조건이 정해지는 컴파일 |
#error | 컴파일 프로세스를 중단하고 오류 알림 생성 |
#warning | 컴파일하는 동안 경고 알림 표시 |
#pragma | 컴파일러에 특정 지침을 제공 |
#include
한 파일의 내용을 현재 파일에 포함하는 데 사용한다.
#inlcude <파일 이름>
또는 #include "파일이름"
이렇게 사용 가능하다.
<>
: 표준 라이브러리나 시스템 디렉토리에서 헤더 파일을 포함""
: 현재 소스 파일이 있는 디렉토리의 파일을 포함한다.
1
2
3
4
5
6
7
8
9
10
11
//헤더파일
#pragma once
int test = 1;
char testC = 't';
//cpp 파일
#include "testInclude.h"
int main()
{
}
1
2
3
4
5
6
7
8
9
10
#line 1 "C:\\Users\\SJH\\Desktop\\Preprocessor\\Preprocessor\\main.cpp"
#line 1 "C:\\Users\\SJH\\Desktop\\Preprocessor\\Preprocessor\\testInclude.h"
#pragma once
int test = 1;
char testC = 't';
#line 2 "C:\\Users\\SJH\\Desktop\\Preprocessor\\Preprocessor\\main.cpp"
int main()
{
}
여기서 #line
은 컴파일러에 현재 파일 이름과 줄 번호를 알려주는 전처리 지시문이다.
사용 목적
- 오류 메시지에 올바른 파일 이름과 줄 번호를 표시하기 위해 사용
- 특정 헤더 파일을 포함하거나 가져올 때, 원래 위치를 추적하기 위해 사용
main.cpp
첫 줄이 실행되었다고 알려주고, testInclude.h
의 첫 줄이 실행됐다는 걸 알려준다.
그 다음, 다시 main.cpp
2번 줄로 돌아왔다고 알려주고 있다.
#include
의 재밌는 점은 헤더 파일만 가능한 것이 아닌, 다른 종류의 파일도 사용 가능하다는 점이다.
이건 텍스트 파일을 #include
한 결과다.
#include
가 지정된 파일의 내용을 그 자리에 텍스트로 삽입하는 역할을 하기에 정상 작동한다.
다만, include
할 파일 내용은 C++ 문법을 지켜야만 한다. 그렇지 않으면 컴파일 오류가 생긴다.
#define
이 지시문은 매크로를 정의하는 데 사용된다. 매크로 이름은 상수 값이나 짧은 코드 조각의 이름을 만들어 줄 수 있다.
#define
으로 정해준 키워드를 전부 치환해준다.
#undef
이건 #define
으로 정의된 매크로를 정의 해제하는 데 사용된다.
기존 매크로를 다시 정의할 때 사용하면 좋다.
#ifdef와 #ifndef
조건부 컴파일에 사용되는 지시어들이다.
#ifndef
는 매크로가 정의되지 않았는지 확인하고 #ifdef
는 매크로가 정의됬는지 확인한다.
#if / #elif / #else / #endif
이건 각각 if
, else if
, else
, end
역할을 맡고있다.
#error
컴파일 오류에 대한 사용자 정의 오류 메시지를 인쇄할 때 사용되는 전처리기 지시어다.
빌드를 강제로 중단시켜 실행하지 못하게 한다.
#warning
이건 표준 전처리 지시문이 아니라 컴파일러에 따라 지원 여부가 다르다.
GCC/Clang 에선 사용 가능하지만, cl에선 지원하지 않는다.
컴파일 중 경고 메시지를 출력하는 용도로 사용한다.
#pragma
이건 컴파일러에 대한 특수 지시어를 사용할 때 사용하는 지시어다.
아까 위에서 봤듯 컴파일러의 상태를 변경할 때 사용하기도 한다.
자주 사용되는 특수 지시어
#pragma once
: 헤더 파일에서 중복 포함 방지를 위해 사용되는 지시문#pragma message
: 컴파일 중 메시지를 출력#pragma pack
: 구조체의 메모리 정렬 방식을 설정#pragma warning
: 컴파일 경고를 제어해 경고 수준을 설정하거나 특정 경고를 비활성화 시킬 수 있다.#pragam optimize
: 컴파일러의 최적화 수준을 제어한다. (cl)에서만 사용 가능
전처리 매크로
매크로는 #define
지시어로 사용하여 정의된 코드다.
매크로는 실행 시점(런타임)이 아닌 컴파일 타임에 코드가 삽입된다.
여기서 컴파일 타임은 전처리 단계 -> 컴파일 단계 -> 어셈블리 단계 -> 링크 단계 이 모든 과정을 포함한다.
정확히 말하자면 전처리 단계에서 이루어진다.
매크로 치환은 문자열 치환을 기반으로 이루어지는데, 매개변수를 받아서 매크로 함수를 사용할 때도 문자열 치환이 핵심이다.
1
#define SQUARE(x) ((x) * (x))
위 매크로는 x 매개변수를 받아 ((x) * (x)) 로 치환한다.
1
int result = SQUARE(5); // 매크로 치환 후 result = ((5) * (5));가 됨
문자열 치환이기 때문에 주의할 점이 있는데, 단순 문자열 치환이므로 연산자 우선순위 문제가 발생할 수 있다.
매크로 사용의 장단점
장점
- 컴파일 타임에 코드가 치횐되기 때문에 함수 호출에 따른 비용과 스택 오버헤드가 없다.
- 자주 호출되는 작은 함수에서 성능상 이점을 얻을 수 있다.
- 코드의 단순화로 가독성을 높일 수 있다.
- 매크로는 인라인 함수처럼 동작하여 함수 호출 없이 직접 치환되므로, 런타임 성능을 개선할 수 있다.
단점
- 매크로는 타입을 검사하지 않기에 의도하지 않은 타입의 인자 전달 시 문제가 발생할 수 있다.
- 디버깅 시 함수 호출 스택을 추적할 수 없기에 매크로가 어디서 호출됐는지 어려울 수 있다.
- 매크로는 컴파일 타임에 정적 치환되므로, 런타임에 동적으로 변경할 수 없다.
매크로 상수와 const 차이
특징 | 매크로 상수 (#define ) | const 상수 |
---|---|---|
타입 | 타입이 없으며, 단순히 텍스트 치환 | 타입이 지정되고 타입 안전 보장 |
디버깅 | 디버깅 시 값을 추적하기 어려움 | 디버깅 시 변수명과 값이 보임 |
범위 | 범위가 없으며 전역적으로 사용됨 | 선언된 범위 내에서만 유효 |
메모리 | 메모리 할당되지 않음, 값은 코드에 삽입 | 메모리에 할당되어 값이 저장됨 |
사용 시점 | 전처리 단계에서 값 치환 | 컴파일 타임 및 런타임에 사용됨 |
매크로의 경우 성능이 중요하면 유용하고, const
는 타입 안정성과 범위가 중요할 때 유용하다.
헤더파일
C++에서 선언을 담고 있는 파일이며, 다른 파일에서 선언한 내용들을 사용할 수 있게 한다.
전처리 단계에서 #include
를 통해 cpp 파일에 삽입된다.
초창기에는 헤더 파일이 여러 번 포함되는 것을 방지하기 위해 헤더 가드를 추가했다.
C++ 11 이후로 #pragma once
를 사용하여 헤더 파일이 한 번만 포함되도록 할 수 있다.
1
2
3
4
5
6
#ifndef HEADER_FILE_NAME_H
#define HEADER_FILE_NAME_H
// 헤더 파일 내용
#endif // HEADER_FILE_NAME_H
1
2
3
#pragma once
// 헤더 파일 내용
inline 함수
1
2
3
4
5
6
7
8
inline int add(int a, int b) {
return a + b;
}
int main() {
int result = add(3, 5); // 인라인 함수 호출
return 0;
}
inline
함수는 헤더 파일에 정의한 짧은 함수다.
inline
함수는 호출되는 부분에 함수 본문이 삽입되는 방식으로 사용된다.
이러면 함수 호출 비용이 들지 않아 최적화에 좋다.
물론 모든 함수가 인라인화 되는 건 아니고, 컴파일러가 판단해 인라인화하지 않기로 결정할 수 있다.
함수 앞 inline
키워드는 강제보단 제안에 하는 키워드라고 생각하면 된다.
매크로 함수와 inline 함수의 차이
특징 | 매크로 (#define ) | 인라인 함수 (inline ) |
---|---|---|
타입 안정성 | 없음 | 있음 |
디버깅 | 어려움 | 쉬움 |
성능 | 함수 호출 오버헤드 없음 | 함수 호출 오버헤드 없음 |
주의 | 연산자 우선순위, 타입 문제 | 인라인화 되지 않는 문제 |