Coding & Programming/C언어 기초(A-Z)

[C언어 기초코딩: 예제로 간단 정리] 11. 전처리기(Preprocessor)

mainCodes 2021. 3. 18. 19:05

JollyTree의 C언어 기초코딩: 예제로 간단 정리 - 11. 전처리기(Preprocessor)

 

안녕하세요 JollyTree입니다(•̀ᴗ•́)و 

C언어는 소스코드를 컴파일하기 전에 소스코드를 처리하는 기능을 가지고 있습니다. 컴파일러에게 컴파일 할때 이렇게 처리해줘 라고 의사를 전달하는 것으로 이것을 전처리기라고합니다. 소스코드에 헤더파일을 포함시키는 용도인 #include, 숫자, 문자열 등을 기호로 표현하는 #define 등이 전처리기에 지시어에 해당됩니다.

전처리를 사용하면 소스코의 가독력을 높일 수 있을 뿐만 아니라, 윈도우, 리눅스 등 플랫폼을 고려하여 소스코드 영역을 구분하여 컴파일 할 수도 있고, 소스코드의 라인, 파일명 등을 컴파일된 코드가 실행 될 때 출력할 수 있습니다.

전처리기라는 용어대로 컴파일 시점에 컴파일러에게 여러가지 조건을 적용할 수 있습니다.

지시어 설명
#include 헤더 등 파일을 컴파일 할때 포함합니다.
#define 기호상수, 함수 등을 매크로로 정의할 수 있습니다. 
#undef #define으로 정의한 매크로를 해제합니다.
#ifdef 만약 매크로가 정의되었다면
#ifndef 만약 매크로가 정의되어 있지 않다면
#if  if문과 유사하며 컴파일할때 조건을 줄 수 있습니다.
#else else문과 동일합니다.
#endif #if의 종료를 의미합니다.
#line 라인번호 지정, __LINE__, __FILE__ 매크로를 재정의 합니다.
#pragma once, warning, pack, message 등 키워드와 연계하여 여러 용도로 사용됩니다. 컴파일러 또는 시스템에 따라 다름니다.

 

전처리기 예제:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <io.h>
#include <stdlib.h>
#include <string.h>
 
//#define 키워드로 매크로 정의
#define MAX_COUNT   100   //기호 상수선언
#define INT32       int   //자료형 재정의
#define TITLE_STR   "mainCodes's Example Codes"
 
//매크로 함수 정의
#define SUM(x, y)   ( (x)+(y) )  
#define MIN(x, y)   ( (x) < (y) ? (x) : (y) )
#define MAX(x, y)   ( (x) > (y) ? (x) : (y) )
 
//중복 선언
#include "once.h"
#include "once.h"  
 
