Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

문제 풀이

8장의 1번 문제를 풀어보도록 하자. hello world를 찍는 문제였다. char 하나에는 하나의 문자만 들어갈 수 있으므로 공백문자까지 포함 11개의 char를 선언해서 사용해야 한다.

#include <stdio.h>

int main(int argc, char **argv)
{
  char h='h';
  char e='e';
  char l='l';
  char o='o';
  char w='w';
  char d='d';
  char r='r';
  char space=' ';

  printf("%c%c%c%c%c%c%c%c%c%c%c\n",
          h,e,l,l,o,space,w,o,r,l,d);
  return 0;
}

배열

그럭저럭 hello world를 출력하긴 했지만, 지나치게 복잡하고 비효율적이라는 느낌이 들 것이다. C 에서 제공하는 원시 데이터 타입이란 것은 말그대로 최소한의 데이터 타입일 뿐으로, 좀 복잡한 프로그램을 만들기 위해서는 이것을 구조화 할 필요가 있다.

이것은 서류철을 만드는 것과 비슷한 과정이라고 보면 될것이다. 서류 하나하나를 일일이 관리하는건 상당히 짜증나는 일일 것이다. 그래서 우리는 (이름 등으로 정리된) 서류철을 만들고 다시 서류철을 보관하기 위한 캐비넷을 사용한다. 자료구조화 하는 것이다.

프로그램에서 사용되는 데이터(자료)들도 이렇게 구조화할 필요가 있다. 이것을 자료구조(:12)라고 한다. 이러한 자료구조들 중에서 가장 기본이 되는게, 배열이다.

배열은 서류철로 볼 수 있다. 서류는 최소단위가 되고, 이것을 나란히 쭉 정리해서 하나로 묶은게 서류철이다. 배열은 최소단위의 데이터를 일렬로 나열한 자료구조다. 서류철 같은 것을 보면 서류를 쉽게 찾기 위해서 인덱스를 한다. 보통은 이름으로 인덱스를 하는데, 배열역시 데이터를 쉽게 찾기 위해서 인덱스를 가지고 있다. 서류철과 다른점은 인덱스가 숫자로 되어 있다는 것 정도가 될 것이다. 이 숫자는 0 부터 시작한다.

다시 문제로 되돌아가보자. 문자열 hello world을 구조적으로 쉽게 표현할 수 있는 방법은 바로 배열을 이용하는 것이다. 이 배열의 최소단위는 문자 즉 char 데이터가 될 것이다. 이것을 위해서 아래와 같이 12 개의 char 형 데이터를 저장할 수 있는 공간을 만들어서 각각의 문자들을 집어 넣으면 된다. 이렇게 자료를 인덱스를 key로 해서 구조화 시킨것을 배열이라고 한다. 인덱스를 통해서 서류철에서 서류를 꺼낼 수 있듯이, 이 인덱스를 이용해서 값(value)를 가져올 수 있다.

attachment:array.png

C 뿐만 아니라, 거의 모든 프로그래밍 언어가 배열을 지원하고 있다. 그리고 배열의 첫번째 인덱스는 1이 아닌 0부터 시작한다. 인덱스가 0부터 시작하는 것은 우리의 일반적인 직관과는 거리가 있어서, 초기에 혼동하기 쉬우니 주의하기 바란다. 배열의 인덱스 뿐만 아니라, 컴퓨터는 모든 것의 첫번째는 0부터 시작한다. 예를 들어 메모리의 첫번째 주소는 0이다.

C에서 배열

배열은 자료구조이므로 구조화가 가능한 모든 데이터들은 배열로 만들어서 선언할 수 있다. 선언방법은 간단하다.
데이터타입  변수명 [크기];

