. 오프셋

여기서 잠시 실습을 중단하고 오프셋(Offset)이라는 용어에 대해 생각해보자. 오프셋이라는 말은 사실 굉장히 전문적인 용어인데 이 용어에 대한 개념이 헷갈리면 코드를 이해하는 데 상당한 오해가 있을 것 같다. 오프셋이란 어떤 기준 번지에서부터의 상대적인 거리라는 뜻이다. 주로 포인터와 함께 사용되며 포인터가 가리키는 시작 번지(Base)에서부터 얼마나 떨어져 있는가를 표현할 때 오프셋이라는 말을 쓴다.

예를 들어 TCHAR *ptr 1234번지를 가리키고 있을 때 1238번지는 ptr과 오프셋 4만큼 떨어져 있다고 표현한다. ptr을 배열이라고 했을 때 배열의 첨자가 바로 오프셋이 되며 1238번지는 ptr[4]로 읽을 수 있다. 그렇다면 int *ptr 1234번지를 가리키고 있을 때 1238번지의 오프셋은 얼마가 되겠는가? 4가 아니라 1이다. 왜냐하면 수형은 크기가 4바이트이기 때문이다. 이 경우 1238번지는 ptr[1]로 바로 읽거나 쓸 수 있다. 오프셋이란 단순히 몇 바이트 떨어져 있는가 만을 나타내는 것이 아니라 기준 번지에서부터 오프셋 위치까지 몇 개의 값이 들어 있는가를 나타낸다.

ApiEdit off 변수도 마찬가지로 기준 번지 buf로부터의 상대적인 거리를 나타내며 이 값이 곧 현재 편집중인 문자의 위치가 된다. buf TCHAR의 배열이므로 off는 곧 buf로부터의 바이트 수라 할 수 있다. off가 현재 12라면 이는 텍스트 버퍼의 처음부터 12바이트만큼 떨어진 번지라는 뜻이며 시작 번지에서부터 off까지 12개의 문자(한글은 절반)가 더 있다는 것을 알 수 있다. 오프셋은 거리의 개념이기 때문에 데이터형은 정수형이다. 음수 오프셋도 문법적으로 가능하기는 하지만 실질적인 의미는 없으므로 오프셋은 0을 포함한 양의 정수라고 할 수 있다.

문자열 버퍼를 다룰 때 포인터를 바로 쓰지 않고 오프셋을 사용하는 이유는 아무래도 정수형이 훨씬 더 다루기 편리하기 때문이다. 함수를 설계할 때나 디버깅할 때 0x20c8ef3a 같은 포인터를 읽는 것과 136 같은 정수를 읽는 것 중 어떤 것이 더 쉽겠는가? buf가 가리키는 번지는 상황에 따라 달라질 수 있지만 오프셋은 buf로부터의 거리이므로 buf가 어떤 번지에 할당되더라도 항상 일정하며 따라서 코드를 분석하거나 버그를 잡을 때 항상 동일한 현상을 목격할 수 있다. 그래서 GetLine, GetOffFromRC 같은 함수들이 문서상의 위치를 전달하고 계산할 때 오프셋을 사용하는 것이다.

오프셋은 어디까지나 buf로부터의 거리에 불과하므로 이 위치에 있는 문자를 읽거나 변경할 때는 포인터로 바꾸어야 한다. 반대로 이미 만들어져 있는 함수의 인수를 구하기 위해 문자의 번지로부터 오프셋을 계산해 내야 할 때도 있다. 오프셋은 언제든지 포인터가 될 수 있고 포인터는 또한 언제든지 오프셋으로 바꿀 수 있다.

오프셋을 포인터로 바꾸려면 기준 번지에 오프셋을 더하면 된다. off위치의 문자를 읽으려면 buf+off의 번지를 읽으면 된다. 또는 buf[off]로 바로 문자를 읽을 수도 있으며 이는 *(buf+off)와 동일한 표현식이다. 포인터에 정수를 더하면 결과값은 포인터가 되므로 기준 번지에 오프셋을 더한 값은 곧 기준 번지에서 오프셋만큼 떨어진 번지를 가리키게 된다.

포인터를 오프셋으로 바꾸려면 포인터에서 오프셋의 기준 번지를 빼면 된다. 단 이 식이 의미가 있으려면 포인터는 기준 번지보다 더 뒤쪽에 있어야 한다. 포인터 p의 오프셋을 구하려면 p-buf식으로 구할 수 있는데 이 식은 이미 GetLine 함수에서 여러 번 사용했다. GetLineSub 함수가 구한 줄 끝 번지 p를 줄의 끝 오프셋으로 바꾸기 위해 e=p-buf; 식으로 오프셋을 구했다. 이 식이 가능한 이유는 포인터에서 포인터를 빼면 결과값은 정수가 되며 따라서 정수형 변수 e가 이 값을 대입받을 수 있기 때문이다.

각각의 함수가 요구하는 인수의 형태가 다르기 때문에 오프셋과 포인터 사이를 자유롭게 전환할 수 있어야 한다. 예를 들어 한글 문자인지 조사하는 IsDBCS 함수는 오프셋을 인수로 요구하는데 만약 p번지에 있는 문자가 한글인지 알고 싶다면 p를 오프셋으로 바꾸어서 IsDBCS(p-buf)를 호출해야 한다. Insert 함수는 삽입 위치를 nPos 오프셋으로 전달받지만 memmove 함수는 포인터를 요구하므로 buf+nPos로 삽입 위치를 포인터로 바꾸어야 한다.

앞으로의 코드에서는 이런 포인터와 오프셋간의 변환이 더 자주 나올 것이며 아주 당연한 듯이 사용할 것이다. 만약 지금 오프셋과 포인터간의 변환이 어렵다고 느껴진다면 아직 C 언어에 대한 공부가 부족하다고 생각하면 틀림없다.