BLOG ARTICLE define | 2 ARTICLE FOUND

  1. 2007.07.08 11. define과 디버깅 (1)
  2. 2007.06.05 3. C 기초문법

11. define과 디버깅

11.1 define

define은 컴파일 직전에 특정 문자를 지정한 문자로 대체해 주는 전처리기 입니다. C의 전처리기에 대해서는 이전 포스트에서 살펴 보았습니다. define은 아래와 같이 선언하고 사용합니다.

#define [dest] [src]
define은 [src]를 소스내에서 [dest]로 사용할 수 있도록 치환해 줍니다. define은 선행처리기로 '=' 이나 ';'를 사용하지 않습니다. define은 프로그래머가 편리하게 작업을 할 수 있도록 해주며, 일반적으로 아래와 같은 용도로 사용합니다.

1. 상수의 용도를 명확하게 하고, 변경을 용이하게 합니다.

게임에서 아래와 같이 for 루프를 돌면서 10명의 적을 처리하는 루틴이 있다고 가정합니다. 소스의 여러곳에 아래와 같이 적의 합인 10이란 숫자가 들어 가게 될 것 입니다.
for(i = 0; i < 10; i++)
{
    /* 처리 */
}

여기서 10은 흔히 매직넘버라고 불리우는 용도가 불분면한 상수로, 숫자로 되어 있기 때문에 소스를 완전히 이해하고 있지 않으면 정확한 의미를 알기 힘듭니다. 또한 적의 숫자에 변경이 있을 경우에는 소스 곳곳의 적의 수 10을 변경된 숫자로 수정하여야 합니다.

만약 적의 수를 define해서 사용하면 의미가 명확해지며, 숫자가 변경되어도 위와 같이 적의 숫자가 사용된 소스의 모든 곳을 변경할 필요 없이 define되어 있는 곳만 변경하면 됩니다.
#define    TOTAL_ENEMY   10

for(i = 0; i < TOTAL_ENEMY; i++)
{
   /* 처리 */
}

가능하면 숫자나 문자열을 직접 사용하는 것 보다는, define, 배열 또는 개발 툴에서 제공하는 방법을 이용하여 상수/변수화 또는 모듈화 시키는 것이 소스를 유지보수하기에 좋습니다.

아래와 같이 소프트웨어 명을 define으로  선언 해 놓으면, 만약 소프트웨어 명이 변경되더라도 소스에서 한 곳만 변경하면 소프트웨어명을 출력하는 모든 곳이 변경됩니다.

#define APP_NAME    "Cocoa"

2. 간단한 매크로를 만듭니다.

define은 ()로 인자를 넘길 수 있기 때문에, 함수와 비슷한 간단한 매크로를 작성하여 편리하게 사용할 수 있습니다. 아래는 매크로로 사용한  몇가지 예입니다.

#define ABS(a)          (((a)>=0)?(a):(-(a)))
절대값을 반환합니다. ()가 많이 쓰인 이유는 a에 연산자가 포함되어 있을 경우에 연산자 우선 순위에 의해 원치 않는 결과를 방지하기 위해서 입니다.

#define NOT_USED(a)     (a = a) 
컴파일러는 일반적인 경고 옵션에서 사용하지 않는 변수에 대해 경고를 내 보냅니다. 만약 잠시 사용을 하지 않을 때, not used 경고를 방지하기 위한 매크로 입니다. 이는
int a;
a = a;
로 경고를 방지 하는 것 보다
int a;
NOT_USED(a);
로 사용하는 것이 의미가 명확합니다.

#define MAX(a,b)        (((a)>(b)) ? (a):(b))
#define MIN(a,b)        (((a)<(b)) ? (a):(b))
a, b를 비교 하여 더 큰 수 또는 작은 수를 반환합니다.

아래와 같이 연산자나 제어문을 변경해서 사용할 수 있습니다. 용도는 명확해 질 수 있지만, C 예약어들이기 때문에 다른 프로그래머가 소스를 볼 경우에 혼돈이 있을 수 있습니다. 필요한 곳에 적절하게 사용하면 소스에 대한 이해를 도와 주고, 변경을 용이  하게 할 수 있습니다.
#define FOREVER     for(;;)
#define AND            &&
#define OR              ||

#define EQUAL(a,b)      ((a)==(b))
#define NEQUAL(a,b)     ((a)!=(b))

#define INC(a)          (a++)
#define DEC(a)          (a--)

MS 윈도우 프로그래밍 환경에서 win.h란 헤더파일을 보시면 define의 적극적인(?) 사용법이 많이 나와있으니, 참조해 보시기 바랍니다.


