C 언어 | 포인트 | 포인터의 형변환


보통의 변수가 변환 연산자로 형변환할 수 있도록 포인터 변수도 캐스트 연산자로 다른 포인터 형식으로 변환할 수 있다.

포인터의 형변환을 이용하기

하드웨어에 가까운 저급 프로그램을 이해하는데 가장 중요한 것은 메모리 구조를 이해하는 것이다. C 언어는 고급 언어에 속하지만, 그 본질은 어셈블리 언어에 가까운 하드웨어 쪽의 저수준 처리도 특기로 여기고 있다. 그 대표적인 개념이 포인터이다.

포인터를 충분히 학습하면 C 언어에 있어서 형은 컴파일러가 변수와 포인터에 액세스할 때에 조작해야 하는 사이즈를 알 수 있기 위한 정보에 불과하다는 것이 알수 있다. 예를 들어, 32비트 컴퓨터에서 int의 배열 iArray[2]의 크기는 총 8바이트이며, 이를 char 형의 포인터로 다루기 위해 8개의 요소를 가지는 char 배열처럼 처리할 수 있다.

코드1

#include <stdio.h>

int main() {
  int iCount , iArray[2] = { 0x02040810 , 0x20408000 };
 unsigned char *cp = (unsigned char *)iArray;

  for(iCount = 0 ; iCount < 8 ; iCount++)
   printf("cp + %d = %d\n" , iCount , *(cp + iCount));

  return 0;
}

코드1은 int 형의 배열 iArray의 첫번째 요소에 주소를 char 형의 포인터 cp에 대입하고 있다. char 형을 중심으로 생각하면 2개의 요소를 가지는 int 형 배열은 32비트 컴퓨터에서는 8개의 요소를 가진 배열로 조작할 수 있기 때문에 for 문은 cp + 8까지의 값을 바이트 단위로 표시하고 있다.

이렇게 강제로 포인터를 변환하여 바이트 단위 또는 워드 단위로 값을 추출하는 것이 가능하다. 형은 한번에 조작할 바이트 크기를 나타내기 위한 정보에 불과하다는 것은 이와 같은 프로그램을 만들어 보면 쉽게 이해할 수 있을 것이다. 다만, int 형에서 원하는 바이트를 추출하는 경우는 일반적으로 이동 및 형 캐스팅 만 제공하므로 코드1과 같은 방식은 드물다.

어떤 기록 방식을 채용하고 있는 컴퓨터에서 이 프로그램을 실행하면 이상한 현상이 발생한다. 예를 들어 Microsoft Windows를 실행하는 Intel x86 호환 CPU의 경우 위와 같은 결과가 나타날 것이다. 프로그램을 보면 메모리는 표1과 같이 처음부터 2,4,8 … 그리고 순서에 값이 저장되는 것을 기대하고 있지만 결과를 보면 표 2와 같이 16, 8, 4, 2 …와 같이 역순으로 배치되어 있는 것을 확인할 수 있다. 왜 이런 결과를 얻게 되는 것일까?

표1 - 기대되는 배열 내용

iArray[0] iArray[1]
2 | 4 | 8 | 16 32 | 64 | 128 | 0

표2 - Intel x86 호환 CPU에서 실행하는 경우 물리적 메모리의 내용

iArray[0] iArray[1]
16 | 8 | 4 | 2 0 | 128 | 64 | 32

Intel의 CPU는 리틀 엔디안(little endian)이라는 방식으로 메모리에 정보를 기록하고 있다. 리틀 엔디안은 멀티 바이트(2바이트 이상의 정보)를 저장할 때, 하위 바이트부터 저장하는 특성을 나타낸다. 이와는 반대로, 모든 정보를 표1과 같이 상위 바이트부터 저장 형식을 빅 엔디안(big endian)이라고 한다. 옛 PDP 시리즈와 Motorola 등은 이 방법을 채용하고 있었다. 지금은 접할 기회가 적다고 생각되지만 빅 엔디안 형식을 채용한 컴퓨터에서 코드1을 실행하면 위의 결과와는 다른 결과를 얻을 수 있다.

문자 등의 바이트 단위의 정보를 취급하는 경우는 신경 쓸 필요는 없지만, 멀티 바이트 포인터 등으로 직접 취급하거나 메모리 덤프 등을 사용하여 디버깅할 경우 이러한 지식이 요구될 수 있다 .

포인터 형에서 다른 포인터 형식으로 변환은 프로그래머의 책임으로 할 수 있었지만, 포인터를 정수로 변환을 하는 것은 가능한가? 원칙적으로는 포인터와 정수는 상호 교환할 수 없다고 규정하고 있다. 그러나 물리적 메모리 주소는 숫자로 표현되고 있기 때문에 많은 처리계에서는 정수와의 상호 교환을 실현할 수 있을 것이다. 그러나 이식성 등을 중시하는 경우 처리계 의존 코드는 피해야 한다.

다만, 포인터와 정수의 상호 교환에서 0은 유일한 예외라고 되어 있다. 일반적으로 포인터는 어떤 변수를 가리키지만, 반드시 유효한 주소를 보유할 수 있다고는 할 수 없다. 어떤 유효한 포인터를 반환하는 함수를 만드는 경우, 받은 인수가 잘못된 값이었기 때문에 함수의 처리를 이행할 수 없는 경우, 함수는 무엇을 반복하면 좋은 것일까 요? 적어도 엉터리 값을 반환 피해야 한다. 그래서 사용되는 것이 0의 포인터이다.

0을 저장하는 포인터는 유효한 포인터와 비교하면 동일하지 않다는 것을 보증하고 있다. 즉, 포인터가 유효한 경우 0과 동일하게 되지 않는다.

포인터에 사용되는 0은 NULL이라는 별명이 주어 있다. 이 NULL이라는 식별자(NULL은 식별자이며, 키워드가 아니다)는 일반적으로 매크로로 정의되어 있다. 매크로에 대해서는 “매크로 상수"에서 자세히 설명하겠지만, 컴파일시에 NULL을 0으로 대체되기 때문에 NULL이라는 식별자와 0은 동일하다고 생각할 수 있다. (NULL == 0)은 성립한다.

포인터가 유효한지 여부를 조사하고 싶으면 NULL로 비교한다. 마찬가지로 포인터를 해제하고 싶다면 NULL 포인터에 대입한다.

char *str = NULL;

유효한 객체를 참조할 수 없는 포인터에는 NULL을 대입하는 것으로 잘못된 포인터임을 어필할 수 있다. NULL 포인터를 참조할 수 없다. NULL 포인터를 참조한 결과는 미정으로되어 있다. 포인터를 반환해야 함수가 처리에 실패하고, 적절한 포인터를 돌려 줄 수 없는 경우 NULL을 반환하여 오류를 알리는 방법은 사용 오래되었기 때문에, 포인터와 NULL의 비교는 프로그램에서 자주 사용되고 있을 것이다.