51. 자료형 변환
1. 키워드
- 형 변환(Type Conversion, Type Casting)
- 암시적 형 변환(Implicit Type Conversion)
- 명시적 형 변환(Explicit Type Conversion)
2. 자료형 변환하기
- C에서는 자료형이 같거나 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 저장하면 자동으로 변환이 된다.
int num1 = 10;
unsigned int num2 = num1; // int 타입과 unsigned int 타입은 자료형이 같음
long long num3 = num1; // long long 타입이 int 타입보다 크기가 큼
- 하지만 자료형이 다르면서 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 저장하면 컴파일 경고가 발생한다.
- 예를 들어 실수에서 소수점 이하 자리를 버리는 기능을 구현하고자 실수를 정수로 저장했을 때 프로그래머가 의도한 상황이지만 컴파일 경고가 발생한다.
- 다음 그림과 같이 자료형의 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 자동 변환되는 것을 형 확장이라 하고, 이런 변환을 암시적 형 변환이라고 한다.
- 반대로 자료형 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 변환되는 것이 형 축소이다.
- 이때 형 축소에서 컴파일 경고가 나오지 않도록 만드는 것을 형 변환이라고 한다.
- 특히 프로그래머가 강제로 자료형을 변환한다고 해서 명시적 형 변환이라 부르기도 한다.
- 형 확장은 값의 손실이 없으므로 컴파일러가 알아서 처리할 수 있다.
- 하지만 형 축소는 값의 손실이 발생하여 컴파일러가 알아서 처리할 수 없으므로 경고가 발생한다.
- 따라서 형 변환으로 의도를 밝혀야만 컴파일러가 안심하고 변환을 하게 된다.
- 즉, 형 변환은 컴파일러에게 자료형을 변환한다는 의도를 명확하게 알려주는 것이다.
- 특히, 형 변환은 구조체와 포인터를 조합하여 사용할 때 유용하게 활용된다.
3. 기본 자료형 변환하기
- 자료형을 지정하여 변환하는 것을 명시적 자료형 변환이라고 하며 변수나 값 앞에 변환할 자료형을 붙인 뒤
()
(괄호)로 묶어주면 된다.
#include <stdio.h>
int main()
{
int num1 = 32;
int num2 = 7;
float num3;
num3 = num1 / num2; // 컴파일 경고 발생
printf("%f\n", num3);
num3 = (float)num1 / num2; // num1을 float 타입으로 변환
printf("%f\n", num3);
return 0;
}
// 4.000000
// 4.571429
num3 = num1 / num2;
와 같이정수 / 정수
를 계산하면 정수(int
타입)4
가 나오고num3
에는4.0000000
이 저장된다.- 이때
num3
은float
타입이라int
타입과 자료형이 달라서 컴파일 경고가 발생한다. - 하지만
num3 = (float)num1 / num2;
와 같이num1
을float
타입으로 강제 변환해 주면실수 / 정수
가 되어 결과도 실수(float
타입)가 된다. - 따라서
num3
에는4.571429
가 저장되고, 컴파일 경고가 발생하지 않는다. - 이처럼 형 변환은 자료형을 명확하게 결정할 수 있다.
4. 포인터 변환하기
- 이번에는 포인터끼리 변환하는 방법이다.
- 이때는 자료형 뒤에 포인터를 나타내는
*
(애스터리스크)를 붙여주고()
로 묶어주면 된다.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
int main()
{
int *numPtr = malloc(sizeof(int)); // 4바이트만큼 메모리 할당
char *cPtr;
*numPtr = 0x12345678;
cPtr = (char *)numPtr; // int 타입 포인터 numPtr을 char 타입 포인터로 변환. 메모리 주소만 저장됨
printf("0x%x\n", *cPtr); // 낮은 자릿수 1바이트를 가져오므로 0x78
free(numPtr); // 동적 메모리 해제
return 0;
}
// 0x78
numPtr
에 메모리를 할당하고 역참조하여0x12345678
을 저장했다.- 그리고
cPtr = (char *)numPtr;
과 같이int
타입 포인터를char
타입 포인터로 변환하여 메모리 주소를 저장했다. - 이 상태에서
*cPtr
과 같이 역참조하면numPtr
의 메모리 주소에 접근하지만cPtr
은char
타입 포인터이므로 1바이트만큼만 값을 가져온다. - 즉,
numPtr
이나cPtr
이나 안에 저장된 메모리 주소는 같지만 자료형에 따라 역참조했을 때 값을 가져오는 크기가 결정되기 때문이다.
- 여기서 메모리 공간에
0x12345678
(리틀 엔디언:78 56 34 12
)이 저장된 상태에서 1바이트char
타입 크기만큼 낮은 자릿수 값을 가져오므로0x78
이 된다.
- 앞의 예제와는 반대로 크기가 작은 메모리 공간을 할당한 뒤 큰 자료형의 포인터로 역참조하면 의도치 않은 값을 가져오게 된다.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
int main()
{
short *numPtr1 = malloc(sizeof(short)); // 2바이트만큼 메모리 할당
int *numPtr2;
*numPtr1 = 0x1234;
numPtr2 = (int *)numPtr1; // short 타입 포인터 numPtr1을 int 타입 포인터로 변환. 메모리 주소만 저장됨
printf("0x%x\n", *numPtr2); // 옆의 메모리를 침범하여 값을 가져옴
// 상황에 따라서 값이 달라질 수 있음
free(numPtr1); // 동적 메모리 해제
return 0;
}
// 0xfdfd1234
- 이번에는
numPtr1
에 2바이트 크기로 메모리를 할당하고 역참조하여0x1234
를 저장했다. - 그리고
numPtr2 = (int *)numPtr1;
과 같이short
타입 포인터를int
타입 포인터로 변환하여 메모리 주소를 저장했다. printf
로*numPtr2
를 출력해 보면1234
앞에 엉뚱한 값이 붙어서 나왔다.- 크기가 작은 메모리를 할당한 뒤 큰 자료형의 포인터로 역참조하면 옆의 메모리 공간을 침범하여 값을 가져오게 된다.
malloc
함수로 2바이트만큼 메모리를 할당했으므로0x1234
(리틀 엔디언:34 12
)만 저장되어 있다.- 하지만 이 상태에서 4바이트
int
타입 크기만큼 값을 가져오면 2바이트 크기를 벗어나서malloc
함수로 할당하지 않은 공간까지 함께 가져오게 된다. - 따라서 의도치 않은 값을 가져오게 되므로 주의해야 한다.
- 참고로 할당되지 않은 공간에는 쓰레기 값이 들어있다.
포인터 변환과 컴파일 경고
- 일반 변수와 마찬가지로 포인터도 변환할 때 자료형이 다르면 컴파일 경고가 발생한다.
int *numPtr = malloc(sizeof(int)); // int 타입 포인터
char *cPtr; // char 타입 포인터
cPtr = numPtr; // 컴파일 경고 발생
free(numPtr);
- 포인터를 다른 자료형으로 변환하면서 역참조하려면 다음과 같이
()
앞에*
를 붙여주면 된다.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
int main()
{
int *numPtr = malloc(sizeof(int)); // 4바이트만큼 메모리 할당
char *cPtr;
*numPtr = 0x12345678;
printf("0x%x\n", *(char *)numPtr); // numPtr을 char 타입 포인터로 변환한 뒤 역참조
free(numPtr); // 동적 메모리 해제
return 0;
}
// 0x78
*(char *)numPtr
과 같이char
타입 포인터로 변환한 뒤 역참조했다.- 따라서
0x12345678
에서 1바이트char
타입 크기만큼 낮은 자릿수 값0x78
을 가져온다.
5. void
타입 포인터 변환하기
void
타입 포인터는 자료형이 정해져 있지 않으므로 역참조 연산을 할 수 없다.- 하지만
void
타입 포인터를 다른 자료형으로 변환하면 역참조를 할 수 있다.
#include <stdio.h>
int main()
{
int num1 = 10;
float num2 = 3.5f;
char c1 = 'a';
void *ptr;
ptr = &num1; // num1의 메모리 주소를 void 타입 포인터 ptr에 저장
// printf("%d\n", *ptr); // 컴파일 에러
printf("%d\n", *(int *)ptr); // void 타입 포인터를 int 타입 포인터로 변환한 뒤 역참조
ptr = &num2; // num2의 메모리 주소를 void 타입 포인터 ptr에 저장
// printf("%f\n", *ptr); // 컴파일 에러
printf("%f\n", *(float *)ptr); // void 타입 포인터를 float 타입 포인터로 변환한 뒤 역참조
ptr = &c1; // c1의 메모리 주소를 void 타입 포인터 ptr에 저장
// printf("%c\n", *ptr); // 컴파일 에러
printf("%c\n", *(char *)ptr); // void 타입 포인터를 char 타입 포인터로 변환한 뒤 역참조
return 0;
}
// 10
// 3.500000
// a
ptr = &num1;
과 같이int
타입 변수num1
의 메모리 주소를ptr
에 저장했다.- 하지만
ptr
은void
타입 포인터라 역참조를 하면 컴파일 에러가 발생한다. - 따라서
*(int *)ptr
과 같이void
타입 포인터를int
타입 포인터로 변환한 뒤 역참조를 해야 한다. - 마찬가지로
void
타입 포인터에float
타입 포인터(메모리 주소)가 들어있다면*(float *)ptr
과 같이float
타입 포인터로 변환한 뒤 역참조를 해야 하고,char
타입 포인터가 들어있다면*(char *)ptr
과 같이char
타입 포인터로 변환한 뒤 역참조를 해야 한다.
6. 구조체 포인터 변환하기
- 자료형 변환을 주로 사용하는 상황은 구조체 포인터를 변환할 때이다.
- 이때는
struct
와 구조체 이름 뒤에*
를 붙여주고()
로 묶어주면 된다.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
struct Data
{
char c1;
int num1;
};
int main()
{
struct Data *d1 = malloc(sizeof(struct Data)); // 포인터에 구조체 크기만큼 메모리 할당
void *ptr; // void 타입 포인터 선언
d1->c1 = 'a';
d1->num1 = 10;
ptr = d1; // void 타입 포인터에 d1 할당. 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음.
printf("%c\n", ((struct Data *)ptr)->c1); // 구조체 포인터로 변환하여 멤버에 접근
printf("%d\n", ((struct Data *)ptr)->num1); // 구조체 포인터로 변환하여 멤버에 접근
free(d1); // 동적 메모리 해제
return 0;
}
// a
// 10
- 구조체 포인터
d1
을 선언하고 구조체 크기만큼 메모리를 할당했다. - 그리고 각 멤버에
'a'
와10
을 저장했다. - 그다음에
ptr = d1;
과 같이ptr
에d1
을 할당했다. - 여기서
ptr
은void
타입 포인터이고d1
은Data
구조체 포인터이지만,void
타입 포인터는 포인터 타입을 가리지 않고 다 받아들일 수 있으므로 컴파일 경고가 발생하지 않는다.
- 이제
ptr
로 멤버에 저장된 값을 출력해 보자. - 그런데
d1
은Data
구조체의 포인터라d1->c
,d1->num1
처럼 멤버에 쉽게 접근할 수 있지만,ptr
은void
타입 포인터라Data
구조체의 형태를 모르는 상태이므로 멤버에 바로 접근을 할 수가 없다. - 따라서 다음과 같이
ptr
을Data
구조체 포인터로 변환한 뒤 멤버에 접근해야 한다.
printf("%c\n", ((struct Data *)ptr)->c1); // 구조체 포인터로 변환하여 멤버에 접근
printf("%d\n", ((struct Data *)ptr)->num1); // 구조체 포인터로 변환하여 멤버에 접근
- 만약
(struct Data *)ptr
처럼 해주면ptr
을Data
구조체 포인터로 변환을 하지만 이 상태로는 멤버에 접근할 수 없다. - 즉,
(struct Data *)ptr
은 다른 포인터에 메모리 주소를 저장할 때만 사용할 수 있다.
(struct Data *)ptr->num1; // 구조체 멤버에 접근할 수 없음. 컴파일 에러
struct Data *d2 = (struct Data *)ptr; // 다른 포인터에 메모리 주소를 저장
ptr
을 구조체 포인터로 변환한 뒤 멤버에 접근할 때는 자료형 변환과 포인터 전체를 다시 한 번()
로 묶어준다.
- 이렇게 해야
ptr
에서->
연산자를 사용하여 구조체 멤버에 접근할 수 있다.
typedef
로 구조체 포인터 별칭을 정의하는 방법
typedef
를 사용하면 구조체 별칭뿐만 아니라 구조체 포인터의 별칭도 정의할 수 있다.
- 다음은 구조체 별칭과 구조체 포인터 별칭을 사용하여 변환하는 방법이다.
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
typedef struct _Data
{
char c1;
int num1;
} Data, *PData; // 구조체 별칭, 구조체 포인터 별칭 정의
int main()
{
PData d1 = malloc(sizeof(Data)); // 구조체 포인터 별칭으로 포인터 선언
void *ptr; // void 타입 포인터 선언
d1->c1 = 'a';
d1->num1 = 10;
ptr = d1; // void 타입 포인터에 d1 할당. 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음.
printf("%c\n", ((Data *)ptr)->c1); // 구조체 별칭의 포인터로 변환
printf("%d\n", ((PData)ptr)->num1); // 구조체 포인터 별칭으로 변환
free(d1); // 동적 메모리 해제
return 0;
}
- 다음과 같이
typedef
로 구조체를 정의하면서 별칭을 만들 때 앞에*
를 붙여주면 구조체 포인터 별칭을 정의할 수 있다.
typedef struct _Data
{
char c1;
int num1;
} Data, *PData; // 구조체 별칭, 구조체 포인터 별칭 정의
// ↑ 앞에 *를 붙여서 구조체 별칭 포인터 정의
- 이때는 구조체 별칭
Data
와 구분하고, 포인터라는 것을 명확하게 표시하기 위해PData
처럼 이름 앞에P
를 붙여준다.
- 구조체 포인터 별칭을 사용하면
*
를 사용하지 않고 포인터를 선언할 수 있다.
ptr
을Data
구조체 포인터로 변환하는 방법도 두 가지가 있다.(Data *)ptr
과 같이 구조체 별칭 뒤에*
를 붙여서 변환할 수도 있고,(PData)ptr
과 같이 구조체 포인터 별칭을 바로 사용하여 변환할 수도 있다.