3. 헤더파일 사용시 중복 오류/경고를 방지합니다.

define은 ifdef, ifndef, else, endif등과 함께 컴파일 시 해당 내용을 포함 또는 미포함되도록  하여, 효율적인 프로그래밍을 할 수 있도록 해 줍니다. 이의 예는 헤더 파일(*.h)에서 가장 흔하고 쉽게 찾아 볼 수 있습니다.

여러 소스파일에서 같은 변수, define등 이 선언된 헤더파일을 중복해서 include할 경우에는  중복선언의 오류가 발생합니다. 이를 방지하기 위해 일반적으로 헤더파일의 처음과 끝을 아래와 같이 처리 합니다.

#ifndef _MY_H    /* _MY_H가 define 되지 않았을 경우에만, endif까지 유효합니다. */
#define _MY_H    /* _MY_H를 define 합니다. */

#define MAX_MAN   10
int g_curman;

#endif /* _MY_H */

위와 같은 소스로 컴파일러는 여러 소스파일에서 헤더파일이 여러번 include 되더라도 _MY_H가 define 되기 전인 첫번째 사용된 include에서만 헤더파일을 포함하고, 이 후는 _MY_H가 define 되어 다시 MAX_MAN이나 g_curman을 선언하지 않기 때문에 오류나 경고를 막을 수 있습니다.


11.2 define을 이용한 디버깅
보통 C 컴파일러 자체에 디버깅 가능 또는 불가능 모드(릴리즈 모드)로  컴파일을 할 수 있는 옵션이 있습니다. 디버깅 모드는 디버그에 편리한 코드와 데이터들이 같이 컴파일 되어 실행파일에 포함되기 때문에, 릴리즈 모드로 컴파일 된 실행파일 보다 일반적으로 크기가 크고 실행속도가 느립니다.

그렇기 때문에 개발시에는 편리를 위해 디버깅 모드로 컴파일을 하며 코딩을 하고, 배포시에는 릴리즈 모드로 배포를 합니다. 이런 기능과 디버거등의 툴 들과는 별도로 사용자가 아래와 같은 디버깅을 위한 코드를 사용하면 쉽게 실행 상태나 오류를 찾아낼 수 있습니다. 

#include <stdio.h>
#include <stdarg.h>

/* _DEBUG가 필요 없을 경우에는 주석 처리 합니다. */
#define _DEBUG

/* _DEBUG가 define 되어 있을 경우, ASSERT와 TRACE를 관련 함수로 define하고, 안되어 있을 경우에는 무효화 합니다.
*/
#ifdef _DEBUG
#   include <assert.h>
#   define ASSERT(e)    assert(e)
#   define TRACE        Trace
#else
#   define ASSERT(e)
    inline void NO_TRACE(char* p, ...)   {}
#   define TRACE        NO_TRACE
#endif // _DEBUG

