C언어의 장점 중 하나는 어느 운영체제나 플랫폼으로 쉽게 이식될 수 있는 이식성(Portability)이다. 유닉스에서 작성한 소스를 윈도우즈로 가져와 컴파일하면 똑같은 동작을 하는 실행 파일을 얻을 수 있다. 그러나 이 이식성은 어디까지나 소스 차원에서 이식 가능성을 의미하는 것이지 컴파일된 결과인 실행 파일은 그렇지 않다. C언어는 이식성이 있지만 C언어를 특정 플랫폼에 맞게 컴파일하여 고유의 실행 파일을 만들어 내는 컴파일러는 본질적으로 플랫폼에 종속적이다.
그래서 각 플랫폼에서 실행되는 컴파일러는 플랫폼의 고유한 기능을 수행하기 위한 지원을 해야 한다. 플랫폼별로 구조나 기능이 다르기 때문에 구현도 약간씩 달라질 수 있는데 예를 들어 메모리를 관리하는 방식이나 실행 파일의 특수한 구조로 인한 코드 배치 방법이 플랫폼별로 고유하다. #pragma 지시자는 플랫폼별로 다른 이런 기능에 대한 지시 사항을 컴파일러에게 전달하는 방법이다. #문자로 시작하므로 전처리 명령처럼 보이지만 컴파일러 지시자이다. #pragma 지시자의 기본 형식은 다음과 같다.
#pragma 토큰문자열
#pragma 다음에 지시 사항을 전달하는 토큰 문자열이 오는데 이 토큰의 종류는 컴파일러별로 다르다. 플랫폼에 종속적인 기능에 대한 지시자이므로 #pragma 지시자는 컴파일러에 대해서 종속적일 수밖에 없다. 그래서 특정 플랫폼을 위한 프로그램을 작성할 때만 사용해야 하며 꼭 이식성을 유지하려면 조건부 컴파일 지시자와 함께 사용해야 한다. 컴파일러는 #pragma 다음의 토큰을 인식할 수 없을 경우 단순히 무시해 버리며 컴파일은 계속 수행한다. 다음은 비주얼 C++ 6.0의 pragma 토큰들이다.
alloc_text, auto_inline, bss_seg, check_stack, code_seg, comment, component, conform
const_seg, data_seg, deprecated, function, hdrstop, include_alias, init_seg, inline_depth
inline_recursion, intrinsic, managed, message, once, optimize, pack, pointers_to_members
pop_macro, push_macro, runtime_checks, section, setlocale, unmanaged, vtordisp, warning
pack
pack 지시자는 이후부터 선언되는 구조체의 정렬 방식을 지정한다. 프로젝트 설정 대화상자에서 구조체 정렬 방식을 각 모듈별로 조정할 수 있지만 pack 지시자는 소스의 중간에서 원하는 구조체에 대해 정렬 방식을 변경할 수 있도록 한다는 점이 다르다. 이 지시자를 사용하면 같은 소스에 있는 두 구조체를 다른 방식으로 정렬할 수 있다. 다음 선언문을 보자.
#pragma pack(2)
struct st1 { short s; int i; };
#pragma pack(4)
struct st2 { short s; int i; };
이렇게 선언하면 st1 구조체는 2바이트 정렬되므로 6바이트를 차지하며 st2는 4바이트 정렬되므로 8바이트를 차지한다. 프로젝트 설정에 지정된 정렬값을 다른 값으로 바꾸고 싶을 때 pack(n) 지시자의 괄호안에 원하는 값을 적어주면 된다. 정렬값의 디폴트는 8이며 n을 생략하여 pack()이라고만 적으면 디폴트 정렬값으로 돌아간다.
pack(n)으로 정렬값을 변경하면 이후부터 선언되는 구조체는 이 정렬값의 영향을 받는다. 만약 특정 구조체에 대해서만 임시적으로 원하는 정렬값을 적용한 후 원래의 정렬값으로 돌아오려면 변경하기 전에 원래 값을 보관해 두어야 하는데 이 때는 push, pop 명령을 사용한다. 컴파일러는 내부에 정렬값 저장을 위한 스택을 유지하고 있으며 이 스택에 정렬 상태를 LIFO 원칙에 따라 저장하고 다시 빼내올 수 있다.
pack(push, n) 명령은 현재의 정렬 상태를 스택에 저장하면서 정렬값을 n으로 변경하는데 n을 생략하면 현재 정렬값을 스택에 저장하기만 한다. pack(pop,n)은 스택의 최상단에 있는 정렬값을 제거하고 새로운 정렬값을 n으로 변경하는데 n을 생략하면 스택에서 꺼낸 정렬값을 새로운 정렬값으로 설정한다. push는 저장과 동시에 다른 값으로 변경하는 경우가 많으므로 보통 n과 함께 쓰며 pop은 저장된 값을 복구시킬 때 사용하는 경우가 많으므로 보통 단독으로 사용한다. push, pop은 원하는만큼 중첩해서 사용할 수 있다. 다음 코드를 보자.
#pragma pack(2)
struct st1 { short s; int i; }; // 2바이트 정렬
#pragma pack(push,4) // 푸시하면서 4바이트 정렬로 바꿈
struct st2 { short s; int i; }; // 4바이트 정렬
#pragma pack(pop) // 원래 정렬값 복원
struct st3 { short s; int i; }; // 2바이트 정렬
최초 정렬값 2를 가지는 상태에서 4로 변경하면서 원래 정렬 상태인 2를 스택에 푸시해 두었다. 그래서 st2 구조체를 선언한 후 다시 팝하면 정렬 상태 2로 복구될 것이다. 어떤 구조체가 반드시 특정 정렬 상태를 가져야 한다면 pack(push, n) 지시자로 원래 정렬상태를 유지하면서 설정을 잠시 변경할 수 있다. 예를 들어 어떤 파일을 읽어야 하는데 이 파일의 헤더가 구조체로 되어 있고 이 구조체는 반드시 1바이트로 정렬되어야 한다면 다음과 같이 이 구조체를 선언해야 한다.
#pragma pack(push,1)
struct Header
{
char Magic[2];
int Version;
char NumRecord;
double xsize, ysize;
};
#pragma pack(pop)
구조체를 선언하기 전에 정렬 상태를 1바이트로 바꾸되 이전의 정렬 상태는 스택에 푸시해 두었으며 구조체 선언이 끝난 후 다시 원래대로 정렬값을 복구한다. 이렇게 하지 않으면 Header 구조체는 프로젝트 설정대로 정렬되어 버리므로 이 구조체로는 파일을 제대로 읽을 수 없을 것이다.
#include<stdio.h> |