1. 선행처리기와 매크로
-선행 처리는 컴파일 이전의 처리를 의미합니다.
선행처리는 선행처리기에 의해서, 컴파일은 컴파일러에 의해서, 그리고 링크는 링커에 의해서 진행이된다. 컴파일 이전에 선행처리의 과정을 거친다는 점에 주목을 해야한다.
컴파일 과정을 거치게 되면 바이너리 데이터로 이루어진 오브젝트 파일이 생성된다. 그렇다면 컴파일 이전에 진행되는 선행처리의 과정을 거치게 되면 어떠한 데이터로 채워진 파일이 생성되는가?
선행처리의 과정을 거쳐서 생성되는 파일도 그냥 소스파일일 뿐이다. 왜냐하면 소스파일의 형태가 그대로 유지되기 때문이다. 선행처리기가 하는 일은 지극히 단순하다. 우리가 삽입해놓은 선행처리 명령문대로 소스코드의 일부를 수정할 뿐인데, 여기서 말하는 수정이란 , 단순치환(substitution)의 형태를 띠는 경우가 대부분이다. 예를 들면 다음은 가장 간단한 선행처리 명령문이다.
#define PI 3.14
이처럼 선행처리 명령문은 #문자로 시작을 하며, 컴파일러가 아닌 선행처리기에 의해서 처리되는 문장이기 때문에 명령문의 끝에 세미콜론을 붙이지 않는다. 그리고 이렇게 구성이 된 명령문은 선행처리기에게 다음과 같은 메시지를 전달한다.
"PI를 만나면 3.14로 치환"
따라서 위의 명령문이 삽입되어 있는 소스 파일은 선행처리의 과정에서 치환되게 된다.
참고로, '선행처리'란, 컴파일 이전의 처리를 의미한다. 따라서 소스파일은 컴파일러에 의해서 컴파일 되기 이전에, 선행처리기에 의해서 선행처리의 과정을 거치게 된다.
2. 대표적인 선행처리 명령문
- #define : Object - like macro
#define : 지시자
PI : 매크로
3.1415 : 매크로 몸체
위 에서 보이듯이 선행처리 명령문은 기본적으로 세 부분으로 나뉘는데, 제일 먼저 등장하는 #define을 가리켜 '지시자'라 한다. 선행처리기가 이 부분을 보고 프로그래머가 지시하는 바를 파악하기 때문에 지시자라 하는 것이다. 그리고 앞서 설명했듯이 #define 지시자는 선행처리기에게 다음과 같은 내용을 지시한다.
"이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환해라"
#define 지시자 뒤에 등장하는 것을 가리켜 '매크로'라 하고, 그 뒤에 등장하는 것을 가리켜 '매크로 몸체 (또는 대체리스트)'라 한다. 따라서 위의 선행처리 명령문은 다음의 내용을 선행처리기에게 지시한다.
"매크로 PI를 매크로 몸체 3.1415로 전부 치환하라!"
결과적으로 PI라는 이름의 매크로는 그 자체로 상수 3.1415가 된셈이다. 참고로 PI와 같은 매크로를 가리켜 '오브젝트와 유사한 매크로(Object-like macro)'또는 간단히 '매크로 상수'라 한다.
#include <stdio.h>
#define NAME "홍길동"
#define AGE 24
#define PRINT_ADDR pust("주소 : 경기도 용인시 \n);
int main(){
printf("이름 : %s \n", NAME);
printf("나이 : %d \n", AGE);
PRINT_ADDR;
return 0;
}
함수의 호출문도 매크로로 정의할 수 있다.
매크로의 이름은 대문자로 정의하는 것이 일반적이다.
- #define : Function -like macro
매크로는 매개변수가 존재하는 형태로도 정의가 가능하다. 매개변수가 존재하는 매크로는 동작방식이 함수와 유사해 '함수와 유사한 매크로(function -like macro)'라 하는데 줄여서 간단히 '매크로 함수'라 부르기도 한다.
#define SQUARE(X) X*X
#define으로 시작하는 것은 매크로 상수의 정의와 동일하다. 여기서 괄호 안에 존재하는 X는 정해지지 않은 임의의 값 또는 문장을 의미한다.
SQUARE(X) 처럼 이러한 패턴 등장시 X*X 유형으로 바꿔라 라는 뜻이다.
매크로를 접한 선행처리기는 SQUARE(X)와 동일한 패턴을 만나면, 무조건 X*X 로 치환한다.
이러한 변환의 결과가 마치 함수의 호출과 유사하다.
이렇게 선행처리기에 의해서 변환되는 과정 자체를 가리켜 '매크로 확장(macro expansion)'이라 한다.
-잘못된 매크로 정의
SQUARE(3+2)
이를 함수의 관점에서 본다면 3과 2의 합인 5를 인자로 전달하는 것이라고 생각하는 것이 당연하다. 즉 25가 반환되어야 하는데 출력결과는 11이다 .
무엇이 문제일까?
먼저 연산을 하고 , 그 연산결과를 가지고 함수를 호출하게끔 돕는 것은 컴파일러이지 선행처리기가 아니다. 그런데 매크로는 선행처리기에 의해서 처리가 된다. 때문에 위의 문장은 단순히 다음과 같이 치환된다.
3+2*3+2
때문에 11로 출력이 된다.
SQUARE((3+2)) 처럼 구성하면
(3+2)*(3+2)로 치환이 되 문제는 해결이 된다.
하지만 이는 함수를 호출하는데 사람에게 주의를 요하는 형태이기 때문에 안정적이지 못하다. 안정적인 형태가 되려면 SQUARE(3+2)자체가 25로출력 되어야 함수답다.
-매크로 몸체에 괄호를 치기
#define SQUARE(X) (X)*(X)
이렇게 선언을 하면 위의 SQUARE(3+2)의 값의 정상적인 출력을 기대할 수 있다.
하지면 여전히 문제가 남아있다.
int num=120/SQUARE(2);
위 문장에서 SQUARE(2)는 4이므로 변수 num이 30으로 초기화 될것을 기대할 수 있다. 그런데 실제로 초기화되는 값은 120이다. 왜냐하면 다음과 같이 치환되어 나눗셈이 먼저 진행되기 때문이다.
120/(2)*(2);
따라서 이런저런 문제를 모두 해결하기 위해서는 다음과 같은 형태로 매크로 함수를 정의해야 한다.
#define SQUARE(X) ((X) * (Y))
이러면 위의 num은 30으로 초기화가 된다.
이처럼 매크로 함수를 정의할 때에는 매크로의 몸체부분을 구성하는 X와 같은 전달인자 하나하나에 괄호를 해야함을 물론이고, 반드시 전체를 괄호로 한번 더 묶어줘야 한다는 사실을 기억해야 한다.
-매크로를 두 줄에 걸쳐서 정의하기
정의하는 매크로의 길이가 길어지는 경우에 가독성을 높이기 위해서 두 줄에 걸쳐서 매크로를 정의하기도 한다. 그런데 다음과 같이 임의로 줄을 변경하면 에러가 발생한다. 기본적으로 매크로는 한줄에 정의하는 것이 원칙이기 때문이다.
#define SQUARE(X)
((X) *(Y))
따라서 매크로를 두 줄 이상에 걸쳐 정의할 때에는 다음과 같이 \문자를 활용해서 줄이 바뀌었음을 명시해야 한다.
#define SQUARE(X) \
((X) *(Y))
-매크로 정의시 , 먼저 정의된 매크로도 사용이 가능
먼저 정의된 매크로는 뒤에서 매크로를 정의할 때 사용 가능하다.
#include <stdio.h>
#define PI 3.14
#define PRODUCT(X, Y) ((X)*(Y))
#define CIRCLE_AREA(R) (PRODUCT((R), (R))*PI)
int main(){
double rad=2.1;
printf("반지름 %g인 원의 넓이 : %g \n", rad, CIRCLE_AREA(rad));
return 0;
}
위 예제의 4행에서는 2행과 3행에 정의한 매크로를 이용해서 매크로함수를 정의하고 있다. 이렇듯 먼저 정의된 매크로는 새로운 매크로를 정의하는데 사용이 가능하다.
-매크로 함수의 장점
매크로 함수를 정의하는 것은 일반 함수를 정의하는 것보다 복잡하다. 그리고 정의하고자 하는 함수의 크기가 크면 매크로로 정의하는것 자체가 불가능할 수 도 있다. 그럼에도 불구하고 함수를 매크로로 정의하는 이유는 무엇일까? 이에 대한 이해를 위해서 매크로 함수의 장점과 단점을 살펴본다. 다음은 함수를 매크로로 정의할 때 얻게되는 장점들이다.
-매크로 함수는 일반 함수에 비해 실행속도가 빠르다.
-자료형에 따라서 별도로 함수를 정의하지 않아도 된다.
그럼 이중에는 실행속도가 빠른이유부터 살펴보자. 우리도 알다시피 함수가 호출되면 ,다음 사항들이 동반된다.
-호출된 함수를 위한 스택메모리의 할당
-실행위치의 이동과 매개변수로의 인자전달
-return문에 의한 값의 반환
따라서 함수의 빈번한 호출은 실행속도의 저하로 이어진다. 반면 매크로 함수는 선행처리기에 의해서 매크로 함수의 몸체 부분이 매크로 함수의 호출 문장을 대신하기 때문에, 위에서 언급한 사항을 동반하지 않는다. 따라서 실행속도상의 이점이 있다.
이제 매크로 함수의 두번째 장점에 대해서 이야기할 차례이다. 전달인자의 자료형에 상관없이 제대로 치환된다. 이 모두가 매크로 함수의 호출문장이 매크로 함수의 몸체부분으로 단순히 치환되기 때문에 가능한 일이다.
-매크로 함수의 단점
바로 위에서 매크로 함수의 장점을 설명하였는데, 이에 못지 않은 단점도 존재한다. 다음은 함수를 매크로로 정의해야 할때 고려해야 할 단점들이다.
-정의하기가 정말로 까다롭다.
-디버깅하기 쉽지않다
-그래서 이러한 함수들을 매크로로 정의한다.
바로 앞에서 설명한 매크로 함수의 장점과 단점을 종합해보면, 다음의 특성을 지니는 함수들을 매크로의 형태로 정의하는 것이 옳다는 결론이 나온다.
-작은 크기의 함수
-호출의 빈도수가 높은 함수
우선 함수의 크기가 작아야 매크로의 형태로 정의하기 편하고 에러의 발생확률이 낮아서 디버깅에 대한 염려를 덜 수 있다. 그리고 호출의 빈도수가 높아야 매크로 함수가 가져다 주는 성능 향상의 이점도 최대한 누릴 수 있다.
3. 조건부 컴파일(Conditional Compilation)을 위한 매크로
매크로 지시자 중에는 특정 조건에 따라 소스코드의 일부를 삽입하거나 삭제할 수 있도록 디자인 된 지시자가 있다.
if문이 조건부 실행을 위한 것이라면, #if ...#endif는 조건부 코드 삽입을 위한 지시자이다.
#include <stdio.h>
#define ADD 1
#define MIN 0
int main(){
int num1, num2;
printf("두 개의 정수 입력 : ");
scanf("%d %d", &num1, &num2);
#if ADD //ADD가 참이라면
printf("%d + %d = %d \n" ,num1, num2, num1+num2);
#endif
#if MIN //MIN이 참이라면
printf("%d + %d = %d \n" , num1, num2, num1-num2);
#endif
return 0;
}
=====실행결과=====
두 개의 정수 입력 : 5 4
5 + 4 = 9
#if문 뒤에는 반드시 #endif문이 등장해야 한다.
2행과 3행에 정의되어 있는 매크로 ADD와 MIN이 각각 1과 0인 관계로 12행은 삽입되지만 16행은 삭제가 되어 위의 실행결과를 보이게 된다.
이 지시자의 조합도 #if .. #endif와 유사하다. #if는 매크로가 참이냐 거짓이냐를 기준으로 동작한다면, #ifdef는 매크로가 정의 되었느냐, 않았는냐를 기준으로 동작한다.
#include <stdio.h>
//#define ADD 1
#define MIN 0
int main(){
int num1, num2;
printf("두 개의 정수 입력 : ");
scanf("%d %d", &num1, &num2);
#ifdef ADD //ADD가 정의 되었다면
printf("%d + %d = %d \n" ,num1, num2, num1+num2);
#endif
#ifdef MIN //MIN이 정의 되었다면
printf("%d + %d = %d \n" , num1, num2, num1-num2);
#endif
return 0;
}
=====실행결과=====
두 개의 정수 입력 : 7 2
7 - 2 = 5
#ifdef문은 매크로가 정의되어있다면 이라는 뜻을 가지고 있기 때문에 정의만 되어있으면 소스코드가 실행이 된다.
위의 2행과 3행에 정의되어 있는 매크로의 값은 중요하지 않기 때문에, 다음과 같이 매크로의 몸체를 생략해서 정의해도 된다.
#define ADD
#define MIN
그리고 이렇게 매크로가 정의되면 소스코드에 있는 ADD와 MIN은 선행처리 과정에서 공백으로 대체된다.(그냥 소멸된다는 뜻)
- #ifndef ... #endif : 정의되지 않았다면
이 지시자의 조합은 #ifdef문의 반대이다. 중간에 있는 n은 not을 의미한다.
이 매크로는 헤더파일의 중복 포함을 막기위해 주로 사용된다.
-#else의 삽입 : #if, #ifdef, #ifndef 에 해당
if문에 else를 추가할 수 있듯이 , #if, #ifdef, #ifndef에도 #else문을 추가할 수 있다.
if문에 else if를 여러개 추가할 수 있듯이 #if문에도 #elif를 여러번 추가 가능하고 끝일 #else로 마무리가 가능하다.
#include <stdio.h>
#define HIT_NUM 5
int main(){
#if HIT_NUM ==5
puts("매크로 상수 HIT_NUM은 현재 5입니다.");
#elif HIT_NUM ==6
puts("매크로 상수 HIT_NUM은 현재 6입니다.");
#else
puts("매크로 상수 HIT_NUM은 5,6은 아닙니다.");
#endif
return 0;
}
4. 매개변수의 결합과 문자열 화
-문자열 내에서는 매크로의 매개변수 치환이 발생하지 않는다.
문자열의 구성을 위한 매크로 함수를 다음의 형태로 정의하였다.
#define STRING_JOB(A, B) "A의 직업은 B입니다. "
그리고는 STRING _JOB(이봉춘, 나무꾼)이라는 매크로 문장이 다음의 문자열을 만들어 낼 것을 기대하였다.
"이봉춘의 직업은 나무꾼입니다."
하지만 다음과 같은 이유 떄문에 선행처리기는 우리의 기대대로 문자열을 만들어내지 못한다.
"문자열 안에서는 매크로의 매개변수 치환이 발생하지 않는다."
따라서 다음 두 매크로 문장은
STRING_JOB(이동춘, 나무꾼)
STRING_JOB(한상순, 사냥꾼)
둘다 "A의 직업은 B입니다",로 치환이 된다. 즉 A와 B는 매크로의 매개변수 치환이 이뤄지지 않는다. 그렇다면 이문제를 해결하기위해는 무엇을 사용할까?
-문자열 내에서 매크로의 매개변수 치환이 발생하게 만들기 : #연산자
그럼 문제의 해결을 위해 매크로의 #연산자를 소개한다. . 이는 다음과 같은 형태로 사용하는 연산자 이다.
위의 문장에는 다음의 뜻이 담겨있다.
"매개변수 ABC에 전달되는 인자를 문자열 "ABC"로 치환해라!"
이렇듯 #연산자는 치환의 결과를 문자열로 구성하는 연산자이다. 따라서 다음 두문장은,
STR(123)
STR(12, 23, 34)
선행처리기에 의해서 각각 다음 문자열로 치환이 된다.
"123"
"12, 23, 34"
그리고 한가지 더 말하면 문자열은 나란히 하면, 하나의 문자열로 간주가 된다. 따라서 다음과 같이 문자열을 선언하는 것도 가능하다.
char *str ="ABC" "DEF";
그리고 이는 다음의 문자열 선언과 동일하다
char *str = "ABCDEF"
따라서 다음과 같이 문장을 구성하는 것도 가능하다.
char *str = STR(12) STR(34);
그리고 이는 다음과 같이 치환이 되어 ,
char *str = "12" "34";
다음의 문자열 선언과 동일해진다.
char *str ="1234";
#include <stdio.h>
#define STRING_JOB(A, B) #A "의 직업은 " #B "입니다."
int main(){
printf("%s \n", STRING_JOB(이동춘, 나무꾼));
printf("%s \n", STRING_JOB(한상순, 사냥꾼));
return 0;
}
==========실행 결과 ==========
이동춘의 직업은 나무꾼입니다.
한상순의 직업은 사냥꾼입니다.
선행처리기에 의해서 다음과 같이 치환된다
"이동춘" "의 직업은" "나무꾼" "입니다."
즉 4개의 문자열이 나란히 선언된 형태이니, 이는 결과적으로 다음과 같다.
"이동춘의 직업은 나무꾼입니다."
-특별한 매크로 연산자 없이 단순히 연결하는 것은 불가능
대학의 학번은 일반적으로 다음과 같은 형태로 조합이 된다.
10 : 입학년도
65 : 학과코드
175: 고유번호
최종학번 : 1065175
이에 우리는 학번을 조합하는 매크로 함수를 정의하고자한다. 이 함수는 다음과 같은 형태로 호출이 된다.
STNUM(10 ,65, 175);
그리고 이 문장은 선행처리기에 의해서 다음과 같이 치환되어야 한다.
1065175
물론 우리는 이러한 기능의 매크로 함수를 지금 당장 정의할 수 있다.
#define STNUM(Y, S, P) YSP
하지만 이를 통해서는 위가 원하는 결과를 얻지 못한다. 매개변수 Y, S, P가 붙어 있기 때문이다. 이렇게 붙여서 표현을 하면 , 이는 Y, S , P의 조합이 아닌 , 하나의 YSP로 인식이 된다 .결국 Y, S, P에 전달되는 값에 상관없이 그냥 YSP로 변환이 이뤄진다.
한칸씩 공백을 넣어줘도 공백 대로 반영이 된다.
-필요한 형태로 단순하게 결합하기 : 매크로 ##연산자
매크로 ##연산자는 매크로 함수의 전달인자를 다른 대상(전달인자, 숫자, 문자, 문자열 등)과 이어줄때 사용한다.
#define CON(UPP, LOW) UPP##00##LOW
위의 매크로 몸체에는 UPP와 00그리고 LOW가 순서대로 이어질 수 있도록 ##연산자가 사용되었따.
따라서 다음과 같은 문장을 삽입하면,
int num= CON(22, 77);
이는 선행처리기에 의해 컴파일 이전에 다음과 같이 변환된다.
int num = 220077;
이렇게 사용해 학번을 해결할 수있다
#define STNUM(Y, S, P) Y##S##P
'휴지통 > C 프로그래밍' 카테고리의 다른 글
도전 프로그래밍 4-1, 4-2 (0) | 2021.01.29 |
---|---|
메모리 관리와 메모리의 동적할당 (0) | 2021.01.27 |
문자와 문자열 관련 함수 (0) | 2021.01.26 |
파일 입출력 (0) | 2021.01.23 |
구조체와 사용자 정의 자료형2 (0) | 2021.01.22 |