C언어

C언어의 기초 - 선행처리기 preprocessor

머리큰개발자 2021. 2. 7. 02:12

*주의 - 개인적으로 학습한 내용이므로 틀린 부분을 알려주시면 진심으로 감사드리겠습니다.

 

1. 선행처리기

 

선행처리기는 compiler 가 본격적으로 compile 하기 전에 header file과 source 파일을 미리 읽어와 선언해주고, 선행처리기의 연산자로 사용한 문장들을 미리 처리해준다.

이게 무슨 소용이냐 싶겠지만, compiler 는 딱 주어진 문장만 compile하여 assembly 언어로 바꿔주기 때문에 compiler가 주어진 문장을 번역할 수 있게 preprocessor 가 준비해준다고 생각하면 편하다.

 

선행처리기의 연산자들은 #을 사용하여 표기한다.

가장 먼저 우리가 계속해서 사용해오던 #include가 있다.

 

1-1. #include

include 는 preprocessor에게 해당하는 파일을 복사해서 붙여넣으라는 뜻이다.

복사해서 붙여넣기 때문에 필요한 모든 파일에 선언을 해줘야 한다.

가령 #include <stdio.h> 는 stdio라는 header file을 그 자리에 붙여넣어 선언해달라는 뜻이다.

stdio.h 는 standard input output으로 input과 output을 담당하는 함수들을 가지고 있다. 

<> 기호는 해당 언어에 대한 툴을 설치할 때 같이 설치되는 표준 라이브러리 폴더에서 파일을 찾아 불러오라는 뜻이고,

"" 기호는 현재 프로그램이 실행중인 폴더에서 먼저 찾고, 없을 경우 표준 라이브러리 폴더에서 찾으라는 뜻이다.

""는 그 파일이 있는 경로를 직접 설정이 가능하다. 예를 들어 asdf.h파일이 있을 경우

#include "../asdf.h" 

처럼 설정할 수 있다는 뜻이다. 여기서 ../ 는 현재 프로그램이 실행중인 폴더 바로 상위의 폴더를 뜻하며 ./의 경우 현재 폴더를 뜻한다.

 

1-2. #define

define은 말 그대로 정의한다는 뜻이다.

주로 상수를 고정시키거나 매크로를 만드는데 사용된다. 

매크로는 편하게 사용하기 위해 자동으로 완성되는 형식을 말하는데, 선행처리기는 define된 단어들을 전부 미리 선언한 것으로 치환한다.

 

예를 들어, #define a 30 을 선언할 경우, 본문에 a가 단독으로 쓰인 경우는 모두 30으로 치환해버린다.

int main(){ 

    printf("%d", a);

}

가 있을 경우 printf("%d", 30); 으로 바꿔버린다. 그렇기 때문에 define 문에 ;를 써야할지 말아야할지는 자신이 잘 조절해서 결정해야한다.

define은 함수와 달리 호출의 형식이 아니고, compile 이전에 수행되는 치환 방식이기 때문에 많이 쓸 수록 코드가 길어진다는 단점이 있으며, 반대로 호출의 형식이 아니기 때문에 수행속도가 빨라진다는 장점이 있다.

 

게다가 많이 쓰는 문법을 매크로로 만들어놓으면 굉장히 편하다.

예를 들어, #define print(x) printf("%d", x)  로 선언해 놓으면 하나의 정수를 출력할 때 print(x)만 쓰면 사용할 수 있다는 뜻이다.

조금 주의할 점은 #define sqr(x) x*x 로 선언할 경우 sqr(3+5)를 쓸 경우 3+5*3+5가 그대로 복사되기 때문에 원래 의도한 64가 아닌 23이 나오는 이상한 결과를 얻을 수 있다. 그렇기 때문에 항상 대입되는 수는 괄호로 묶어서 써주는게 좋다. sqr(x) ((x)*(x)) 같은 형식으로.

 

그리고 여러 문장도 한 번에 선언할 수 있다. 예를 들어 자주 쓰이는 swap의 경우

#define swap(a,b) int tmp = *(a); *(a) = *(b); *(b) = tmp;로 선언할 경우

integer type a,b를 swap(a,b) 만으로 바꿀 수 있게 된다. 

예전에는 {}로 묶어야만 변수선언을 할 수 있었지만, 요즘에는 그냥 된다.

 

또 변수 선언할 때 귀찮도록 긴 타입들을 줄여서 쓸 수도 있다.

#define ll long long 을 쓸 경우 long long 타입의 변수를 ll a; 로 선언할 수도 있다.

 

cf) typedef