char* get_filename(char* path)
{
    static char filename[256= "";
    char* ptr = NULL;
 
    ptr = strrchr(path, '\\');
 
    if (ptr == NULL)
        strcpy(filename, path);
    else
        strcpy(filename, ptr + 1);
 
    return filename;
}
 
int main()
{
    INT32 a, b;
    INT32 sum;
 
    /* 1. #ifdef #else #endif, #if #else #endif
    */
#ifdef __LINUX    //__LINUX 전용
    printf(">> 이 코드는 리눅스 시스템을 위한 코드입니다.\n");
#else
    printf(">> 이 코드는 윈도우 시스템을 위한 코드입니다.\n");
#endif
 
#if 0 //조건이 거짓이므로, #else까지는 컴파일에서 제외됨
    printf("합을 구할 두 수를 입력하세요\n");
    scanf("%d"&a);
    scanf("%d"&b);
#else 
    a = 10;
    b = 20;
#endif
 
    sum = SUM(1020);
    printf(">> 합      = %d\n", sum);
    printf(">> 작은 수 = %d\n", MIN(1020));
    printf(">> 큰수    = %d\n", MAX(1020));
 
 
    /* 2. pragma 예제(once, message, warning, push, pop)
    */
    {
        TEST_ST test_st;
        EXAM_ST exam_st;
#pragma message(">> pragma_msg:: 구조체 크기를 구하기 전----\n")
        printf(">> test_st 크기 = %d\n"sizeof(test_st));
        printf(">> exam_st 크기 = %d\n"sizeof(exam_st));
#pragma message(">> pragma_msg:: 구조체 크기를 구하기 후----\n")
    }
 
#pragma warning(disable: 4996)
    {
        char filename[100= "mainCodes.txt";
        if (access(filename, 0!= 0)
        {
            printf("'%s' 이 존재하지 않습니다.\n", filename);
            exit(0);
        }
    }
 
    /* 3. 내장 매크로 이용
    */
    {
        char filepath[256= "";
        char* filename = NULL;
 
        strcpy(filepath, __FILE__);
        filename = get_filename(filepath);
 
        printf(">> %s > %s-%s %s:%d\n", TITLE_STR, __DATE__, __TIME__, filename, __LINE__);
    }
 
    return 0;
}
cs


실행결과:

 

/* 1. #ifdef #else #endif, #if #else #endif, #define
*/

#ifdef __LINUX 에서 '__LINUX'는 임의로 지정한 것으로 만약 전처리기는 __LINUX가 #define __LINUX 1 과 같은 형태로 정의되어 있을 경우 #ifdef 내의 코드를 컴파일 하지만 __LINUX 가 정의되어 있지 않으므로 #else의 printf()함수 코드를
컴파일에 포함합니다. 컴파일에서 제외된 문장들에 문법 오류가 있더라도 컴파일에서 제외되었기 때문에 컴파일러는 오류가 있든 없든 고려하지 않습니다. 

#if 0은 조건식을 무조건 0 으로 지정했기 때문에 scanf()함수를 이용하여 a, b 변수 값을 입력 받는 코드를 컴파일에서 아예 제외시키고 #else의 하드코딩된 a = 10;, b = 20;의 코드를 컴파일합니다. 즉, #ifdef, #if는 소스코드를 운영환경에 맞게 조건적으로 컴파일 하는데 주로 사용됩니다.

#define MAX_COUNT   100 과 같은 단순 기호 상수 정의는 반복적으로 100이라는 숫자가 소스코드내 여러분데에서 사용될 경우 MAX_COUNT로 대신사용할 수 있으며 일괄적으로 상수값을 변경할 수 있어 편리합니다.

SUM, MIN, MAX는 매개변수를 가지는 매크로 함수로 일반함수 보다는 함수 호출 과정이 없어 속도가 빠르다는 장점을 가지고 있습니다. 하지만, 일부 환경을 제외하고는 요즘처럼 컴퓨팅 파워가 높아진 상황에서는 속도에 크게 영향을 주지 않을 것 같습니다. 


/* 2. pragma 예제(once, message, warning, push, pop)
*/
pragma 시스템 환경에 따라 조금씩 차이가 있는 지시어로 유용하게 사용되는 지시어 중에 하나 입니다.

예제는 동일한 헤더를 2번 포함하고 있습니다. 간혹 여러 개의 소스코드로 구성된 프로젝트라면 여러 코드에서 동일한 라이브러리 헤더를 포함시키는 일도 자주 발생하게 됩니다.

    //중복 선언
    #include "once.h"
    #include "once.h"

당연히 컴파일러는 당연히 중복된 변수, 함수 등에 대한 오류를 보여줄겁니다.

    error C2011: 'test': 'struct' type redefinition
    error C2011: 'exam': 'struct' type redefinition

once.h는 이런 중복 선언 방지를 위해 소스코드의 앞 부분과 뒤 부분에 #ifndef #endif, #define을 사용하였습니다. 이렇게 되면 __ONCE_H__ 매크로의 정의 여부를 검사하여 소스코드가 1회만 반영되어 중복 선언에 대한 오류가 발생하지 않게 됩니다.

 

Visual Studio 2019는 헤더를 생성하면  .h 헤더 파일 제일 앞에 자동으로 #pragma once를 선언해 주는데 이는 #ifndef #endif와 동일한 목적으로 사용됩니다. #pragma 지시어는 표준이지만 뒤에 정의된 once는 컴파일러에 따라 다를 수 있습니다.

    #ifndef __ONCE_H__
    #define __ONCE_H__
        문장;
        문장;
        ...
    #endif

잠시 main()함수로 돌아가서 #pragma message("문자열");은 컴파일 과정에 메시지를 보여줍니다. Visual Studio 2019의 경우 Output 창에서 확인할 수 있습니다.

once.h에서 TEST_ST와 EXAM_ST의 구조체를 보면 멤버 변수의 이름과 자료형이 모두 동일합니다. 멤버 변수별로 크기를 보면 char str[2]; 는 2바이트, int num;은 4바이트이므로 총 6바이트가 되어야 합니다. 


그런데 구조체의 크기를 sizeof()로 구해 보면 TEST_ST는 총 8바이트이고, EXAM_ST는 6바이트가 나옵니다. 왜 이럴까요?
구조체의 크기는 시스템 환경(운영체제, 컴파일러)에 따라 다릅니다. Visual Studio는 구조체 정렬의 기본값이 8바이트로 설정되어 있기 때문에 TEST_ST의 크기가 8바이트로 계산되는 것입니다. 

만약, 서로 다른 시스템/운영체제 간 통신을 할 때 데이터를 주고 받는데 이때 구조체의 크기가 다르면 오류가 발생하기 때문에 구조체의 크기 단위는 동일하게 조정해주는 것이 필요합니다. 

이때 EXAM_ST와 같이 구조체 전과 후에 구조체 정렬 단위를 조정해 줄 수 있습니다. #pragma pack(push, 1)은 1바이트 크기로 정렬하는 의미이고, #prgma pack(pop)는 다시 원래대로 복구하라는 의미입니다. 그래서 EXAM_ST 는 우리가 예상한 6바이트의 크기되는 것입니다.

    typedef struct test {
        char str[2];
        int  num;
    }TEST_ST;

    #pragma pack(push, 1) //1바이트 크기로 정렬
    typedef struct exam {
        char str[2];
        int  num;
    }EXAM_ST;
    #pragma pack(pop)     //복구

 

(참고) Visual Studio 2019는 #pragma  지시어를 사용하지 않고 프로젝트 속성에서 동일하게 구조체를 1바이트 크기로 정렬할 수 있습니다. 아래 그림을 참고하세요.

 

다시 main()함수로 와서 코드 중간의 access()함수는 컴파일 하면 deprecated되었다는 에러 메시지를 보여주며 access()는 더이상 사용되지 않으니 대신 _access()를 사용할 것을 권장 합니다. 

 

    error C4996: 'access': The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant
    name: _access. See online help for details. #pragma warning(disable: 4996)

당연히 표준이나 개선된 내용에 따라 함수를 사용해야 하지만, 필요에 따라 예전의 함수 이름을 사용하고자 한다면 이런 에러 메시지를 출력하지 않도록 할 수 있습니다.

소스코드 중간에 #pragma warning(disable: 4996)를 선언하면 컴파일러는 C4996 에러를 disable하게 됩니다.


/* 3. 내장 매크로 이용
*/
C언어는 내부적으로 정의된 내장 매크로를 지원합니다. 대표적인 내장 매크로는 __DATE__, __TIME__, __LINE__, __FILE__ 있습니다.

실행결과에서도 알 수 있듯이 __DATE__는 현재 날짜, __TIME__은 현재 시간, __LINE__은 소소코드의 라인번호 그리고 __FILE__은 파일명이 포함된 전체 경로를 보여줍니다.

 

이상 JollyTree였습니다 (•̀ᴗ•́)و