BLOG ARTICLE C언어 | 11 ARTICLE FOUND

  1. 2007.07.08 11. define과 디버깅 1
  2. 2007.06.21 10. struct, union, enum, typedef 2
  3. 2007.06.17 9. 배열 (array)
  4. 2007.06.16 8. 포인터 (pointer) 4
  5. 2007.06.15 7. C 함수 (function) 4
  6. 2007.06.14 6. 제어문
  7. 2007.06.13 5. 연산자
  8. 2007.06.12 4. 변수 2
  9. 2007.06.05 3. C 기초문법
  10. 2007.06.04 2. 소스코드, 컴파일, 링크 6
  11. 2007.06.03 1. C언어 공부를 위한 준비 9

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 언어 기초' 카테고리의 다른 글

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
AND

10. struct, union, enum, typedef

struct, union, enum, typedef 등은 새로운 자료형을 만들고, 문자화 된 상수 리스트들을 만들고, 변수 타입명을 대치하는 등, C에서 자료를 다루고 접근하는데 있어서 많은 편리한 기능들을 제공합니다. 이번 장에서는 이에 대해서 간단하게 알아 보겠습니다.

10.1 struct (구조체)
C에는 구조체(structure)라고 불리우는 사용자가 정의할 수 있는 자료형이 있습니다. 구조체는 하나 이상의 변수를 포함할 수 있는 집합체라고 보시면 됩니다.

1) 구조체의 필요성

어떤 프로그램이 직원들의 사원번호, 이름, 나이, 성별들을 관리한다고 가정합니다. 아래와 같이 직원 개인의 정보에 관한 변수들이 있습니다.

int employee_id;                    /* 사원번호 */
char employee_name[12];    /* 이름 */
int employee_age;                  /* 나이 */
int employee_sex;                  /* 성별 */

직원에 관련된 부분을 처리할 때, 대부분 이 변수들은 같이 사용될 것 입니다. 아래와 같이 직원정보를 저장하는 함수는 관련된 모든 변수들을 인자로 넘겨야 합니다.

int set_employee(int id, char* name, int age, int set);

또한 정보를 얻을 때도 함수는 반환값이 하나밖에 없으니, 아래와 같이 함수를 만들어 주어야 합니다.

int get_emploeyee_age(int id);
char* get_emploeyee_name(int id);
int get_emploeyee_sex(int id);

같이 사용되는 변수들이기 때문에, 하나로 그룹화를 해 두면 사용이 편할 것 같습니다.


2) 구조체의 선언

이제 구조체에 대해서 알아 보겠습니다. 구조체는 아래와 같이 선언합니다.

struct [구조체 명]
{
    [변수]
    [변수]
       .
       .
       .
};

[구조체명] 역시 변수명이나 함수명 사용 규칙과 동일하며, [변수]는 C에서 사용 가능한 모든 변수들이 올 수 있습니다.

1) 장에서 사용한 employee_ 로 시작하는 직원 변수들을 하나의 직원 구조체로 묶어 보겠습니다. 아래와 같이 구조체를 선언합니다.

struct employee
{
    int id;   
    char name[12];
    int age;
    int sex;
};

구조체의 멤버변수들은 employee라는 구조체 안에 있으므로, 직원에 관련된 변수임을 알리는 employee_를 생략하였습니다. 이제 위의 employee 정보를 저장하고 불러오는 함수들을 구조체를 사용하여 아래와 같이 단일 인자나 반환값으로 사용할 수 있습니다.

int set_employee(struct employee emp);
struct employee get_employee(int id);

구조체는 이와같이 변수의 사용을 명확히 하고, 함수로 전달이나 반환을 용이하게 해줍니다.


3) 구조체의 사용

이제 구조체를 사용하는 간단한 소스를 보겠습니다.

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

struct employee
{
    int id;
    char name[12];
    int age;
    int sex;
};

void print_employee(struct employee emp);

int main(int argc, char* argv[])
{
    struct employee emp1;
    struct employee emp2;

    emp1.id = 1;
    strcpy(emp1.name, "def");
    emp1.age = 29;
    emp1.sex = 1;

    emp2.id = 2;
    strcpy(emp2.name, "abc");
    emp2.age = 34;
    emp2.sex = 2;

    print_employee(emp1);
    print_employee(emp2);
}

void print_employee(struct employee emp)
{
    char c;
    
    if(emp.sex == 1)
        c = 'M';
    else    
        c = 'W';
        
    printf("%d: name %s, age %d, sex %c\n", emp.id, emp.name, emp.age, emp.sex);
}

struct employee emp1;
구조체는 다음과 같이 사용합니다.

struct [구조체 이름] [변수명];

emp1.id = 1;
구조체의 사용은 아래와 같이 .을 구분자로 하여 사용합니다. (-> 도 있지만, 포인터 설명시 하겠습니다.)

[구조체 명].[구조체 멤버 변수 명]

print_employee(struct employee emp);
위에서 예를 든 것과 같이 emp하나만 인자로 넘겨주면, 직원의 id, age, sex를 프린트 할 수 있습니다.

여기서는 포인터의 사용으로 구조체의 이해를 어렵게 하지 않기위해 포인터를 사용하지 않았지만, 일반적으로 구조체를 함수의 인자로 사용할 때는 포인터를 사용합니다. 포인터로 주소만 넘기면 스텍 메모리를 4바이트를 사용하지만, 구조체를 사용하면 구조체 크기만큼 스텍을 사용하기 때문입니다.

구조체의 크기가 적을 경우에는 문제가 되지 않지만, 클 경우에는 문제의 소지가 있고 값을 변경할 경우에 문제가 있을 수 있습니다. (이는 다시 설명하겠습니다.)

그러므로 구조체를 함수의 인자로 사용할 때에는 포인터를 사용하는 것이 좋습니다. 아래는 포인터로 변경한 소스입니다.

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

struct employee
{
    int id;
    char name[12];
    int age;
    int sex;
};

void print_employee(struct employee* emp);

int main(int argc, char* argv[])
{
    struct employee emp1;
    struct employee emp2;

    emp1.id = 1;
    strcpy(emp1.name, "abc");
    emp1.age = 29;
    emp1.sex = 1;

    emp2.id = 2;
    strcpy(emp2.name, "def");
    emp2.age = 34;
    emp2.sex = 2;

    print_employee(&emp1);
    print_employee(&emp2);
}

void print_employee(struct employee* emp)
{  
    char c;

    if(emp->sex == 1)
        c = 'M';
    else   
        c = 'W';
       
    printf("%d: name %s, age %d, sex %c\n", emp->id, emp->name, emp->age, emp->sex);
}

void print_employee(struct employee* emp);
함수 선언 시 emp 앞에 *)를 넣어 포인터로 넘기도록 변경합니다.

print_employee(&emp1);
print_employee(&emp2);
구조체 대신에 구조체의 포인터를 넘기도록, 주소연산자(&)를 사용합니다.
 
emp->id, emp->name, emp->age, emp->sex
구조체가 포인터일 경우에는 멤버변수를 가르킬 때, "." 대신, "->"를 사용하여야 합니다.


10.2 union (공용체)
공용체는 struct를 union으로 선언한다는 것 이외에는 구조체와 똑 같이 선언 합니다.

union [공용체 명]
{
    [변수]
    [변수]
       .
       .
       .
};

한 가지 다른 점은 멤버 변수들간에 메모리 영역을 공유한다는 것입니다. 그러므로 공용체의 크기는 멤버 변수 중 가장 큰 변수의 크기를 갖습니다. 아래의 소스로 같은 메모리 영역을 공유 한다는 의미에 대해서 알아 보겠습니다.

#include <stdio.h>

int main()
{
    union my_int 
    {
        unsigned int  i;
        unsigned char c[4];
    };

    int i; 
    union my_int mi;

    mi.i = 100;

    for(i = 0; i < 4; i++)
    {
        printf("%d\n", mi.c[i]);
    }

    printf("%x, %x, %x\n", &mi, mi.c, &(mi.i));

    return 0;
}

1) 메모리 공유의 의미

union my_int 
{
    unsigned int  i;    
    unsigned char c[4];
};
위와 같은 선언은 메모리에서 다음과 같이 자리 잡습니다.
사용자 삽입 이미지
mi는 위와같이 메모리 어디인가에 4바이트의 영역을 차지 하고 있습니다. i와 c의 메모리 시작 주소는 위와 같이 동일합니다.

이는 하단의 다음 코드에서
printf("%x, %x, %x\n", &mi, mi.c, &(mi.i));
세 변수의 메모리 주소를 출력해 보면,
> bffffc7c, bffffc7c, bffffc7c
위와 같이 동일하게 출력되는 것을 확인할 수 있습니다.

위에 mi가 공용체가 아닌 구조체로 선언되었다면, 아래와 같이 메모리에 위치할 것입니다.
사용자 삽입 이미지
보시는 바와 같이 mi.i와 mi.c가 각각의 메모리 블럭에 자리 잡고 있습니다.
 

2) 빅 엔디언, 리틀 엔디언

mi.i = 100;
for(i = 0; i < 4; i++)
{
    printf("%d\n", mi.c[i]);
}

mi.i에 100을 대입하면, 같이 메모리를 사용하는 mi.c 배열의 값에도 변경이 있습니다. 위는 제 맥에서 아래와 같이 출력 됩니다.
> 0
> 0
> 0
> 100
보시는 바와 같이 c[3]에 100이 들어 가 있습니다. 제 테스트 환경은 모토롤라 CPU를 사용하는 PPC 아이맥이기 때문에 c[3]에 먼저 값이 들어 가 있습니다. 인텔 CPU를 사용하는 환경에서는 c[0]에 100이 들어 갑니다.

이 차이는 cpu에 따라 바이트를 저장하는 순서가 다르기 때문입니다. 이 순서의 차이는 빅 엔디안과 리틀 엔디안으로 불리어지며, 빅 엔디안은 큰 쪽이 먼저 저장되고 리틀 엔디안은 반대로 작은 쪽이 먼저 저장됩니다.

4바이트를 가진 mi.i는 16진수로 표현하면, 0x00000064입니다. 0x는 16진수라는 것을 의미하며 2자리 당 1바이트 입니다. 빅 엔디언은 이 값이 [00][00][00][64]와 같이 그대로 메모리에 들어 가기 때문에 mi.c[3]에서 100(16진수로는 0x64)이 출력됩니다.

하지만 리틀 엔디언은 값이 위와 반대로 [64][00][00][00]와 같이 저장되기 때문에 mi.c[0]에서 100이 출력 됩니다.

전통적으로 모토롤라 계열의 CPU들은 빅 엔디언, 인텔 계열의 CPU들은 리틀 엔디언을 사용합니다. 넣고 빼는 방법은 동일하기 때문에 값에는 변화가 없습니다. 그렇기 때문에 빅 엔디언/리틀 엔디언에 관해서 신경 쓸 필요는 거의 없습니다.

하지만 위와 같이 union의 사용과 같이 바이트 열의 제어 시에는 유의해야 합니다.


10.3 enum (열거형 상수)

enum은 열거형 상수입니다. 비슷한 속성을 가진 값들이나 내용을 일련된 정수(int)로된 상수로 나타내어 줍니다. enum은 다음과 같이 선언합니다.

enum [열거형 상수명] { [상수명] = [값], ... } [열거형 변수];

[열거형 상수명]은 구조체나 유니온 처럼 변수 선언시 필요한 이름이며 생략할 수 있습니다.
[상수명]은 정수대신 사용할 상수의 이름 입니다. 이 값은 = [값]으로 초기화 할 수 있으며, 값을 생략하면 첫번째 상수부터 0, 1, 2... 차례대로 설정됩니다.
[열거형 변수]
는 enum 값을 사용할 변수 이며, 선언시에는 생략 가능합니다. 열거형 상수 값을 가지는 변수들은 정수형입니다.

아래의 소스를 보시면 쉽게 열거형 변수에 대해서 이해 할 수 있습니다.

#include <stdio.h>

int main()
{
    enum Usergrade { MASTER=0, LEVEL1, LEVEL2, LEVEL3 } u3;
   
    enum Usergrade u1 = MASTER;
    int u2 = LEVEL1;

    u3 = LEVEL2;

    printf("%d, %d, %d\n", u1, u2, u3);
    printf("%d, %d, %d, %d\n", MASTER, LEVEL1, LEVEL2, LEVEL3);

    return 0;
}

enum Usergrade { MASTER=0, LEVEL1, LEVEL2, LEVEL3 } u3;
Usergrade라는 enum 상수들을 선언 합니다. MASTER=0이며 기본 시작값이 0이기 때문에, =0을 생략하여도 의미는 같습니다. MASTER 뒤로 LEVEL1, LEVEL2, LEVEL3은 차례대로 1, 2, 3의 값이 들어 갑니다. 만약 MASTER=10으로 시작하면 그뒤로 차례대로 11, 12, 13의 값이 들어 갑니다.

