C언어) 전처리기 안내서 1

Lapis0875·2022년 10월 24일
0

c언어

목록 보기
14/21
post-thumbnail

📱전처리기의 세계를 여행하는 히치하이커들을 위한 안내서 (1)

전처리기란, 이름 그대로 C언어의 컴파일러가 동작하기 전에 동작하는 프로그램이에요. 전처리기를 사용하면, 프로그램 개발과 유지보수를 쉽게 할 수 있으며 코드의 가독성도 높일 수 있어요. 또한, 프로그램의 포팅도 쉬워져요.
우리가 C언어 코드상에 #을 붙이고 쓴 구문들을 전처리 지시자라고 하는데, 전처리기는 이들을 분석하고 대치해주는 역할을 해요. 전처리기의 세계로 떠나는 여행, 준비되셨나요?

전처리기의 구문, 전처리 지시자

전처리 지시자를 위한 구문은 C언어의 나머지 부분과 독립적인 문법을 가져요. 전처리 지시자가 영향을 미치는 범위는 파일 내에 전처리 지시자가 있는 위치에서 시작해, 다른 지시자에 의해 효력이 없어질 때 까지거나 파일의 끝까지에요.
전처리기는 C를 알지 못해요.

매크로 정의 지시자 #define

#define은 C언어 내에 매크로를 정의하는 지시자에요. 이 지시자로 할 수 있는 작업은 크게 3가지가 있어요.

  • 기호 상수
  • 문자열 대치
  • 인자가 있는 매크로

정의가 길어질 경우, 현재 행의 끝에 역슬래시 \를 삽입해 다음 행에 연결해서 쓸 수 있어요.

1. 기호 상수 매크로

#define 식별자 상수

프로그램 내 모든 식별자상수 로 대치돼요.

기호 상수를 선언하는 예제 코드에요.

#define N 60
#define PI 3.14
#define EOF (-1)				// End Of File
#define NULL 0

기호 상수를 활용하는 예제 코드에요.

#include <stdio.h>
#define PI 3.14		// 파이 값. 
// 기호 상수로 정의해둔 값을 바꾸면 관련 함수들의 PI값도 같이 바뀌어 유지보수가 용이해요.

double circumference(double r)
{
	return 2.0 * r * PI;	// 원의 둘레 계산
}

double area(double r)
{
	return r * r * PI;		// 원의 넓이 계산
}

int main(void)
{
	double r;
    printf("반지름을 입력하세요 : ");
    scanf("%lf", &r);
    printf("반지름이 %.3lf인 원의 둘레 : %.3lf\n", r, circumference(r));
    printf("반지름이 %.3lf인 원의 넓이 : %.3lf\n", r, area(r));
	return 0;
}

실행 결과는 아래와 같아요.

반지름을 입력하세요 : 5.0
반지름이 5.000인 원의 둘레 : 31.400
반지름이 5.000인 원의 넓이 : 78.500

2. 문자열 대치

#define 식별자 문자열

프로그램의 모든 식별자는 문자열로 대치돼요.

간단한 예제로 활용해보며 이해해볼게요.

#include <stdio.h>
#define EQUALS ==
#define AND &&
#define OR ||

int main(void)
{
    int a = 0, b = 0;
    printf("두 정수 입력 : ");
    scanf("%d%d", &a, &b);
    if (a % 2 EQUALS 1 AND b % 2 EQUALS 1)
        printf("두 수는 모두 홀수.\n");
    else if (a % 2 OR b % 2)
        printf("두 수 중 적어도 하나는 홀수.\n");
    return 0;
}

==, &&, ||의 논리 연산자들을 매크로로 정의했어요. AND와 OR이라니, 마치 파이썬같네요.

3. 인자가 있는 매크로

함수와 비슷한 모양을 가지는 매크로에요.
#define 식별자(인자목록) (인자로 대치할 구문)

앞서 선언했던 원의 면적을 구하는 area함수를 매크로로 만들어보면 아래와 같아요.

#define AREA(x) ((x) * (x) * PI)

전처리기는 매개변수에 해당하는 부분을 실제 인자로 대치해요.
앞서 원의 둘레와 면적을 구하는 예제를 인자를 가지는 매크로로 다시 작성해볼게요.

#include <stdio.h>
#define PI 3.14		// 파이 값. 
#define CIRCUMFERENCE(r) (2.0 * (r) * PI)
#define AREA(r) ((r) * (r) * PI)

int main(void)
{
	double r;
    printf("반지름을 입력하세요 : ");
    scanf("%lf", &r);
    printf("반지름이 %.3lf인 원의 둘레 : %.3lf\n", r, CIRCUMFERENCE(r));
    printf("반지름이 %.3lf인 원의 넓이 : %.3lf\n", r, AREA(r));
	return 0;
}

위 코드를 전처리기가 전처리 지시자들을 모두 대치한 이후의 모습은 아래와 같아요.

// #include 부분은 생략할게요
int main(void)
{
	double r;
    printf("반지름을 입력하세요 : ");
    scanf("%lf", &r);
    printf("반지름이 %.3lf인 원의 둘레 : %.3lf\n", r, (2.0 * (r) * 3.14));
    printf("반지름이 %.3lf인 원의 넓이 : %.3lf\n", r, ((r) * (r) * 3.14));
	return 0;
}

인자를 갖는 매크로는 c언어 문법으로 선언한 함수들과 유사해보이지만, 몇가지 차이점을 가져요.
1. 매크로는 인자의 형을 검사하지 않아요.

#define AREA(r) ((r) * (r) * PI)

r에 어떤 인자가 대응되는지 형 검사를 하지 않아요. 기존에 double area(double r) 의 경우, 인자로 받는 r 값은 double 형이어야만 하지만 AREA의 경우는 그렇지 않다는 이야기에요.

