이 문서는 swap함수를 단계별로 단점을 극복해가며 작성한 것이다.
- 모두가 아는 swap 함수
void swap(void *a, void *b);
int main() {
int a = 10, b = 20;
swap(&a, &b);
printf("a = %d, b = %d", a, b);
return 0;
}
void swap(void *a, void *b) {
int t = *(int *)a;
*(int *)a = *(int *)b;
*(int *)b = t;
}
단점
- 위 swap함수는 int형만 swap이 가능하다.
따라서 다음에는 모든 타입에 대하여 swap이 가능하도록 해보자.
enum Type { INT, DOUBLE }; //Type Code라 한다.
void swap(void *a, void *b, enum Type t) {
switch (t) {
case INT: {
int tmp = *(int *)a;
*(int *)a = *(int *)b;
*(int *)b = tmp;
break;
}
case DOUBLE: {
double tmp = *(double *)a;
*(double *)a = *(double *)b;
*(double *)b = tmp;
break;
}
}
}
int main() {
int a = 10, b = 20;
swap(&a, &b, INT);
double c = 3.14, d = 5.14;
swap(&c, &d, DOUBLE);
}
C언어에는 Java의 Object처럼 모든 타입을 담을 수 있을 수 없기에
위와 같이 열거형으로 타입을 주었다.
단점들
- (1) Type Code를 사용하면 항상 switch문을 이용하게 된다.
- (2) 새로운 Type Code를 부여하면, case문을 그에 맞게 추가해야하는 번거로움이 있다.
이번에는 Type Code 대신 dataSize:size_t를 swap의 인수로 받아서 동적 할당을 통해 구현해보았다.
void swap(void *a, void *b, size_t dataSize){
void *t = malloc(dataSize);
memcpy(t, a, dataSize);
memcpy(a, b, dataSize);
memcpy(b, t, dataSize);
free(t);
}
int main() {
int a = 10, b = 20;
swap(&a, &b, sizeof(int));
printf("a = %d, b = %d\n", a, b);
double c = 3.14, d = 5.14;
swap(&c, &d, sizeof(double));
printf("c = %f, d = %f\n", c, d);
}
위와 같이 세번 째 인수로 바꾸려는 자료형의 크기를 전달하면,
swap에서 void* t 를 해당 크기에 맞게 동적 할당하여
swap을 구현할 수 있었다.
단점들
- (1) malloc()의 성공이 보장되지 않는다.
- (2) 함수 호출의 오버헤드가 발생할 수 있다.
- (3) OS측면에서 malloc()의 호출은 상당히 부담이 되는 작업이다.
따라서 다음에는 동적 할당을 하지 않고 하는 방법을 생각해보자.
#define SWAP(x, y, t) \
(t) = (x);
(x) = (y);
(y) = (t);
int main() {
int a = 10, b = 20, t1;
SWAP(a, b, t1);
printf("a = %d, b = %d\n", a, b);
double c = 3.14, d = 5.14, t2;
SWAP(c, d, t2);
printf("c = %f, d = %f\n", c, d);
}
이번에는 SWAP을 매크로 함수로 구현해보았다. SWAP의 세번 째 인수인 t는 사용자로부터 받는 인수이다.
단점
- SWAP의 호출 회수만큼 해당 자료형의 메모리 변수를 선언해야 하므로 스택 메모리를 많이 차지하게 된다.
이번에는 위의 스택 메모리를 절약하기 위해 매크로 함수를 조금 바꿔보았다.
#define SWAP(x, y, T) {\
T t = (x);\
(x) = (y);\
(y) = t; }
int main() {
int a = 10, b = 20;
SWAP(a, b, int);
printf("a = %d, b = %d\n", a, b);
double c = 3.14, d = 5.14;
SWAP(c, d, double);
printf("c = %f, d = %f\n", c, d);
}
위 코드에서는 SWAP의 세번 째 인자T에 swap할 자료형을 받게 했다.
그리고 { }블록을 두어 t를 그 안에서 선언하므로써, 불필요한 스택 메모리의 사용을 없앴다.
단점
- Dangling-Else가 발생할 수 있다.
int a = 10, b = 20;
if(a > b){
SWAP(a, b, int);
}
else{
;
}
//위와 같이 if문 안에 SWAP함수를 두면,
//전처리기에 의해 변환된 if절의 마지막이
// }; 로 끝나게 되는 문제점이 발생한다.
위 코드에서의 Dangling-else가 발생하는 현상을 막기 위해서는 SWAP함수의 정의부를 do-while 문으로 감싸주는 방법이 있다.
#define SWAP(x, y, T) do{ \
T t = (x);\
(x) = (y);
(y) = t; } while(0)
단점
- 위 코드에서의 단점은 딱히 보이지 않았다.
- 하지만 꼭 매크로 함수를 써야할까? 그리고, 일반 함수를 사용한다면 동적할당을 하지 않고 구현할 수는 없을까?
void swap(void *a, void *b, size_t dataSize) {
char *pA = (char *)a;
char *pB = (char *)b;
for(size_t i = 0; i < dataSize; i++, pA++, pB++) {
char t = *pA;
*pA = *pB;
*pB = t;
}
}
int main(){
int a = 10, b = 20;
swap(&a, &b, sizeof(int));
printf("a = %d, b = %d\n", a, b);
double c = 3.14, d = 5.14, t2;
SWAP(c, d, t2);
printf("c = %f, d = %f\n", c, d);
}
위 swap함수에서는 바꿀 것을 1Byte 단위로 꺼내와서 복사한다. 그러기 위해 char* 와 char을 이용했다.
결과
- 위 코드는 일반 함수를 이용해 완벽하게(?) 구현해낸 swap 함수이다.
다음은 리눅스 v2.6.39.4에 있는 sort.c의 swap함수이다.
static void generic_swap(void *a, void *b, int size) {
char t;
do {
t = *(char *)a;
*(char *)a++ = *(char *)b;
*(char *)b++ = t;
} while( --size > 0);
}
의문점
- 위 코드는 GCC 컴파일러에서는 문제없이 작동하지만, Visual Studio C Compiler에서는 컴파일 타임에 오류가 발생한다. 이유가 뭘까..?
정답
- GCC에서는 void* 에 대한 증감 연산이 가능하다. 하지만 Visual Studio C Compiler는 이를 허용하지 않는다.
- *(char *)a++ = *(char *)b; 가 수행되는 순서
- (1) a를 char*형으로 캐스팅한다.
- (2) 역참조를 하여 우항의 값을 대입한다. (이때, 캐스팅효과는 역참조 후 풀리게 된다.)
- (3) a(void*형)에 대하여 ++연산을 수행한다.
- (4) void*에 대하여 ++ 연산을 하기에 Visual Studio C Compiler에서는 수행이 불가하지만, GCC에서는 수행이 가능한 것이다.
- (5) Visual Studio C Comipler에서도 수행가능하게 하려면 코드를 다음과 같이 고쳐야 한다.
*((char *)a)++ = *(char *)b;
*((char *)b)++ = t;