마지막에 이 상수를 사용하기 위해 u3을 선언되어 있습니다. 변수는 꼭 enum 선언시 선언될 필요는 없습니다.

enum Usergrade u1 = MASTER;
int u2 = LEVEL1;
u3 = LEVEL2;
enum 변수를 선언(구조체와 선언이 유사합니다.)하고, 정수형에 대입하고, 위에서 enum 선언시 , 선언된 변수에 enum 상수값들을 넣어 봅니다.

enum을 사용하는 이유는 소스 작성시 의미를 명확하게 하고, 상수들을 일괄되고 안정되게 사용하기 위해서 입니다.

예를 들어 아래와 같은 소스에서 전자 보다는 후자가 더욱 명확하고 이해하기 쉽습니다.
if(user_grade == 0)
    printf("You are Master");
---------------------------------------------------------------
if(user_grade == MASTER)
   printf("You are Master");


10.4 typedef

typdef는 변수형을 사용자가 지정한 이름으로 사용할 수 있게 해줍니다. typedef는 적절하게 사용하여야 하며, 의미에 맞게 이름을 지어 주어야 합니다. 그렇지 않고 지나치게 많이 변수형들을 대치하거나, 작성자만 알수 있는 이름을 사용하면 제 3자가 소스를 볼 경우에 이해를 어렵게 할 수 있습니다.

1) typedef 선언

typedef  [기존 변수 형] [새로운 변수 형];

[기존 변수 형]은 int, struct [구조체명], enum [enum 명] 등과 같이 C 변수형 또는 사용자가 지정한 변수 형이 올 수 있습니다.

[새로운 변수형]은 위의 기존 변수형을 대신해서 사용할 사용자 정의 변수타입 이름입니다.

예를 들면 unsigned int는
typedef unsigned int uint;
위와 같이 선언해 놓으면 uint a;와 같이 편한게 unsigned int형의 변수를 선언할 수 있습니다.

2) 구조체와의 사용

typdef는 구조체에서 특히 편리하게 사용할 수 있습니다. 위에서 살펴본 employee 구조체를 다시 한번 보겠습니다.

struct employee
{
    int id;
    char name[12];
    int age;
    int sex;
};

typedef struct employee emp;

struct employee e1;
emp e2;

위에 e1과 e2는 똑같이 employee 구조체 변수를 선언하고 있습니다. e2는 typedef된 구조체를 사용하므로 e1보다 간단하게 선언할 수 있습니다. 구조체에서 typedef는 아래와 같이 선언할 때 같이 사용하면 더욱 편리합니다.

typedef struct
{
    int id;
    char name[12];
    int age;
    int sex;
} employee;

employee e1;

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

11. define과 디버깅  (1) 2007.07.08
9. 배열 (array)  (0) 2007.06.17
8. 포인터 (pointer)  (4) 2007.06.16
7. C 함수 (function)  (4) 2007.06.15
6. 제어문  (0) 2007.06.14
AND

9. 배열 (array)

이번 장에서는 배열에 대해서 알아 보겠습니다. 배열과 포인터는 비슷하게 사용하지만, 메모리상에서의 의미는 많이 다릅니다. 중간 중간의 메모리에 관련된 이미지들을 설명과 함께 유심히 보시면, 이해가 쉬울 것입니다.

9.1 선언과 초기화

1) 배열의 선언

배열은 동일한 타입의 변수들의 집합체이며, 아래와 같이 선언합니다.

[변수 타입] 변수명[[배열크기]];

[변수 타입]과 변수명은 이전에 본 변수 선언과 같습니다. 배열은 변수명에 []를 붙입니다.
[배열크기]는 배열의 원소 갯수를 나타냅니다.

#include <stdio.h>

int main()
{
    int i; 
    char num[10];

    for(i = 0; i < 10; i++)
    {
        num[i] = i;

        printf(">> num[%d] = %d =>", i, num[i]);
        printf("*(num+i) = %d\n", i, *(num + i));
    }      
   
    return 0;
}

char num[10];
위는 char형 데이터를 10개 담을 수 있는 배열을 선언 한 것 입니다. 위와 같이 10개의 배열을 만들면 0 부터 9까지 사용할 수 있습니다. nums[10]은 사용할 수 없습니다. 0 부터 시작하기 때문에 주어진 크기 - 1까지 사용할 수 있습니다.

사용자 삽입 이미지
메모리를 보면 좌측과 같이 char 형이기 때문에 1바이트를 차지하고, 연속되게 자리하고 있습니다.

배열 num은 메모리상에서 시작되는 주소를 가지고 있습니다. 주소라는 의미에서 배열 num은 char* num과 의미가 같습니다.

그렇기 때문에 사용 시에도 포인터와 똑 같이 사용할 수 있습니다.  이는 포인터도 마찬가지 입니다.









2) 배열의 초기화

배열은 일반 변수와 마차가지로 선언시 값을 설정할 수 있습니다.

int a[5] = { 0, 2, 4, 6, 8 };

위를 보면 0, 2, 4, 6, 8로 배열원소가 다섯개인 것을 알수 있습니다. 그렇기 때문에 이와 같이 초기화 하면서 배열을 선언할 때에는 크기를 생략할 수 있습니다. 그렇기 때문에 아래와 같은 표현도 가능합니다.

int a[] = { 0, 2, 4, 6, 8 };

또한 아래와 같이 선언한 크기보다 적은 값으로 사용할 수도 있습니다.  이 경우에는 앞의 a[0] 부터 차례로 값이 들어 가며, a[5] 부터 마지막 배열인 a[9]까지는 0으로 설정됩니다.

int a[10] = { 0, 2, 4, 6, 8 };

9.2 문자열
C에서의 문자열의 NULL ('\0')로 끝나는 char형 배열입니다. 문자열은 char형의 배열과 포인터를 사용하여 아래와 같이 선언될 수 있습니다.

#include <stdio.h>

int main()
{
    char* p = "hello";
    char a1[6] = "hello";
    char a2[6] = {'h', 'e', 'l', 'l', 'o', '\0' };

    printf("%s\n", p);
    printf("%s\n", a1);
    printf("%s\n", a2);

    return 0;
}

위의 p와 a1의 경우에는 "hello" 뒤에 자동으로 '\0'을 붙여 줍니다. 그러므로 "hello" 문자열이 메모리에서 차지하는 크기는 6입니다.

사용자 삽입 이미지
문자열을 포인터와 배열로 선언할 때 아래와 같은 차이점이 있습니다. 사용시에는 거의 차이점이 없습니다.

char* p = "hello";
"hello"를 스택 메모리에 할당하고 이 주소(1051)를 p에 반환합니다.

char a1[6] = "hello";
a1에 6바이트를 할당하고 문자열 "hello"를 저장합니다.

좌측의 이미지를 보면 위의 의미를 이해하실 수 있습니다. 좌측이 배열의 경우이고 우측이 포인터의 경우 입니다.




char a2[6] = {'h', 'e', 'l', 'l', 'o', '\0' };
1바이트씩 수동으로 설정하는 a2는 사용자가 위와 같이 문자열 마지막에 '\0'으로 막아 주어야 합니다. 막아 주지 않을 경우 printf는 a2 다음 메모리 영역에서 '\0'이 올 때 까지 출력합니다. C에서는 아래와 같이 문자열을 출력한다고 생각하시면 됩니다.

while(1)
{
    if(*p == '\0')
       break;

    printf("%c", *p);
    p++;
}

위와 같이 '\0'을 만날 때 까지 메모리를 1증가해 가면서 계속 한문자씩 출력합니다. 이는 언제 '\0'이 나올지 모르고, p의 크기를 넘어가는 경우에는 정당한 메모리 영역이 아닌 부분에 접근하기 때문에 심각한 오류를 불러 올 수 있습니다.

그렇기 때문에 C에서 문자열을 사용할 때는 마지막에 '\0'으로 막는 다는 것을 가장 신경써야 하는 부분입니다.


9.3 다차원 배열
위에서 본 배열은 1차원 배열입니다. 다음과 같이 다차원 배열을 선언할 수 있습니다. 일반적으로 4차원 이상의 배열은 사용하는 경우가 거의 없습니다.

int n1[4][4];          // 2차원 배열
int n2[4][4][4];      // 3차원 배열

2차원 배열의 설명을 위해 2D 게임에서 맵을 놓고 생각해 보겠습니다. 5X5 크기로 맵을 만든다고 가정합니다. 0으로 되어 있는 부분은 캐릭터가 갈 수 없고, 1로 된 부분이 길로 캐릭터가 지나갈 수 있습니다.

사용자 삽입 이미지
int map[5][5] =
{
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 1, 0 },
    { 0, 0, 0, 1, 0 },
    { 0, 0, 0, 1, 0 }
};

2차원 배열을 위에 그림으로 보면, 흰색으로 된 부분이 갈 수 있는 길 입니다. 만약 캐릭터가 갈 수 있는 길인지 없는지 판단해야 하는 함수가 있다면 아래와 같이 구현됩니다.

/* y는 세로, x는 가로 위치 */
int can_go(int x, int y)
{
   /* 0 or 1 반환 */
    return map[y][x];
}

아래의 소소로 맵을 실제로 출력해 보도록 하겠습니다.

include <stdio.h>

int map[5][5] =
{
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 1, 0 },
    { 0, 0, 0, 1, 0 },
    { 0, 0, 0, 1, 0 } 
};

void show_map()
{
    int i, j;

    for(i = 0; i < 5; i++)  /* 세로 */
    {
       for(j = 0; j < 5; j++) /* 가로 */
       {
          /* 출력 */
          printf("%d ", map[i][j]);
       }
       printf("\n");  /* 다음 행으로 */
    }  
}

int main()
{
    show_map();

    return 0;
}


9.4 포인터 배열

1) main의 포인터 배열 char* argv[]

int main(int argc, char* argv[]);
위와 같은 main의 선언을 많이 보았습니다. 이전에 배운대로 argc는 인자의 갯수이며, 두번째 인자 char* argv[]는 실행 시 입력정보를 넘기는 포인터의 배열입니다.  argv, argc에 관한 자세한 정보는 이전 3장. C 기초문법 3.2 함수  3) 인자 부분을 참고하세요.

포인터는 배열과 같은 의미로 보면 되기 때문에 이는 아래와 같이 보고, 사용할 수 있습니다.

char argv[][]

실제 이렇게 선언할 수는 없지만 위의 의미로 보면 argv[]가 한 문자열을 나타내기 때문에, char[][]는 문자열들의 배열 입니다.

> ./[실행파일 명] abc def [enter]
로 어떤 실행파일을 실행하면 시스템에서 아래와 같이 두 변수를 만든 후에 넘겨 준다고 생각하시면 됩니다.

int argc = 3;
char* argv[] =

    "[실행파일명]",
    "abc",
    "def"
};

아무 소스코드에서나 main에 아래와 같은 코드를 넣으면, 입력값을 확인할 수 있습니다.
for(i = 0; i < argc; i++)
{
    printf("%s\n", argv[i]);
}

2) 포인터와 배열의 차이

위에서 포인터 배열과 2차원 배열은 동일하다고 했으나, 몇 가지 중요한 차이점이 있습니다. 아래의 소스를 보겠습니다.

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

int main()
{
    char a[2][3];
    char* p[2];

    strcpy(a[0], "ab");
    strcpy(a[1], "cd");

    p[0] = "ab";
    p[1] = "cd";

    printf("%s\n", a[0]);
    printf("%s\n", a[1]);
    printf("%s\n", p[0]);
    printf("%s\n", p[1]);

    return 0;
}

#include <string.h>
strcpy 함수 사용을 위해 string.h 파일을 인클루드 합니다.

char a[2][3];
char* p[2];
위의 두 선언은 메모리에서 각각 아래와 같이 할당됩니다. char형 2차원 배열은 선언한 만큼 6바이트(2X3)의 메모리를 갖습니다. 그에 비해 char포인터 형 변수는 주소(4바이트)를 2개 가질 수 있는 4바이트 영역을 할당 받습니다.

사용자 삽입 이미지

strcpy(a[0], "ab");
strcpy(a[1], "cd");
"ab" 문자열을 배열 a[0]으로 복사 합니다.
"cd" 문자열을 배열 a[1]으로 복사 합니다.

strcpy(p[0], "ab");
위와 같은 소스는 컴파일은 되지만, 심각한 오류가 일어 날 수 있습니다. 초기화 되지 않은 포인터 변수 p[0]에는 현재 어떤 쓰레기 값이 있는지 알 수 없습니다. p[0]에 들어있는 값(주소)로 "ab"를 복사하기 때문입니다.

위와 같이 사용할려면 아래와 같이 전장에서 배운 malloc으로 할당 받고 정당한 주소를 돌려 받은 후, 사용하여야 합니다.
p[0] = (char *)malloc(3);

p[0] = "ab";
p[1] = "cd";
"ab"를 스텍 메모리에 할당한 후, 주소값을 p[0]에 대입합니다.
"ab"를 스텍 메모리에 할당한 후, 주소값을 p[1]에 대입합니다.

