2024. 11. 15. 23:02ㆍProgramming Language/C
26-1 선행처리기와 매크로
선행처리는 컴파일 이전의 처리를 의미한다
일반적으로 컴파일의 과정안에 포함되어 있는 것으로 이야기하지만, 선행 처리의 과정과 컴파일의 과정은 구분이 된다.
선행처리기의 일 간단히 맛보기
컴파일러에 비해서 선행처리기의 역할은 매우 단순하다.
쉽게 말해서 "단순한 치환"의 작업을 거친다.
선행처리기에게 무엇인가를 명령하는 문장은 #으로 시작한다
//선행 처리기 이전 소스파일
#define PI 3.14
int main(void){
....
num = PI * 3.5;
....
}
↓
//선행 처리기 이후 소스파일
int main(void){
....
num = 3.14 * 3.5;
....
}
이렇게 명령문은(#define)은 소멸되고 PI는 단순 치환된다.
26-2 대표적인 선행처리 명령문
#define: Object-like macro
#define PI 3.1415
↑ ↑ ↑
지시자 매크로 메크로 몸체
PI를 3.1415로 무조건 치환해라라는 의미이다.
선행처리기에 의해서 처리된 문장들의 결과는 아래와 같다.
#define: Function-like macro
이 매크로는 어떤 패턴이 등장하면 다른 유형으로 전환시키는 매크로이다.
#define SQUARE(X) X*X
↑ ↑
이 패턴을 이 모양으로 바꿔라
그래서
SQUARE(123); ----> 123 * 123;
SQUARE(NUM); ----> NUM * NUM;
이렇게 변환시킨다
이런 변환 과정을 매크로 확장(macro expansion)이라고 한다
이 결과는
와 같이 출력된다.
여기서 볼 곳은
이 부분인데 우리는 3 + 2가 진행된 결과인 5 * 5의 결과로 25가 출력되기를 기대했지만 결과는 11이였다.
그 이유는
SQUARE(3 + 2) ----> 3 + 2 * 3 + 2
로 결과가 3 + 6 + 2 의 결과인 11이 나온것이기 때문이다.
그렇기에 사실은
SQUARE((3+2)) -------> (3 + 2) * (3 + 2)
로 값을 만들어야 맞게 결과가 나온다.
그런데 이렇게 하면 사용하기에 너무 불편하기에 그냥 확장 부분에 괄호를 덧 씌워 주면 문제가 해결된다.
그런데 이 상황에서도 문제인 경우가 있는데 만약 이 SQUARE 자체를 연산을 위해서 사용한다면 가령,
num = 120 / SQUARE(2)
라면 그 치환된 내용은
num = 120/(2)*(2)
로 내가 원하는 120 / 4의 결과를 낼 수 가 없다.
그렇기 때문에 전체를 한번 소괄호로 더 묶어주면 문제가 해결된다.(완벽한 해결이 아닌 최선의 해결책 이라고 봐야한다)
매크로를 두 줄에 걸쳐서 정의하는 방법
이렇게 매크로를 두줄에 걸쳐서 정의하려고 하면 에러가 발생한다.
그렇기에 메크로를 두 줄에 걸쳐서 정의할 수 있도록 하게 하기 위해서는 \를 개행 하는 곳에 넣어줘야 한다
이것의 뜻은 다음 라인에 매크로에 대해서 더 작성하겠다는 것을 명시하는 기능이다.
먼저 정의된 매크로의 사용
매크로를 정의할때 그 매크로 이전에 먼저 정의한 매크로를 사용하는것도 가능하다.
이렇게 위에서 만든 매크로를 사용해서 매크로를 정의할 수 있다.
이 매크로를 사용하는 코드를 한번 확인해보면
이렇게 rad값만 있으면 해당 값을 출력 해볼 수 있다.
이렇게 매크로를 정의할때 매크로를 사용할 수 도 있다.
일반함수와 비교한 매크로 함수의 장점
매크로 함수는 일반 함수에 비해 실행 속도가 빠르다.
그 이유는 함수의 호출을 완성하기 위해서는 별도의 메모리 공간이 필요하고 호출된 함수의 이동 및 반환의 과정을 거쳐야하는데 반면 매크로 함수는 정의된 몸체로 치환이 이뤄지기에 위와같은 과정이 없기에 빠를 수 밖에 없다.
또한 매크로는 자료형에 따라서 별도로 함수를 정의하지 않아도 된다.
전달되는 인자의 자료형에 구분받지 않기에 자료형에 의존적이지 않다.
일반함수와 비교한 매크로 함수의 단점
반면 단점 또한 존재하는데 정의하기가 정말 까다롭고 디버깅하기가 쉽지 않다.
선행처리 후 컴파일러에 의해 에러가 감지되기 때문에 오류가 난 것을 잡기가 쉽지가 않다는 것이다.
왜냐면 컴파일러는 컴파일 된 이후의 내용을 기준으로 오류를 말해주기 때문이다.
먼저 하나의 함수를 보면
int DiffABS(int a, int b){
if(a > b)
return a - b;
else
return b - a;
}
라는 함수가 있다고 생각해보자.
이걸 매크로로 선언 하기 위해서는
#define DIFF_ABS(X, Y) ((X) > (Y) ? (X) - (Y) : (Y) - (X))
으로 작성할 수 있을것 같은데 이는 함수에 비해서 매우 구성하기가 힘들고 이렇게 사용해서 문제가 있을 경우에도
오류를 컴파일러가 해당 부분에서 에러라고 알려주질 않는다.
함수를 매크로로 정의하기 위한 조건
함수를 매크로로 정의하기 위한 조건은 먼저 작은 크기의 함수일때이다.
한 두줄 정도의 크기의 작은 함수가 아니라면 매크로로 정의하는 것이 쉽지 않기에 오류가 발생할 확률이 높아진다.
그리고 if ~ else, for와 같이 실행의 흐름을 컨트롤 하는 문장도 매크로로는 정의하기 쉽지 않다.
다른 조건은 호출 빈도수가 높은 함수일 경우일때다.
함수를 매크로로 정의하는 데에는 성능적 측면이 고려된다.
호출 빈도수가 높지 않다면 굳이 매크로로 함수를 정의하는 수고를 할 이유가 없기 때문이다.
26-3 조건부 컴파일(Conditional Compilation)을 위한 매크로
#if ... #endif : 참이라면
매크로에 정의된 어떤 값이 참인지 거짓인지에 따라서 컴파일의 결과가 달라지도록 할 수 있는 메크로이다.
#ifdef...#endif: 정의되었다면
#ifndef...#endif: 정의되어 있지 않다면
#else의 삽입 : #if, #ifdef, #ifndef에 해당
#if와 #ifdef와 #ifndef는 모두 #else를 추가해서 조건이 참이 아닌 경우 컴파일 대상에 포함시킬 문장들을 구성할 수 있다.
#elif 의 삽입: #if에만 해당
#if의 경우는 추가적인 조건을 설정할 수 있는 #elif를 사용하는 것이 가능하다.
위의 조건중 하나의 문장만 컴파일의 대상으로 포함된다.
26-4 매개변수의 결합과 문자열화
문자열 내에서는 매크로의 매개변수 치환 불가
#define STRING_JOB(A,B) "A의 직업은 B입니다."
라고 매크로를 생성했을때 우리는 A와 B가 추후에 STRING_JOB(홍길동, 의사)라고 사용했을때 "홍길동의 직업은 의사입니다."라고 출력되기를 바라지만 이렇게 문자열의 안에 매개변수가 존재할 경우에는 치환이 불가능하다.
그렇기에
STRING_JOB(이상순,가수);- ====> "A의 직업은 B입니다."
STRING_JOB(김건모,개그맨);- ====> "A의 직업은 B입니다."
과 같이 기대대로 치환되지 않는 결과를 나타낸다.
그 이유는 위에서 말했듯이 문자열안에서는 치환이 일어나지는 않기 때문이다.
이럴때 고려할 수 있는 연산자가 # 연산자이다.
문자열 내에서 매크로 매개변수 치환 : # 연산자
#define STR(ABC) #ABC
라고 사용할 수 있는데 이건 매개변수 ABC에 전달되는 인자를 문자열 ABC로 치환해라 라는 것이다.
사용 예제를 보면
STR(123) ====> "123"
STR(12, 23, 34) =====> "12, 23, 34"
와 같이 매크로 확장의 결과가 나온다.
#연산자를 이용한 문제 해결
보통 둘 이상의 문자열의 선언을 나란히 하면 이는 하나의 문자열 선언으로 인식된다.
char * str = "ABC" "DEF";
↓
char * str = "ABCDEF";
이 두 코드를 동일한 것으로 인식한다.
이 점에서 착안해서 맨 위에 " A의 직업은 B입니다."를 한번 해결해보자.
#define STRING_JOB(A, B) #A "의 직업은 " #B "입니다."
라고 작성해주면 #A + 의 직업은 + #B + 입니다로 한 문장으로 되면서 전달인자를 A와 B에 문자열로 넣으면서 해결을 할 수 있다.
이를 실행해보면
의 결과를 출력했다.
매크로 연산자 없이 단순 연결은 불가능하다.
# 매크로 연산자 없이는 이 전달된 인자들은 단순하게 붙여주는 작업은 불가능하다.
예를 들어서 1012534라는 번호가 있다고 생각해보자.
여기서 앞 두자리는 번호 생성년도, 그 다음 두자리는 생성월이고 나머지 3글자는 식별 코드라고 생각해보자.
10 12 534
생성년도 생성월 고유식별코드
이 모든게 붙어 있어야 하나의 구분을 위한 코드라고 생각해보자.
이걸 전달 인자로 각각 전달 받고 하나의 코드로 변환한다고 할때 이걸 메크로를 사용한다면 위에서 사용한 매크로 연산자를 사용해야한다.
만약 연산자를 안쓴다면
#define STNUM(Y,M,I) YMI ===> 치환 대상 YMI를 연결해 두면 그냥 YMI로 인식된다.
#define STNUM(Y,M,I) Y M I ===> 치환은 모두 되는데 연결되어 있지 않기에 10 12 534와 같이 치환된다.
#define STNUM(Y,M,I) ((Y)*100000 + (M)*1000 + (I))
===> #연산자를 모르는 상태에서 최선의 해결책임..
그런데 사실 마지막에 있는 예시도 완전한 해결책은 아닌게 전달되는 숫자에 따라서 값이 원하는 결과를 출력하지 않을 수 있기 때문이다.
여기서 보면 첫번째 printf문에서는 별 문제 없이 컴파일 되나 두번째 printf문을 보면 세번째 인자가 034인것을 알 수 있는데 이 034를 8진수 숫자로 인식해서 결과를 출력하는 경우가 있다.
그렇기 때문에 저 방법도 완벽한 방법이 아니다.
우리는 그냥 단순하게 연결만 하기를 바라는데 8진수를 인식하면서 생각할 요소가 생기기 때문이다.
필요한 형태대로 단순 결합: ## 연산자
#define CON(UPP,LOW) UPP ## 00 ## LOW
이건 매개변수 UPP과 LOW에 전달되는 인자를 UPP00LOW로 단순하게 연결해서 치환하는 매크로이다.
이를 사용해보면
int num = CON(22, 77); ===> 220077
이렇게 ## 연산자를 사용해서 위에서 해결되지 않던 학번을 만들어보자면
#define STNUM(Y, M, I) Y ## M ## I
와 같이 매크로를 작성해야 학번을 단순 치환할 수 있다
'Programming Language > C' 카테고리의 다른 글
열혈 C - Chapter 27. 파일의 분할과 헤더파일의 디자인 (0) | 2024.11.17 |
---|---|
열혈 C - Chapter 25. 메모리 관리와 메모리의 동적 할당 (3) | 2024.11.15 |
열혈 C - Chapter 24 파일 입출력 (0) | 2024.11.03 |
열혈 C - Chapter 23 구조체와 사용자 정의 자료형2 (0) | 2024.10.31 |
열혈 C - Chapter 22 구조체와 사용자 정의 자료형1 (0) | 2024.10.28 |