profile image

L o a d i n g . . .

Unit 58. 자료형 변환하기

58.1 기본 자료형 변환하기

자료형을 지정하여 변환하는 것을 명시적 자료형 변환(explicit type conversion, type casting)이라고 하며 변수나 값 앞에 변환할 자료형을 붙인 뒤 ( ) (괄호)로 묶어주면 된다.

  • (자료형)변수
  • (자료형)값
#include <stdio.h>

int main()
{
    int num1 = 32;
    int num2 = 7;
    float num3;

    num3 = num1 / num2;      // 컴파일 경고 발생
    printf("%f\n", num3);    // 4.000000

    num3 = (float)num1 / num2;    // num1을 float로 변환
    printf("%f\n", num3);         // 4.571429

    return 0;
}

num3 = num1 / num2;와 같이 정수 / 정수를 계산하면 정수(int) 4가 나오고 num3에는 4.0000000이 저장된다. 이때 num3 float형이라 int와 자료형이 달라서 다음과 같이 컴파일 경고가 발생한다.

 

하지만 num3 = (float)num1 / num2;와 같이 num1 float로 강제 변환해주면 실수 / 정수가 되어 결과도 실수(float)가 된다. 따라서 num3에는 4.571429가 저장되고, 컴파일 경고가 발생하지 않는다.

 

-> 형 변환은 자료형(타입)을 명확하게 결정할 수 있다.

 

 

58.2 포인터 변환하기

포인터끼리 변환 할 때는 자료형 뒤에 포인터를 나타내는 *(애스터리스크)를 붙여주고 괄호로 묶어주면 된다.

  • (자료형 *)포인터
#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);   // 0x78: 낮은 자릿수 1바이트를 가져오므로 0x78

    free(numPtr);    // 동적 메모리 해제

    return 0;
}

numPtr이나 cPtr이나 안에 저장된 메모리 주소는 같지만 자료형에 따라 역참조했을 때 값을 가져오는 크기가 결정된다.

 

따라서 메모리 공간에 0x12345678(리틀 엔디언: 78 56 34 12)이 저장된 상태에서 1바이트 char 크기만큼 낮은 자릿수 값을 가져오므로 0x78이 된다.

 

자료형이 다른 포인터에 메모리 주소가 이리저리 옮겨 다닌다고 해도 메모리 공간은 그대로이고, 값을 가져오는 크기만 달라지는 것이다.

 

+ 포인터를 다른 자료형으로 변환하면서 역참조하려면 다음과 같이 ( ) (괄호) 앞에 역참조 연산자 *를 붙여주면 된다.

  • *(자료형 *)포인터

 

 

58.3 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);     // 10: void 포인터를 int 포인터로 변환한 뒤 역참조

    ptr = &num2;    // num2의 메모리 주소를 void 포인터 ptr에 저장
    // printf("%f\n", *ptr);         // 컴파일 에러
    printf("%f\n", *(float *)ptr);   // 3.500000: void 포인터를 float 포인터로 변환한 뒤 역참조

    ptr = &c1;      // c1의 메모리 주소를 void 포인터 ptr에 저장
    // printf("%c\n", *ptr);         // 컴파일 에러
    printf("%c\n", *(char *)ptr);    // a: void 포인터를 char 포인터로 변환한 뒤 역참조

    return 0;
}

ptr = &num1;과 같이 int형 변수 num1의 메모리 주소를 ptr에 저장했다. 하지만 ptr void 포인터라 역참조를 하면 컴파일 에러가 발생다. 따라서 *(int *)ptr와 같이 void 포인터를 int 포인터로 변환한 뒤 역참조를 해야 한다.

 

 

 

58.4 구조체 포인터 변환하기

자료형 변환을 주로 사용하는 상황은 구조체 포인터를 변환할 때다. 이때는 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);      // 'a' : 구조체 포인터로 변환하여 멤버에 접근
    printf("%d\n", ((struct Data *)ptr)->num1);    // 10  : 구조체 포인터로 변환하여 멤버에 접근

    free(d1);    // 동적 메모리 해제

    return 0;
}

여기서 ptr void 포인터이고 d1 Data 구조체 포인터이지만, void 포인터는 포인터 타입을 가리지 않고 다 받아들일 수 있으므로 컴파일 경고가 발생하지 않는다.

 

또, ptr void 포인터라 Data 구조체의 형태를 모르는 상태이므로 멤버에 바로 접근을 할 수가 없다. 따라서ptr Data 구조체 포인터로 변환한 뒤 멤버에 접근해야 한다.

 

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

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

 

 

 

 

Unit 59. 포인터 연산 사용하기

59.1 포인터 연산으로 메모리 주소 조작하기

포인터 연산은 포인터 변수에 +, - 연산자를 사용하여 값을 더하거나 뺀다. 또는, ++, -- 연산자를 사용하여 값을 증가, 감소시킨다.

  • 포인터 + 값
  • 포인터 - 값
#include <stdio.h>

int main()
{
    int numArr[5] = { 11, 22, 33, 44, 55 };
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = numArr;    // 배열 첫 번째 요소의 메모리 주소를 포인터에 저장

    numPtrB = numPtrA + 1;    // 포인터 연산
    numPtrC = numPtrA + 2;    // 포인터 연산
    
    printf("%p\n", numPtrA);    // 00A3FC00: 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrB);    // 00A3FC04: sizeof(int) * 1이므로 numPtrA에서 4가 증가함
    printf("%p\n", numPtrC);    // 00A3FC08: sizeof(int) * 2이므로 numPtrA에서 8이 증가함

    return 0;
}

실행 결과는 다음과 같다.

00A3FC00 (메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐)
00A3FC04
00A3FC08

포인터 연산은 포인터 자료형의 크기만큼 더하거나 뺀다.