반대로 배열 a[0] = "ab"로는 사용할 수 없습니다. 시스템에서 이미 a[0]에 1012라는 변경이 불가능한 주소를 할당 받고 위치하고 있기 때문입니다.

위의 작업이 완료되면 메모리는 아래와 같이 값들이 들어 갑니다.
사용자 삽입 이미지
printf("%s\n", a[0]);
printf("%s\n", a[1]);
printf("%s\n", p[0]);
printf("%s\n", p[1]);

배열과 포인터는 의미는 틀리지만, 위와 같이 사용은 똑 같이 할 수 있습니다. 그렇기 때문에 아래와 같이도 사용할 수 있습니다.

printf("%s\n", *(a+0));
printf("%s\n", *(a+1));
printf("%s\n", *(p+0));
printf("%s\n", *(p+1));

이제 소스를 컴파일 하고 출력하여, 결과를 확인하여 봅니다.

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

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

8. 포인터
 
이번 장은 C를 배울 때 가장 어렵다는 포인터에 대한 내용입니다. 포인터는 메모리에 대한 이해만 있으면, 그다지 어렵지 않습니다.

컴퓨터는 아래와 같이 동작합니다. 하드디스크에서 메모리로 데이터를 읽거나 쓰고, CPU는 메모리에 있는 명령어들과 데이터를 레지스터라고 불리우는 저장소로 불러와 처리합니다.
사용자 삽입 이미지
우리가 만든 하드디스크에 있는 실행파일은 실행시에 코드와 데이터 모두 메모리에 저장된 후 사용됩니다. 그렇기 때문에 선언하는 전역, 지역 변수들 모두 할당된 크기로 메모리에 저장됩니다. 

CPU는 메모리에 접근하기 위하여 1byte단위로 메모리에 주소를 할당해 놓고 사용합니다. 우리는 사용하기 쉽게 int a; 로 선언하고 a라는 변수명으로 사용하지만, 실제 컴퓨터는 a라는 이름 대신 메모리 주소로 접근합니다. 이 때 사용하는 주소를 포인터라고 보시면 됩니다.


8.1 변수의 메모리 주소

사용자 삽입 이미지
포인터는 간단히 이야기 하면 메모리 상에서 주소를 저장하는 변수 입니다.  좌측 그림과 같이 메모리가 1byte 단위로 길게 늘어선 구조로 생각하고, 메모리에 있는 데이터에 접근을 하기 위해서는 메모리 상의 주소가 있어야 합니다.

int a = 3;
위의 코드가 실행되면 int 크기인 4byte만큼의 메모리가 할당되고, 그 할당된 메모리에는 3이 저장됩니다. 이것을 그림으로 좌측과  같습니다.

1011로 시작되는 숫자가 메모리상의 주소를 나타냅니다. 이 숫자는 제가 임의로 나타낸 것이고, 프로그램 실행 시 OS에 의해서 할당됩니다. 한 칸은 1byte입니다.

a의 주소는 1014이며, int는 4바이트이므로 1014~1017까지 위치합니다.
 


 이제 실제로 a변수의 주소를 출력해 보겠습니다.
#include <stdio.h>

int main()
{
    int a = 3;

    printf("A addr: 0x%u => %d", &a, a);

    return 0;
}

printf("A addr: 0x%u => %d", &a, a);
%u는 unsigned int를 출력합니다. 메모리 주소는 일반적으로 10진수 보다 16진수(%x)를 일반적으로 사용하지만, 지금은 10진수를 사용하겠습니다.

눈여겨 보아야 할 것은 주소 연산자(&) 입니다. 변수 앞에 &를 붙이면 그 변수에 들어 있는 값이 아니라, 그 변수가 메모리에서 위치한 주소를 나타냅니다. 위를 컴파일하면 아래와 같이 나옵니다.  
 
> A addr: 1014 => 3

1014가 변수 a의 메모리상의 위치 입니다. 1014는 제가 만든 임의의 수이고 실행 해 보시면, 실제 a의 메모리 주소가 출력됩니다.


8.2 포인터(*)의 이해

1. 포인터의 선언

위와 같은 메모리상의 주소를 보관하기 위한 변수가 포인터입니다. 위의 소스에 포인터 변수를 사용해 보겠습니다. 포인터는 변수 앞에 *를 넣어 선언합니다.

#include <stdio.h>

int main()
{
    int a = 3;
    int *p;
    
    p = &a;
    
    printf("A addr: 0x%u => %d", p, *p);

    return 0;
}
 
int *p;
a 변수의 주소를 저장하기 위해 포인터 변수 p를 선언합니다. 포인터 변수의 선언에서 *의 위치는 변수형과 변수명 사이에 위치하며, 아래의 선언 모두 유효합니다.
int* p;
int *p;
int * p;

p = &a;
포인터 변수 p에 a의 주소를 넣습니다.주의할 점은 a의 값 3이 들어가는 것이 아니라, 메모리 주소가 들어 갑니다.

printf("A addr: 0x%u => %d", p, *p);
포인터 변수는 p처럼 그냥 사용하면 주소가 들어 가지만, 앞에 * 표시를 해주면 그 주소에 있는 값을 나타냅니다. p는 a 변수의 주소를 가지고 있기 때문에 *p는 a의 값인 3입니다.


2) 포인터 변수의 크기

포인터는 주소를 나타내기 때문에 동일한 크기를 가지고 있으면 됩니다. 32비트 주소 체계를 가지고 있는 컴퓨터에선 32비트의 크기만 있으면 됩니다.

그런데 포인터는 모든 변수 타입에 줄 수 있습니다. 그러면 char *, int *, float*는 어떤 의미가 있을 까요? 일단 아래의 소스로 각 타입의 포인터 변수의 크기를 알아 보겠습니다.

#include <stdio.h>
int main()
{
    printf("%d, %d, %d\n", sizeof(int *), sizeof(char *), sizeof(float *));

    return 0;
}

위의 소스를 실행하면 셋 모두 동일한 크기를 출력하며, 대부분의 컴퓨터에서 4(4byte = 32bit)를 출력합니다.

그러면 포인터 변수는 int와 char등과 달리 같은 크기를 가지고 있는데, 선언 시에 변수타입은 어떤 의미가 있는지 알아 보겠습니다.

#include <stdio.h>

int main()
{
    int n = 3;
    char c = 'c';

    int* pn = &n;
    char* pc = &c;

    printf("char point: %u, %u, %c\n", pc, pc + 1, *pc + 1);
    printf("int point: %u, %u, %d\n", pn, pn + 1, *pn + 1);

    return 0;
}

위의 코드를 실행 시키면 아래와 같이 출력됩니다.

> char point: 3221224580, 3221224581, d
> int point: 3221224576, 3221224580, 4

위에서 주소의 숫자는 중요하지 않고, 컴퓨터 마다 다르게 나옵니다.

주소 부분을 눈여겨 보면 char형 포인터는 1 증가시 3221224580에서 3221224581로 1이 증가되었습니다. 하지만 int형 포인터는 3221224576에서 3221224580로 4가 증가 되었습니다.

포인터 변수에 1을 더하면 그 타입의 크기만큼 주소가 증가 합니다. pn + 2이면 2X4=8로 12가 증가되게 됩니다. 이유는 int형 데이터를 다루기 위해서는, 4byte가 한 단위가 되어야 합니다. 1씩 증가되면 올바르게 메모리에서 int형 자료들을 다룰 수가 없습니다.

이는 아래의 그림과 같습니다.
사용자 삽입 이미지


3) 포인터 변수의 초기화

모든 변수들이 정확한 값을 가져야 하지만, 특히 메모리에 직접 접근하는 포인터 변수는 반드시 초기화 된 후에 사용하여야 합니다. 아래의 소스를 보겠습니다.

#include <stdio.h>

int main()
{
    int* pn;
    
    printf("%u, %d", pn, *pn);
    
    return 0;
}  

위와 같이 포인터 변수가 초기화 되지 않고 사용할 경우에는 매우 위험합니다.  지역변수 int* pn은 스택 메모리에 자리잡기 때문에, int * 를 위해 4byte가 할당되지만, 그 안에 내용은 이전에 쓰던 내용이 있을지 어떤 값이 있을지 모릅니다.
 
 *pn으로 그 주소의 4byte int형을 출력하는데, 주소에 들어있는 값이 초기화 되지 않은 쓰레기값이기 때문에 어떤 주소를 참조할지, 그 주소에는 어떤 값이 있을지 모르기 때문입니다.
 
잘못된 포인터(메모리)의 사용은 프로그램 실행시에 치명적인 오류를 일으킬 수 있기 때문에, 항상 주의하여 사용하여야 합니다.


4) 메모리 할당과 해제

우리가 지금까지 보았던 변수들의 최고 크기는 double로 8바이트까지 저장할 수 있습니다. 그러면 10mb되는 이미지를 메모리로 불러와서 작업을 할려면 어떻게 해야 할까요?
 
답은 malloc이란 함수로 시스템으로 부터 메모리를 할당 받아야 합니다. malloc은 다음과 같이 선언되어 있습니다.

void * malloc(size_t size);
여기서 size_t는 unsigned int로 보시면 됩니다. (후에 typedef가 나올 때, 설명하겠습니다.) size는 필요한 메모리의 byte수 입니다.

함수에서 void는 반환값이 없다는 뜻이지만, void 포인터(void*) 는 형없이 반환할테니, 필요에 따른 변수타입의 포인터로 형변환(type casting)해서 사용하라는 뜻입니다. 시스템은 size만큼의 메모리 할당에 성공하면 그 메모리가 시작하는 포인터를 반환하고, 메모리 부족 등의 이유로 할당에 실패할 경우에는 NULL을 반환합니다. NULL은 0(typedef)이며 포인터 등 메모리와 관련해선 0 대신 NULL로 사용합니다. 

아래의 소스를 보겠습니다.

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

int main()
{
    int i;  
    char* p;

    p = (char *)malloc(8);
    if(p == NULL)
    {
        printf("fail to alloc\n");
        return 0;
    }

    *p = 'H';
    *(p+1) = 'e';
    *(p+2) = 'l';
    *(p+3) = 'l';
    *(p+4) = 'o';
    *(p+5) = '\n';
    *(p+6) = '\0';

    printf("%s", p);
    
    free(p);

    return 0;
}

p = (char *)malloc(8);
malloc으로 8byte의 메모리를 요구합니다. 시스템은 다른 곳에서 사용하지 않는 8byte 메모리를 할당해서 그 시작 주소를 반환합니다.

 NULL이 반환되어 메모리 할당에 실패하였을 경우에는 오류메시지를 출력하고, 종료합니다.

사용자 삽입 이미지
*p = 'H';              // *(1013) = 'H';
*(p+1) = 'e';        // *(1014) = 'e';
*(p+2) = 'l';         // *(1015) = 'l';
*(p+3) = 'l';         // *(1016) = 'l';
*(p+4) = 'o';        // *(1017) = 'o';
*(p+5) = '\n';      // *(1018) = '\n';
*(p+6) = '\0';       // *(1019) = '\0';

메모리에 "Hello\n" 문자를 한바이트 씩 할당 합니다. *p+1로 하면 포인터(*)가 우선순위에 있으므로 값(*p)에 1을 더하는 의미가 됩니다.

그러므로 좌측 변수가 올자리에 와서는 안되기 때문에 컴파일 시 오류가 발생합니다. 그래서 주소가 1 증가되는 곳에 할당 시에는 괄호를 사용하여 *(p+1) = 'e'; 로 사용하여야 합니다.

C에서 문자열은 '\0'로 끝나는 char형 배열입니다. 배열은 이 다음장에 설명을 하겠습니다. 문자열은 마지막에 반드시 '\0'로 막아야 한다는 것을 명심해야 합니다. (NULL == '\0' == 0)

printf("%s", p);
printf의 %s는 문자열을 출력할때 사용합니다. 위의 소스를 실행하면 Hello\n를 출력합니다.

free(p);
마지막 free 함수는 이제 메모리 사용이 끝났으니, 반환하겠다는 의미입니다. freep이 후로는 포인터 p를 사용하여서는 안됩니다. free는 malloc과 한쌍으로 malloc으로 할당 받은 메모리는 반드시 free로 반환을 해야 합니다.

특히 반복되는 작업에서 사용한 메모리를 반환하지 않으면, 메모리 누수가 계속 일어나 프로그램과 OS에 치명적인 오류를 일으킬 수 있습니다.

이상 포인터에 대해서 간단히 설명하였습니다. 포인터는 다음 장에 설명 할 배열과 밀접한 관계에 있기 때문에, 다음 장에서 다시 논해 보도록 하겠습니다.

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

10. struct, union, enum, typedef  (2) 2007.06.21
9. 배열 (array)  (0) 2007.06.17
7. C 함수 (function)  (4) 2007.06.15
6. 제어문  (0) 2007.06.14
5. 연산자  (0) 2007.06.13
AND

