반응형

구조체의 값을 다른 값으로 직접 변환할 수 없지만, 구조체에 대한 포인터를 다른 형식에 대한 포인터로 변환할 수 있다. 이를 응용하여 물리적 구조에 호환되는 구조체에 대한 포인터를 변환하여 사용할 수 있다.

포인터로 형변환(casting)

이번에는 구조체에 대한 포인터를 사용해 형변환하는 기술에 대해 설명한다. 이 기법은 구조체의 특성을 생각하면, 의미가 다른 구조로 값을 변환하는 반칙 행위와 같은 것이다. 구조 설계자는 경우에 따라 이러한 사용 방법을 의도하고 있지 않을 가능성도 있고, 잘못 취급하면 프로그램이 충돌 디버그를 복잡하게 만들 수 있다는 것을 인식하지 않을 수도 없다.

구조체는 다른 형태의 구조체에 대입할 수 없으며, 형변환도 허용되지 않는다. 예를 들어 다음과 같은 코드는 컴파일시에 에러 판정을 받는다.

struct Point pt = { 10 , 100 };
struct Size sz = pt;

캐스트 연산자를 사용하여 sz = (struct Size)pt;라고 기술한 경우도 동일하다. 다른 구조체 형은 형태에 호환성이 없기 때문에 대입은 인정되지 않는다.

그래서 포인터의 형태에 대해 생각해 보자. 포인터 형은 컴파일러가 포인터를 간접 참조할 때에 사이즈를 식별하는 것에 불과했다. 구조체의 경우도 마찬가지로 구조체에 대한 포인터는 구조체의 형(인스턴스 메모리 사이즈)를 결정하기 위한 정보에 불과하다. 다음의 구조를 보자.

struct Point { int x , y; };
struct Size { int width , height; };

이러한 Point 구조체와 Size 구조체는 다른 형태이므로 의미적인 호환성은 없다. 그러나 멤버를 보면 이러한 구조체는 int 형의 멤버를 2개 보유한다는 공통점이 있다. 이것은 Point 구조체와 Size 구조체의 인스턴스에 할당된 메모리 크기가 동일함을 나타낸다. 32비트 컴퓨터의 경우 이러한 구조체의 인스턴스는 8바이트가 될 것이다.

이 사실만 안다면, 구조체에 대한 포인터를 다른 포인터 형식으로 변환하여 인스턴스를 제어할 수 있다. 8바이트라는 실체는 int 형의 두 가지 요소를 가진 배열로 인식할 수 있다면, char 형의 8 개의 요소를 가진 배열로 인식할 수 있다. 당연히 사이즈가 같으므로 Point 형의 인스턴스를 Size 형의 포인터로 취급할 수도 있다.

코드1

#include <stdio.h>

struct Point { int x , y; };
struct Size { int width , height; };

int main() {
    struct Point pt = { 400 , 300 };
    struct Size *psz = (struct Size *)&pt; 

    printf("&pt.x = %p : pt.y = %p\n" , &pt.x , &pt.y);
    printf("&psz->width = %p , &psz->height = %p\n" , &psz->width , &psz->height);
    printf("psz->width = %d : psz->height = %d\n" , psz->width , psz->height);
    return 0;
}

코드1은 Point 형의 pt 변수의 주소를 Size 형의 포인터 psz 변수에 대입하고 있다. 이들은 논리적인 형태의 호환성은 없지만, 메모리에 저장되어 있는 물리적 정보를 통제한다는 점에서 생각하면 문제는 없다. Size 형도 Point 형이 멤버 이름이 다를 뿐이고, 실질적인 구성은 동일하다. 결과를 보면, 이들이 동일하다는 것을 확인할 수 있다.

런타임 상태에 따라 주소는 다르지만 &pt.x과 &psz->width의 메모리 주소가 동일하다는 것이 중요하다. &pt.y과 &psz->height가 동일한 지도 말할 것도 없다. psz 변수가 가리키는 인스턴스는 Point 형이든 Size 인 메모리 크기라는 사실에는 변함이 없다.

