Posts C언어 서로 다른 구조체 포인터 캐스팅의 원리
Post
Cancel

C언어 서로 다른 구조체 포인터 캐스팅의 원리

개요

C언어에서 서로 다른 구조체 포인터를 캐스팅할 때의 작동 원리를 정리한다.

이를 통해 C에서도 구조체 레이아웃을 이용해 객체 지향 언어의 ‘업캐스팅’과 유사한 동작을 구현할 수 있음을 확인할 수 있다.

분석 대상 코드

구조체 정의

서로 다른 멤버를 가진 구조체가 있다고 하자.

1
2
3
4
5
6
7
8
9
10
typedef struct parent_s {
    struct parent_s *prev;
    struct parent_s *next;
} parent_t;

typedef struct child_s {
    struct child_s *prev;
    struct child_s *next;
    int data; // parent_t 구조체에는 존재하지 않음
} child_t;

캐스팅 시도 코드

아래 코드는 child_t*parent_t*로 캐스팅한 뒤 포인터 멤버 값을 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>

int main() {
    child_t child;

    // child_t 초기화
    child.prev = NULL;
    child.next = calloc(1, sizeof(child_t));
    child.data = 10;

    child.next->prev = &child;
    child.next->next = NULL;
    child.next->data = 20;

    printf("=== 원본 child ===\n");
    printf("child.prev = %p\n", (void*)child.prev);
    printf("child.next = %p\n", (void*)child.next);
    printf("child.data = %d\n", child.data);
    printf("child.next->data = %d\n", child.next->data);

    // child_t를 parent_t 포인터로 캐스팅
    parent_t *parent_ptr = (parent_t *)&child;
    printf("\n=== 1단계: child_t -> parent_t 캐스팅 ===\n");
    printf("parent_ptr->prev = %p\n", (void*)parent_ptr->prev);
    printf("parent_ptr->next = %p\n", (void*)parent_ptr->next);

    return 0;
}

실행 결과

1
2
3
4
5
6
7
8
9
=== 원본 child_t ===
child.prev = (nil)
child.next = 0x57722c1732a0
child.data = 10
child.next->data = 20

=== child_t-> parent_t 캐스팅 ===
parent_ptr->prev = (nil)
parent_ptr->next = 0x57722c1732a0

child.nextparent_ptr→next 의 주소값이 동일함을 통해, 서로 다른 구조체 포인터 간 캐스팅이 동작했음을 확인할 수 있다.

작동 원리 분석

이 동작이 가능한 이유는 두 구조체의 앞부분 메모리 레이아웃이 동일하기 때문이다.

64비트 시스템 기준으로 포인터는 8바이트 크기를 갖는다.

1
2
3
4
5
6
7
8
9
10
typedef struct parent_s {
    struct parent_s *prev; // 8바이트
    struct parent_s *next; // 8바이트
} parent_t;

typedef struct child_s {
    struct child_s *prev; // 8바이트
    struct child_s *next; // 8바이트
    int data; // 4바이트
} child_t;

메모리 배치의 앞부분 16바이트가 child_tparent_t 가 서로 동일하다.

1
2
3
4
5
6
7
8
9
10
11
[child_t 메모리]
 ┌───────────┬───────────┬───────
 │ prev(ptr) │ next(ptr) │ data  │
 │   8Byte   │   8Byte   │ 4Byte │
 └───────────┴───────────┴───────

[parent_t 메모리]
 ┌───────────┬───────────
 │ prev(ptr) │ next(ptr) │
 │   8Byte   │   8Byte   │
 └───────────┴───────────
  • 앞 16바이트(prevnext)가 동일하므로 캐스팅 후에도 포인터 접근이 정상 동작
  • 원본 메모리는 그대로 유지되므로 주소값이 일치함

C++의 업캐스팅과 비교

C언어의 구조체 상속 기법은 C++의 업캐스팅과 유사하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
using namespace std;

// 부모 클래스
class Parent {
public:
    Parent* prev;
    Parent* next;

    Parent() : prev(nullptr), next(nullptr) {}

    void link(Parent* p) {
        this->next = p;
        if (p) p->prev = this;
    }
};

// 자식 클래스
class Child : public Parent {  // 상속
public:
    int data;

    Child(int d = 0) : Parent(), data(d) {}

    void printData() {
        cout << "Child data: " << data << endl;
    }
};

int main() {
    Child c1(42);          // data = 42
    Child* childPtr = &c1; // 원래 포인터
    Parent* parentPtr = childPtr; // 업캐스팅

    // 포인터 주소 출력
    cout << "Child pointer (Child*):  " << (void*)childPtr << endl;
    cout << "Parent pointer (Parent*): " << (void*)parentPtr << endl;

    // 데이터 접근 비교
    cout << "Child data via Child*:  " << childPtr->data << endl;
    cout << "Child data via Parent*: "
         << ((Child*)parentPtr)->data << endl; // 다운캐스팅 후 접근

    // 포인터 멤버 prev/next 비교
    parentPtr->prev = nullptr;
    parentPtr->next = nullptr;
    cout << "prev pointer via Child*:  " << (void*)childPtr->prev << endl;
    cout << "prev pointer via Parent*: " << (void*)parentPtr->prev << endl;
}

출력 시 Child*Parent* 주소가 동일하게 나타난다.

1
2
3
4
5
6
Child pointer (Child*):  0x7ffee8d3a8b0
Parent pointer (Parent*): 0x7ffee8d3a8b0
Child data via Child*:  42
Child data via Parent*: 42
prev pointer via Child*:  0x0
prev pointer via Parent*: 0x0

두 언어에서 상속을 하는 방법과 특징을 정리하면 아래와 같다.

언어방법특징
C구조체 포인터 캐스팅메모리 레이아웃 동일 시 안전
C++상속 + 업캐스팅타입 호환을 컴파일러가 보장

다운캐스팅

마찬가지로 C 구조체를 이용해 다운캐스팅도 가능하다. 업캐스팅한 포인터를 다시 원래 구조체 타입으로 변환하면, 동일한 메모리 영역을 참조하므로 원래 값이 그대로 유지된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main() {
    child_t child;

    // child_t 초기화
    child.prev = NULL;
    child.next = calloc(1, sizeof(child_t));
    child.data = 10;

    child.next->prev = &child;
    child.next->next = NULL;
    child.next->data = 20;

    // child_t를 parent_t 포인터로 캐스팅
    parent_t *parent_ptr = (parent_t *)&child;

    // parent_t 포인터를 다시 child_t 포인터로 캐스팅
    child_t *child_ptr = (child_t *)parent_ptr;
    printf("\n=== parent_t -> child_t 다운캐스팅 ===\n");
    printf("child_ptr->data = %d\n", child_ptr->data);
    printf("child_ptr->next->data = %d\n", child_ptr->next->data);

출력 결과는 다음과 같다.

1
2
3
=== parent_t -> child_t 다운캐스팅 ===
child_ptr->data = 10
child_ptr->next->data = 20

업캐스팅 후 다운캐스팅을 수행해도 메모리 주소가 변하지 않기 때문에datanext->data는 원래 값 그대로 유지된다. 단, 원래부터 parent_t 타입으로 선언된 객체를 child_t로 다운캐스팅하면 정의되지 않은 동작(Undefined Behavior, UB)이 발생할 수 있으므로 주의해야 한다.

주의 사항

  • 구조체 레이아웃이 변경되면 캐스팅 동작이 보장되지 않는다.
  • 구조체 상속 기법은 유지보수 시 신중하게 사용해야 한다.

참고자료

This post is licensed under CC BY 4.0 by the author.

ARP 개념 정리

-