7. C 함수 (function)
C에서 함수는 특정한 작업을 처리하는 코드의 한 단위이며, C를 구조적인 언어로 만들어 주는 가장 중요한 수단 입니다. 이번 장에서는 함수의 사용방법, 가변인자, 프로토 타입, 함수를 사용해야 하는 이유에 대해서 알아 보겠습니다.


7.1 사용 방법

이전에도 잠시 알아 보았지만, 함수의 헤더 부분은 아래와 같이 작성 됩니다.

[반환값] 함수명 ([인자], ...)

1) 반환값

반환값에는 int, float, char, ..., 사용자가 정의한 구조체(structure)등 반환(return)할 변수 타입이 옵니다.

int를 반환할 경우에는 반환값을 생략할 수 있지만, 명확히 써주는 것이 좋습니다. 함수가 코드만 수행하고 값을 반환하지 않을 때는 아래와 같이 void를 사용합니다.
void showEnemy();

반환값이 있을 경우 함수 내에서 반드시 return을 사용하여 올바른 타입으로 반환하여야 합니다. return은 반환값 내에 괄호를 할 수도 있고, 그냥 변수나 상수만 쓸 수도 있습니다. 아래 두개 모두 올바른 표현 입니다.
return 3;
return (3);


2) 함수명

함수명은 실제 그 함수의 코드를 확인하지 않고, 함수명만 보더라도 그 함수가 어떤 용도로 사용되는지 짐작할 수 있도록 작성하여야 합니다. 함수명은 변수명과 같이 아래와 같은 규칙이 적용됩니다.

  • 알파벳 대소문자([A-Z][a-z]), 숫자([0-9]), 언더바(_)를 사용할 수 있습니다.
  • 첫 글자에 숫자가 올 수 없습니다.
  • 대소문자를 구별합니다.
   
3) 인자

함수명 다음에는 괄호가 오고,  괄호 내부에는 인자들이 위치 합니다. 인자는 함수를 호출하는 곳에서 넘겨 주는 정이며, 매개변수라고도 합니다.

각 인자는 아래와 같이 , 로 구분합니다.
int get_man(int n1, int n2, int n3);

인자를 넘길 필요가 없을 때는 아래와 같이 비워 둡니다.
int get_man();

인자는 필요한 만큼의 갯수를 넘길 수 있지만, 가능한 5개를 넘지 않는 것이 좋습니다. 많은 인자가 필요할 경우에는 나중에 나올 구조체를 사용하여야 합니다.


>> 가변인자
printf와 같은 함수는 인자 수가 사용자의 필요에 따라 가변적입니다. C에서는 필요에 따라서는 가변인자를 사용할 수 있습니다. 주어진 인자들의 평균값을 출력하는 가변인자의 사용예를 보겠습니다.

이 부분은 이해가 가지 않으면, 다음 내용으로 그냥 넘어 가셔도 상관 없습니다. 포인터 및 다른 강좌가 진행된 후에 다시 확인해 보시기 바랍니다. 

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

int get_average(int total, ...)
{
    int i;  
    int avr = 0;
    va_list valist;
    
    va_start(valist, total);

    for(i = 0; i < total; i++)
    {
        avr += va_arg(valist, int);
    }
    
    va_end(valist);

    avr = avr/total;

    return avr;
}

int main(int argc, char* argv[])
{
    printf("%d\n", get_average(4, 50, 100, 100, 50));

    return 0;
}

#include <stdarg.h>
va_ 류의 가변인자 함수를 사용할려면, stdarg.h 파일을 인클루드해야 합니다.

int get_average(int total, ...)
"..."는 가변인자임을 나타내며, 항상 가변인자 함수의 마지막 인자가 되어야 합니다. 또한 가변인자는 반드시 하나 이상의 일반 인자를 가지고 있어야 합니다. int get_average(...); 와 같이는 사용할 수 없습니다.

C에서 가변인자 함수는 그리 똑똑하지 않습니다. 넘어 오는 인자의 갯수와 타입을 알 수 없습니다. 그래서 printf와 같이 여러 타입을 자유롭게 사용할려면 "%d %s" 처럼 넘어 오는 인자의 갯수와 종류를 알려 주는 인자를 앞에 두어야 합니다.

여기서는 모두 int형으로 간주하므로 total 인자로 인자의 갯수를 받습니다.

va_list valist;
가변인자를 사용하기 위해 va_list 변수를 선언 합니다.

va_start(valist, total);
가변인자 처리를 시작함을 알립니다. va_start의 두번째 인자는 "..." 바로 앞의 인자(마지막 일반 인자)를 넘겨 줌으로서 스택에서 바로 다음에 오는 가변인자의 시작위치를 알립니다.
valist에는 가변인자의 첫번째 값의 포인터가 들어 갑니다.
     
avr += va_arg(valist, int);
va_arg는 가변인자의 값을 반환하여 줍니다. 두번째 인자는 변수의 타입입니다. 변수타입의 크기만큼 valist의 포인터를 이동하여, 다음 변수를 얻어 올 수 있도록 합니다.

va_end(valist);
가변인자 처리가 종료됨을 알립니다. 이 후로는 다시 가변인자를 처리할 수 없습니다.

printf("%d\n", get_average(4, 50, 100, 100, 50));
구해 온 평균값을 출력합니다. get_average의 4는 평균값을 구해올 인자가 4개 라는 의미입니다.

가변인자는 이후에 디버깅 부분에서 다시 설명하겠습니다.


7.2 프로토타입 선언
함수의 프로토 타입은 소스의 상단이나 헤더파일에서 이런 형식으로 함수가 존재 한다는 것을 미리 선언해 놓는 것을 의미합니다.

#include <stdio.h>

int main(int argc, char* argv[])
{
    printf("%d", get_number());
    return 0;
}

int get_number()
{
    return 10;
}

이전 C 컴파일러들은 위와 같은 소스를 컴파일할 때는 오류를 발생하였습니다. 컴파일이 위에서 아래로 진행이 되기 때문입니다. main에서 get_number()라는 함수가 나오는데 이에 대한 정보가 그 이전 어디에도 없기 때문입니다.

하지만 요즘의 C 컴파일러들은 뒤에 나올 함수의 프로토타입 선언 없이도 위와 같은 코드가 컴파일 되며, 다른 소스파일에 있는 함수도 컴파일이 됩니다.

하지만 프로토 타입을 선언하지 않으면, 컴파일 시 함수의 인자들에 관해 체크하지 않습니다. 이는 보이지는 않지만 실행 시 오류의 원인이 되며, 이런 오류는 찾아 내기가 힘듭니다.

이런 이유로 함수의 프로토타입 선언은 반드시 해주는 것이 좋습니다. 위의 소스에 함수의 프로토 타입을 선언해 보겠습니다.

#include <stdio.h>

int get_number();

int main(int argc, char* argv[])
{
    printf("%d", get_number());
    return 0;
}

int get_number()
{
    return 10;
}

위와 같이 함수가 나오는 상단에 함수명이 있는 부분에 세미콜론(";")을 더해 줍니다. 소스파일 상단이나 헤더파일에 함수의 프로토 타입을 작성해 두면, 컴파일 시 함수의 프로토 타입과 맞게 사용하는지 검사도 할 수 있고 어떤 소스 파일에는 어떠한 함수들이 있고 어떻게 사용하는지에 관해 한눈에 알아 볼 수 있습니다.


7.3 함수를 사용하는 이유

함수의 필요성과 효율성에 대한 예를 들어 보겠습니다. 호프집에서 준비해야할 맥주잔의 갯수를 출력하는 프로그램을 만든다고 가정합니다. 또한 이 호프집에는 아래와 같이 네 종류의 손님 그룹이 있다고 가정합니다.
  • 20대 / 남 / 평균 소비 잔 수 = 20
  • 20대 / 여 / 평균 소비 잔 수 = 10
  • 30대 / 남 / 평균 소비 잔 수 = 10
  • 30대 / 여 / 평균 소비 잔 수 =  5

아래 네 그룹을 위해 각각 준비할 잔수를 출력하는 소스 입니다.

#include <stdio.h>

#define M20_CUP         20
#define M30_CUP         10
#define W20_CUP         10
#define W30_CUP         5

int main(int argc, char* argv[])
{
    int m20, m30, w20, w30;

    m20 = 120;  
    m30 = 110;  
    w20 = 90;
    w30 = 55;

    printf("20 (M): %d\n", M20_CUP * m20);
    printf("30 (W): %d\n", M30_CUP * m30);
    printf("20 (M): %d\n", W20_CUP * w20);
    printf("30 (W): %d\n", W30_CUP * w30);
 
    return 0;
}

m20, m30, w20, w30은 그룹별 손님 수 입니다. 그룹별로 손님수와 평균 사용 맥주잔 수를 곱해서 필요한 잔수를 출력 합니다. 소스를 실행 시키면 아래와 같이 출력 됩니다.

> 20 (M): 2400
> 30 (W): 1100
> 20 (M): 900
> 30 (W): 275

여기까지는 이상이 없지만 사용자가 출력 포맷을 아래와 같이 바꾸어 줄 것을 요구합니다.
printf(">> 30 (W): %d\n", W30_CUP * w30);

이를 위해서는 위의 소스에서 printf문의 네 곳을 모두 위와 같이 변경해 주어야 합니다.

또한 계산 방법에 변경이 생겼습니다. 기존 계산 방법에서 여유로 10개씩 추가할 것을 요구하였습니다. 변경된 내용은 아래와 같습니다.
printf(">> 30 (W): %d\n", W30_CUP * w30 + 10);

위와 같이 또 관련된 부분을 모두 변경해야 합니다. 만약 그룹이 100개 라면 끔찍한 작업이 될 것 입니다. 많은 시간도 소비하고 오타가 나올 확률도 많습니다.

또 한가지 문제점은 다른 소스에서도 사용하고 있다면, 관련 파일 마다 모두 출력 포맷이나 계산방법을 변경해야 합니다. 그래서 다음과 같이 함수로 만들어 처리해 보겠습니다.

#include <stdio.h>

#define M20_CUP         20
#define M30_CUP         10
#define W20_CUP         10
#define W30_CUP         5

#define SEX_MAN         1
#define SEX_WOMAN       2

/* 나이대, 인원수, 성별을 받습니다. */
void print_cup_count(int age, int sex, int man);

int main(int argc, char* argv[])
{
    int m20, m30, w20, w30;

    m20 = 120;  
    m30 = 110;  
    w20 = 90;
    w30 = 55;

    print_cup_count(20,  SEX_MAN, m20);
    print_cup_count(30,  SEX_MAN, m30);
    print_cup_count(20,  SEX_WOMAN, w20);
    print_cup_count(30,  SEX_WOMAN, w30);
 
    return 0;
}

void print_cup_count(int age, int man, int sex)
{
    int cup;
    int count;
    char temp;

    if(sex == SEX_MAN)
    {
        if(age == 20)
            count = M20_CUP;
        else
            count = M30_CUP;

        temp = 'M';
    }       
    else
    {
        if(age == 20)
            count = W20_CUP;
        else
            count = W30_CUP;

        temp = 'W';
    }

    cup = man * count;

    printf("%d (%c): %d\n", age, temp, cup);
}

위와 같이 출력되는 부분을 조건에 맞게 함수로 만들었습니다. 이제 포맷이 변동되거나 계산방법이 변경되어도 print_cup_count 함수의 한 곳만 수정하면 모든 곳에 적용이 됩니다. 그리고 필요한 모든 곳에서 편리하게 이용할 수 있습니다.

여기서는 한줄 내용을 함수로 만들었지만, 복잡한 내용일 수록 그 효율성은 더욱 증대 됩니다.

함수나 main등에 몇 백줄의 코드가 들어 가면, 이해하거나 테스트, 오류를 찾기가 점점 힘들어 집니다. 이럴 경우도 적당한 동작별로 코드를 나누어 별개의 함수로 만드는 것이 좋습니다.

이로서 C에서 중요한 부분인 변수, 연산자, 제어문, 함수에 대한 간단한 설명을 모두 마쳤습니다. 하지만 변수, 연산자, 함수, 제어문에 관해서는 아직도 알아야 할 내용이 많이 남아 있습니다. 이는 앞으로도 계속 설명될 것입니다.

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

9. 배열 (array)  (0) 2007.06.17
8. 포인터 (pointer)  (4) 2007.06.16
6. 제어문  (0) 2007.06.14
5. 연산자  (0) 2007.06.13
4. 변수  (2) 2007.06.12
AND

6.1 조건문
조건문은 주어진 조건에 맞는 실행을 선택하는 역활을 합니다. C에는 if와 switch가 있습니다.

1) if / else if / else

if는 대부분의 언어에 비슷하게 존재하는 대표적인 조건문으로 아래와 같은 문법으로 사용합니다.

if([조건]) { [코드] }

if문 다음의 괄호 안의 [조건]이 0이 아니면 다음에 나오는 [코드]를 실행하고, 0이면 실행하지 않습니다. 일단 사용 예 부터 보겠습니다.

int a = 2;
if(a < 3)
{
    printf("TRUE");
}

