C언어는 플랫폼 (윈도우나 안드로이드, 유닉스 등)에 상관없이 이식이 가능한 언어이지만, 언어를 이용해 고유의 실행 파일을 만들어내는 컴파일러는 플랫폼 종속적이다. 그러므로 컴파일러는 각각의 플랫폼의 고유 기능을 수행할 수 있도록 지원해야한다. 이때 사용하는 명령어가 #pragma이다.
#pragma 는 #으로 시작하므로 전처리문으로 보이지만 "컴파일러 지시자"라고 따로 분류된다. #pragma 지시자의 기본 형식은 "#pragma "token""이다. 이 "token"의 종류는 컴파일러별로 다르며 플랫폼 종속적이므로 플랫폼 이식성을 유지하려면 조건부 컴파일 지시자와 함께 사용하는 것이 좋다. 컴파일러는 #pragma 다음의 토큰을 인식할 수 없을 경우 단순히 무시하고 컴파일을 계속 수행한다.
VS의 #pragma "token"은 이 페이지에서 확인할 수 있다. 토큰들중에는 내용이 굉장히 어려운 것도 있고 간단한 것도 있다. 이 포스팅에서는 간단하고 유용한 토큰만 몇가지 소개한다.
1. #pragma once
헤더파일 선두에 적어서 헤더파일을 한번만 포함하게 해서 컴파일 시간을 절약하고 중복 정의를 막는다.
일부러 헤더파일을 두번씩 쓰진 않겠지만 헤더파일이 중첩되다보면 두번씩 포함되는 경우가 있으므로 이를 방지해준다. 이는 #pragma 지시자가 아닌 #ifndef 전처리문으로도 동일한 효과를 낼 수 있다. #ifndef가 C언어 표준이므로 이를 쓰는 것이 플랫폼 이식성에는 더 좋다. 아래는 사용 예시이다.
//헤더파일을 중복 포함되지 않게 하는 방법
//방법 1. #pragma once를 사용한다.
#pragma once
#include < iostream >
//그외 헤더파일들...
//방법 2. #ifndef를 사용한다.
#ifndef HEADER_FILE_ONCE_INCLUDE
#define HEADER_FILE_ONCE_INCLUDE
//헤더 파일 내용들
#endif //HEADER_FILE_ONCE_INCLUDE
//방법2의 원리는 다음과 같다.
//#ifndef는 매크로가 정의되어 있지 않으면 #endif 까지의 내용을 컴파일한다.
//그 안에서 매크로를 정의하고 밑으로 헤더파일을 정의한다.
//그러면 다시 같은 구문을 만났을 때는 매크로가 정의되어 있으므로 재컴파일되지 않는다.
2. #pragma pack(n),
이후 선언되는 구조체의 메모리 정렬 방식을 지정한다. 구조체의 메모리 정렬 기본값은 8byte이고 프로젝트 설정에서 바꿀 수도 있지만 #pragma pack(n) 명령어를 쓰면 소스 중간에도 정렬 값을 바꿀 수 있다. #pragma pack()이라고 ()안을 비워서 쓰면 다시 기본값인 8로 정렬된다.
이 정렬값은 컴파일러에 미리 지정되어 있는 스택에 저장되는데 #pragma pack(push,n)은 현재의 정렬값을 스택에 저장하고 새로운 정렬값인 n으로 바꾼다. 이 때, n을 생략하면 현재의 정렬값을 스택에 저장만 하고 따로 바꾸진 않는다. #pragma pack(pop,n)은 스택에 저장되어 있는 정렬값으로 현재의 정렬값으로 바꾸고 스택에 저장되어 있던 값은 n으로 바꾼다.
주로 #pragma pack(push,n)과 #pragma pack(pop)을 같이 써서 특정한 구조체의 정렬값을 임시로 바꾸는 식으로 사용된다.
#include < iostream>
struct s1 { long c1; double i1; }; //기본이 8바이트 정렬이므로 구조체 s1은 16byte를 차지한다. 8(4) + 8(8) = 16
#pragma pack(2)
struct s2 { long c2; double i2; }; //2바이트씩 정렬하게 했으므로 구조체 s2는 12바이트를 차지한다. 4(4) + 8(8) = 12
#pragma pack(push,1)
struct s3 { bool b; long ll; }; //현재의 정렬값(2)를 스택에 저장하고, 1로 정렬한다. 1(1) + 4(4) = 5
#pragma pack(pop)
struct s4 { long c2; double i2; }; //저장되어 있던 정렬값을 꺼낸다.
#pragma pack()
struct s5 { long c3; double i3; }; //다시 8바이트로 정렬되어서 구조체 s3은 16byte를 차지한다.
using namespace std;
int main()
{
cout << "구조체 s1의 크기 : " << sizeof(s1) << "byte" << endl;
cout << "구조체 s2의 크기 : " << sizeof(s2) << "byte" << endl;
cout << "구조체 s3의 크기 : " << sizeof(s3) << "byte" << endl;
cout << "구조체 s4의 크기 : " << sizeof(s4) << "byte" << endl;
cout << "구조체 s5의 크기 : " << sizeof(s5) << "byte" << endl;
}
3. #pragma region, #pragma endregion
코드를 구역으로 나눠서 정리해서 접어두기 위해 사용한다. 컴파일에는 따로 영향을 미치지 않는다.
구역의 시작은 #pragma region 으로 표시하고, 끝은 #pragma endregion으로 표시한다. region token 뒤에 적는 문구는 그 구역을 지칭하기 위해 적는 문구로 아무렇게나 적어도 컴파일에 영향을 미치지 못하고 무시된다.
나는 클래스를 설계할 때 몇가지 규칙을 가지고 멤버 변수나 멤버 함수를 배치하는데 이 때 비슷한 분류의 멤버 변수나 멤버 함수를 묶어두고 싶을때 이 명령어를 주로 사용한다.
class Point
{
int x;
int y;
public:
void Print() const // const 함수: 함수 안에서 멤버 변수의 값을 변경할 수 없음을 의미
{
//const 함수 내에서는 const 함수만 호출할 수 있다.
cout << "(" << x << "," << y << ")";
}
public:
#pragma region operator method //여기서부터
Point operator+ (Point arg)
{
//cout << "\noperator+ () 함수호출" << endl;
Point ret;
ret.x = this->x + arg.x;
ret.y = this->y + arg.y;
return ret;
}
//아래의 const는 리턴값을 바로 변경할 수 없다는 뜻이다.
// (p3++) + p2 같은 식으로 쓸 수 없다는 뜻
const Point operator-- () //전위 --연산자
{
--x; --y;
return (*this);
}
//후위 연산자라고 자동으로 우리가 생각되는 방식으로 작동하진 않는다.
//그러므로 후위연산자를 구현하기 위해서 원래 값을 따로 저장해뒀다가 그 값을 리턴해주자.
const Point operator-- (int) //후위 --연산자
{
Point tmp = *this;
--(*this);
return tmp;
}
#pragma endregion operator method //여기까지 접을 수 있다.
public:
//생성자 정의
Point(int _x = 0, int _y = 0) : x(_x), y(_y) { }
};