Skip to content

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 타입보다 크기가 큼


  • 하지만 자료형이 다르면서 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 저장하면 컴파일 경고가 발생한다.
  • 예를 들어 실수에서 소수점 이하 자리를 버리는 기능을 구현하고자 실수를 정수로 저장했을 때 프로그래머가 의도한 상황이지만 컴파일 경고가 발생한다.


float num1 = 3.56f;
int num2 = num1; // 실수형 값을 정수형 변수에 저장하여 컴파일 경고 발생


  • 다음 그림과 같이 자료형의 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 자동 변환되는 것을 형 확장이라 하고, 이런 변환을 암시적 형 변환이라고 한다.
  • 반대로 자료형 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 변환되는 것이 형 축소이다.
  • 이때 형 축소에서 컴파일 경고가 나오지 않도록 만드는 것을 형 변환이라고 한다.
  • 특히 프로그래머가 강제로 자료형을 변환한다고 해서 명시적 형 변환이라 부르기도 한다.


001


  • 형 확장은 값의 손실이 없으므로 컴파일러가 알아서 처리할 수 있다.
  • 하지만 형 축소는 값의 손실이 발생하여 컴파일러가 알아서 처리할 수 없으므로 경고가 발생한다.
  • 따라서 형 변환으로 의도를 밝혀야만 컴파일러가 안심하고 변환을 하게 된다.
  • 즉, 형 변환은 컴파일러에게 자료형을 변환한다는 의도를 명확하게 알려주는 것이다.
  • 특히, 형 변환은 구조체와 포인터를 조합하여 사용할 때 유용하게 활용된다.


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이 저장된다.
  • 이때 num3float 타입이라 int 타입과 자료형이 달라서 컴파일 경고가 발생한다.
  • 하지만 num3 = (float)num1 / num2;와 같이 num1float 타입으로 강제 변환해 주면 실수 / 정수가 되어 결과도 실수(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의 메모리 주소에 접근하지만 cPtrchar 타입 포인터이므로 1바이트만큼만 값을 가져온다.
  • 즉, numPtr이나 cPtr이나 안에 저장된 메모리 주소는 같지만 자료형에 따라 역참조했을 때 값을 가져오는 크기가 결정되기 때문이다.


002


  • 여기서 메모리 공간에 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 앞에 엉뚱한 값이 붙어서 나왔다.
  • 크기가 작은 메모리를 할당한 뒤 큰 자료형의 포인터로 역참조하면 옆의 메모리 공간을 침범하여 값을 가져오게 된다.


003


  • 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 타입 포인터를 다른 자료형으로 변환하면 역참조를 할 수 있다.


*(자료형 *)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에 저장했다.
  • 하지만 ptrvoid 타입 포인터라 역참조를 하면 컴파일 에러가 발생한다.
  • 따라서 *(int *)ptr과 같이 void 타입 포인터를 int 타입 포인터로 변환한 뒤 역참조를 해야 한다.
  • 마찬가지로 void 타입 포인터에 float 타입 포인터(메모리 주소)가 들어있다면 *(float *)ptr과 같이 float 타입 포인터로 변환한 뒤 역참조를 해야 하고, char 타입 포인터가 들어있다면 *(char *)ptr과 같이 char 타입 포인터로 변환한 뒤 역참조를 해야 한다.


6. 구조체 포인터 변환하기

  • 자료형 변환을 주로 사용하는 상황은 구조체 포인터를 변환할 때이다.
  • 이때는 struct와 구조체 이름 뒤에 *를 붙여주고 ()로 묶어주면 된다.


(struct 구조체이름 *)포인터
((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;과 같이 ptrd1을 할당했다.
  • 여기서 ptrvoid 타입 포인터이고 d1Data 구조체 포인터이지만, void 타입 포인터는 포인터 타입을 가리지 않고 다 받아들일 수 있으므로 컴파일 경고가 발생하지 않는다.


  • 이제 ptr로 멤버에 저장된 값을 출력해 보자.
  • 그런데 d1Data 구조체의 포인터라 d1->c, d1->num1처럼 멤버에 쉽게 접근할 수 있지만, ptrvoid 타입 포인터라 Data 구조체의 형태를 모르는 상태이므로 멤버에 바로 접근을 할 수가 없다.
  • 따라서 다음과 같이 ptrData 구조체 포인터로 변환한 뒤 멤버에 접근해야 한다.


printf("%c\n", ((struct Data *)ptr)->c1);   // 구조체 포인터로 변환하여 멤버에 접근
printf("%d\n", ((struct Data *)ptr)->num1); // 구조체 포인터로 변환하여 멤버에 접근


  • 만약 (struct Data *)ptr처럼 해주면 ptrData 구조체 포인터로 변환을 하지만 이 상태로는 멤버에 접근할 수 없다.
  • 즉, (struct Data *)ptr은 다른 포인터에 메모리 주소를 저장할 때만 사용할 수 있다.


(struct Data *)ptr->num1;             // 구조체 멤버에 접근할 수 없음. 컴파일 에러
struct Data *d2 = (struct Data *)ptr; // 다른 포인터에 메모리 주소를 저장


  • ptr을 구조체 포인터로 변환한 뒤 멤버에 접근할 때는 자료형 변환과 포인터 전체를 다시 한 번 ()로 묶어준다.


004


  • 이렇게 해야 ptr 에서 -> 연산자를 사용하여 구조체 멤버에 접근할 수 있다.


typedef로 구조체 포인터 별칭을 정의하는 방법

  • typedef를 사용하면 구조체 별칭뿐만 아니라 구조체 포인터의 별칭도 정의할 수 있다.


typedef struct 구조체이름 {
    자료형 멤버이름;
} 구조체별칭, *구조체포인터별칭;


  • 다음은 구조체 별칭과 구조체 포인터 별칭을 사용하여 변환하는 방법이다.


(구조체별칭 *)포인터
((구조체별칭 *)포인터)->멤버
(구조체포인터별칭)포인터
((구조체포인터별칭)포인터)->멤버
#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를 붙여준다.


  • 구조체 포인터 별칭을 사용하면 *를 사용하지 않고 포인터를 선언할 수 있다.


PData d1 = malloc(sizeof(Data)); // 구조체 포인터 별칭으로 포인터 선언


  • ptrData 구조체 포인터로 변환하는 방법도 두 가지가 있다.
  • (Data *)ptr과 같이 구조체 별칭 뒤에 *를 붙여서 변환할 수도 있고, (PData)ptr과 같이 구조체 포인터 별칭을 바로 사용하여 변환할 수도 있다.


printf("%c\n", ((Data *)ptr)->c1);  // 구조체 별칭의 포인터로 변환
printf("%d\n", ((PData)ptr)->num1); // 구조체 포인터 별칭으로 변환

References