위의 코드는 (a < 3)이 참(1)이므로 {}내의 내용이 실행되어 printf를 출력합니다. 만약 변수 a를 int a = 4;와 같이 변경하면 거짓(0)이 되므로 printf의 내용을 출력하지 않습니다.

if 다음 실행해야 될 코드가 한줄일 경우에는 아래와 같이 {}를 생략할 수 있습니다. 이는 나중에 나올 for, while문 등도 마찬가지 입니다.

if(a < 3)
    printf("TRUE");

if는 else if, else와 더불어 여러 조건들을 검사 할 수 있습니다. else if는 if의 조건 다음에 나오는 조건들을 검사합니다. else 는 if나 else if의 마지막에 위치하면 위의 조건들이 모두 일치하지 않을 때만 실행됩니다.

if와 else는 한 if문에서 하나씩만 올 수 있지만, else if는 여러개가 올 수 있습니다. 사용예는 아래와 같습니다.

int a = 2;
if(a < 3)
    printf("one");
else if(a < 6)
    printf("two");
else
    printf("three");
   
첫번째 if문은 a가 3보다 작을 경우 실행되고, 위와 같이 a가 2일 경우에는 "one"을 출력하고 그 다음에 오는 else if, else의 코드들은 실행되지 않습니다.

만약 a가 4로  첫번째 if문의 조건이 거짓(0)이되면, 그 다음의 else if문에서 다음 조건을 검사합니다. a가 4면 6보다 작으므로 "two"를 출력하고, 뒤의 else문은 실행이 되지 않습니다.

마지막 else의 경우는 위의 if, else if의 두 조건인 a가 3보다 작지 않고 6보다 작지 않은 조건에 걸리지 않는다면 "three"를 출력합니다.

>> 조건 연산자
간단하게 한가지 조건을 검사하고 변수에 값을 저장 할 때는 이전 연산자에서 잠시 본 조건 연산자를 사용할 수 있습니다. 조건 연산자는 아래와 같이 사용합니다.

[조건] ? [A] : [B];

위의 조건은 if문 괄호내의 조건과 동일합니다. [조건]이 참(1)일 경우 [A]의 값을 반환하고, 거짓(0)일 경우 에는 [B]의 값을 반환합니다. 아래는 사용 예 입니다.

int a = 2;
int b;
b = (a < 3) ? 100 : 200;

a = 2이므로 (a < 3)는 참(1)입니다. 참이므로 첫번째 숫자인 100을 반환합니다. 이를  if문을 사용하는 동일한 코드를 작성하면 아래와 같습니다.

if(a < 3)
    b = 100;
else
    b = 200;


2) switch

if문과 더불어 많이 사용하는 조건문이 switch문 입니다. 한 변수(int, char)에 대하여 많은 조건을 검사해야 할 경우에는 switch문이 가독성이 좋을 수도 있습니다. switch문은 아래와 같이 사용할 수 있습니다.

switch([조건])
{
    case [값] :
        [code];
        break;
    case [값] :
        [code];
        break;
    default :
        [code];
        break;
}

switch  괄호내에 있는 [조건]의 값이 각각의 case의 값과 일치할 경우에는 해당 case 문내의 [code]가 실행 됩니다. case 문 내의 코드가 끝나는 곳에는 break;가 있어야 다음 case 문으로 실행이 되지 않습니다.

default는 생략이 가능하며 if문의 else와 마찬가지로 위의 모든 case 조건에 해당 사항이 없을 때, default내의 [code]가 실행됩니다. 아래는 사용 예입니다.

int a = 2;

switch(a)
{
    case 1:
        printf("1\n");
        break;
    case 2:
        printf("2\n");
        break;
    case 3:
        printf("3\n");
        break;
    default:
        printf("not 1 or 2 or 3");
        break;
}    

위에서 변수 a의 값을 1, 2, 3, 그 외 다른 수로 변경하면서 컴파일/실행해 보시면 switch문을 이해하실 수 있습니다. a의 값을 1부터 차례대로 증가시키면서 변경하면 1, 2, 3, not 1 or 2 or 3이 출력 됩니다.

만약 case 2의 break;를 삭제하면 2가 되더라도 다음 break;를 만나는 case 3까지 실행됩니다. 코드는 아래와 같습니다.

switch(a)
{
    case 1:
        printf("1\n");
        break;
    case 2:
        printf("2\n");
    case 3:
        printf("2 or 3\n");
        break;
    default:
        printf("not 1 or 2 or 3");
        break;
}
   
위의 코드는 다른 부분은 위와 동일 하지만, a가 2일 경우에는 case 2에 break;가 없기 때문에 2를 출력하고 case 3까지 내려와 2 or 3을 출력한 후에 break;문을 만나 switch문을 나가게 됩니다.


6.2 반복문
어떤 작업에 대하여 일정한 반복이 필요할 경우에 사용되는 반복문입니다. C의 for와 while에 대하여 알아 보겠습니다.

1) for

for는 아래와 같이 사용합니다.

for([초기화]; [조건]; [변경])
{
    [코드]
}

[초기화] 부분은 for문이 반복되기 전에 한번만 실행되며, 이곳에서 for문의 반복 횟수를 나타내는 변수등을 초기화 합니다.

[조건]은 항상 반복전에 검사되며 참(1)일 경우에는 계속 진행되고, 거짓(0)일 경우에는 for문을 종료합니다.

[변경]은 [코드]가 실행 된 후에 실행되는 부분으로 일반적으로 반복 횟수를 나타내는 변수를 증감하여, for문이 원하는 [조건]에서 종료할 수 있도록 합니다.

사용예는 아래와 같습니다.

int i;
for(i = 0; i < 10; i++)
{
    printf("%d\n", i);
}

위의 for문이 10번 반복되면서 0~9까지 출력하는 소스입니다. 위의 소스는 아래와 같이 실행됩니다.
  1. 초기화 부분에서 i = 0로 저장됩니다.
  2. i < 10인지 검사하고, 참이면 코드 부분이 실행되고 거짓이면 for문이 종료됩니다.
  3. printf가 i를 출력합니다.
  4. i가 1 증가 됩니다.
위에서 2~4 부분은 4까지 오면 다시 2로 가서 계속 반복 됩니다. 이렇게 반복되면서 printf가 9를 출력하고 i가 10이 될 때에는  2에서 i < 10 조건이 거짓(0)이 되므로 for문이 종료됩니다.


2) while

while은 아래와 같이 사용됩니다.

while([조건])
{
    [코드]
}

[조건]이 참(1)일 동안은 [코드]가 계속 반복해서 실행됩니다. 위의 0부터 1까지 출력하는 for문을 while로 변경하면 다음과 같습니다.

int i = 0;
while(i < 10)
{
    printf("%d\n", i++);
}

for문도 마찬가지지만 while 사용시에는 특히 (i < 10)의 조건 부분이 원하는 만큼만 반복하고 나올 수 있도록 유의하여야 합니다. 위에 printf문의 i++가 같이 i가 증가되는 부분이 없으면, (i < 10)은 항상 참이 되므로, 프로그램이 무한이 반복되는 무한루프에 빠지지 됩니다.

while(1)
{
    printf("d");
}

위에 while문은 조건 부분이 항상 1이므로 무한히 반복되며 d를 출력합니다. 멈추기 위해서는 Ctrl+c등 강제적인 수단을 써야 합니다. for문은 아래와 같이 하면 무한루프가 됩니다.

for(;;)
{
    printf("d");
}


6.3 break, continue, goto

위의 for와 while 같은 반복문 내에서 또는 단독으로 사용되는 continue와 break, goto에 대해서 알아 보겠습니다.

1.break

break가 실행되면 무조건 그 반복문은 그 시점에서 종료됩니다. break는 반복문에서 특정한 조건을 만나면 반복을 중지해야 할 경우에 사용됩니다.

for(i = 0; i < 10; i++)
{
    if(i == 5)
        break;
    printf("%d\n", i);
}

이전 소스코드에서 위와 같이 break;문이 있으면 (i == 5)일 경우가 되면 if문 내의 break가 실행되면서 그 순간 for문은 종료됩니다. 소스를 위와 같이 수정하고 실행 시켜면 프로그램은 0~4까지만 출력하고 종료 됩니다.


2. continue

break가 무조건 반복을 종료한다면, continue는 continue 다음의 코드를 실행하지 않고 반복은 계속 됩니다. 아래의 소스를 보겠습니다.

for(i = 0; i < 10; i++)
{
    if(i == 5)
        continue;
    printf("%d\n", i);
}

(i == 5)일 경우에는 if문내의 continue가 실행되며, continue 아래의 모든 코드는 실행되지 않고 건너 뛰게 됩니다. 그런 후 i는 6으로 증가 되어 이 후로는 for문의 조건을 만족하는 동안 코드는 반복됩니다.

위의 코드는 프로그램을 실행시키면 5를 제외한 0, 1, 2, 3, 4, 6, 7, 8, 9를 출력합니다.


3. goto

goto문은 위의 break와 continue와는 다르게, 반복문 이외에도 자유롭게 사용할 수 있습니다. 아래는 goto문의 사용법입니다.

goto [LABEL];

[LABEL]:

실행 시 위의 goto를 만나게 되면, 무조건 해당 [LABEL]이 있는 곳으로 건너 뛰어, 그 이후로 부터 실행 됩니다. [LABEL] 의 선언은 C의 일반적인 선언과 틀리게 마지막에 ":"을 사용하는 것에 유의하셔야 합니다.
 
while(i < 10)
{
    if(i == 7)
        goto END;
    
    printf("%d\n", i++);
}

printf("while end");

END:

위의 소스는 i가 7이되면 중간의 모든 명령을 건너뛰고 해당 레벨인 END로 가게됩니다. 이런 무지막지성(?) 때문에 소스에 혼란을 줄 수 있고, 예측할 수 없는 상황을 야기할 수도 있기 때문에 대부분 goto를 사용하지 말것을 권합니다.

하지만 아주 간혹(정말 간혹 입니다.) goto는 쓸데없이 많은 조건문(if 등)을 생략할 수 있게 해주고, 오히려 더 의미를 명확하게 해줄 경우가 있습니다.

그렇지만 당분간은 없는 명령어로 생각하시는 것이 좋습니다.

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

8. 포인터 (pointer)  (4) 2007.06.16
7. C 함수 (function)  (4) 2007.06.15
5. 연산자  (0) 2007.06.13
4. 변수  (2) 2007.06.12
3. C 기초문법  (0) 2007.06.05
AND

5. 연산자

C에서 연산자는 변수나 상수에 특정한 연산을 하는 C에서 예약된 기호들이며, 일반적으로 수학에서 많이 사용하는 기호와 연산이 유사합니다.

1) =

"="는 변수에 값을 대입할 때 사용하는 연산자 입니다. 일반적으로 사용하는 것과 반대 방향으로 우측에 있는 값을 좌측 변수에 대입 합니다. 상수 뿐만 아니라 아래와 같이 변수, 함수의 반환값, 포인터 등에도 사용할 수 있습니다.

int num1 = 3;
int num2 = GetNum(); /* GetNum은 함수로 후에 설명하겠습니다. */
int num3 = count + 1;
char* p = &n; /* *는 포인터를 나타내는 연산자로 후에 설명하겠습니다. */


2) +, -, *, /, %

실생활에서도 많이 사용하는 더하기(+), 빼기(-), 곱하기(*), 나누기(/), 나머지(%) 계산에 관한 연산자 들입니다.

연산자에는 우선순위가 있습니다. 일반적인 연산 순서는 좌에서 우로 갑니다. 하지만 () 연산이 최상위에 있고, *, /, % 연산이 +, - 보다 위에 있습니다.

그러므로 아래와 같은 코드가 있다면.

int num1 = 4+4/2;
int num2 = (4+4)/2;

num1에는 6, num2에는 4가 들어 갑니다.

이 연산자들은 또한 "="의 앞에 결합되어 사용할 수 있습니다. 의미는 아래와 같습니다.
int a = 1;

a += 3;        /* a = a + 3;  과 동일 */
a -= 3;        /* a = a - 3;  과 동일 */
a *= 3;        /* a = a * 3;  과 동일 */
a /= 3;        /* a = a / 3;  과 동일 */
a %= 3;        /* a = a % 3;  과 동일 */


3) ==, !=, <, <=, >, >=

위의 연산자들은 두 값의 사이에서 비교하는 연산자이며, 비교값이 참일 경우에는 1, 거짓일 경우에는 0이 됩니다.

  • "a == b" 는 a와 b가 같은지 비교합니다.
  • "a != b" 는 a와 b가 틀린지 비교합니다.
  • "a < b" 는 a가 b보다 작은지 비교합니다.
  • "a <= b" 는 a가 b보다 작거나 같은지 비교합니다.
  • "a > b" 는 a가 b보다 큰지 비교합니다.
  • "a >= b" 는 a가 b보다 크거나 같은지 비교합니다.