void Trace(char* format, ...)
{
    static unsigned int s_cur = 0;

    va_list list;

    va_start(list, format);

    printf("@ %04d> ", s_cur);
    vprintf(format, list);
    printf("\n");
    va_end(list);

    fflush(stdout);
    s_cur++;


int main()
{
    int i; 
    for(i = 0; i < 10; i++)
    {
        TRACE("%d = %d", i, i);
        ASSERT(i < 10);

        /* 필요한 작업 */
    }
}

#   define ASSERT(e)    assert(e)
assert는 인자의 값이 참(1)일 경우에는 동작 없이 그냥 수행 되지만, 거짓(0)일 경우에는 메시지를 출력하고 프로그램을 종료합니다.

ASSERT(i < 10);
위는 i 값이 10보다 같거나 클경우 종료됩니다. 위의 소스에서 10을 다른 작은 수로 변경하면,  ASSERT내의 값이 거짓일 경우에 종료됩니다. assert는 중요한 변수 반드시 어떤 값이나 범위를 유지해야 할 경우 사용하면, 예기치 않는 변수의 값의 변동 시에 나타나는 오류를 미리 알려 주고 방지할 수 있습니다.

void Trace(char* format, ...)
대부분 C 개발툴들이 같이 사용할 수 있는 디버거 툴을 제공하고, 이를 사용하여 변수, 메모리의 값들과 상태를 확인할 수 있습니다.

이와는 별도로 프로그램 실행시에 처리결과를 실시간으로 빠르게 보고 확인해야 할 경우가 있습니다. 이를 위해 Trace란 함수를 환경과 취향에 맞게 사용하면 편리합니다. GUI 개발툴에서는 편리한 Trace 툴을 제공하는 것도 있으니, 사용하시는 개발툴을 확인해 보시기 바랍니다.

Trace 함수의 출력 부분을 파일로 변경하면, 어플리케이션의 LOG로도 사용이 가능합니다. 이는 특히 서버 프로그램의 현재 상태를 검사하거나, 지난 오류를 찾는데 유용합니다.
이상으로 C언어 기초 배우기를 마무리 할려고 합니다. 갑작스레 시작하여 두서도 없고, 내용이 많이 부실했던 것 같습니다. 오류와 오타가 있는 부분은 지적과 조언 부탁 드리겠습니다. 나중에 시간이 나면 정리도 하고 C언어 활용하기로 못다한 많은 얘기들을 해볼려고 합니다.

그동안 외도(?)를 많이 하였는데, 앞으로 당분간은 이 블로그의 본연의 목적인 cocoa 프로그래밍에 주력해 볼려고 합니다.

'프로그래밍 강좌 > C 언어 기초' 카테고리의 다른 글

11. define과 디버깅  (1) 2007.07.08
10. struct, union, enum, typedef  (2) 2007.06.21
9. 배열 (array)  (0) 2007.06.17
8. 포인터 (pointer)  (4) 2007.06.16
7. C 함수 (function)  (4) 2007.06.15
6. 제어문  (0) 2007.06.14

아래는 C를 처음 배울 때 자주 볼수 있는 기본 소스입니다. 아래의 소스로 C의 전처리기, 함수, 주석, 문법 등 C에 대한 기본적인 사항들을 알아보겠습니다.

#include <stdio.h>

int main(int argc, char* argv[])
{
    printf("Hello! World \n");
    return 0;
}


3.1 전처리기

가장 먼저 #include <stdio.h>가 보입니다. 여기서 include 앞의 #는 C에서 전처리기를 의미합니다. stdio는 standard input output의 약자로 C의 표준 입출력 함수들이 선언되어 있는 헤더 파일입니다.

1) 컴파일러의 처리

전처리란 컴파일러가 작업을 하기 편리하도록 컴파일 전에 미리 처리되는 작업을 의미합니다. 이전에 본바와 같이 컴파일 작업이은 개발자가 작성한 소스를 읽어 가며(이 작업을 파싱이라고 합니다), 기계가 읽을 수록 번역하는 작업을 의미합니다.

이 작업을 쉽게 하기위해 컴파일러가 미리 처리 해야 할 작업들이 전처리 작업이며, 이를 구별하기 위해 "#" 를 앞에 둡니다.

C에는 #if/#endif, #line, #pragma 등 많은 전처리기가 있습니다. 나머지는 나중에 따로 자세히 알아보고, 여기서는 가장 많이 쓰이는 #include, #define만 알아보겠습니다.

#define MY_A    3
int a = MY_A;

위와 같은 a.h파일이 있다고 가정합니다.

#include "a.h"

void printA()
{
    printf("a=%d", a);
}

위와 같이 a.c 파일이 있으면 컴파일러는 컴파일전에 include란 전처리 명령을 보고, 아래와 같이 만든 후에 컴파일을 시작합니다.

int a= 3;

void printA()
{
    printf("a=%d", a);
}

컴파일러는 #include <stdio.h>를 확인하고, a.h파일을 읽어 위와 같이 만듭니다. 그 다음  또 하나의 전처리기 #define을 처리합니다. MY_A로 되어 있는 모든 곳을 3으로 변경합니다.


2) include

소스파일에서 공통으로 참고해야 될 부분을 따로 헤더 파일로 만듭니다. 보통 여러 소스파일에서 참조 될 define, 데이터 타입, 변수, 함수 프로토 타입 등을 정의해 놓습니다.

컴파일러는 소스에서 함수호출이나 변수 사용시, 반드시 그 함수의 프로토 타입이나 변수의 형(type)을 알고 있어야 합니다. 그래서 각 소스 파일(*.c, *.h)들은 사용하는 함수나 변수가 정의되어 있는 헤더 파일(*.h)을 include하여 사용합니다.

include는 <>와 ()로 사용될 수 있습니다. 일반적으로 C 표준 라이브러리 헤더 파일엔 "<>", 사용자 정의 헤더 파일에는 "()"를 사용합니다. 예를 들면 아래와 같습니다.

#include <stdio.h>
#include "my.h"

define은 아래와 같이 A를  "A"로 컴파일 시, 대치하란 의미입니다.
#define A      "A"

define에 많은 설명이 필요하므로, 다음에 자세히 사용법에 대해서 알아보겠습니다.


