C++/카루의 C++ 강좌

[카루의 C++ 강좌] 1-6. 전처리기 - include, define

카루-R 2020. 3. 21. 00:40
반응형

오늘은 C++의 문법에서 조금 벗어난, 전처리기에 관해 이야기를 해 보겠습니다.

전처리기

우리가 C++ 프로그램을 만들면 단순히 .cpp 파일이 .exe파일로 변하는 것이라고 생각할 수도 있는데 그렇지 않습니다.

우선 '전처리기'가 #이 붙은 모든 문장을 처리합니다. 이 친구는 컴파일러가 아니기 때문에 C++의 문법을 따르지 않아요. 전처리기의 작업이 끝나면, 컴파일러가 .cpp 파일을 .o 또는 .obj의 파일 형태로 컴파일합니다. 그 다음 링커가 .obj 파일을 묶어서 하나의 .exe 파일을 만들어냅니다. 우리는 C++ 프로그램을 작성할 때마다 위와 같은 과정을 반복합니다.

전처리기가 담당하는 전처리 지시문에는 include, define, region, pragma, if, ifdef 등이 있는데, 오늘은 가장 많이 쓰이는 include 와 define을 알아보겠습니다.

나머지 전처리기에 관해선 '[C++ 심화] 5. 전처리기 - if, region, pragma'를 참고해 주세요.

#include

.cpp 파일을 만들 때 main() 함수를 정의해서 사용하는 것처럼, std::printf()나 std::scanf()등을 사용하려면 이 함수들을 어딘가에서 정의하고 있어야 합니다. 함수들의 정의가 담긴 파일을 헤더 파일이라고 부르며, 일반적으로 .h의 확장자를 가지지만 C++ 표준 헤더파일의 경우 확장자를 가지지 않습니다. iostream도 헤더파일입니다. 원래 구 버전 C++에서는 iostream.h였으나 몇십년 전 부터 .h가 떨어지고 그냥 iostream만 사용합니다.

이렇게 함수 및 클래스, 변수에 대한 선언 또는 정의가 들어있는 헤더 파일을 사용하려면 #include<> 문장을 사용해야 합니다.

#include <헤더파일이름>

이게 무슨 뜻이냐면, <>안의 헤더파일의 내용을 모조리 통째로 복사해서 이 위치에 복사해,라는 의미입니다. 말 그대로 진짜 다 복사합니다. 그래서 프로그램의 크기가 커지고 필요 없는 기능도 다수 포함하게 됩니다.

이러한 단점을 해결하기 위해 C++20부터 전혀 새로운 방식인 모듈을 도입합니다.

'[C++ 특수] 1. 모듈과 import, export' 를 참고해 주세요.

헤더 파일의 종류는 매우 많아서 그 종류를 모두 알 순 없습니다. 하지만, 일단 지금은 만 포함해도 충분히 많은 기능을 쓸 수 있으니 알아두세요. 필요한 헤더 파일은 그때그때 설명하겠습니다.

참고로 헤더파일 내부에서도 또다른 해더 파일은 #include로 포함하고 있는데, 에서 거슬러 올라가다보면 를 포함하고 있습니다. 이는 C언어의 헤더 파일의 C++ 버전입니다. 이 안에 std::printf()와 std::scanf()가 있습니다. 일단 몇 가지 헤더파일 목록을 소개하겠습니다.

<iostream>: C++에서 가장 많이 사용하는 헤더파일. 기본입출력에 관한 기능을 포함한다.
<string>: 문자열 타입(정확히는 클래스)인 std::string과 관련 함수를 포함하는 헤더파일
<chrono>: 시간 관련 헤더 파일
<filesystem>: 파일 시스템 관련 헤더 파일
<thread>: 쓰레드 관련 헤더 파일
<iomanip>: 입출력 조정자에 관한 헤더파일.
<fstream>: 파일 스트림 관련 헤더파일
<sstream>: std::stringstream이 있는 헤더파일
<vector>: std::vector이 있는 헤더파일

 

