반응형

Build의 순서는 아래와 같다.

소스코드 -> 전처리 -> 컴파일 -> 링크 -> 실행

C언어를 처음 공부할 때, #define을 사용하지 않으면 전처리 과정을 거치지 않는다고 생각한 적이 있다.

#으로 시작하는 문장은 전부 전처리기 지시자임에도 #include는 전처리기라고 생각하지 않았다. 항상 C언어를 실습할 때, 의무적으로 #inlcude <stdio.h>를 작성한 폐해이다.

전처리란 무엇인가?

컴파일 전에 처리해야 하는 일이고 전처리를 수행하는 장치를 전처리기라고 한다.

전처리기는 헤더 파일을 불러오거나, 소스 파일 내부의 특정 문자열을 상수 또는 문자로 치환하거나, 조건에 따라서 코드의 일부를 컴파일하거나 컴파일하지 못하게 하는 선택 기능을 제공한다.

궁금증이 하나 발생한다.

#define은 왜 사용하는가? 단순히 변수나 함수를 선언 및 호출하면 되지 않는가? 일반 변수 및 함수와 매크로(#define)의 차이는 무엇인가?

이를 이해하기 위해선 컴파일 단계를 이해할 필요가 있다.

컴파일이란 무엇인가? 기계어로 번역하는 번역기이다. 왜 기계어로 번역하는가? 소스 코드는 사람이 이해하기 쉬운 문장이며, 기계는 이해할 수 없다. 그러므로 기계가 이해할 수 있는 문장으로 번역해줘야 한다.

C언어가 생기기 이 전, 개발자들은 기계(컴퓨터, CPU)를 직접 제어하기 위해 기계어(어셈블리어)를 직접 작성하였다. 하지만 CPU는 다양했고, CPU의 종류에 따라 어셈블리어 작성하는 방법도 각기 달랐다고 하며, 간단한 프로그램에도 코드가 굉장히 길어졌다. 

이 문제점을 해결하기 위해, 기계어를 직접 사람이 작성하지 않아도 되는 번역기를 개발했는데 그것이 컴파일러이다. 개발자들은 더 이상 기계의 종류를 상관하지 않아도 된다.


C언어는 소스코드 중복을 줄이고 코드의 재사용성을 높이기 위해서 함수를 제공한다. 하지만, 함수는 호출될 때 *스택 프레임이 일어나기 때문에 속도가 느려지는 단점이 있다. 따라서 코딩할때는 매크로를 함수와 같은 기능으로 작성하지만 실제 기계어로 번역될 때는 직접 코딩한 것처럼 처리(치환, 전처리 단계에서 매크로가 소스코드로 변환)된다.

함수 호출 시, 스택이라는 메모리 공간에 매개 변수, 반환 포인터 값, 지역 변수 순서로 저장되는 값을 스택 프레임이라고 한다.

즉, 저장 공간인 스택을 메모리 낭비를 줄이고 실행 속도를 높이기 위해 매크로를 사용한다. 하지만 매크로는 소스코드로 바로 변환되므로 실행 파일 크기가 커지며, 재귀호출을 할 수 없다는 단점을 가지므로 적절하게 사용하는 것이 중요하다. 

요즘엔 컴퓨터의 메모리가 굉장히 넉넉하기 때문에 메모리 걱정을 하지 않을 때가 많지만, 메모리 공간이 넉넉치 않은 임베디드 시스템과 같은 환경에서는 매크로를 많이 사용한다.


아래 매크로 함수와 일반 함수 예시를 보겠다.

#include <stdio.h>

#define MUL(x, y) x*y

int mul(x, y)
{
	return x*y;
}
int main()
{
	int a, b;
    
    scanf("%d %d", &a, &b);
    printf("%d", MUL(a+1, b+1));
    printf("%d", mul(a+1, b+1));
}

// result
// a=3, b=4 입력
// 8
// 20

왜 결과값이 8, 20으로 다른 값이 나올까?  매크로는 전처리 단계에서 소스 코드로 변경된다고 했다. 

MUL(a+1, b+1)을 소스 코드로 치환되면 a+1*b+1 이다.

그러므로 3+1*4+1로 계산되어 8이라는 결과가 출력된다. mul과 같은 결과를 내기 위해선, 

#define MUL(x, y) ((x) * (y)) 로 변경하면 된다. 소스코드로 치환하게 되면, ((a+1)*(b+1))이다.

728x90
반응형

+ Recent posts