티스토리 뷰

 

 본 강좌는 아래 동영상 강좌와 같이 진행됩니다. 되도록이면 동영상과 같이 보시는 것을 추천합니다.

 

 유튜브 채널 가기

 

 강좌 21편 동영상 보기

 


 

 지난 강좌 2편에 소스코드를 실행파일로 만드는 단계를 알아보면서, 처음 단계로 '전처리기'가 전처리문을 처리한다는 것을 배웠습니다. 이번 시간에는 이 '전처리문' 중에서 많이 사용하는 것들 위주로 사용방법을 알아보도록 하겠습니다.

 

 

 

 1. 전처리문

 

 전처리문은 컴파일 과정이 일어나기 전에 전처리기에서 실행되기 때문에, 컴파일 이후 또는 프로그램 실행 도중의 값과는 전혀 상관이 없습니다. 프로그램의 실행 흐름이나 변수의 값 같은 것에도 당연히 영향을 끼치지 않습니다. 이해가 쉽게 '컴파일 하기 전에 소스코드를 정리'한다고 보시면 되겠습니다. 이제 전처리문의 종류에 대해 알아봅시다.

 

 ◆ 파일 포함 (#include)

 지금까지 모든 프로그램에는 #include 구문을 사용해왔기 때문에 친숙할 것입니다. 이 구문에 대해서는 이전에도 간단히 설명했지만, 다시 한번 알아보도록 합시다.

 

// 컴파일러에 미리 지정된 폴더 안에 파일이 있을 때

#include <파일명>

 

// 그 밖의 경로에 파일이 있을 때

#include "경로를 포함한 파일명"

 

 '<>'는 컴파일러 상에 미리 지정된 폴더 안에 해당 파일이 있을때 씁니다. 비주얼 스튜디오 상단 메뉴에서 '프로젝트' → '속성' 을 골라봅시다. 메뉴 맨 아래에 있습니다.

 

 왼쪽에서 '구성 속성'의 'VC++ 디렉터리'를 골라주면, 오른쪽에 여러 항목이 보이는데, 그 중에서 '포함 디렉터리' 부분이 '<>'안의 파일을 찾는 곳이 됩니다.

 

 우리가 만들거나 다른 곳에서 가져온 파일을 포함시키려면 보통은 ""를 사용하게 됩니다. 이 안에 별도의 경로 없이 파일명만 넣으면 현재 소스 파일이 있는 폴더 안에서 찾으며, 경로와 파일명을 같이 넣으면 해당 경로에서 파일을 찾게 됩니다.

 

#include <stdio.h>   // 컴파일러에 지정된 폴더 내에서 찾음

#include "abc.h"   // 현재 프로젝트 폴더 내에서 찾음

#include "C:\test\abc.h"   // 해당 드라이브 경로 내에서 찾음

#include "..\abc.h"   // 현재 소스 파일의 상위 폴더 내에서 찾음

 

 ◆ 식별자 정의, 정의 해제 (#define, #undef)

 #define은 #include 다음으로 자주 쓰이는 전처리문으로, 임의의 내용을 특정한 식별자로 선언합니다.

 

#define 식별자 내용

 

 다음을 봅시다.

 

#define APPLE 10

#define NAME "사과"

 

void main()

{

    printf("%s %d", NAME, APPLE);

}

 

실행 결과

 

사과 10

 

 '10' 이라는 숫자만 보면 무엇인지 알아보기 어렵고, 같은 '10'을 가지고 프로그램 내 여러 곳에서 사용하는 경우, 이런 식으로 #define 문을 이용해 치환해 사용하는게 편리합니다. 문자열 같은 경우는 ""안에 값을 넣어준 것을 볼 수 있습니다. 이렇게 한 이유는 '', "" 안에 #define문으로 선언된 식별자가 들어갈 수 없기 때문입니다. #define문에 자료형 같은 개념은 없으며, 단지 식별자 뒤에 오는 내용을 식별자로 만들어 주는 것입니다.

 

 그러므로 아래 처럼도 가능합니다.

 

#define APPLE 10

#define NAME "사과"

#define OUTPUT printf("%s %d", NAME, APPLE)

 

void main()

{

    OUTPUT;

}

 

 또한 간단한 메크로 선언도 가능합니다.

 

#define CALC(x, y) ((x) * (y))

...

printf("%d\n", CALC(2, 4));

...

 

실행 결과

 

8

 

 위의 예에서는 각각 x, y에 2, 4를 대응시켜 치환합니다. 함수는 처리 결과값을 return문을 통해 돌려주지만, #define문으로 정의된 메크로는 CALC(2, 4) 부분에, 대신 ((2) + (4))를 치환시키는 것에 불과합니다. 이 경우 메크로에 들어오는 인자에 따라 오류의 위험성이 있으니, 괄호를 써 대비하는게 좋습니다. 아래 예를 봅시다.

 

#define CALC1(x, y) (x) * (y)

#define CALC2(x, y) x * y

 

CALC1(1 + 1, 2 + 2) → (1 + 1) * (2 + 2) → 결과값 8

CALC2(1 + 1, 2 + 2) → 1 + 1 * 2 + 2 → 결과값 5

 

 괄호가 없을 경우 곱셈이 우선이 되어 결과값이 다르게 나옵니다.

 

#define CALC3(x, y) (x) + (y)

#define CALC4(x, y) ((x) + (y))

 

2 * CALC3(2, 4) → 2 * (2) + (4) → 결과값 8

2 * CALC4(2, 4) → 2 *((2) + (4)) → 결과값 12

 

 내용 전체에 괄호를 넣느냐 빼느냐에 따라 위의 예처럼 결과가 달라질 수 있습니다. 그러므로, 이렇게 메크로로 사용할 때에는 내용 전체와 각각의 인자에 괄호를 반드시 써주는 습관을 들이도록 합시다.

 

 이렇게 #define문으로 정의된 식별자를 해제하는 방법도 알아봅시다.

 

#undef 식별자

 

 #undef는 보통 쓸 일이 많지 않으나, 간혹 같은 이름의 식별자를 사용하는 헤더파일들을 한 소스 파일에 포함시켜 사용하는 경우 등 일부 쓰이기도 하니 알아두도록 합시다.

 

 ◆ 조건 처리(#if, #ifdef, #ifndef, #else, #elif, #endif)

 전처리문으로도 조건을 판별해 특정 구문을 포함하거나 포함하지 않을 수 있습니다.

 

#if 조건1

조건1이 참일때

#elif 조건2

조건1이 거짓이고 조건2가 참일때

#else

조건1, 2가 거짓일때

#endif

 

 기본적으로 if문과 거의 비슷합니다. 단지 조건처리 마지막에 #endif을 써줘서 끝을 알려줘야 한다는 차이점이 있습니다. #elif, #else는 필요하지 않으면 생략이 가능합니다.

 

#define NUM 10

 

void main()

{

#if NUM < 10

    printf("NUM은 10보다 작습니다.\n");

#elif NUM > 10

    print("NUM은 10보다 큽니다.\n");

#else

    printf("NUM은 10입니다.\n");

#endif

 

실행 결과

 

NUM은 10입니다.

 

 위는 예를 들기 위해 만든 코드이고, 실제로 전처리문은 프로그램의 흐름을 제어하는게 목적이 아니라, 소스코드를 컴파일 전에 정리한다고 보면 되겠습니다. 위의 코드가 전처리기를 거치면 세번째 printf() 문만 남고 나머지는 컴파일 되지 않습니다.

 

 주의할 점으로 전처리문은 컴파일 전에 실행되기 때문에 프로그램 내의 변수를 조건으로 둘 수 없습니다.

 

void main()

{

    int a = 1;

#if a == 1

    printf("a는 1입니다.\n");

#else

    printf("a는 1이 아닙니다.\n");

#endif

}

 

실행 결과

 

a는 1이 아닙니다.

 

 변수는 프로그램이 실행돼야 생성되고 메모리상에 값이 생기기 때문에, 그 이전에 처리되는 전처리문에서는 이 값을 판단할 수 없습니다. 그러므로 위와 같은 경우에는 if문을 사용해야 합니다.

 

 다음으로 #ifdef, #ifndef 문에 대해 알아보겠습니다.

 

#ifdef 메크로이름

메크로가 정의되어 있을때

#else

메크로가 정의되어 있지 않을때

#endif

 

#ifndef 메크로이름

메크로가 정의되어 있지 않을때

#else

메크로가 정의되어 있을때

#endif

 

 #ifdef는 메크로가 정의되어 있는지를, #ifndef는 메크로가 정의되어 있지 않은지를 판별합니다.

 

#define NUM 10

 

void main()

{

#ifdef NUM

    printf("NUM은 %d입니다.\n", NUM);

#else

    printf("NUM은 정의되지 않았습니다.\n");

#endif

}

 

실행 결과

 

NUM은 10입니다.

 

 #ifdef, #ifndef는 헤더파일이 여러개일 때, 같은 헤더파일을 중복으로 포함시키는 경우를 방지하기 위해 많이 쓰입니다. 아래와 같은 헤더파일이 있다고 할때

 

// abc.h

 

typedef struct _people

{

    char name[80];

    int age;

} PEOPLE;

 

 이 파일을 한 소스 파일에 두번 포함시켜보도록 합시다.

 

#include <stdio.h>

#include "abc.h"

#include "abc.h"

 

void main()

{

......

}

 

 컴파일하면 같은 구조체를 재정의 할 수 없다면서 에러를 내뿜게 됩니다. 이것을 해결하기 위해 abc.h를 고쳐보도록 하겠습니다.

 

#ifndef _ABC_H_

#define _ABC_H_ 0

 

typedef struct _people

{

    char name[80];

    int age;

} PEOPLE;

 

#endif

 

 처음 abc.h가 #include문에 의해 소스파일에 포함되면 _ABC_H_가 정의되어 있지 않으므로 그 아래 내용들이 소스파일에 포함됩니다. 그러나, abc.h를 두번째 포함시키면 _ABC_H_를 #define문으로 정의했기 때문에 소스파일에 포함되지 않아, 에러가 나지 않게 됩니다.

 

 디버깅할때만 실행될 코드를 원하는 경우에도 많이 쓰입니다. 비주얼 스튜디오에서는 프로젝트를 만들때 기본적으로 여러가지를 미리 정의해주는데, 이를 확인해 봅시다. 상단 메뉴에서 '프로젝트' → '설정'을 눌러 봅시다.

 

 

 왼쪽의 '구성 속성' → 'C/C++' → '명령줄' 항목을 눌러봅시다. 왼쪽 위 '구성' 부분이 'Debug'로 되어 있어야 합니다. 오른쪽에 보이는 것들이 미리 정의된 항목들입니다. 이 중에서 우리가 이용할 것은 _DEBUG 입니다.

 

#include <stdio.h>

void main()

{

#ifdef _DEBUG

    printf("디버그 모드입니다.\n");

#else

    printf("디버그 모드가 아닙니다.\n");

}

 

 위의 코드를 실행하면 Debug 모드일때는 '디버그 모드입니다.'가 출력됩니다.

 

 위의 툴바에서 Release 모드로도 바꿔서 실행해 봅시다. '디버그 모드가 아닙니다.'가 출력될 것입니다. 참고로 Release 모드일때는 _RELEASE 로 판별할 수 있습니다.

 

 ◆ 특수 전처리문(#pragma)

 #pragma문은 특수한 기능을 하지만, 개발툴마다 포함된 전처리기가 다르기 때문에 동작 또한 다릅니다. 그렇기 때문에 여기서는 대부분의 개발툴에서 공통적으로 많이 사용되는 하나의 용법만 알아보고 넘어가도록 하겠습니다.

 

#pragma once

 

 위 구문은 헤더파일을 중복으로 포함하는 것을 방지 해 줍니다. 이것은 위에서 헤더파일의 중복 포함을 방지하기 위해 #ifndef문을 사용했던 것과 같은 효과를 볼 수 있습니다. 아까의 abc.h를 고쳐보도록 합시다.

 

#pragma once

 

typedef struct _people

{

    char name[80];

    int age;

} PEOPLE;

 

 요즘은 거의 다 지원하지만, 옛날 개발툴 중에는 지원하지 않는 것도 있으므로, 지원되지 않는 경우 #ifndef로 바꿔 쓰도록 합시다.

 

 

 

 

 2. 프로그램에 적용

 

 먼저 'my.h' 파일에 중복 방지 코드를 넣어 봅시다.

 

// 중복 포함 방지

#ifndef _MY_H_

#define _MY_H_

 

// 구조체
typedef struct _student
{
    char name[20]; // 이름
    int scoreKOR; // 국어점수
    int scoreMAT; // 수학점수
    int scoreENG; // 영어점수
    int scoreSCI; // 과학점수
    char *comment; // 평가
} STUDENT;

// 입력
int inputGrade(int index);

// 계산
int calcGrade(int index, int *scoreTotal, float *scoreAverage, char *grade);

// 출력
int outputGrade(int index);

// 메모리 해제
int FreeMemory(int index);

 

#endif

 

 다음으로는 'my.c' 파일을 고쳐 봅시다.

 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "my.h"

 

// 최대 학생 수

#define MAX_STUDENT 20

// 임시 저장 공간 크기

#define TEMPLEN 1024


STUDENT student[MAX_STUDENT];

// 입력
int inputGrade(int index)
{
    char temp[TEMPLEN] = "\0"; // 임시 저장
    int len = 0; // 문자열 길이

 

    // 최대 학생수보다 많으면

    if (index > MAX_STUDENT - 1)

        return -1;

 

    do
    {
        printf("이름을 입력하세요 : ");
        scanf_s("%s", temp, sizeof(temp));
        // 문자열 길이 구하기
        len = strlen(temp) + 1;

 

 학생 구조체 배열의 개수를 식별자로 정의 해뒀고, 최대 학생수보다 많이 입력하려 하면 오류코드로 '-1'을 돌려주고 함수를 끝내도록 수정했습니다. 배열은 0부터 시작하기 때문에, MAX_STUDENT에 '-1'을 해준 것에 유의합시다. 덧붙여 임시 저장 변수의 크기 또한 숫자가 아니라 식별자로 바꿔줬습니다. 끝으로 'main.h'를 고쳐봅시다.

 

......

menuChoice = getchar();

// 메뉴 처리
switch (menuChoice)
{
    case '1': // 학생 성적 입력
    {
        // 사용자 입력
        if (inputGrade(index) == -1)

        {

            // 예외 처리

            printf("입력 인원을 초과했습니다.\n");

            getchar();

            break;

        }
        index++;

    }
    break;

 

......

 

 inputGrade() 함수의 리턴값을 받아 학생수가 제한을 초과할 경우, 안내 메시지를 보여주고 곧바로 빠져나가도록 수정했습니다. 그 다음 getchar()는 이전 시간에 입력 버퍼 오류를 잡기 위해 써준 것과 같은 이유에서 써줬습니다. 다음 시간에는 파일 입출력에 대해 알아보고, 성적 관리 프로그램을 저장하고, 불러올 수 있도록 수정해 보도록 하겠습니다.

 

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/05   »
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