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

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

  • (포인터 + 값)->멤버
  • (포인터 - 값)->멤버

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

struct_pointer_add.c

#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;
}

실행 결과

30 40
50 60

구조체 배열 d를 선언한 뒤 첫 번째 요소의 메모리 주소를 ptr에 저장했습니다. 이제 ptr로 구조체 배열에 접근할 수 있겠죠?

구조체 포인터는 (ptr + 1)->num1와 같이 포인터 연산을 한 뒤 괄호로 묶어줍니다. 그리고 화살표 연산자를 사용하여 멤버에 접근할 수 있습니다(괄호는 반드시 사용해야 합니다). 결과적으로 (ptr + 1)->num1은 구조체 배열의 인덱스로 접근하는 d[1].num1과 같습니다.

구조체 Data의 크기는 4바이트짜리 int형 멤버가 두 개 들어있으므로 8바이트입니다. 따라서 포인터 연산을 하면 8바이트씩 메모리 주소에서 더하거나 뺍니다. 만약 구조체가 커져서 int형 멤버가 10개가 된다면 40바이트씩 더하거나 빼게 됩니다.

▼ 그림 59‑5 구조체와 포인터 연산

이번에는 void 포인터에 구조체 3개 크기만큼 동적 메모리를 할당한 뒤 포인터 연산을 해보겠습니다.

  • ((struct 구조체이름 *)포인터 + 값)->멤버
  • ((struct 구조체이름 *)포인터 - 값)->멤버

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

struct_void_pointer_add.c

#include <stdio.h>
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일
#include <string.h>    // memcpy 함수가 선언된 헤더 파일

struct Data {
    int num1;
    int num2;
};

int main()
{
    void *ptr = malloc(sizeof(struct Data) * 3);    // 구조체 3개 크기만큼 동적 메모리 할당
    struct Data d[3];

    ((struct Data *)ptr)->num1 = 10;        // 포인터 연산으로 메모리에 값 저장
    ((struct Data *)ptr)->num2 = 20;        // 포인터 연산으로 메모리에 값 저장

    ((struct Data *)ptr + 1)->num1 = 30;    // 포인터 연산으로 메모리에 값 저장
    ((struct Data *)ptr + 1)->num2 = 40;    // 포인터 연산으로 메모리에 값 저장

    ((struct Data *)ptr + 2)->num1 = 50;    // 포인터 연산으로 메모리에 값 저장
    ((struct Data *)ptr + 2)->num2 = 60;    // 포인터 연산으로 메모리에 값 저장

    memcpy(d, ptr, sizeof(struct Data) * 3);    // 동적 메모리가 구조체 배열의 형태와 같은지 
                                    // 확인하기 위해 동적 메모리의 내용을 구조체 배열에 복사

    printf("%d %d\n", d[1].num1, d[1].num2);    // 30 40: 구조체 배열의 멤버 출력
    printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2);
                                                // 50 60: 포인터 연산으로 메모리의 값 출력

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

    return 0;
}

실행 결과

30 40
50 60

문법이 복잡해 보이지만 어렵지 않습니다. ((struct Data *)ptr)->num1은 앞에서 배운 구조체 포인터로 변환하는 방법입니다. 이 상태에서 포인터 연산을 하려면 ((struct Data *)ptr + 1)->num1와 같이 ptr을 구조체 포인터로 변환한 뒤 값을 더해주면 됩니다. ->(화살표 연산자) 연산자를 사용하려면 반드시 괄호로 묶어줍니다.

이제 포인터 연산을 통해 메모리에 값을 저장합니다. 만약 (ptr + 1)->num1처럼 ptr에 포인터 연산을 하더라도 ptr은 void 포인터라 Data 구조체의 형태를 모르기 때문에 멤버에 접근할 수 없고 컴파일 에러가 발생합니다.

((struct Data *)ptr + 1)->num1 = 30;    // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 1)->num2 = 40;    // 포인터 연산으로 메모리에 값 저장

((struct Data *)ptr + 2)->num1 = 50;    // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 2)->num2 = 60;    // 포인터 연산으로 메모리에 값 저장

그리고 포인터 연산으로 값을 저장한 결과가 Data 구조체 배열의 형태와 같은지 확인하기 위해 memcpy(d, ptr, sizeof(struct Data) * 3);처럼 동적 메모리의 내용을 구조체 배열 d에 복사했습니다.

다음과 같이 printf로 구조체 배열 d의 멤버를 출력해보면 포인터 연산을 통해 저장한 값이 출력되는 것을 볼 수 있습니다. 즉, 동적 메모리에 저장된 값의 위치가 구조체 배열의 형태와 같고, 동적 메모리 내용을 그대로 복사했기 때문에 같은 값이 나옵니다. 또한, 포인터 연산으로도 동적 메모리의 값을 출력할 수 있습니다.

printf("%d %d\n", d[1].num1, d[1].num2);    // 30 40: 구조체 배열의 멤버 출력
printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2);
                                            // 50 60: 포인터 연산으로 동적 메모리의 값 출력
▼ 그림 59‑6 구조체 포인터와 포인터 연산