본문 바로가기
C언어

C언어 선행처리

by FraisGout 2020. 7. 5.

선행처리(preprocess)?

선행처리란 실행 파일을 생성하는 과정에서 소스 파일 내에 존재하는 선행처리 지시문을 처리하는 작업을 의미합니다.

 

이러한 선행처리 작업은 컴파일하기 전 선행처리기(preprocessor)에 의해 먼저 처리됩니다.

 

선행처리기는 코드를 생성하는 것이 아니라, 컴파일러가 컴파일하기 좋도록 소스를 재구성해 주는 역할만을 합니다.

 

선행처리문의 특징

1. 선행처리문은 선행처리 문자(#)로 시작합니다.

 

2. 선행처리문은 코드 내에서 하나의 라인을 모두 차지하며, 선행처리문 뒤에 C언어 코드를 추가하여 같이 사용할 수 없습니다.

 

3. 선행처리문은 다른 C언어의 명령문과는 달리 맨 뒤에 세미콜론(;)을 붙이지 않습니다.

 

4. 선행처리문은 소스 파일 어디에나 위치할 수 있지만, 선행처리문이 위치한 곳에서부터 파일의 끝까지만 영향을 미칩니다.

, 주석은 같은 라인의 선행처리문 뒤에 위치할 수 있습니다.

 

선행처리 지시자(preprocessing directives)

선행처리 문자(#)로 시작하는 선행처리 지시자는 다음과 같습니다.

 

선행처리 지시자

설명

#include

외부에 선언된 함수나 상수 등을 사용하기 위해, 함수나 상수가 포함된 외부 파일을 현재 파일에 포함할 때 사용함.

#define

함수나 상수를 단순화해주는 매크로를 정의할 때 사용함.

#undef

#define 지시자로 이미 정의된 매크로를 삭제할 때 사용함.

#line

__LINE__ 매크로와 __FILE__ 매크로를 재정의할 때 사용함.

#error

지정한 오류 메시지를 출력하고, 컴파일 과정을 중단하고자 할 때 사용함.

#pragma

프로그램의 이식성을 위해 운영체제별로 달라지는 지시사항을 컴파일러에 전달할 때 사용함.

#if, #ifdef, #ifndef,

#elif, #else, #endif

조건부 컴파일 지시자

 

#include

#include 선행처리 지시자는 외부에 선언된 함수나 상수 등을 사용하기 위해서 헤더 파일을 현재 파일에 포함할 때 사용합니다.

 

선행처리기는 #include 지시자 뒤에 나오는 파일 이름을 보고 해당 파일을 찾아서 그 내용을 현재 파일에 포함해 줍니다.

 

 

 

#include 선행처리 지시문에서 파일 이름을 표시하는 방법에는 다음과 같이 두 가지 방법이 있습니다.

 

 

 

1. #include <stdio.h>

 

2. #include "myStdio.h"

 

 

 

C언어에서 제공하는 표준 헤더 파일을 포함할 때에는 보통 꺾쇠괄호(<>)를 사용합니다.

 

꺾쇠괄호를 사용하여 파일 이름을 표시하면, 선행처리기는 가장 먼저 표준 시스템 디렉터리에서 파일 이름에 해당하는 헤더 파일을 찾습니다.

 

하지만 표준 시스템 디렉터리에서 파일 이름에 해당하는 헤더 파일을 찾지 못하면, 현재 작업 디렉터리도 검색합니다.

 

 

 

사용자가 직접 작성한 헤더 파일을 포함할 때에는 보통 큰따옴표("")를 사용합니다.

 

큰따옴표를 사용하여 파일 이름을 표시하면, 선행처리기는 가장 먼저 현재 작업 디렉터리에서 파일 이름에 해당하는 헤더 파일을 찾습니다.

 

하지만 파일 이름에 해당하는 헤더 파일을 찾지 못하면, 표준 시스템 디렉터리도 검색합니다.

 

 

 

따라서 결과적으로 이 두 방법에 큰 차이는 없지만, 많은 개발자가 이 기준에 맞춰 코드를 작성하고 있습니다.

 

#define

#define 선행처리 지시자는 함수나 상수를 단순화해주는 매크로를 정의할 때 사용합니다.

 

매크로는 함수나 상수에 이름을 붙임으로써, 해당 매크로가 무엇을 가리키고 있는지를 명확하게 나타내 줍니다.

 

따라서 코드의 가독성을 증가시키고, 코드를 훨씬 더 읽기 편하게 해줍니다.

 

 

 

#define 지시자의 문법은 다음과 같습니다.

 

문법

#define 식별자 대체리스트

 

 

 

선행처리기는 #define 선행처리 지시문의 식별자(identifier)를 단순히 대체 리스트(replacement-list)로 치환해 주기만 합니다.

 

이러한 과정을 매크로 확장(macro expansion)이라고 합니다.

 

 

 

#define 선행처리 지시문에서 식별자는 매크로(macro)라고 부르는 사용자가 미리 정의한 약어입니다.

 

이때 매크로 이름인 식별자는 C언어의 변수 이름 생성 규칙과 똑같은 생성 규칙을 따라서 작성해야 합니다.

 

따라서 매크로 이름 중간에는 공백을 넣을 수 없지만, 실제값은 공백을 가질 수 있습니다.

 

또한, 매크로끼리 중첩하여 사용할 수도 있습니다.

 

, 문자열에 포함된 매크로 이름에 대한 치환 작업은 이루어지지 않습니다.

예제

#include <stdio.h>

 

#define PI 3.14

 

 

 

int main(void)

 

{

 

double radius = 12;

 

 

 

printf("원주율을 나타내는 PI의 값은 %.2f입니다.\n", PI);

 

printf("원의 면적은 %.2f * %.2f * %.2f = %.2f입니다.\n", PI, radius, radius, PI * radius * radius);

 

return 0;

 

}

 

위의 예제에서 PI라는 매크로는 3.14로 전부 단순 치환됩니다.

 

하지만 문자열에 포함된 PI라는 문자는 이러한 치환 작업에서 제외됩니다.

 

 

 

위처럼 값을 나타내는 매크로를 객체 같은 매크로(object-like macro)라고도 합니다.

 

 

매크로 함수란?

C언어에서는 #define 선행처리 지시문에 인수로 함수의 정의를 전달함으로써, 함수처럼 동작하는 매크로를 만들 수 있습니다.

 

이러한 매크로를 함수 같은 매크로(function-like macro) 또는 매크로 함수라고 합니다.

 

 

 

다음 예제는 SUB(X, Y)PRT(X)라는 매크로 함수를 정의하고 사용하는 예제입니다.

 

예제

#include <stdio.h>

 

#define SUB(X,Y) X-Y

 

#define PRT(X) printf("계산 결과는 %d입니다.\n", X)

 

 

 

int main(void)

 

{

 

int result;

 

int num_01 = 15, num_02 = 7;

 

 

 

result = SUB(num_01, num_02);

 

PRT(result);

 

return 0;

 

}

 

함수와 매크로 함수

매크로 함수는 일반 함수와는 달리 단순 치환만을 해주므로, 일반 함수와 완전히 똑같은 방식으로 동작하지는 않습니다.

 

 

 

다음 예제는 일반 함수와 매크로 함수와의 차이를 보여주는 예제입니다.

 

예제

#include <stdio.h>

 

#define SQR(X) X*X

 

#define PRT(X) printf("계산 결과는 %d입니다.\n", X)

 

 

 

int main(void)

 

{

 

int result;

 

int x = 5;

 

 

 

result = SQR(10);

 

PRT(result);

 

result = SQR(x);

 

PRT(result);

 

result = SQR(x+3);

 

PRT(result);

 

return 0;

 

}

 

 

위의 예제에서 맨 마지막의 매크로 함수는 예상한 결과와는 전혀 다른 결괏값을 반환합니다.

 

선행처리기는 매크로 정의에서 모든 XX+3으로 대체합니다.

 

따라서 SQR(x+3)은 다음과 같이 대체되어 계산됩니다.

 

 

 

x+3*x+3 = 5+3*5+3 = 5+15+3 = 23

 

 

 

예상한 결괏값은 8*8=64였지만 전혀 다른 결괏값이 반환되는 것입니다.

 

일반 함수는 인수를 프로그램이 실행 중일 때 전달받지만, 매크로 함수는 인수를 컴파일 이전에 미리 치환하기 때문입니다.

 

 

 

따라서 이와 같은 오류를 미리 방지하기 위해서는 다음 예제의 번 코드처럼 각 인수를 모두 괄호(())로 묶어줘야 합니다.

 

예제

#include <stdio.h>

 

#define SQR(X) ((X)*(X)) // 매크로 함수는 이처럼 모든 인수를 괄호로 묶어줘야 함.

 

#define PRT(X) printf("계산 결과는 %d입니다.\n", X)

 

 

 

int main(void)

 

{

 

int result;

 

int x = 5;

 

 

 

result = SQR(10);

 

PRT(result);

 

result = SQR(x);

 

PRT(result);

 

result = SQR(x+3);

 

PRT(result);

 

return 0;

 

}

 

 

위의 예제는 앞선 예제를 수정하여 정상적인 결과를 반환하게 해주는 예제입니다.

 

이처럼 매크로 함수가 일반 함수와 같이 동작하기 위해서는 다음과 같은 사항에 주의하여 작성해야 합니다.

 

 

 

1. 매크로 함수의 전체를 괄호(())로 감싸야 합니다.

 

2. 매크로 함수의 인수들도 각각 괄호로 감싸야 합니다.

 

3. 매크로 함수를 호출할 때에는 증감 연산자(++, --)나 복합 대입 연산자 등은 사용하지 않는 것이 좋습니다.

 

매크로 함수는 얼핏 함수처럼 보이지만 일반 함수와는 전혀 다른 시간대에 이루어지는 다른 과정임을 명심해야 합니다.

매크로 함수의 장단점

매크로 함수의 장점은 다음과 같습니다.

 

 

 

1. 매크로 함수는 단순 치환만을 해주므로, 인수의 타입을 신경 쓰지 않습니다.

 

2. 매크로 함수를 사용하면 여러 개의 명령문을 동시에 포함할 수 있습니다.

 

3. 함수 호출에 의한 성능 저하가 일어나지 않으므로, 프로그램의 실행속도가 향상됩니다.

 

 

 

매크로 함수의 단점은 다음과 같습니다.

 

 

 

1. 원하는 결과를 얻는 정확한 매크로 함수의 구현은 어려우며, 따라서 디버깅 또한 매우 어렵습니다.

 

2. 매크로 함수의 크기가 증가하면 증가할수록 사용되는 괄호 또한 매우 많아져서 가독성이 떨어집니다.

 

 

 

따라서 매크로 함수는 크기가 큰 함수보다는 간단한 함수를 대체하는 데 사용하는 것이 좋습니다.

 

### 연산자

### 연산자는 선행처리기 연산자로 #define 선행처리 지시문에서만 사용되는 연산자입니다.

 

C언어에서 토큰(token)이란 컴파일러가 인식하는 최소 단위의 문자나 문자열을 의미합니다.

 

이 두 연산자는 바로 이러한 토큰 단위의 연산에서 사용됩니다.

 

# 연산자

# 연산자는 매크로 함수의 대체 리스트 안의 인수 앞에 사용하여, 토큰을 문자열로 변환시켜줍니다.

 

해당 토큰은 실인수로 치환되면서 양쪽에 위치한 큰따옴표("")를 포함해 그대로 문자열 상수로 변환됩니다.

 

# 연산자를 사용하면 문자열 안에 매크로 함수로 전달된 인수를 포함시킬 수 있습니다.

 

예제

#include <stdio.h>

 

#define SQR(X) printf(""#X"의 제곱은 %d입니다.\n", ((X)*(X)))

 

 

 

int main(void)

 

{

 

int x = 5;

 

 

 

SQR(x);

 

SQR(3+4);

 

return 0;

 

}

 

 

## 연산자

## 연산자는 두 개의 토큰을 하나의 토큰으로 결합해 주는 선행처리기 연산자입니다.

 

이 연산자는 함수 같은 매크로뿐만 아니라 객체 같은 매크로의 대체 리스트에도 사용할 수 있습니다.

 

이 연산자를 사용하면 변수나 함수의 이름을 프로그램의 런타임에 정의할 수 있습니다.

 

 

 

다음 예제에서는 XN(n)이라는 매크로 함수를 사용하여 변수의 이름을 동적으로 작성하고 있습니다.

 

예제

#include <stdio.h>

 

#define XN(n) x ## n

 

 

 

int main(void)

 

{

 

int XN(1) = 10;

 

int XN(2) = 20;

 

 

 

printf("x1에 저장된 값은 %d입니다.\n", x1);

 

printf("x2에 저장된 값은 %d입니다.\n", x2);

 

return 0;

 

}

 

다음 예제는 # 연산자와 ## 연산자를 이용하여, 동적으로 작성한 변수의 이름에 접근하는 예제입니다.

 

예제

#include <stdio.h>

 

#define XN(n) x ## n

 

#define PRT_XN(n) printf("x"#n"에 저장된 값은 %d입니다.\n", x ## n)

 

 

 

int main(void)

 

{

 

int XN(1) = 10;

 

int XN(2) = 20;

 

 

 

PRT_XN(1);

 

PRT_XN(2);

 

return 0;

 

}

 

 

미리 정의된 매크로(predefined macro)

C언어에서는 컴파일러가 참고해야 할 정보를 알려주기 위해서 몇몇 매크로를 미리 정의하여 제공하고 있습니다.

 

미리 정의된 매크로는 #define 선행처리 지시자로 정의하지 않아도 사용할 수 있으나, 사용자가 재정의할 수는 없습니다.

 

 

 

C언어에서 제공하는 미리 정의된 매크로는 다음과 같습니다.

 

미리 정의된 매크로

설명

__DATE__

선행처리가 수행된 날짜를 "Mmm dd yyyy"형식으로 나타낸 문자열

__TIME__

선행처리가 수행된 시간을 "hh:mm:ss"형식으로 나타낸 문자열

__FILE__

현재 소스 파일의 이름을 나타내는 문자열

__LINE__

현재 소스 파일에서 처리중인 라인 번호를 나타내는 문자열

__STDC__

컴파일러가 C언어 표준을 따르면 1로 설정함.

__STDC_HOSTED__

호스트 환경이 아니면 0, 호스트 환경이면 1로 설정함.

 

예제

#include <stdio.h>

 

 

 

int main(void)

 

{

 

printf("선행처리가 수행된 날짜는 %s입니다.\n", __DATE__);

 

printf("선행처리가 수행된 시간은 %s입니다.\n", __TIME__);

 

printf("현재 소스 파일에서 처리중인 라인 번호는 %d입니다.\n", __LINE__);

 

printf("__STDC__ : %d\n", __STDC__);

 

printf("__STDC_HOSTED__ : %d\n", __STDC_HOSTED__);

 

return 0;

 

}

 

#line

#line 선행처리 지시자는 __LINE__ 매크로와 __FILE__ 매크로를 재정의할 수 있게 해줍니다.

 

라인 번호는 int형 타입으로, 파일명은 문자열로 전달됩니다.

 

이 선행처리 지시자는 사용자가 직접 사용하기보다는 주로 컴파일러가 오류 메시지를 위해 사용합니다.

 

#error

#error 선행처리 지시자는 지정한 오류 메시지를 출력하고, 컴파일 과정을 중단시킵니다.

 

주로 조건부 컴파일 선행처리 지시자와 함께 사용하여 디버깅에 사용됩니다.

 

#undef

#undef 선행처리 지시자는 #define 선행처리 지시지와 정확히 반대되는 동작을 수행합니다.

 

, 이미 정의되어 있는 매크로를 취소하는 동작을 수행합니다.

 

 

 

따라서 #define 선행처리 지시자에 의해 정의되는 매크로가 치환하는 범위는 #define 지시자가 정의된 위치부터 #undef 지시자에 의해 취소되는 위치나 파일의 끝까지가 됩니다.

 

, 위에서 살펴본 미리 정의된 매크로는 사용자가 임의로 정의를 취소할 수 없습니다.

'C언어' 카테고리의 다른 글

C언어 참조  (0) 2020.07.05
C언어 컴파일  (0) 2020.07.05
C언어 입력 & 출력  (0) 2020.07.05
C언어 구조체  (0) 2020.07.05
C언어 문자  (0) 2020.07.05

댓글