2023. 1. 5. 14:27ㆍC언어 복습
1. 어서트 재정의
assert라는 함수는 디버깅을 하는 데 있어서 자주 활용되는 함수이다. 그런데 매크로 함수를 적절하게 활용하면 이런 assert함수를 좀 더 보기좋게 바꿀 수 있다. 아래의 코드를 보자.
#define ASSERT(condition, msg)
if (!(condition))
{
fprintf(stderr, "%s(%s: %d)\n", msg, __FILE__, __LINE__);
__asm { int 3 }
}
void Func(void)
{
int month = 20;
ASSERT(month < 12, "invalid month number");
}
우리가 기존에 활용했던 assert함수는 assert(month < 12); 정도였을 것이다. 그런데 이번에 보게 된 ASSERT매크로 함수는 에러 문구에 해당하는 문자열이 하나 더 들어갔다. 이렇게 했을 때의 장점은 다음과 같다.
- assert를 실행시켰을 때, 실패를 하면 호출 스택이 assert함수 내부로 찍히게 된다. 하지만 만약 assert가 아니라 Func라는 함수의 이름이 찍힌다면 디버깅이 더 쉬울 것이다. 어떤 함수에서 오류가 났는지 좀 더 편하게 볼 수 있기 때문이다.
- 참고로 __asm { int 3 } 은 어셈블리어로 프로그램 실행을 그곳에서 중지시키는 코드이다. 따라서 ASSERT매크로에 매개 변수로 집어넣은 month < 12 가 거짓이면 __asm { int 3 }을 만나고 프로그램이 중지된다. 단, 이는 x86에서의 이야기고 다른 플랫폼은 다를 수 있으니 검색을 통해 충분히 알아본 후에 조건부 컴파일로 이 문제를 해결하면 된다.
- 매크로 함수에 있는 __asm { int 3 }에서 프로그램이 멈추면 5번째 줄에 있는 __asm { int 3 }에서 멈추는 것이 아니라 11번째 줄인 ASSERT매크로 함수를 사용하는 부분에서 중단점이 걸리게 된다. 그럼 invalid month number라는 문자열을 바로 볼 수 있고 사람의 입장에서 바로 어디가 잘못 되었는지 알 수 있게 되는 것이다.
2. 전처리 명령어와 장단점
C언어를 배웠던 입장에서 #define, #include, #if와 같은 유명한 전처리 명령어는 꽤 봐왔기 때문에 굳이 적지 않으려고 한다. 다만 기존에 몰랐던 전처리 명령어가 있어서 그것만 간단하게 정리하고 빠르게 넘어갈 것이다.
# 기능
아래 코드의 실행 결과를 보면 눈치챌 수 있겠지만 #define 뒤에 나오는 #명령은 매크로 함수에서 받은 매개 변수를 문자열 자체로 인식하게 만드는 기능이다. 여기서 중요한 것은 서식문자에 나올법한 %나 쌍따옴표같은 것들도 전부 문자열의 일부로 치환해버린다는 것이다. 잘 활용하면 매우 유용해보여서 적어보았다.
#define str(s) #s
printf("%s\n", str(\n));
printf("%s\n", str("\n"));
printf("%s\n", str(int main));
/*
실행 결과
"\n"
int main
*/
##기능
이번에 볼 코드는 ##이라는 기능에 대한 예제이다. 이거는 좀 특이해서 아래의 예제 코드를 먼저 보고 글을 보는 것이 이해가 편할 것이라고 생각한다.
#define PRINT(n) printf("%d\n", g_id_##n);
int g_id_none = 0;
int g_id_teacher = 1;
int g_id_student = 2;
PRINT(number) /* 컴파일 오류 */
PRINT(none) /* g_id_none의 값 출력 */
PRINT(student) /* g_id_student의 값 출력 */
##의 기능은 매개 변수로 들어온 키워드 자체를 해당 위치에 치환시킨다. 즉, 정말 말 그대로 복사, 붙여넣기를 시키는 것이다. 그래서 저렇게 변수의 이름의 일부를 매개 변수로 전달했을 때, 변수의 값이 출력되며 이를 이용해서 변수의 이름을 이용한 접근이 가능하다.
장점
- 함수 호출이 아닌 곧바로 코드를 복붙하는 개념으로 함수 호출로인한 과부하가 없다.
- C에서 불편한 것들 중 일부를 매크로를 이용한 꼼수로 편하게 해결할 수 있다.
단점
- 디버깅이 굉장히 불편하고 어렵다.
- \를 이용해서 읽기 좋게 바꾸더라도 중단점을 매크로 내부에 찍을 수는 없다.
활용 예제 1 - 튜플
#include <stdio.h>
#define MONSTER_DATA \
MONSTER_ENTRY(0, "pope", 100) \
MONSTER_ENTRY(1, "big rat", 30) \
MONSTER_ENTRY(2, "mama", 255) \
MONSTER_ENTRY(3, "dragon", 300000) \
int main(void)
{
size_t i;
int ids[] = {
#define MONSTER_ENTRY(id, name, hp) id,
MONSTER_DATA
#undef MONSTER_ENTRY
};
const char* names[] = {
#define MONSTER_ENTRY(id, name, hp) name,
MONSTER_DATA
#undef MONSTER_ENTRY
};
int healths[] = {
#define MONSTER_ENTRY(id, name, hp) hp,
MONSTER_DATA
#undef MONSTER_ENTRY
};
for (i = 0; i < sizeof(ids) / sizeof(int); i++)
{
printf("%3d %6d %s\n", ids[i], healths[i], names[i]);
}
return 0;
}
활용 예제 2 - getter (OOP의 그 getter 맞음)
#include <stdio.h>
#define MONSTER_STRUCT \
MONSTER_MEMBER(int, id) \
MONSTER_MEMBER(const char*, name) \
MONSTER_MEMBER(int, hp) \
typedef struct {
#define MONSTER_MEMBER(type, name) type name;
MONSTER_STRUCT
#undef MONSTER_MEMBER
} monster_t;
#define MONSTER_MEMBER(type, name) \
type get_mob_##name(const monster_t* mob) \
{ \
return mob->name; \
} \
MONSTER_STRUCT
#undef MONSTER_MEMBER
int main(void)
{
monster_t mob;
mob.id = 0;
mob.name = "Pope Mob";
mob.hp = 10001;
printf("%3d %6d %s\n", get_mob_id(&mob), get_mob_hp(&mob), get_mob_name(&mob));
return 0;
}
솔직히 보면서 좀 신기했다... 이런 식으로 코드를 짠 적이 한 번도 없었기 때문이다... 확실히 유용해보이는데 그 와중에 매크로 함수의 단점을 확실하게 느낄 수 있었다. 천천히 차근차근 읽어보면 읽히기는 하는데 실수가 날 일이 정말 많아보이는 코드라고 생각한다. 사실 getter뿐만 아니라 C++의 함수 오버로딩같은 것도 위와 같이 구현되어 있는 것은 아닐까라고 생각하게 되었다. 또한 그동안 매크로 함수가 실수를 유발하기 쉽고 디버깅이 불편해져서 사용하는 것을 무작정 꺼려했는데 매크로 함수의 사용을 다시 생각해보게 되었다.
'C언어 복습' 카테고리의 다른 글
POCU C언어 정주행 17회차 - restrict, C99 기능 (0) | 2023.01.07 |
---|---|
POCU C언어 정주행 16회차 - inline 함수, inline 주의점, 해결 방법 (1) | 2023.01.07 |
POCU C언어 정주행 14회차 - malloc과 free, 메모리 함수, 메모리 관리 기법 (0) | 2022.12.31 |
POCU C언어 정주행 13회차 - 예외 처리, 나쁜 오류 처리, 오류 처리 전략 (0) | 2022.12.30 |
POCU C언어 정주행 12회차 - 가변 인자 함수 (2) | 2022.12.29 |