3.2 함수
C 소스를 보면 대부분이 함수로 이루어져 있을 만큼, 함수는 C뿐만 아니라 많은 언어에서 중요한 부분입니다. 함수는 같은 혹은 다른 소스 파일 뿐만 아니라, 경우에 따라서는 다른 실행파일에서도 호출될 수 있습니다.

C에서 함수는 아래와 같은 구조를 가지고 있습니다.

[반환값 타입] [함수명] ([인자], ...)
{
    [몸통]
}

이제 함수의 각 구성요소에 대해서 자세히 알아보겠습니다.

1) 반환값 타입

반환값 타입은 함수가 종료될 때, 함수를 호출한 곳으로 돌려주는 값의 형(type)을 의미합니다. 반환값은 return 이란 예약어로 반환값 타입과 같은 형의 데이터를 반환합니다.

어떤 함수가 값을 반환 할 필요가 없을 때는 void로 선언합니다.
void my_func();

만약 반환값이 생략되어 my_func()와 같이 함수 이름이 먼저 나오면, 컴파일러는 int형을 반환하는 것으로 간주합니다.

위의 main은 int형을 반환합니다. main은 일반적으로 아무 오류없이 끝났다는 0을 반환합니다.


2) 함수명

위의 소스에서 #include 아래에 main 함수가 있습니다. main은 C에서 미리 정의된 함수로 프로그램의 시작 지점이라고 할 수 있습니다.

함수명은 함수가 행동하는 동작에 맞고, 누구나 쉽게 유추해 볼 수 있도록 작성되어야 합니다. 함수나 변수의 이름을 붙이는 공통된 방법을 명명법이라고 하는데, 이는 프로그램 작성 시 중요합니다.


3) 인자

함수 이름 다음에는 괄호안의 목록들을 함수의 인자라고 부릅니다. 이 인자는 함수를 호출 하는 곳에서 보내 주는 데이터입니다. 인자는  앞에 타입이 오고 바로 뒤에 사용 할 인자명이 옵니다. 인자가 여러개일 경우에는 ","로 구별합니다.

값을 받을 필요가 없을 시에는 void my_func()와 같이 괄호 내를 빈 상태로 둡니다. 

int main(int argc, char* argv[])

위의 main을 보면 인자는 int형의 argc와 char 배열의 포인터 형 인자인 argv를 받습니다. 위에서도 언급한 바와 같이 main은 사용자가 호출하는 것이 아니라, 프로그램 시작시 자동으로 호출됩니다.

우선 첫번째 int형 변수 argc는 뒤에 나오는 argv의 갯수를 넘겨 줍니다. 두번째 나오는 argv는 프로그램 실행 시 넘겨지는 인자에 대한 정보를 가지고 있습니다. OS에 따라서 기본적으로 넘어오는 갯수가 1~2개로 조금씩 다를 수 있습니다.

일반적으로 argv의 첫번째에는 실행파일명이 들어 있습니다. 그 뒤로는 실행파일 실행 시 사용자가 넘겨주는 인자들이 들어 있습니다.

예를 들어 아래와 같은 코드가 있습니다. (아래 소스의 내용은 이해를 못하셔도 됩니다.)

#include <stdio.h>

int main(int argc, char* argv[])
{
    int i;

    for(i = 0; i < argc; i++)
    {
        printf("ARGV[%d] %s\n", i, argv[i]);
    }

    return 0;
}

이 함수를 컴파일 하고, 실행파일 명이 a.out이라고 가정합니다. 프롬프트 상태에서 실행시키면 아래와 같이 출력합니다.
./a.out
ARGV[0] ./a.out
기본적으로 argc는 1이고 argv 첫번째에는 실행파일 명이 넘어 왔습니다.

이번에는 아래와 같이 a.out을 실행시킵니다.
./a.out abc def ghi
ARGV[0] ./a.out
ARGV[1] abc
ARGV[2] def
ARGV[3] ghi
실행파일명 다음에는 사용자가 입력한 인자들이 넘어 옵니다.

이와같이 main 함수의 첫번째, 두번째 인자(argc, argv)로 사용자가 실행 시 넘기는 인자를 알수가 있고, 인자를 받아 특별히 처리해야 할 경우에 사용하면 됩니다.

참고로 아래와 같이 함수의 인자를 선언할 수 있지만, 이와 같은 함수 표기법은 사용하지 않는 것이 좋습니다.
int main(argc, argv)
int argc;
char* argv[];
{
    printf("Hello!, World\n");
    return 0;
}


4) 몸통(body)

함수 명 뒤에 "{" 로 시작해서  "}"로 끝나는 부분을 함수의 몸통이라고 합니다. 여기서 함수가 수행해야 될 내용들을 작성하면 됩니다.

