C언어의 가장 중요한 개념인 포인터는 C언어를 공부한 사람이라면 누구나 사용할 줄 알고, 누구나 들어보았을것이다. 하지만 포인터를 그냥 사용하는것과 잘 사용하는것은 완전히 다른차원의 이야기 이다. 필자는 오늘 포인터를 잘 사용하는법에대한 글을 작성하도록 하겠다.
C언어 프로그램은 다음과 같은 메모리 구조를 따른다.
높은 주소 |
---|
Stack |
Heap |
BBS |
Data |
Text |
struct S{
char c; //1B
int x; //2B
}
다음과같은 코드가 있을때 struct S의 크기는 몇일까?
5바이트라고 생각할 수도 있지만 실제로는 8바이트아다.
왜냐하면 int는 4바이트 정렬을 요구하기 때문이다.
CPU는 데이터를 가져올 때 4바이트 경계에 맞춰 일기를 선호한다. 만약 주소가 정렬되지 않으면 속도가 느려지거나, ARM 같은 아키텍처에서는 세그멘테이션 폴트(접근오류)가 발생할 수 있다.
따라서 컴파일러는 char 다음에 3바이트의 패팅을 추가하여 int가 4바이트 경계에 오도록 한다. 따라서 struct S의 크기는 8바이트가 되는 것이다.
int arr[3] = {1, 2, 3};
int* p = arr;
p + 1 은 다음 int 주소를 가리킨다. 실제 메모리 주소는 4바이트 증가하는것이다.
이와같이 포인터 산술은 타입 단위로 이루어진다.
float f = 3.14;
int* pi = (int*) &f;
다음과같은 타입 재해석은 Srtict aliasing rule(스트릭트 에일리어싱 룰)을 위반한다.
여기서 문제는 컴파일러가 float과 int는 같은 메모리를 가리키지 않는다고 가정하고 최적화하기 때문이다. 따라서 포인터 pi를 읽었을 때 예상치 못한 결과가 발생할 수 있다.
이런 오류를 방지하고 안전한 방법은 다음과 같다.
union{
float f;
int i;
} u;
u.f = 3.14;
printf("%x", u.i);
이와같이 union을 사용하면 타입이 같은 메모리 영역을 공유할 수 있다.
오늘은 포인터와 메모리에 대한 조금 심화적인 내용을 알아보았다. 이 글의 내용은 실제 C언어 개발을 하며 겪었던 문제를 바탕으로 작성하였기 때문에 필자와 비슷한 문제를 겪고있다면 도움이 될 것이다. 이번 글은 여기서 마무리 하도록 하겠다.