예를들어 정수 데이터 10개를 저장하기 위한 배열은 아래와 같이 선언할 수 있다.
int  mydata[10];
매우 간단하다. 이제 우리가 목적으로 하는 hello world를 저장하기 위한 배열은 다음과 같이 만들면 될 것이다. 변수명은 hello 로 하도록 하겠다.
char hello[12];
여기에서 약간 의문시 되는 점이 있을 것이다. 우리가 저장하고자 하는 hello world는 11자인데, 12개의 공간을 가지도록 선언했기 때문이다. 이는 \0을 저장하기 위한 공간을 하나더 필요로 하기 때문이다. \0을 만나면 프로그램은 문자열이 여기에서 끝났다고 판단을 하게 된다. 만약 \0을 만나지 못한다면, 저장공간을 초과해서 \0을 만날때까지 계속 데이터를 읽을 려고 할 것이다. 프로그램은 바보라는 것을 명심해야 한다. 어디가 시작이고 어디가 끝인지를 명확하게 해주어야만 한다.

자 그럼 만들어진 배열 hello 에 hello world를 저장해 보도록 하자.

문제 double 데이터 16 개를 저장하기 위한 배열을 선언해 보자.

배열 선언과 정의

배열에 값을 저장하는 데에는 두가지 방법이 있다. 하나는 선언과 동시에 값을 대입하는 것이다.
#include <stdio.h>