nt n1 = (3 == 4);
int n2 = (3 != 3);
int n3 = (3 > 1);
int n4 = (3 > 10);
int n5 = (3 <= 4);

여기서도 = 연산자가 비교 연산자(==, !- 등) 보다 연산순위가 낮기 때문에 괄호를 사용하지 않더라도 비교 연산이 먼저지만 보기 쉽게 하기위해 괄호를 하였습니다.

위에서 값들은 각각 n1는 0, n2는 0, n3는 1, n4는 0, n5는 1의 값을 같습니다.


4) ++, --

증감 연산자는 변수의 값을 1씩 더하거나 빼기를 합니다. 즉 a++은 a = a + 1; 과 같은 의미이며, b--는 b = b - 1; 와 같은 의미입니다. ++, -- 증감 연산자에서 중요한 점은 변수의 앞과 뒤에 위치할 수 있으면, 이것은 연산 순서의 변경을 가지고 옵니다.

++(or --)이 변수 앞에 오면 그 라인의 명령이 실행되기 전에 먼저 ++(or --)가 먼저 실행되고,  ++(or --)이 변수 뒤에 오면 그 라인의 명령이 실행된 후에 ++(or --)가 실행됩니다.

#include <stdio.h>

int main(int argc, char* argv[])
{
    int a = 2;
    int b1 = 4;
    int b2 = 4;

    int c1 = a + b1++;
    int c2 = a + ++b2;
   
    printf("%d, %d, %d, %d", c1, c2, b1, b2);

    return 0;
}

위의 소스를 컴파일 후 실행 하면 "6, 7, 5, 5"가 출력됩니다.

int c1 = a + b1++; 은
int c1 = a + b1;
b1 = b1 + 1;
과 동일하며,

int c2 = a + ++b2; 은
b2 = b2 + 1
int c2 = a + b2; 
와 동일합니다.

5) &&, ||, !

"A && B"는 AND 연산을 하며 A와 B 두개의 값이 모두 0이 아닐 경우, 1을 반환 하고 그 외에는 0을 반환합니다.

"A || B"는 OR 연산을 하며 A와 B 두개의 하나 이상이 0이 아닐 경우, 1을 반환 하고 그 외에는 0을 반환합니다.

"!A"는 A가 0이 아닐 경우에는 0, 0일 경우에는 1을 반환합니다. !A는 (A == 0)과 의미가 같습니다.

아래의 소스를 보면 위의 내용을 확인할 수 있습니다.
#include <stdio.h>

int main(int argc, char* argv[])
{
    printf("%d, ", 0 > 1 && 2 > 1);
    printf("%d, ", 0 > 1 || 2 > 1);
    printf("%d\n", !(0 > 1 && 2 > 1));

    return 0;
}

위의 소스를 컴파일 후, 실행하면 0, 1, 1로 출력 됩니다.

printf("%d, ", 0 > 1 && 2 > 1);
&&, ||은 연산자 우선 순위가 낮기 때문에 <, > 보다 늦게 실행됩니다. (0 > 1) && (2 > 1)과 같은 의미입니다. 우선 0 > 1은 거짓(0)이고 2 > 1은 참(1)이므로 위는 "0 && 1"과 의미가 같습니다. && 연산자는 양쪽 모두 0이 아닐때만, 1을 반환하므로 0이 됩니다.

printf("%d, ", 0 > 1 || 2 > 1);
위는 0 > 1은 거짓(0)이고 2 > 1은 참(1)이므로 위는 "0 || 1"과 의미가 같습니다. || 연산자는 하나 이상만 0이 아니면 1을 반환하므로 1이 됩니다.

printf("%d\n", !(0 > 1 && 2 > 1));
(0 > 1 && 2 > 1)는 위 첫번째와 같은 값이며 0입니다.  "!0" (0 == 0)이 되기 때문에 1을 반환합니다.
 

6) &, |, ^, ~, >>, <<

다음은 이전 변수 에서 잠시 살펴 본, 비트에 관한 연산자 들입니다. 이전에 말씀 드린 것과 같이 bit는 0과 1 두가지 경우를 나타낼 수 있는 컴퓨터에서 사용하는 최소 단위입니다.

1. and 연산자(&)
&는 비트 단위로 and 연산을 합니다. &는 양쪽이 다 같이 1일 경우에만 참(1)입니다. 1바이트 에서 &연산은 아래와 같습니다.

#include <stdio.h>

int main(int argc, char* argv[])
{
    unsigned char a = 153;
    unsigned char b = 83;
    unsigned char c;
   
    c = a & b;

    printf("%d", c);

    return 0;
}

1바이트형의 양수만 쓰는 unsigned char형으로 a, b, c를 위와 같이 선언 합니다. 참고로 153은 2진수로 10011001이고 83은 01010011이며 c = a & b 연산은 아래의 그림과 같이 처리 됩니다. (2진수 -> 10진수 변환은 이전 장에 언급되었습니다.)
사용자 삽입 이미지
a, b의 같은 번째의 bit를 비교해서 두개 모두 1일 경우에만, c의 같은 위치가 1이되며 나머지는 0이 됩니다. 위의 소스를 컴파일 하면 17이란 10진수가 나오는데 이는 2진수로 00010001로 위의 c와 같습니다.

2. or 연산자(|)
or 연산자 |는 두 bit를 비교하여 둘 중 어느 한쪽에 1이 있으면 1이 됩니다. 위의 소스 중에서 c = a & b; 부분을 c = a | b;로 변경하시고 컴파일 후 실행하면 확인할 수 있습니다.
사용자 삽입 이미지
실행 후 확인해 보면 10진수로 219가 출력되며, 이는 2진수 11011011로 위의 c와 동일합니다.

3. xor 연산자(^)
xor은 두 비트를 비교하여 서로 다른 값일 경우에만 1이 됩니다. or과 마찬가지로 위의 소스 에서 c = a & b; 부분을 c = a ^ b;로 변경하시고 컴파일 후 실행하면 확인할 수 있습니다.
사용자 삽입 이미지
실행 후 확인해 보면 10진수로 202가 출력되며, 이는 2진수 11001010으로 위의 c와 동일합니다.

4. not 연산자(~)
not은 위의 경우와는 달리 2개의 값에 대한 연산이 아니라, 1개의 값에 대한 연산을 하며, 1은 0으로 0은 1로 비트값을 반전시킵니다. 마찬가지로 위의 소스에서 c = a & b; 부분을 b = ~a;로 변경하시고 확인할 수 있습니다.
사용자 삽입 이미지
실행 후 확인해 보면 10진수로 202가 출력되며, 이는 2진수 01100110으로 위의 b와 동일합니다.

5. 우 쉬프트 연산자(>>)
우 쉬프트 연산은 숫자 만큼 bit를 우측으로 밀어 내고, 밀려 난 비트는 사라집니다. 좌측에 남은 비트는 0으로 채워집니다. 위의 소스에서 c = a & b; 부분을  c = a >> 3;로 변경하시고 확인할 수 있습니다.
사용자 삽입 이미지
위의 이미지를 보면 a를 오른쪽으로 3칸 밀고, 밀려난 좌측의 흑색 부분은 사라집니다.  그 후 비워진 앞부분(연두색)은 0으로 채워집니다.

실행 후 확인해 보면 10진수로 19가 출력되며, 이는 2진수 0001001로 위와 동일합니다.

5. 좌 쉬프트 연산자(>>)
좌 쉬프트 연산은 위의 우 쉬프트 연산과 방식이 동일하며 방향만 다릅니다. 위의 소스에서 c = a & b; 부분을  c = a << 3;로 변경하시고 확인할 수 있습니다.
사용자 삽입 이미지
위의 이미지를 보면 a를 왼쪽으로 3칸 밀고, 밀려난 우측의 흑색 부분은 사라집니다.  그 후 비워진 뒷부분(연두색)은 0으로 채워집니다.

실행 후 확인해 보면 10진수로 200이 출력되며, 이는 2진수 11001000로 위와 동일합니다.

7)  기타

그 외 이 전에 보았던 sizeof, 조건(a ? b:c), ',', '.', '->' 등이 있습니다.

이전장에서 보았던 sizeof는 변수 타입의 크기를 반환합니다.

a ? (b:c)는 a가 참(1)일 경우에는 b, 거짓(0)일 경우에는 c를 반환합니다. 이는 제어문에서 다시 언급하겠습니다. 그외의 것도 각각 함수, 구조체 언급 시에 설명하겠습니다.

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

7. C 함수 (function)  (4) 2007.06.15
6. 제어문  (0) 2007.06.14
4. 변수  (2) 2007.06.12
3. C 기초문법  (0) 2007.06.05
2. 소스코드, 컴파일, 링크  (6) 2007.06.04
AND

C에서 변수는 말 그대로 변할 수 있는 값을 의미하며, 변수는 메모리에서 값을 저장할 수 있는 저장소 입니다.  "[변수타입] [변수명];"와 같이 변수를 선언하고 사용할 수 있습니다.

int a;

위에는 a라는 int(정수)형 변수를 선언한 예 입니다.  int가 변수타입 ,  a가 변수 명입니다.

4.1 변수 타입
변수 타입은 문자, 정수, 부동소수점 등 변수에서 저장할 값의 종류를 의미합니다.  C에서는 아래와 같은 변수타입을 제공합니다.
사용자 삽입 이미지

1) char

char 는 1바이트 크기를 가지고 있으며, ASCII 코드의 한 문자 또는 0~255(-128~127)의 숫자를 저장할 수 있습니다. C에서 char의 한 문자는 홑따움표(')를 사용하여 표현하고, 문자열은 쌍따움표(")를 사용하여 표현합니다.


2) short, int , long

일반적인 정수를 표현하기 위한 변수형입니다. 흔히 int는 해당 시스템이 가장 빠르게 처리할 수 있는 크기라고 하며, 일반적으로 2 또는 4Byte의 크기를 가집니다.


3) float, double

float과 double은 부동소수점을 표현하는데 사용됩니다. 모든 데이터를 2진수로 인식하는 컴퓨터에서는 부동소수점을 처리할 때, 약간의 문제가 있습니다. 이 부분에 대해서는 후에 기회가 되면 다시 언급하겠습니다.


4) signed/unsigned

각 변수 타입 앞에는 signed 또는 unsigned 를 선언할 수 있습니다. 생략된 경우에는 signed로 간주합니다. signed와 unsigned는 음수를 포함할 지, 포함하지 않을지를 결정합니다.

1byte인  char를 예로 들면 256가지 경우의 수를 표현할 수 있습니다. 이는 양수만 사용할 경우에는 0~255까지 표현할 수 있지만, 음수도 포함할 경우에는 -128~127까지 표현할 수 있습니다.

4바이트인 int형일 경우에는 비트로 환산하면 4X8=32Bit, 즉 232 까지 표현할 수 있습니다.

참고로 기계가 인식할 수 있는 단위를 비트(bit)라고 합니다. 1bit는 0과 1 (On/Off)의 두가지 경우의 값을 나타낼 수 있습니다. 1 Byte는 8개의 bit로 이루어져 있습니다. 그러므로 2에 8승, 즉 256가지 값을 표현할 수 있습니다.

소스에서 char c = 9; 라는 코드는 실행 시 메모리에 변수 c를 위해 1Byte(8bit)를 할당하고 값은 9로 저장되는데 , 이 9는 컴퓨터에서는 이진수 00001001로 아래와 같이 메모리에 저장됩니다.
사용자 삽입 이미지
위의 2진수는 아래와 같이 10진수로 변환할 수 있습니다.
(0 X 27) + (0 X 26) + (0 X 25) + (0 X 24) + (1 X 23) + (0 X 22) + (0 X 21) + (1 X 20)

첫번째와 네번째만 1이므로 우측으로 부터 첫번째와 두번째 연산인 (1 X 23) + (1 X 20)만 유효하므로 9로 표현됩니다. 2진수를 이해하고 있어야 C에서 사용하는 쉬프트 나 비트 연산을 이해할 수 있습니다.


5) 변수 타입 크기

각 변수타입들의 크기는 일반적으로 아래의 출력내용과 비슷하나, 시스템에 따라서 약간 틀릴 경우도 있습니다. 아래의 코드로 사용하는 시스템에서 변수 타입별로 크기를 확인하실 수 있습니다.

#include <stdio.h>

int main(int argc, char* argv[])
{
    char c;
    short s;
    int n; 
    long l;
    float f;
    double d;

    printf("char: %d\nshort: %d\nint: %d\nlong: %d\nfloat: %d\ndouble: %d\n",      sizeof(c), sizeof(s), sizeof(n), sizeof(l), sizeof(f), sizeof(d));
   
return 0;
}

위의 소스를 컴파일 하고 실행보면, 아래와 같이 각 변수타입 별 바이트 수를 확인하 실 수 있습니다. 아래는 제 맥의 터미널에서 실행해 본 화면 입니다.
사용자 삽입 이미지


4.2 변수명

1) 작성 규칙

