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에 포인터 연산을 하더라도 ptrvoid 포인터라 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 구조체 포인터와 포인터 연산