즉, 계산식은 "sizeof(자료형) * 더하거나 빼는 값"이 된다.

 

다음은 증가, 감소 연산자를 사용한 예이다.

  • 포인터++
  • 포인터--
  • ++포인터
  • --포인터
#include <stdio.h>

int main()
{
    int numArr[5] = { 11, 22, 33, 44, 55 };
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = &numArr[2];    // 배열 세 번째 요소의 주소를 포인터에 저장

    numPtrB = numPtrA;
    numPtrC = numPtrA;

    numPtrB++;    // 포인터 연산
    numPtrC--;    // 포인터 연산

    printf("%p\n", numPtrA);    // 00A3FC08: 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrB);    // 00A3FC0C: sizeof(int) * 1이므로 numPtrA에서 4가 증가함
    printf("%p\n", numPtrC);    // 00A3FC04: sizeof(int) * -1이므로 numPtrA에서 4가 감소함

    return 0;
}

증가 연산자는 + 1과 같다. 따라서 sizeof(int) * 1이 되므로 numPtrA에서 4가 증가한다. 마찬가지로 감소 연산자는 - 1과 같으므로 sizeof(int) * -1이고 numPtrB에서 4가 감소한다.

 

 

59.2 포인터 연산과 역참조 사용하기

포인터 연산으로 조작한 메모리 주소도 역참조 연산을 사용하여 메모리에 접근할 수 있다. 

#include <stdio.h>

int main()
{
    int numArr[5] = { 11, 22, 33, 44, 55 };
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = numArr;    // 배열 첫 번째 요소의 주소를 포인터에 저장

    numPtrB = numPtrA + 1;    // 포인터 연산. numPtrA + 4바이트
    numPtrC = numPtrA + 2;    // 포인터 연산. numPtrA + 8바이트

    printf("%d\n", *numPtrB);    // 22: 역참조로 값을 가져옴, numArr[1]과 같음
    printf("%d\n", *numPtrC);    // 33: 역참조로 값을 가져옴, numArr[2]와 같음

    return 0;
}

 

 

59.3 void 포인터로 포인터 연산하기

void 포인터는 자료형의 크기가 정해져 있지 않기 때문에 +, -로 연산을 해도 얼마만큼 이동할지 알 수가 없다. 따라서 void 포인터는 포인터 연산을 할 수 없다.

 

void 포인터로 포인터 연산을 하고 싶다면 다른 포인터로 변환한 뒤 연산을 하면 된다(Visual Studio, Windows).

  • (자료형 *)void포인터 + 값
  • (자료형 *)void포인터 - 값
  • ++(자료형 *)void포인터
  • --(자료형 *)void포인터
  • ((자료형 *)void포인터)++
  • ((자료형 *)void포인터)--
#include <stdio.h>
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일

int main()
{
    void *ptr = malloc(100);    // 100바이트만큼 메모리 할당

    printf("%p\n", ptr);               // 00FADD20: 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", (int *)ptr + 1);    // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
    printf("%p\n", (int *)ptr - 1);    // 00FADD1C: 다른 포인터로 변환한 뒤 포인터 연산

    void *ptr2 = ptr;    // 메모리 주소를 변화시킬 때는 다른 포인터에 보관
    printf("%p\n", ++(int *)ptr2);     // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
    printf("%p\n", --(int *)ptr2);     // 00FADD20: 다른 포인터로 변환한 뒤 포인터 연산

    printf("%p\n", ((int *)ptr2)++);   // 00FADD20: 다른 포인터로 변환한 뒤 포인터 연산
    printf("%p\n", ((int *)ptr2)--);   // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산

    free(ptr);

    return 0;
}

ptr int 포인터로 변환하여 포인터 연산을 한다.

증가, 감소 연산자를 변수 앞에 사용할 때는 자료형 변환 앞에 연산자를 붙이면 된다.

 

 

59.4 구조체 포인터로 포인터 연산하기

구조체 포인터도 포인터 연산을 할 수 있다. 다음과 같이 포인터 연산을 한 부분을 ( ) (괄호)로 묶어준 뒤 -> (화살표 연산자) 연산자를 사용하여 멤버에 접근하면 된다.

  • (포인터 + 값)->멤버
  • (포인터 - 값)->멤버
#include <stdio.h>

struct Data {
    int num1;
    int num2;
};

int main()
{
    struct Data d[3] = { { 10, 20 }, { 30, 40 }, { 50, 60 } };    // 구조체 배열 선언과 값 초기화
    struct Data *ptr;    // 구조체 포인터 선언

    ptr = d;    // 구조체 배열 첫 번째 요소의 메모리 주소를 포인터에 저장

    printf("%d %d\n", (ptr + 1)->num1, (ptr + 1)->num2);    // 30 40: 구조체 배열에서 멤버의 값 출력
                                                            // d[1].num1, d[1].num2와 같음

    printf("%d %d\n", (ptr + 2)->num1, (ptr + 2)->num2);    // 50 60: 구조체 배열에서 멤버의 값 출력
                                                            // d[2].num1, d[2].num2와 같음

    return 0;
}

구조체 배열 d를 선언한 뒤 첫 번째 요소의 메모리 주소를 ptr에 저장했다.

 

구조체 포인터는 (ptr + 1)->num1와 같이 포인터 연산을 한 뒤 괄호로 묶어준다. 그리고 화살표 연산자를 사용하여 멤버에 접근할 수 있다(괄호는 반드시 사용해야함). 

 

구조체 Data의 크기는 4바이트짜리 int형 멤버가 두 개 들어있으므로 8바이트이다. 따라서 포인터 연산을 하면 8바이트씩 메모리 주소에서 더하거나 뺀다.

복사했습니다!