...이 외에도 수도 없이 많습니다. 여러개의 헤더를 사용하려면 #include 문을 여러번 사용합니다.

#include <iostream>
#include <string>
#include <initializer_list>
#include "myheader.h"

 위 처럼 "" 를 사용하여 쓸 수 있습니다만, 이때는 현재 디렉토리에 있는 헤더 파일을 찾습니다. 직접 만든 헤더를 사용할 때 위와 같이 사용합니다.

 

#define

 매크로를 정의하거나, 매크로 상수 또는 매크로 함수를 만들 때 쓰입니다. 단, 매크로 상수는 다음시간에 다룰 constexpr 상수로, 매크로 함수는 inline 함수로 대체할 수 있기 때문에 #define 문은 매크로 정의에만 사용하시길 추천드립니다.

매크로 상수를 정의할 때의 사용법은 아래와 같습니다.

#define PI 3.141592

 딱 봐도 감이 오시죠? PI라는 이름의 매크로 상수를 정의했고, 이 값은 3.141592입니다.

등호를 사용하지 않음에 유의하세요. 전처리기는 C++의 문법을 따르지 않는다고 거듭 말씀드립니다.

#include <iostream>
#define PI 3.141592

int main()
{
    int r;
    std::cout << "원의 반지름을 입력하세요: ";
    std::cin >> r;

    std::cout << "원의 둘레의 길이는 " << 2 * PI * r << "입니다." << std::endl;
    return 0;
}

 매크로의 비밀은 '치환'에 있습니다. 자료형도 묻지도 따지지도 않고 그냥 무시하고 글자에만 집중하면서 치환합니다. 위에서는 소스코드에서 PI라고 써진 부분을 3.141592로 치환합니다. 즉, 전처리가 끝난 후 위의 소스코드는 아래처럼 바뀝니다.

/*
*    여기에는 iostream 헤더 파일의 내용이 복사되어 있을겁니다.
*/
int main()
{
    int r;
    std::cout << "원의 반지름을 입력하세요: ";
    std::cin >> r;

    std::cout << "원의 둘레의 길이는 " << 2 * 3.141592 * r << "입니다." << std::endl;
    return 0;
}

 이렇게 치환이 되기 때문에 소스코드에서 의미있는 값, 의미있는 상수에 이름을 붙이고 싶을 때 매크로 상수를 사용하곤 했습니다. 그러나 C++11에서 constexpr 키워드가 도입되면서 버려진 기능이 되었습니다.

매크로 함수역시 함수는 아니지만, 전처리기만의 기법으로 신선한 느낌을 주는 매크로입니다.

#include <cstdio>
#define OUT(str) std::printf( #str "\n");
#define MAIN_END(val) return val;

int main()
{
    OUT(지금 매크로 함수 테스트 중 입니다.)
    OUT(참고로 지금 세미콜론도 붙이지 않았습니다.)
    MAIN_END(0)
}

 std::printf()만 사용하므로 <cstdio>만 포함했습니다. 뭐 iostream 쓰셔도 상관은 없어요.

지금 보시면, C++의 문법과는 상당히 거리가 먼 것을 볼 수 있습니다.

OUT() 매크로는 인자를 하나 받아서 std::printf()에 넘겨줍니다. 그런데, 인자 str 앞에 #이 있죠?

전처리기에 의해서 #str => "str" 로 치환이 됩니다. 즉, OUT()안에 들어온 것은 무엇이든지 쌍따옴표로 감싸주게 됩니다. 그리고 뒤의 "\n"도 쌍따옴표 문자열이니 둘이 붙어버리겠죠. 또한 세미콜론이 붙어있기 때문에, OUT() 매크로를 쓰면 세미콜론을 붙이지 않아도 됩니다. 마지막에는 return 0; 대신 MAIN_END(0)을 사용했습니다.