변수명은 용도에 따라서 아래의 규칙에 따라 개발자의 마음대로 정할 수 있습니다.
  • 알파벳 대소문자([A-Z][a-z]), 숫자([0-9]), 언더바(_)를 사용할 수 있습니다.
  • 첫 글자에 숫자가 올 수 없습니다.
  • 대소문자를 구별합니다.

>> 아래는 유효한 변수명의 예 입니다.
int myData; int my_data; int mydata_1;

>> 아래는 사용할 수 없는 변수명의 예 입니다.
int my data; int 3mydata;

>> 아래의 temp와 Temp는 서로 다른 변수로 인식 합니다.
int temp; int Temp;


2) 명명법

함수도 마찬가지지만 변수명은 소스를 이해하기 쉽게 만드는데 많은 영향을 줍니다.

윈도우 API 함수와 MFC에서 주로 사용되는 헝가리안 표기법이 있습니다. 변수의 타입과 용도를 알기쉽게 만들어 주는 표기법입니다. 간단하게 예를 들면 아래와 같습니다.

char*  g_pszAppName;
g_ 는 전역변수를 의미하며, p는 포인터, sz은 NULL로 끝나는 문자를 의미 합니다. 그뒤의 이름은 의미별로 대문자로 구별합니다. 이런식의 표기법은 소스 중간에서 g_pszAppName을 보더라도 쉽게 타입과 용도를 추측할 수 있습니다.

하지만 일반적으로 unix C 프로그래머들은 대문자를 define, 전역변수나 특수한 용도가 아니면, 많이 사용하지 않았습니다. 대소문자 구분 대신 "_"를 주로 사용하였습니다. 위의 예를 들면 아래와 같습니다.
char* app_name;

그외 java나 objective-c에선 아래와 같은 명명법을 선호합니다.
char* appName;

어떤 것이 좋은지는 저도 알 수 없고,  윈도우 프로그래머라고 해서 꼭 헝가리안 표기법을 사용하거나 unix 나 java 프로그래머라고 해서 꼭 위와 같이 하지는 않습니다.

하지만 한가지 확실한 점은 일관된 규칙으로 의미있는 변수명을 사용해야 소스의 이해 및 관리가 쉽습니다. 간단히 예를 들면 만약 남녀의 수를 합산하는 코드가 있다면, 1)의 소스 보다 2)의 소스가 이해하기가 더욱 쉽습니다.

1) a = b + c;
2) total_count = man_count + woman_count;


4.3 변수 선언 위치
C에서 변수는 함수의 처음 위치에서 선언되어야 합니다. 하지만 C++ 부터 변수를 함수내에 사용하는 곳에서 자유롭게 선언할 수 있게 되었습니다.

오래된 C 컴파일러는 아래의 좌측 이미지와 같이 함수에서 사용하는 모든 변수들은 함수가 시작되면서 선언되어야 합니다.  하지만 C++의 장점을 포용한 현재의 C 컴파일러들은 아래의 우측과 같이 실행코드 중간에 int j; 와 같이 변수를 선언하는 것을 허용합니다.
사용자 삽입 이미지 사용자 삽입 이미지


4.4 변수 유효 범위
변수는 선언되는 위치에 따라서 전역(global)변수와 지역(local)변수로 나누어 집니다.

#include <stdio.h>

int temp = 5;

int main(int argc, char* argv[])
{
    int temp = 3;

    printf("%d", temp);

    return 0;
}

위에서 int temp = 10; 으로 함수 밖에서 선언된 변수는 전역변수이며, 어느 곳에서나 사용될 수 있습니다.

main 함수 내의 int temp = 3; 으로 선언된 변수는 지역변수이며, main 함수 내에서만 사용할 수 있습니다. 

함수 내에서는 지역변수가 우선됨으로 위의 소스를 실행하면, 지역변수 temp의 3이 출력됩니다. main함수 내의 int temp =3; 라인을 삭제 후 컴파일 하면 전역변수 temp의 5가 출력됩니다.

전역변수와 지역변수의 큰 차이점은 전역변수는 어플리케이션 메모리 에서 힙(heap)이라 불리우는 곳에, 지역변수는 지역변수와 더불어 함수인자 등에 사용되는 스택(stack)에 위치 합니다.

스택은 시스템에 따라 제한된 일정한 크기를 가지고 있습니다. 그렇기 때문에 큰 배열 등의 대용량의 메모리가 필요한 경우에는 전역변수나 malloc등을 이용하여 힙영역에 위치 시키거나, 인자로 전달할 때는 포인터를 사용하여야 합니다.


4.5 static 변수
변수타입 앞에 static 이란 키워드를 사용할 수 있습니다. static 변수는 함수내에 지역변수로 선언 되더라도 스택이 아닌 힙 영역에 위치합니다. 그래서 한번 사용하고 값이 사라지는 일반 지역변수와는 달리 계속 그 값을 유지하고 있습니다.

#include <stdio.h>

void test1()
{
    int n = 0;
    printf("test1: %d\n", n);

    n = n + 1;
}

void test2()
{
    static int n = 0;
    printf("test2: %d\n", n);
   
    n = n + 1;
}

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

    test2();
    test2();
    test2();

    return 0;
}

위의 소스코드를 컴파일하고 실행하면, 아래와 같은 결과를 확인할 수 있습니다.
사용자 삽입 이미지
test1 함수의 지역변수 n은 함수가 호출될 때마다 스택에 새로 생성되며, 0으로 초기화 됩니다. 그래서 함수 마지막에 1을 더 해주어도 항상 0으로 출력됩니다.

test2 함수의 static 변수 n은  어플리케이션 실행 시에 0으로 초기화되어 힙영역에 위치하기 때문에, 함수가 재호출되어도 항상 그 값을 유지하고 있습니다.

이상 변수에 관해서 알아 보았는데, 몇 가지 언급 되지 않은 내용이 있습니다. 전역 변수에서 static 사용, extern 키워드 등이 있습니다. 이는 여러 소스 파일을 사용할 경우에 사용되며, 추후에 다시 언급하겠습니다.

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

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

아래는 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
2. 소스코드, 컴파일, 링크  (6) 2007.06.04
1. C언어 공부를 위한 준비  (9) 2007.06.03
AND

이번에는 C언어를 이용하여 실행할 수 있는 파일을 만드는 관정에 대해서 알아보겠습니다. 일반적으로 아래와 같은 과정으로 작성한 소스 파일이 실행파일로 만들어집니다.
사용자 삽입 이미지
지금부터 위의 내용에 대하여 알아 보겠습니다.


2.1 소스코드 작성

1) 소스 코드?

코드는 프로그래밍 언어에 따라 작성된 내용을 의미합니다. 일반적으로 많이 사용되는 코딩이란 말은 코드를 작성하는 작업을 말하며, 에디터에서 소스 파일을 작성하는 것을 의미합니다.

소스란 재료라는 본래 의미와 마찬가지로 컴파일을 하여 실행파일을 만들어 내기 위해 필요한 소스(재료)란 의미입니다. 최종 목적 파일을 만들기 위해 필요한 모든 파일들을 소스파일이라 하고, 일반적인 소스 파일은 C 코드가 들어 있는 파일을 의미합니다.

C에서 헤더파일은 *.h, 소스파일 *.c의 확장자를 갖습니다.


2) 통합 개발 툴

많은 개발툴들이 IDE(Integrated Development Environment)라고 불리우는 통합 개발 환경을 제공합니다. 이는 컴파일러, 링커, 에디터, 디버그, UI 에디터, 메뉴얼 등 개발에 필요한 도구들을 모아 놓은 개발툴입니다. 맥에서는 Xcode, 윈도우에서는 VisualStudio 등이 있습니다.

사용자 삽입 이미지
일반적으로는 개별 텍스트 에디터를 사용하는것 보다, IDE에서 제공하는 에디터를 사용하는 것이 가장 편리합니다.

좌측 이미지는 맥의 대표적인 개발환경인 Xcode의 모습이며, 개발에 편리를 주는 많은 기능을 내장하고 있습니다.




3) 디버깅

디버깅이란 프로그램 실행 시 나오는 각종 오류들을 제거하는 작업을 의미합니다. 이를 위해 많은 개발환경에서 디버거란 디버깅을 도와주는 편리한 툴들을 제공합니다.

아래는 Xcode에서 디버거를 실행중인 모습니다.  코드를 한 줄씩 또는 브레이크 포인트라고 불리우는 위치까지 실행시켜 가면서 각종 변수 값들과 메모리를 등을 확인할 수 있습니다.
사용자 삽입 이미지

사용하는 툴의 디버거 사용방법을 반드시 알아두어야, 원인을 찾기 어려운 오류를 추적하고 제거하는 시간을 단축할 수 있습니다.


2.2 컴파일
컴파일이란 작성한 소스파일을 이진파일로 바꾸어 주는 작업을 의미합니다. 쉽게 이야기 하면 사람이 작성하고 편집할 수 있는 소스 파일을 컴퓨터가 이해할 수 있도록 바꾸어 주는 작업을 말합니다.

1) 목적파일

C 컴파일러는 *.c 소스 코드를 컴파일 하여, 목적파일(object file)이라 불리우는 *.o, *.obj 등의 컴파일된 파일을 생성합니다. 이 파일들은 바로 실행을 할 수 없지만, 컴파일 다음 작업인 링크에 사용되어 실행 파일을 만듭니다.

아래 좌측은 소스 파일이며, 사람이 이해가 가능합니다. 우측은 컴파일한 목적파일로 짐작이 가는 문자들이 있지만 사람이 이해할 수 없습니다. 반대로 기계는 우측의 목적파일이 이해(처리)하기 쉽습니다.
사용자 삽입 이미지 사용자 삽입 이미지


2) 디버그 모드와 릴리즈 모드

사용자 삽입 이미지
대부분의 IDE들은 빌드 시 디버그 모드와 릴리즈 모드를 선택할 수 있습니다.

디버그 모드는 개발에 편리를 주기 위해서 많은 디버깅에 편리한 코드들이 들어갑니다. 그렇기 때문에 릴리즈 모드에 비해 실행파일도 크고 속도도 느리지만,  디버깅 작업을 하기에는 편합니다.

릴리즈 모드는 디버깅에 필요한 모듈을 제외되고 컴파일되기 때문에, 디버깅 모드에 비해서 실행파일 크기가 작고 속도가 빠릅니다.

개발시에는 편리를 위해 디버깅 모드로 작업을 하고, 최종 배포시에는 릴리즈 모드로 컴파일 하여 배포 합니다.


3) 오류(error)와 경고(warning)

컴파일을 하다 보면 오타와 잘못된 문법으로 인해 오류와 경고를 만나게 됩니다. 오류는 잘못된 코딩으로 인해 컴파일을 할 수 없는 경우를 의미합니다. 경고는 컴파일은 되지만 문제가 일어날 소지가 있을 경우에 나오는 메시지입니다.

사용자 삽입 이미지
좌측을 보면 오류 하나와, 경고 하나가 나오고 컴파일이 중지 되었습니다.

printf 함수 뒤에 있는 C 문법에 어긋난 hi로 인해 오류가 났습니다. 경고는 int a;라고 변수를 선언은 해놓고 사용은 하지 않았다는 경고입니다.



컴파일을 하기 위해선 경고가 있어도 가능하기 때문에 처음 프로그래밍을 시작하는 분들은 경고를 무시하는 경우가 있는데, 경고도 무조건 제거하여야 합니다. 경고를 제거하는 것은 예기치 못한 동작에 대한 위험을 줄이고, 안쓰는 변수 제거 및 확인, 정확한 형 변환 등 올바른 코딩습관을 가지게 해줍니다.

2.3. 링크
실행파일을 만들기 위해서는 컴파일 뒤에 링크 라는 작업이 필요합니다. 링크는 컴파일된 목적파일과 필요한 라이브러리를 이용해, 해당 시스템에서 실행할 수 있는 파일을 만듭니다.

일반적으로 링크는 아래와 같은 작업을 수행합니다.
사용자 삽입 이미지


1) 라이브러리

이전 소스와 같이 printf를 사용하면, 이 함수는 어딘가 반드시 구현되어 있어야 합니다. printf는 C 표준 라이브러리에 포함되어 있으며, 실행파일에 이 라이브러리도 포함되어야 실행시 printf를  호출하여 사용할 수 있습니다.

라이브러리는 자주 사용되는 함수, 모듈들을 미리 컴파일 시켜 놓은 목적파일의 집합체로 생각하시면 됩니다. C 함수들은 C 표준 라이브러리에 들어 있습니다.

라이브러리는 사용자도 만들 수 있으며, 자주 사용되는 코드들을 용도 별로 모아 라이브러리를 구축해 놓으면 다음 개발 시에 관련 파일의 복사나 재 컴파일 등의 작업 없이 편리하게 사용할 수 있습니다.


2) 라이브러리의  종류

여기서 한가지를 보면 항상 라이브러리가 실행파일에 포함된다는 것 입니다. 이는 실행파일 크기가 증가 한다는 이야기이고, 프로그램 실행 시 메모리도 많이 사용하게 된다는 의미입니다.