이 사실은 시스템을 개발하는 설계자들에게 중요한 요소이다. 시스템은 규모가 커질수록 향후의 확장과 버그를 줄이고, 보다 최적의 프로그램(필요한 메모리의 감소와 속도 이식 등)이 요구된다. 이러한 프로그램의 확장에 있어서, 구조체와 함수 관계는 시스템의 설계(디자인)에 직접 영향을 주는 존재이다. 그렇다면 구조체와 함수 관계는 유연한 것이어야 하며, 시스템을 확장할 때에 기존의 프로그램 코드를 모두 다시 작성하여 다시 컴파일해야 한다 같은 디자인은 피해야 한다.

시스템 개발자는 향후 확장에 대비한 함수의 구현 방법으로, 예를 들어 이 포인터에 의한 구조체의 제어를 사용할 수 있다. 포인터를 사용하면 인스턴스에 관계없이, 멤버를 조작하는 것만으로 원하는 정보를 얻거나 설정할 수 있는 시스템이 제공하는 기능의 역할을 이행할 수 있다.

코드2

#include <stdio.h>

struct Color {
    char *name;
    int r , g , b;
};
struct ColorEx {
    char *name;
    int r , g , b , a;
};

void SetColor(struct Color *color) {
    printf(
        "%s r = %d : g = %d : b = %d\n" ,
        color->name , color->r , color->g , color->b
    );
}

int main() {
    struct Color color = { "Color" , 0xFF , 0 , 0 };
    struct ColorEx colorEx = { "ColorEx" , 0 , 0xFF , 0 , 0xA0 };

    SetColor(&color);
    SetColor((struct Color *)&colorEx);

    return 0;
}

코드2는 Color 구조체에 대한 포인터를 받는 함수 SetColor() 함수를 정의하고 있다. 이 함수는 Color 구조체의 멤버 값을 표시할 뿐이지만 그 동작은 중요하지 않다.

시스템의 형편상 지금까지 색상 정보에 대해 Color 구조체를 사용해오고 있었지만, 새로운 알파 값을 추가한 ColorEx을 다루어야 않게 되었을 경우를 가정해보자. 코드2는 이러한 요구를 해결하는 방법의 힌트가 되는 것이다. 이 때, 새롭게 정의하는 ColorEx 구조체는 기존에 사용하던 Color 구조체와 메모리 구조 수준에서 호환성을 얻기 위해서 name, r, g, b까지의 멤버를 동일한 위치에 선언한다. 그리고 추가하는 정보를 그때부터 선언하는 것이다. 코드2에서 추가 정보인 a 멤버를 끝으로 선언하고 있는 것을 확인할 수 있다.

앞에 멤버부터의 구조가 Color 구조체와 동일하기 때문에, ColorEx 구조체의 메모리 구조는 Color 구조체와 호환성이 있다고 생각된다. 이 경우은 기존에 사용하던 Color 구조체에 대한 포인터를 받는 함수에 ColorEx 구조체의 포인터를 전달해도, 아무 문제도 발생하지 않는 것이다. 왜냐하면 기존 사용하던 Color 구조체에 대한 포인터를 받는 함수는 ColorEx 구조체의 추가 정보에는 원래 관심이 없기 때문이다. ColorEx 구조체도 Color 구조체를 기반으로 하고 있기 때문에 name, r, g, b 멤버에 대해 Color 구조체로 조작하는데 문제는 없다.

이렇게 SetColor() 함수에 ColorEx 구조체의 포인터를 넘겨도 문제가 없다. 이러한 설계를 기반으로 한 시스템은 기존의 코드를 변경하지 않고 ColorEx 구조체를 새롭게 정의하여 기존과는 다른 알파 값을 처리보다 편리한 함수 등을 시스템에 추가할 수 있을 것이다. 코드2는 colorEx 구조체 변수를 전달할 때 명시적 형변환을 실시하고 있지만, 이것은 컴파일러에서 경고를 받을 수 있기 때문이다. 더 유연하게 설계하고자 한다면 SetColor() 함수에서 받을 포인터를 void*로 하는 방법도 있다.

더불어 유연성을 추구하는 SetColor() 함수가 향후 확장한 ColorEx 구조체의 예기치 않은 형식을 처리할 수 있도록 하려면, 구조체에 그 구조체 자체의 크기를 저장하는 멤버를 추가한다. 이 기법은 "형의 사이즈"로 증명한다.



반응형

+ Recent posts