즉 위의 코드는 아래처럼 바뀝니다.

/* <cstdio>의 내용들이 이 위치에 복사됨 */

int main()
{
    std::printf( "지금 매크로 함수 테스트 중 입니다." "\n");
    std::printf( "참고로 지금 세미콜론도 붙이지 않았습니다." "\n");
    return 0;
}

이해가 잘 안 되실 수 있습니다. 그냥 단순히 치환에 의해 이렇게 바뀌는 것이니 보고만 넘어가셔도 됩니다. 실제로 이걸 쓸 일은 없을겁니다. 아니 쓰면 안 됩니다.

참고로 매크로를 정의할 때 이름만 써도 됩니다.

#define OCL_LIBRARY

이렇게 하면 소스코드에서 OCL_LIBRARY라고 되어 있는 부분은 전처리 시 모두 삭제됩니다.

 

매크로의 문제점

아래는 매크로를 사용하여 C++의 문법을 모두 파괴한 나쁜 예제입니다.

#include <iostream>
#define 메인_함수_시작 int main() {
#define 메인_함수_끝 return 0; }
#define 출력(문장) std::cout << #문장;
#define 출력_(변수) std::cout << 변수;
#define 다음줄 std::cout << std::endl;
#define 입력(변수) std::cin >> 변수;
#define 정수형 int
#define 연결(앞, 뒤) 앞 ## 뒤
#define 할당(값) = 값;
#define 문자열 std::string

메인_함수_시작
    정수형 변수 할당(30)
    문자열 이름 할당(문자열())

    출력(이름을 입력하세요: )
    입력(이름)

    연결(변, 수) 할당(50)
    출력(당신의 이름은 )
    출력_(이름)
    출력(이군요! 반가워요.)
    다음줄
메인_함수_끝

 실행 결과:

이름을 입력하세요:카루
당신의 이름은카루이군요! 반가워요.

 비주얼 스튜디오는 C++ 표준과는 다르게 한글로 된 식별자(매크로의 이름, 변수의 이름, ...)를 지원합니다.

그래서 위의 소스코드처럼 전혀 다른 언어를 만들어버릴 수 있습니다.

이게 가능한 이유는 매크로는 치환만 하기 때문이죠.

연결(변, 수) 할당(50)  // #define 연결(앞, 뒤) 앞 ## 뒤

소스코드를 보면 저는 '변수'라는 변수를 정의했는데 이게 가능한 이유는 ##연산자가 무엇이든 앞뒤를 연결해주기 때문입니다. '변 ## 수'를 '변수'로 만들어버려요.

위의 소스코드는 아래처럼 치환됩니다.

int main() {
    int 변수 = 30;
    std::string 이름 = std::string();

    std::cout << "이름을 입력하세요:";
    std::cin >> 이름;

    변수 = 50;
    std::cout << "당신의 이름은";
    std::cout << 이름;
    std::cout << "이군요! 반가워요.";
    std::cout << std::endl;
    return 0; }

 닫는 중괄호가 저기에 있는 이유도 치환때문에 그렇습니다. 아무튼 이렇게 작성해도 알아보기 힘든 코드를 매크로로

범벅을 했으니 못알아보는 건 당연합니다. 그냥 웃고 넘어가세요.


1단원을 무사히 마친 여러분들께 휴식과 웃음을 주고자 쉬어가는 코너로 만들어보았습니다.

원래 이런 건 만우절에 올려야 제맛인데...뭐 어쩔 수 없죠. 아무튼 이제 C++의 기본중의 기본은 익히셨습니다.

앞으로 더욱 어려운 내용들이 많아지겠지만, 잘 해내실거라 믿습니다. 질문이 생기면 언제든지 환영합니다. 댓글로 달아주세요.

참고: 본 강좌의 연계 심화 강좌는

"[C++ 심화] 5. 전처리기 - if, region, pragma",

"[C++ 특수] 1. 모듈과 import, export"입니다.

반응형