그렇기 때문에, 아래와 같은 형태의 구문도 전처리기는 처리할 수 있어요.

AREA("반지름");	// (("반지름") * ("반지름") * 3.14)
// 물론 컴파일은 실패할거에요!
  1. 매크로는 인자들을 여러번 평가해요.
    이전에 단항 연산자들 중 증감연산자를 배웠을거에요. 증감연산자를 사용한 시점에, 값을 받기 전 또는 후에 피연산자의 값이 1씩 증가한다고 기억할거에요.
    그렇다면, 아래의 함수 호출과 매크로 호출은 어떻게 될까요?
#include <stdio.h>
#define PI 3.14
#define AREA(r) ((r) * (r) * PI)

double area(double r)
{
	return r * r * PI;		// 원의 넓이 계산
}

int main(void)
{
  double r = 5.0;
  printf("매크로 : %.3lf\n", AREA(r++));
  r = 5.0;		// r 초기값을 동일하게 설정해요.
  printf("매크로 : %.3lf\n", area(r++));
}

실행 결과는 다음과 같아요.

매크로 : 94.200
매크로 : 78.500

어라? 분명 인자로 넘긴건 같은 r++일텐데 값이 다르네요. 이는 매크로가 단순히 문장을 대치했기 때문에, 인자들이 여러번 평가되었기 때문이에요. 위 코드를 전처리기가 처리한 후의 상태로 보면 아래와 같아요.

// #include는 생략할게요.

double area(double r)
{
	return r * r * 3.14;		// 원의 넓이 계산
}

int main(void)
{
  double r = 5.0;
  printf("매크로 : %.3lf\n", ((r++) * (r++) * 3.14));
  r = 5.0;		// r 초기값을 동일하게 설정해요.
  printf("매크로 : %.3lf\n", area(r++));
}

전처리기의 동작 이후 코드를 보면, r++가 AREA의 경우 두번 호출되었어요. 따라서, r 값이 두번 증가했기 때문에 의도했던 5.0 5.0 3.14가 아닌 5.0 6.0 3.14을 계산한거에요.

⚠️인자가 있는 매크로를 선언할 때 주의해주세요!
1. 매크로를 정의할 때, 올바른 평가 순서를 유지하기 위해 괄호를 충분히 사용해줘야 해요.
예시 코드 :

#define AREA(r) r * r * PI

AREA(a+b) // ==> (a + b * a + b * 3.14) != ((a + b) * (a + b) * 3.14)
  1. 매크로를 정의할 때, 매크로 이름과 인자 사이에 공백이 있으면 안돼요
    예시 코드 :
#define AREA (r) ((r) * (r) * PI)

AREA(5.0) // ==> (r) ((r) * (r) * 3.14) (5.0)
  1. 매크로를 정의할 때, 세미콜론 사용에 유의해주세요.
    예시 코드 :
#define AREA(r) ((r) * (r) * PI);

if (r > 0) x =  AREA(r);
else x = -1;

==>

if (r > 0) x = ((r) * (r) * 3.14);;
else x = -1;

문자열화 연산자 #

말 그대로, 전처리기가 문자열로 만들어줘요.

#define stringify(x) #x

stringify(Hello World!)
// => "Hello World!"

문자열화 연산자를 사용해, 디버깅 코드를 간단하게 만들 수 있어요.
기존에 일일히 쓰던 아래와 같은 디버깅 코드가 있어요.

printf("i = %d\n", i);
printf("j = %d\n", j);
// ...

매번 각 변수 이름을 적고 인자도 적고... 번거로워요!
하지만 문자열화 연산자를 사용해 인자가 있는 매크로를 만들면 간단해져요.

#define DEBUG_VAR(x) printf(#x" = %d", x)

DEBUG_VAR(i);
DEBUG_VAR(j);

토큰화 연산자 ##

문자열화 연산자와 유사하게 생겼으나, 기능은 조금 달라요. 문자열화 연산자는 매크로 인자를 문자열로 만들었다면, 토큰화 연산자는 인자를 토큰으로 만들어줘요. 여기서 토큰이란, 변수나 함수 이름 등을 말해요.

#define PRINT_AGE(x) printf(#x"의 이름은 %s이고, 나이는 %d살 입니다.\n", x, age_##x)
char *a = "철수", *b = "영희";
int age_a = 20, age_b = 21;

PRINT_AGE(a);	// printf("a의 이름은 %s이고, 나이는 %d살 입니다.\n", a, age_a);
PRINT_AGE(b);	// printf("b의 이름은 %s이고, 나이는 %d살 입니다.\n", b, age_b);

위 예시에서, PRINT_AGE 매크로 함수에서 age_##x 로 토큰화 연산자를 사용하고 있어요. 인자로 받은 변수명을 age 뒤에 붙여서, age_a, age_b와 같은 변수명으로 만들고 있어요. 다시 말해, age_a, age_b와 같은 토큰으로 사용할 수 있게 인자로 받은 a, b를 age 뒤에 결합시켜준 거에요.

C99) 인자를 갖는 매크로의 인자 생략하기

C90에서는 반드시 인자를 줘야 하지만, C99에서는 인자를 갖는 매크로를 사용할 때, 인자를 생략하고 사용할 수 있어요.

#define PRINT_AGE(x) printf(#x" = %d", age_##x)

PRINT_AGE();	// printf(" " " : %d\n", age_);

배운 내용들을 정리해보고 있어요. 잘못 기재된 내용이 있다면, 댓글로 알려주시면 수정할게요.

profile
새내기 대학생 개발자에요 :D

0개의 댓글