함수 내부는 "들여쓰기"라고 불리는 방법으로 의미와 범위에 맞게 Tab 키를 사용하여 단락들을 들여 써주면 가독성이 더욱 좋습니다.

아래는 두 개의 정수를 받아 앞에 수가 크면 1, 뒤에 수가 크면 2, 같으면 0을 반환하는 함수입니다. 이해는 할 필요가 없고 전체적인 모양만 보시면 됩니다. 왼쪽은 Tab을 이용하여 들여쓰기를 한 경우고 아래는 하지 않은 경우입니다. 내용을 모르더라도 왼쪽의 경우가 눈에 더 쉽게 들어 옵니다.
사용자 삽입 이미지사용자 삽입 이미지

위를 보면 마지막에 세미콜론(;)의 모습이 자주 보입니다. C 변수나 함수 선언이 끝나거나, 단락이 끝나면  ";"를 사용합니다. 흔히 세미콜론으로 막는다. 라는 표현을 씁니다.

C 컴파일러는 코드상의 공백, 탭, 개행문자(엔터)를 무시합니다. 아래의 네 가지 코드 모두 컴파일러는 동일하게 봅니다. 하지만 사람이 볼 때 쉽게 볼 수 있도록 코딩을 해야 하며, 그런 의미에서 3번과 4번이 무난해 보입니다.

for(i = 0;i<3; ++){printf("%d", i);}

for(i = 0; i < 3; i++) { printf("%d", i); }

for(i = 0; i < 3; i++) {
    printf("%d", i);
}


for(i = 0; i < 3; i++)
{
   printf("%d", i);
}


3.3 주석

주석은 프로그램시 참고해야 될 내용이나 중요한 사항이 있을 경우 개발자가 기록하는 내용으로, 컴파일 및 프로그램 수행에는 전혀 영향을 미치지 않습니다.

C에서 주석을 사용할 경우에는 "/*" 와 "*/"의 사이에 위치 하며, 아래와 같이 사용합니다.
/* 주석입니다. */

아래와 같이 주석내에 주석은 사용할 수 없습니다.
/*
/*
이 부분은 오류가 납니다.
*/
*/

1) 주석의 사용

사용자 삽입 이미지

주석은 위와 같이 자유롭게 사용할 수 있습니다. 소스파일, 함수, 변수에 대한 설명, 주의 사항, 파일 수정 히스토리, 저작권 등 필요에 따라 소스에 추가 할 수 있습니다.

주석을 사용하는 이유는 먼 훗날 다시 소스를 봐야할 경우나, 본인 이외에 다른 개발자가 소스를 손대야 할 경우에 소스 이해에 많은 도움을 줄 수 있습니다.

주석은 타이핑을 해야하는 약간의 노력이 필요하지만, 소스를 볼 때의 편리성에 비하면 작은 노력으로 큰  것을 얻습니다. 필요한 곳에 항상 주석을 붙이는 습관을 들이시기 바랍니다.


2) 코드에 사용

주석은 설명 외에 코드 사이에 주석 처리를 하여 테스트 및 디버깅 시에도 사용할 수 있습니다. 아래는 테스트시 필요에 의해 무조건 1을 반환해야 하는 상황이라 가정하고, 원 코드를 주석처리 해 놓았습니다. 테스트가 끝나면 주석을 제거하고 밑에 라인을 삭제합니다.

int plusNumber(int a, int b)
{
    / *return (a + b); */
    return 1;
}



3) 단일 라인 주석

C++에는 한 줄 주석을 달 수있는 "//"가 추가 되었습니다. 하지만 그 사용의 편리함으로 인해 대부분의 C 컴파일러가 "//" 주석을 지원합니다. 순수 C코드라면 호환성을 위해서 사용하지 않는 것이 좋으나, 필요하거나 편하면 사용해도 무방할 것 같습니다.

"//" 주석은 이름 그대로 "//" 뒤부터 한 라인만 주석처리 됩니다. 간단한 주석이 필요할 때 사용하시면 됩니다.

// a와 b를 더한 값을 반환
int plusNumber(int a, int b)
{
    / *return (a + b); */
    return 1;  // 임시로 테스트
}

'프로그래밍 강좌 > C 언어 기초' 카테고리의 다른 글

6. 제어문  (0) 2007.06.14
5. 연산자  (0) 2007.06.13
4. 변수  (2) 2007.06.12
3. C 기초문법  (0) 2007.06.05
2. 소스코드, 컴파일, 링크  (6) 2007.06.04
1. C언어 공부를 위한 준비  (9) 2007.06.03