int main()
{
    char hello[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
    printf("%s\n", hello);
}
중괄호 "{","}" 사이에 각각의 원소 데이터를 넣으면 된다. 데이터와의 구분은 ,을 통해서 이루어진다. 1부터 5까지 저장하는 int 형 배열은 다음과 같이 선언할 수 있을 것이다.
int data[5] = {0, 1, 2, 3, 4, 5};

값이 여러개가 중복이 될 경우가 있을 수 있다. 예를 들어 배열의 크기가 100인데, 모든 값을 1로 해서 선언하고 싶을 때가 있을 것이다. 이경우 ,뒤에 데이터를 쓰지 않으면 된다. 그럼 가장 마지막 데이터로 배열의 끝까지 채워지게 된다.
int data[100]={0,};

아래의 프로그램을 실행시키고 결과를 확인해 보도록 하자.
#include <stdio.h>

int main()
{
    char hello[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
    int data[100] = {0,};
    printf("%s\n", hello);
    printf("%d\n", data[2]);
}

단 문자열의 경우 예외적으로 괄호 {}를 사용하지 않고, 쌍따움표를 이용해서 직접 선언할 수 있도록 지원하고 있다. 위의 코드는 아래와 같이 좀더 간단하게 표현할 수 있다.
    char hello[12] = "hello world\0";

배열의 데이터에 접근

서류철에서 원하는 서류에 접근하기 위해서 인덱스를 사용하듯이, 배열역시 인덱스를 통해서 접근할 수 있다. 이 인덱스는 0부터 시작되는 정수로 배열첨자라고 부르기도 한다. 접근 방법은 간단하다. 괄화 [] 안에 꺼내오기 원하는 데이터의 배열첨자를 넣어주기만 하면 된다.
char hello[12] = {'h','e','l','l','o',' ','w','o','r','l','d','\0'};
printf("%c", hello[4]);
아래의 배열 이미지를 보면 hello[4] 가 o를 가져오리란걸 쉽게 예상할 수 있을 것이다.

attachment:array.png

대입연산자를 이용하면 배열의 원하는 위치에 데이터를 쓰는 것도 가능하다. hello[4]의 값을 'w'로 바꾸어보자.
hello[4] = 'w';

이제 이전에 배웠던 루프문을 이용해서, 변수 hello 의 값을 출력하는 프로그램을 만들어 보자.
#include <stdio.h>

int main()
{
    char hello[12] = "hello world\0";
    int i = 0;
    for (i = 0; i < 12; i++)
    {
        printf("%c", hello[i]);
    }
    printf("\n");
}

잘못된 배열첨자의 사용

배열의 크기가 12 인데, 이를 초과해서 데이터를 집어 넣거나, 배열첨자를 초과해서 데이터를 가져오는 경우를 생각해 보자.

유닉스는 다중 사용자, 다중 프로세스를 지원하고 있다. 이말은 동시에 여러개의 프로그램들이 돌아갈 수 있음을 의미한다. ps(:12)는 현재 실행중인 프로세스(:12)의 목록을 보여주는 유닉스(:12) 프로그램이다. ps 를 이용해서 현재 떠있는 프로세스의 목록을 알아보도록 하자.

]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 Jan30 ?        00:00:35 init [3]
root         2     1  0 Jan30 ?        00:00:00 [keventd]
root         3     1  0 Jan30 ?        00:00:00 [kapmd]
root         4     1  0 Jan30 ?        00:00:00 [ksoftirqd_CPU0]
root         5     1  0 Jan30 ?        00:04:44 [kswapd]
root         6     1  0 Jan30 ?        00:00:00 [bdflush]
root         7     1  0 Jan30 ?        00:00:04 [kupdated]
root         8     1  0 Jan30 ?        00:00:00 [mdrecoveryd]
root        12     1  0 Jan30 ?        00:08:54 [kjournald]
root        71     1  0 Jan30 ?        00:00:00 [khubd]
root      1181     1  0 Jan30 ?        00:00:00 [kjournald]
root      1480     1  0 Jan30 ?        00:01:11 syslogd -m 0
root      1484     1  0 Jan30 ?        00:00:00 klogd -x
rpcuser   1521     1  0 Jan30 ?        00:00:00 rpc.statd
root      1607     1  0 Jan30 ?        00:00:00 /usr/sbin/apmd -p 10 -w 5 -W -P
root      1644     1  0 Jan30 ?        00:05:22 /usr/sbin/sshd
root      1658     1  0 Jan30 ?        00:00:00 xinetd -stayalive -reuse -pidfil
root      1671     1  0 Jan30 ?        00:00:16 crond
daemon    1693     1  0 Jan30 ?        00:00:00 /usr/sbin/atd
root      1711     1  0 Jan30 tty1     00:00:00 /sbin/mingetty tty1
root      1712     1  0 Jan30 tty2     00:00:00 /sbin/mingetty tty2
... ...
아무리 간단한 유닉스 시스템이라고 하더라도 최소한 30개 이상의 프로세스가 떠있을 것이다. 그렇다면 해결해줘야 할 문제가 있다. 프로세스는 메인메모리 상에서 실행되고, 자신이 사용하는 데이터를 역시 메모리에 올려놓고 읽거나 쓰게 된다. 그런데 다른 프로세스가 자신의 메모리 영역을 침범하면 안될 것이다. 유닉스 운영체제(:12)는 프로세스를 관리하면서, 동시에 각 프로세스가 다른 프로세스의 메모리영역을 침범하는지를 감시를 한다. 만약 다른 프로세스의 메모리 영역을 침범했다면, 운영체제는 프로세스를 강제로 종료시켜버리게 된다.

우리는 아주 간단하게 이런 프로그램을 작성할 수 있다.
#include <stdio.h>

int main()
{
    char hello[12] = "hello world!! My site is Joinc";
    int i = 0;
    printf("%s", hello[i]);
    printf("bye bye\n");
}

이 프로그램은 char 형의 배열인 hello 를 위해서 12의 크기를 할당했다. 그러나 12byte를 초과하는 문자열을 집어 넣고 있음을 알 수 있다. 이 프로그램을 컴파일 해서 실행시키면 다음과 같은 에러메시지가 떨어질 것이다. 프로그램의 이름은 array_test 로 하겠다.
# ./array_test
Segmentation fault
printf("%s", hello[i]); 를 하면서, 자신에게 할당되지 않은 영역을 읽어 들이려고 시도를 하는 중에, 다른 프로세스가 사용하는 메모리 영역을 침범했고 이 때문에 Segmentation fault메시지를 출력하면서 강제 종료되어 버렸다. bye bye\n은 출력조차 되지 못했다.

attachment:array2.png

앞으로 많은 프로그램을 작성할 건데, 가장 흔하게 볼 수 있는 에러메시지가 Segmentation fault가 될 것이니, 지금 부터 익숙해지기를 바란다. 인간은 서류철의 용량을 초과해서 서류가 들어오면, 억지로 용량을 늘리던지, 안되면 풀로붙이던지 하는 등의 유도리를 발휘해서 업무를 처리할 수 있지만, 우리의 컴퓨터는 그렇게 똑똑하지 못한점 이해를 해주어야 한다. 프로그램을 작성할 때는 할당한 공간을 초과했는지, 제대로 사용했는지를 엄격하게 검사해야 한다.

위의 프로그램은 제대로 수행되는 경우도 있을 것이다. 왜냐면 쓸려고 하는 메모리 영역이 다른 프로세스에 의해서 사용되지 않는 자유상태일 수도 있기 때문이다. 이 경우에는 다른 프로세스 메모리 영역을 침범한 행위는 아니므로, 에러가 발생하지는 않을 것이다. 물론.. 제대로된 결과가 나올 것이라는건 보장할 수가 없다. 자유상태이기 때문에, 프로세스가 실행되고 있는 도중에, 다른 프로세스에게 해당 메모리 영역이 할당되어버릴 수 있기 때문이다. 이럴 경우 Segmentation fault 를 출력하면서 종료되어 버릴 것이다.

문자열의 출력

다시 문제로 되돌아가 보자. for 문과 배열을 이용해서 좀 더 쉽게 문제를 풀기는 했지만, 문자열 하나 출력할려고 루프문까지 사용하는건 합리적이지 못하다. printf 함수를 이용하면 쉽게 이 문제를 풀 수 있다. 이 함수는 주어진 변수를 형식화해서 출력할 수 있도록 도와준다.
#include <stdio.h>

int main()
{
    char hello[12] = "hello world\0";
    printf("%s", hello);
}
이걸로 문제는 간단히 해결됐다. "%s" 는 주어진 인자를 문자열로 인식해서 출력하는 포맷 옵션이다.

문제 char 배열을 받아들여서 문자열로 출력하는 함수를 작성해보자.

문자열 복사

문자열을 배열에 복사하는 간단한 방법에 대해서 생각해보자. 다음과 같이 코드를 작성하면 어떻게 될까.
int main()
{
    char hello[12];
    hello = "hello world\0";
}
문제 없을 거라고 생각할 수도 있지만, 컴파일 하면 아래와 같은 에러메시지와 함께 컴파일 실패하게 된다.
# gcc -o array array.c
array.c: In function `main':
array.c:6: incompatible types in assignment
대입연산자는 같은 타입에 대해서만 허용되는 연산자이기 때문에, 배열에 문자열을 대입하는건 C 문법에 어긋나기 때문이다. 결국 문자열을 처리하기 위해서는 별도의 함수를 만들어서 사용하는 수 밖에 없다. 다행히 C 는 표준 라이브러리 형식으로 몇개의 유용한 문자열 처리 함수를 제공한다. strcat(3), strcpy(3)와 같은 함수가 문자열 처리를 위한 대표적인 함수다. 다음은 strcpy 함수를 이용해서 문자열을 복사한 프로그램이다.
#include <stdio.h>

int main()
{
    char hello[12];
    strcpy(hello,"hello world\0");
    printf("%s\n", hello);
}

문제풀이

문제 char 배열을 받아들여서 문자열로 출력하는 함수를 작성해보자.

문제는 다양한 방식으로 풀 수 있을 것이다. 아래는 다양한 방식중 하나이다.
#include <stdio.h>

void print_string(char a[])
{
    int i=0;
    while(a[i] != '\0')
    {
        printf("%c", a[i]);
        i++;
    }
}
int main()
{
    char hello[12] = "hello world\0";
    print_string(hello);
}