실행파일에 포함되는 라이브러리를 정적(static) 라이브러리라고 합니다. 같은 정적 라이브러리를 사용하는 프로그램이 많을수록 하드웨어, 메모리 같은 시스템 낭비를 초래 합니다. 그래서 나온 것이 공유(shared), 런타임(runtime) 라이브러리를 사용하는 것 입니다.

여러 실행파일들은 라이브러리에 대한 정보만 가지고 있고, 한 라이브러리를 같이 사용하기 때문에 위의 하드웨어나 시스템자원 사용에 있어 이익을 가지고 옵니다. 아래의 그림을 보면 공유 라이브러리를 사용하는 것이 정적 라이브러리 사용에 있어서 파일용량이나 메모리를 절약할 수 있음을 보여 줍니다.
사용자 삽입 이미지


3) 오류(error)

컴파일과 마찬가지로 링크도 오류과 경고를 알려 줍니다. 오류가 발생하면 실행파일은 생성되지 않습니다. 컴파일과 마찬가지로 링크도 모든 오류과 경고를 확인하고 제거하여야 합니다.

일반적으로 링크 오류는 소스코드에서 사용한 변수나 함수들을 링크시에 다른 목적파일 또는 라이브러리에서 실제 구현된 변수나 함수를 찾을 수 없을 경우 발생합니다.

사용자 삽입 이미지
좌측은 컴파일까지는 성공하였으나 링크 시 오류가 발생한 경우 입니다.

extern이란 예약어로 컴파일러는 something이란 변수가 어디선가 선언되어 있다고 예측하고 컴파일을 완료 합니다.

링크 시에는 something이 선언되어 있는 곳을 찾을 수 없기 때문에 오류를 내고 링크를 중지합니다.

관련 목적파일, 라이브러리를 링크 옵션에 포함시켰는지, 선언은 되었는데 구현은 되지 않았는지, 이름과 타입이 동일하진 여부를 확인하셔야 됩니다. (선언과 구현은 나중에 설명하겠습니다.)
이상 프로그래밍과 관련된 몇가지 자주 쓰이는 용어들을 살펴 보았습니다.  다음 장 부터 본격적으로 C 언어에 대한 설명을 시작하겠습니다.


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

6. 제어문  (0) 2007.06.14
5. 연산자  (0) 2007.06.13
4. 변수  (2) 2007.06.12
3. C 기초문법  (0) 2007.06.05
1. C언어 공부를 위한 준비  (9) 2007.06.03
AND

시작하면서...
처음엔 잘 모르시는 분들도 쉽게 따라 해 볼 수 있도록 튜토리얼을 만들어 볼려고 했습니다. 그런데  포스트를 계속 올리다 보니, C에 관해 질문해 오시는 분들도 많고 주위에서도 따라 해보기는 하는데 이해를 전혀 할 수 없다는 이야기를 들었습니다.

이런 이유로 프로그래밍이나 C언어에 관한 지식이 없으면 용어나 소스코드 작성 시 이해가 힘들기 때문에, 간단하게 이곳의 튜토리얼을 이해할 수 있는 정도의 C언어 강좌를 시작해 볼려고 합니다.

대상은 처음 C언어를 공부하시는 분들이며, 최대한 쉽게 설명해 보겠습니다.
 
1.1 C 언어의 역사와 특징
C는 1972년 Unix를 만들기 위해 Dennis Ritchie에 의해 만들어 졌습니다. 나온지 오래된 언어로 이 후 나온 언어들에 비해 불편한 점과 객체지향 프로그래밍을 구현하는데 어려움이 있지만, 아직까지 많이 사용되는 언어입니다.  

C는 오래된 역사와 시스템 프로그래밍과 같은 로우레벨 작업이 가능하고, 컴파일 되어 빠른 실행 파일을 작성할 수 있기 때문에 많은 플랫폼에서 사용되고 있습니다. 또한 게임 부터 각종 어플리케이션, 하드웨어 제어등 다양한 분야에서 사용됩니다.

현재 많이 쓰이는 C++, Objective-C, PHP등의 언어들이 C를 기본으로 하고 있고, C 문법은 거의 FM이라고 할수 있어 C를 기본으로 익히면 필요에 따른 프로그램 언어들을 배우기가 매우 용이 합니다.

이런 이유로 C는 처음 프로그램을 배우는 입문자에게 아직도 많이 권해 지고 있는 언어 입니다. 보다 자세한 C의 역사와 특징은 많은 곳에 언급되어 있으니 이곳에서는 이정도로 생략하겠습니다.


1.2 C 언어 공부하기
이곳에서는 간단하게 튜토리얼을 이해할 수 있을 정도의 기본적인 C 강좌를 할 것입니다. 체계적으로 C를 배우기 위해서는 관련서적을 구입하여 보실 것을 권장합니다.

1) 추천 서적

C와 관련하여 많은 서적이 있으니, 서점에서 직접 확인하여 보기 편하고 각자에게 맞는 서적을 구입하시면 됩니다. 일반적으로 많이 보는 C 관련 서적을 2개와 프로그래밍에 필요한 시스템에 관련된 기본 지식을 얻을 수 있는 책을 추천 합니다.

사용자 삽입 이미지
열혈강의 C 프로그래밍
윤성우 지음 / 프리렉

후배가 C를 배우고 싶다고 하여, 몇 년전 친구에게 요새 초보자가 보기에 괜찮은 C 서적이 뭐가 있는지 물어 보니 이 책을 추천해 주었습니다. 쉽게 설명되어 있어 C언어 공부를 시작하는 사람들이 보기에 가장 적당하다고 합니다.


사용자 삽입 이미지
C언어 프로그래밍
Brian W. Kernighan, Dennis M. Ritchie/대영사

1978년 나온 C의 바이블이라고 불리우는 The C Programming Language의 번역본 입니다. C를 배우기 위해 거의 필수적으로 봐야 할 서적이라고 할 수 있습니다. 하지만 바이블인 만큼 내용이 너무 교과서적(?)이라 위의 서적과 같이 병행하면서 보시면 좋습니다.


사용자 삽입 이미지
성공과 실패를 결정하는 1%의 프로그래밍 원리
HISAO YAZAWA(번역:예승철)/성안당

이전에 Inside the IBM PC라는 컴퓨터 시스템에 관련된 책이 있었습니다. 절판되지 않았다면 그 책을 추천했을 것이지만, 그와 비슷하고 심도는 조금 낮아진 것 같은 이 책도 적극 추천 합니다. C를 공부하기 전, 또는 공부하면서 이런 류의 책을 보시면 훨씬 이해가 쉽습니다.

위의 도서 이미지는 kangcom.com 에서 가지고 왔으며, 링크는 kangcom.com의 해당 도서로 링크되어 있습니다.


2) 보는 방법

책을 보실 때는 아래의 사항을 지키시면서 보시는게 좋습니다.

1. 백견불여일타
소스 코드는 백번 보는 것보다 직접 쳐보는 것이 더욱 효과적입니다. 책에 나오는 예제들은 무조건 직접 쳐보는 것이 좋습니다.

머리로 이해 하는 것도 중요하지만, 기본 문법과 자주 쓰이는 함수들이 습관화 되고 손에 익을 때 까지 가능한 한 많이 쳐보는 것이 좋습니다. 직접 타이핑을 하다 보면 오타가 나오고 컴파일을 위해 오류를 수정해야 하는데, 이 작업을 반복하면 반복할 수록 컴파일 전에 오류를 내는 습관을 줄일 수 있습니다.
 
2. 최소 2번이상 읽기
바이블로 여겨지는 책은 반드시 3번 정도 다시 읽어 보아야 합니다. 아는만큼 보인다고, 처음에 대충 이해를 하거나 잘 못 이해하고 넘어 갔던 부분들도 몇 번 다시 읽게 되면 작자가 설명하고자 하는 정확한 의미를 알 수 있습니다.

3. 꼼꼼하게 읽기
모든 책이 마찬가지 겠지만 대충 아는 것 같다고 그냥 넘겨 보면  안됩니다. 한 줄이지만 중요한 내용이 나올 수도 있고, 아는 내용이더라도 복습하는 의미로 꼼꼼하게 책을 읽는 것이 좋습니다. 빨리 책을 끝내는 것 보다 최대한 이해하도록 하고, 소스 코드 같은 경우에는 변경해 가면서 응용해 보는 것이 좋습니다. 


1.3 C 컴파일 환경 만들기
C 뿐만 아니라 다른 언어도 책만 보는 것 보다, 직접 타이핑을 해보고 컴파일을 하는 것이 더욱 이해가 용이합니다. C 코드를 테스트 해 볼수 있는 환경을 만들기 위하여, 맥/리눅스/윈도우에서 필요한 툴들을 알아보겠습니다.

1. Mac OS X

맥 OSX에서는 GCC, VI를 이용하여 C 소스코드를 작성하고 컴파일 할 수 있습니다. 간편하게 XCode를 이용할 수도 있습니다. Xcode는 이곳의 튜토리얼 1.1 Xcode 구하기에서 다운로드 및 설치에 대해 참고하실 수 있습니다.

Xcode를 이용하는 방법은 다음과 같습니다. 메뉴에서 File/New Project를 클릭합니다. 아래와 같이 Command Line Utility 항목에서 Standard Tool을 선택 하고 Next를 클릭한 후, 적당한 프로젝트명을 입력합니다.
사용자 삽입 이미지

위의 작업이 완료되면 Xcode에서 main.c를 열고 아래와 같이 책이나 이곳에 나오는 C 코드를 입력하고 테스트 해 보실 수 있습니다.
사용자 삽입 이미지

Xcode이외에 직접 gcc를 이용할 수 있습니다. 아래 2. Linux의 내용은 맥 OS X에서도 똑같이 적용이 가능합니다.


2. Linux

리눅스에는 GCC라는 C/C++ 컴파일러가 있습니다. 어떻게 보면 IDE (프로그램 통합 개발 환경)없이 리눅스 프롬프트 모드에서 직접 컴파일러나 링커를 실행 시키고 make를 이용하는 가장 원초적인 환경에서 작업 하는 것이 기본적인 이해에 좋습니다.

Linux(unix)는 C와 밀접한 관계에 있으므로, 대부분 C 컴파일러와 개발툴들이 포함되어 있습니다. 만약 사용하는 리눅스에 gcc가 설치되어 있지 않을 경우에는, 사용하는 리눅스 패키지에 있는 설치 툴들을 이용해 gcc 또는 developement, build 등으로 되어 있는 패키지를 인스톨 하시거나, 직접 다운로드 받아 설치하셔야 합니다.

에디터로는 vi, emacs등을 사용하실 수 있습니다. 아래는 맥의 BSD 유닉스 환경(아래의 모습은 리눅스와 완전 동일 합니다.)에서  hello, world를 편집하고 컴파일, 실행해 본 화면입니다.
사용자 삽입 이미지

사용자 삽입 이미지


3. MS Windows

윈도우즈 환경에선 대표적인 Visual C++이나 VS 2005가 있지만, 무료이고 용량도 작은 DEV-C++ 공개 컴파일러를 추천합니다.

VC의 경우에는 초보자가 공부를 위해 사용하기에 환경이 지나치게 복잡하고, 책들의 예제와는 다른 환경이나 메시지가 나올 수 있습니다. 예를 들면 strcpy 함수를 사용하면 strcpy_s로 사용하라고 경고 메시지를 내보냅니다. (경고는 끌 수 있습니다)

MS측에선 C 런타임 라이브러리의 안정성을 높이기 위해서 만들었다고는 하지만, 혼돈이 있을 수 있습니다. C언어 공부를 위해선 Dev-C++을 사용하는 것이 더 좋습니다. Dev-C++는  http://www.bloodshed.net/dev/devcpp.html 에서 다운 받으실 수 있습니다.

테스트를 위해선 설치를 하고 실행시킨 후, 메뉴에서 파일/새로만들기/프로젝트를 클릭합니다. 새로운 프로젝트에서 Console Application을 선택하고, 적당한 프로젝트 명을 입력하고 C를 선택한 후에 확인 버튼을 클릭합니다.
사용자 삽입 이미지

아래는 편집 화면이고 컴파일 후에 실행하여 결과를 확인할 수 있습니다.
사용자 삽입 이미지

위와 같이 소스를 작성 하면 컴파일 후 실행하면, 프롬프트 창이 너무 빨리 사라져 결과를 알 수 없습니다. 원래 main.c는 return 0; 위에 system("PAUSE"); 라는 라인이 자동으로 추가되어 있었는데, 이곳에서는 소스를 똑같이 보이기 위해 삭제했습니다.

실제 사용시에는 system("PAUSE"); 라인 위에 소스를 입력하시면, 결과를 확인할 수 있습니다.
이상 C를 공부하는데 필요한 내용과 어플리케이션에 관해 알아 보았습니다. 다음 장에서는 C 프로그램을 위한 기본적인 내용과 용어에 관해 설명하겠습니다.

'프로그래밍 강좌 > 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
AND