728x90

C코드를 보다보면 container_of와 offsetof라는 함수가 정의되어있다.

더 정확히 말하자면... 둘다 함수는 아니고 매크로 함수라는 차이점이 있다.

사실 이는 아주 마법 같은, 다른 언어에서 할 수 없는 미친행동이 가능한데 그 이유는,


멤버 변수로 해당 구조체를 반환하는 짓이 가능하다는 점이다...


그 이유는 container_of와 offsetof라는 때문이다.

먼저 offsetof라는 매크로함수를 보자.


offsetof


특정 구조체에서 해당 변수가 메모리상에서 얼마나 떨어져있는지를 알려준다.

#define offsetof(s,m)   (size_t)&(((s *)0)->m)

해당 매크로는 stddef.h에 정의되어있으므로 c의 경우 stddef.h를, c++의 경우 cstddef를 추가해주면된다.


이 녀석은 구조체에서 해당 변수가 메모리상에서 얼마나 떨어졌는지 본다.

이를 이해하려면 C에서 구조체가 메모리상에서 어떻게 할당되야하는지 봐야한다.


typedef struct _tagTest {
int a;
int b;
int c;
} test;

위와 같은 test라는 구조체가 존재 한다고 가정하자.


test t;

그럼 위와같은 변수가 존재한다고 가정하자.

t의 주소는 100이라고 가정했을때,

t.a의 주소는 t의 주소와 같다.

배열과 구조체, 공용체에서는 제일 선두의 녀석과 그 구조체 인스턴스의 주소는 항상 같다.


test t;
printf("%p\n", &t);
printf("**********\n");
printf("%p\n", &(t.a));
printf("%p\n", (char *) &t + offsetof(test, a));
printf("**********\n");
printf("%p\n", &(t.b));
printf("%p\n", (char *) &t + offsetof(test, b));
printf("**********\n");
printf("%p\n", &(t.c));
printf("%p\n", (char *) &t + offsetof(test, c));

해당 코드가 존재한다고 가정하고 결과를 예측해보자.


0x7ffee5ede930
**********
0x7ffee5ede930
0x7ffee5ede930
**********
0x7ffee5ede934
0x7ffee5ede934
**********
0x7ffee5ede938
0x7ffee5ede938

그러면 위처럼 출력이 된다.

즉 t와 t.a는 같은 주소이며 t.b, t.c는 t.a로 부터 각각 int크기만큼 떨어져있다.

해당 컴퓨터에서 int는 4바이트이므로 결국 4바이트 떨어져있게 된다.


이제 container_of을 보도록하자.


container_of


특정 구조체에서 멤버변수의 주소를 가지고 있을 경우 그 본체의 주소를 반환한다.

#define container_of(ptr, type, member) \
((type *) ((char *) (ptr) - offsetof(type, member)))

이 매크로 함수는 보통 정의되어있지 않고 만들어서 써야한다.

linux커널, libuv등 여러 C라이브러리에서는 이 container_of를 정의해서 사용하고 있다.


이 매크로 함수의 의미는 ptr이 특정 구조체에 속해있을경우(멤버변수 라면) 그 구조체를 반환한다는 매우 사기적은 매크로 함수이다.

가령 이런경우에 유용하게 사용할 수 있다.


typedef struct _tagGun {
int bullet;
int since;
} gun;

typedef struct _tagPerson {
int age;
gun _g;
} person;

int main() {
person p = {1, {20, 2002}};
gun *g = &p._g;
printf("%p\n", &p);
printf("**********\n");
printf("%p\n", container_of(g, person, _g));
printf("%p\n", container_of(g, person, _g));
return 0;
}

person은 gun이라는 무기를 멤버변수로 가진다고 가정하자.

여기서 우리는 p의 _g를 gun *g에 연결하였다.

그러니까 우리는 p의 _g의 값은 알고있는 상태라고 가정하자.


printf("%p\n", container_of(g, person, _g));

그렇다면 우리는 이 총의 소유주가 누구인지 container_of를 이용해서 알아낼 수 있다.

첫번째 파라메터로는 타겟 포인터를, 두번째 변수로는 그 타겟 포인터를 가지고 있는 구조체이름을,

세번째로는 해당 구조체에서 타겟포인터와 같은 타입의 멤버변수를 적어줘야한다.

즉 g와 _g는 무조건 같은 타입이어야한다는 의미, 위의 예제의 경우 1번과 3번은 둘다 gun으로 같은 타입이다.


0x7ffee223f930
**********
0x7ffee223f930
0x7ffee223f930

실행해보면 둘이가 동등한걸 확인할 수있다.


이를 어디다 쓰는고?


아주 다양하게 쓰일 수 있으며 아주 사기적인 성능을 자랑하여 아주 많이 사용한다.

매크로함수 + 포인터 캐스팅이 조화되어서 오류가 날 확률이 높으니 조심해서 써야할 것이다.

아주 자주쓰이는 예시를 들어주겠다.


typedef struct _tagNode {
int data;
struct _tagNode *left;
struct _tagNode *right;
} node;

int main() {
node c = {10, NULL, NULL};
node b = {20, &c, NULL};
node a = {30, &b, NULL};

node **c_ptr = &b.left;
node *parent = container_of(c_ptr,node,left);

printf("c : %d\n",*c_ptr);
printf("b : %d\n",parent->left);
printf("**********\n");
printf("c : %d\n",(*c_ptr)->data);
printf("b : %d\n",parent->data);
return 0;
}

보면 알겠지만 c_ptr은 c의 포인터를 가르키고 있다.

사실상 c를 가르킨다고 봐도 무방한데(진짜로 가르킨다는 뜻은 아님)

c만 아는 상태에서 부모를 알아낼 수 있게 된다.


c : -527132384
b : -527132384
**********
c : 10
b : 20

c를 아는 상태에서 부모를 알아낼 수 있다.

'Usage > C' 카테고리의 다른 글

[C][C-lang][memory.h]memset함수와 배열의 초기화  (2) 2018.07.09
[C][C-lang][mac][xcode]fmod사용하기  (0) 2017.06.09

+ Recent posts