typedef는 앞선 define 의 활용 방법 중 타입을 줄여쓰는 것과 비슷하다.

#define ll long long 과 비슷하게 typedef long long ll; 로 활용할 수 있다.

 

두 가지는 얼핏 비슷해 보이지만, typedef의 경우 선행처리되지 않는다는 점이 다르기 때문에 선언 끝에 ;를 붙여야하는등 아예 영역이 다르다. 

거기다가 typedef 는 매크로가 아니기 때문에 치환 되는 것과는 달리 타입에 별명을 붙이는 일을 한다. 

 

결정적으로 다른 점은 한 번만 선언되어야 하는 타입에는 #define은 쓰일 수 없고 typedef는 쓰일 수 있다는 것이다.

예를 들어 struct 를 각 방식으로 만들어보자.

 

#define tag struct tag1 { int a; char c[20]; float b;}

typedef struct tag1 {int a; char c[20]; float b;} tag;

 

각각은 처음 선언시에는 동일하다.

tag x = {1,"ABC", 3.14};

tag x = {1,"ABC",3.14};

 

하지만 다른 형식을 사용할 때 문제가 발생한다.

tag * p = &x;

만약 tag 를 define으로 치환한다면 struct tag1{} * p = &x; 가 되기 때문에 같은 태그의 다른 타입(포인터 타입으로 바뀜)으로 새로 선언되기 때문에 재정의의 오류가 발생하게 된다.

반면 typedef는 struct tag1을 tag라고 부르겠다는 별명을 정한 것이므로 tag * p 는 struct tag1 타입을 가리키는 포인터 타입으로 인식이 된다.

 

1-3. #, ## 연산자

#연산자는 string처럼 인식되도록 바꿔준다.

가령 #define prt(x) printf("asdf" x)로 할 경우 x에 string 타입을 넣지 않는 이상 에러가 날 것이다.

하지만 #define prt(x) printf("asdf" #x) 로 할 경우 x위치에 들어오는 값을 string 형식으로 바꿔주기 때문에 가능하다. 

거기에 ##을 붙일 경우 원래 있던 token 에 맞춰서 붙여준다. 

예를 들어 #define prt(x) printf("%d", a##x) 로 정의할 경우

int a1=1; prt(1) 을 할 경우 printf("%d", a1)으로 바꿔준다.

 

특수한 경우 굉장히 많이 쓰이기 때문에 알아두면 굉장히 유용하다.

 

1-4. #ifdef #ifndef #else #endif #if #elif 

선행처리자에 의해 수행되는 조건문이다. 

#ifdef DEBUG 처럼 선행처리자의 시점에서 이미 정의된 것인지 아닌지에 따라서 컴파일할 때 문장을 남겨두나 가려두나의 차이가 있다. 여기서 DEBUG라는 임시 이름은 숫자나 값이 배정되어 있지 않아도 되며 단순히 #define DEBUG 까지만 선언해도 define 됐다고 가정하여 #ifdef 가 수행된다.

 

#ifndef 는 #ifdef 와 반대로 정의가 되었으면 실행하는게 아닌, 정의가 되지 않았으면 실행되는 것이다.

 

#else 는 #ifdef나 #ifndef가 false일 경우 수행할 문장들을 뜻하며 #endif는 #ifdef 든 #ifndef 든 끝에 항상 '무조건' 쓰여야 한다.

 

#if 와 #elif 의 경우 조건문을 쓸 수 있다. 

#define DEBUG 만 했을 경우 #if DEBUG 는 수행될지 안될지 정확하게 파악할 수 없다. 

#ifdef 와 다르게 참, 거짓을 판별하기 때문에 ==, !=, <=, >=, <, >, &&, ||, ! 연산이 사용 가능하다.

하지만 선행처리기 수준에서 처리되는 조건문이기 때문에 컴파일시 생성되는 변수들은 사용하면 어떻게 작동될지 예측하기 힘들기 때문에 #define 에서 처리되는 값이거나 상수(정수)만을 사용해야한다.

 

1-5. #error

우리가 흔히 코드를 짜다 보면 맞닥뜨리는 에러같은 형식을 만들 수 있다.

가령 #ifndef DEBUG #error error! 라고 만들어두면 DEBUG 가 정의되지 않았을 시 선행처리기 수준에서 error 메세지가 나온다. 

 

1-6. #pragma

표준적인 기능들이 아닌, 사용자가 사용하는 compiler 가 제공하는 선행 처리 기능들을 사용하고 싶을 때 사용한다.

이는 compiler에 의존하므로 다른 compiler 와 호환되지 않을 수도 있기 때문에 잘 확인하고 써야 한다.