본문 바로가기

컴퓨터/언어,프로그래밍

[C언어] C언어 배열,문자,포인터

C 프로그래밍


<배열, 문자열, 포인터>


1. 개 요

  포인터 연산과 포인터의 다양한 사용법은 C 언어의 특징 중 하나이다. 특히 중요한 점은 포인터가 배열의 주소 대신 사용될 수 있다는 것이다. 배열은 첨자변수를 사용하며 같은 종류의 데이터 값을 여러 개 나타낼 수 있다. 포인터의 또 다른 특징으로 함수 호출시 변수의 주소를 전달함으로써 그 변수의 값을 바꿀 수 있다는 점을 들 수 있다. 즉 다른 언어의 주소에 의한 호출(call-by-reference)과 같은 효과를 갖게 된다. 문자열은 문자의 배열이며  배열이름은 그 자신이 포인터이고, 문자열은 단지 문자들의 배열이다. 이러한 이유로 배열, 문자열 및 포인터의 개념은 상호 관련된다. 포인터는 기억장소 내의 어떤 요소에 대한 주소이다. 대부분의 언어와는 달리 C 언어는 포인터 계산을 할 수 있다. 게다가 포인터 표현식을 아주 유용하게 이용할 수 있기 때문에 포인터 계산은 이 언어의 장점 중 하나이다. 그러나 자세한 사항들을 모두 이해하기 전에는 초심자들은 포인터를 이용하는 데 있어서 다소 어려움을 경험할 것이다. 이번 보충학습을 통해서는  그러한 미묘한 문제들을 기본적으로 다루고자 한다.

2. 배      열

  배열은 같은 형을 갖는 다수의 변수가 요구될 때 이용한다. 예를 들어 선언

      int x[3];

은 int형의 3개의 변수를 할당하는데, 참조는 x[0], x[1], 그리고 x[2]로 할 수 있다. 항상 배열의 인덱스 또는 첨자는 0에서 시작한다.

  다음의 프로그램은 5개의 점수를 읽어 합을 계산하고, 정렬(sort)하여 인쇄하며, 점수들의 합과 평균값을 인쇄하는 것이다.

    #define  CLASS_SIZE  5

    main()

    {

    int i, j, score[CLASS_SIZE], sum = 0, temp;

        printf("\n입력 %d 점수: “, CLASS_SIZE);

        for(i = 0; i < CLASS_SIZE; ++i) {

      scanf("%d", &score[i]);

           sum += score[i];

        }

    /* 점수의 버블분류 */

        for(i = 0; i < CLASS_SIZE - 1; ++i)

        for(j = CLASS_SIZE - 1; i < j; --j)

                if(score[j-1] < score[j]) {

                temp = score[j-1];

                    score[j-1] = score[j];

                    score[j] = temp;

                }

         printf("\n\n 분류된 점수:\n");

         for(i = 0; i < CLASS_SIZE; ++i)

         printf("\n score[%d] = %7d", i, score[i]);

         printf("\n\n%23d%s\n%23.1f%s\n\n",

         sum, "은 모든 점수의 합계“.

         (double) sum / (double) CLASS_SIZE,

             "은 반 평균“);

         }


  이 프로그램을 실행하면, 프롬프트

    입력 5 점수:

가 화면에 나타날 것이다.

  만약 정수 68, 43, 97, 51, 77이 입력되면 화면에서 다음과 같은 출력을 볼수 있게 된다.


    입력 5 점수: 68 43 97 51 77


    분류된 점수:

        score[0] =    97

        score[1] =    77

        score[2] =    68

        score[3] =    51

        score[4] =    43


                336 은 모든 점수의 합계

                67.2 은 반 평균

  프로그램에서 버블정렬은 점수를 정렬하기 위해 이용된다. 이 구조는 전형적으로 내포된 for 루프에서 구성된다. 이때 한쌍의 요소에 있어 순서를 비교하는 것은 내부루프의 몸체에서 수행된다. 만일 비교하고자 하는 요소들의 순서가 뒤바뀌어 있다면 그 값은 교환된다. 이러한 교환은 아래의 코드에 의해 수행된다.

     temp = score[i];

     score[i] = score[j];

     score[j] = temp;

  첫 번째 문장에서 변수 temp는 score[i]의 값을 일시적으로 저장하기 위해 이용한다. 다음 문장에서 score[i]의 기억장소에 저장된 값은 score[j]의 값으로 갱신된다. 마지막 문장에서 score[j]의 값은 temp에 있는 원래의 score[i]값으로 갱신된다. 주어진 자료를 사용하여 프로그램을 손으로 풀어 보면 2개의 내포된 for 루프구조의 버블정렬이 어떻게 정렬된 요소를 갖는 배열을 얻는지 알수 있을 것이다.  버블정렬이라는 이름은 바깥 루프의 각 단계에서 작업이 끝난 값들 중 원하는 값이 자기순서의 위치에 찾아간다는 데서 유래한 것이다. 다른 정렬기법들은 보는 데는 별 문제가 없다. 그러나 항목의 수가 많고 코드가 반복적으로 사용된다면 프로그램의 효율성은 굉장히 중요한 고려사항이 된다. 마지막 printf()문에서 인수로 나타나는 식

    (double) sum/(double) CLASS_SIZE

은 캐스트(cast) 연산자를 이용한 것이다.

  문장에서 (double) sum과 (double) CLASS_SIZE의 작용은 sum의 int값을 double로 기호상수 CLASS_SIZE의 int값을 double로 전환시키는 것이다. 캐스트 연산자는 나누기 연산자보다 높은 우선순위를 갖기 때문에 나눗셈이 수행되기 전에 캐스트가 수행된다. 만일 캐스트가 이용되지 않았다면 정수 나눗셈이 수행되어 소숫점 부분은 없어지게 된다.

3. 문 자 열

스트링(strings)이라고 말하는 문자열은 단지 문자의 배열이다. 다음 프로그램은 문자열의 사용법을 자세히 설명해 준다.

    /*** have a nice day ***/

    #define     LINESIZE    100

    main()

    {

    char     c, line[LINESIZE];

    int       i;

    printf("\nhi! what is your name? ");

    for(i = 0; (c = getchar()) != '\n'; ++i)

        line[i] = c;

    line[i] = '\0';

    printf("\nnice to meet you ");

    for(i = 0; line[i] != '\0'; ++i)

    putchar(line[i]);

    printf(".\nyour name spelled backwards is ");

    while(i != 0)

        putchar(line[--i]);

    printf(".\n\nhave a nice day!\n\n");

    }


이 프로그램이 실행되면 라인

    hi! what is your name?

이 인쇄된다. 이것은 사용자에게 응답을 촉구하므로 ‘프롬프트’라 불린다. 그러한 메시지가 없으면 사용자는 프로그램이 입력을 요구하는지 알지 못할 수도 있다. 만일 사용자가 Alice B. Carole이라 답하면 화면에는 아래와 같이 나타난다.

    hi! what is your name? Alice B. Carole


    nice to meet you Alice B. Carole.

    your name spelled backwards is eloraC .B ecilA.


    have a nice day!


프로그램 nice-day의 분석

    #define    LINESIZE    100

  이 문장은 line으로 지정된 문장배열의 최대크키를 지정한다. 이 프로그램에서 사용자는 100문자 이상의 문자를 타자하지 않는다고 가정하자.

    char    c, line[LINESIZE];

    int      i;

c는 char형 변수이다. line은 LINESIZE의 값에 해당되는 개수의 요소를 갖는 char형 배열이다. i는 int형 변수이다.

    printf("\nhi! what is your name? ");

  이것은 프롬프트이다. 프로그램은 지금 캐리지 리턴이 뒤따르는 이름을 타자하기를 기다린다.

    for(i = 0; (c = getchar()) != '\n'; ++i)

    line[i] = c;

  변수 i는 0으로 치환된다. getchar()는 한 문자를 얻어 c에 치환한다. 그리고 newline 문자인가 검사한다. 그렇지 않다면 c의 값이 배열원소 line[i]에 치환하고 i를 하나 증가시킨다. for 루프는 newline 문자를 입력받을 때까지 반복적으로 실행된다.

    line[i] = '\0';

  for 루프가 끝난 다음 널 문자 \0이 원소 line[i]에 치환된다. 관례적으로 모든 문자열은 널 문자로 끝난다. 문자열 처리하는 printf()와 같은 함수는 널 문자 \0을 문자열의 끝을 나타내는 표기로 이용한다.

기억장소에서 배열 line은 다음과 같다.

A

l

i

c

e

 

B

 

C

a

r

o

l

e

\0

0   1  2   3  4   5  6   7  8   9  10 11  12 13 14  15 16      99

    printf("\nnice to meet you ");

  line을 프린트하면 다음과 같다.

    nice to meet you

    for(i=0; line[i] != '\0'; ++i)

    putchar(line[i]);

  line[0]으로 시작되는 배열 line의 각 원소들은 문자열 끝표시(\0)를 만날 때까지 프린트된다.

    printf(".\nyour name spelled backwards is ");

  마침표는  앞 라인의 끝에 프린트된다.

    while(i != 0)

    putchar(line[--i]);

 

  이 while 루프의 시작부분에서 i의 값은 15이다. (캐리지 리턴이 뒤따르는 “Alice B. Carole"이 타자되었다고 가정). 식 --i는 먼저 i의 값을 1씩 감소시킨 뒤 이 새로운 값을 식에 치환한다. 이 루프는 배열에 저장된 문자를 거꾸로 프린트하는 작용을 한다.

    printf(".\n\nhave a nice day!\n\n");

  마침표는 앞 라인의 끝에 프린트한다.

4. 포 인 터

  포인터는 단지 기억장소 내의 어떤 것에 대한 주소이다. 배열이름은 그 자체가 포인터이기 때문에 배열과 포인터의 사용은 밀접하게 관련된다. 다음 프로그램은 이들의 관계를 설명하기 위해서 작성되었다.

    main()

    {

    char    c = 'A', *p, s[100], *strcpy();

    p = &c;

    printf("\n%c%c%c", *p, *p+1, *p+2);

        s[0] = 'A'; s[1] = 'B'; s[2] = 'C'; s[3] = '\0';

        p = s;

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

        strcpy(s, "\nshe sells sea shells by the seashore");

        printf("%s", s);

        p += 17;

        for(; *p != '\0'; ++p) {

       if(*p == 'e')

         *p = 'E';

           if(*p == ' ');

             *p = '\n';

        }

        printf("%s\n\n",s);

    }

   이 프로그램의 결과는 다음과 같다.

    ABC

    ABCABCBBC

    she sells sea shells by the seashore

    she sells sea shElls

    by

    thE

    sEashorE


프로그램 ABC의 분석


    char    c = 'A', *p, s[100], *strcpy();

  c는 char형 변수로서 값 ‘A'로 초기화된다. p는 char에 대한 포인터형 변수이다. s는 100개의 원소를 갖는 char형 배열이다. strcpy()는 char에 대한 포인터를 반환하는 표준 라이브러리 함수이다. 만일 함수에서 int형이 아닌 다른 값을 반환한다면 반환되는 값의 형이 선언되어야 한다.

    p = &c;

  기호 &는 주소연산자이다. 식 &c의 값은 변수 c의 메모리상에서의 주소이고, c의 주소가 p에 치환된다. 즉, p는 c에 대한 포인터이다.

    printf("\n%c%c%c", *p, *p+1, *p+2);

  형식 %c는 식의 값을 문자를 출력하는 데 사용된다. 기호 *는 참조 또는 간접연산자이다. 식 *p는 p가 가리키는 것의 값을 갖는다. p는 c를 가리키고 c는 값 ‘A'를 가지고 있으므로, 이것은 *p의 값이고 A가 인쇄된다. 식 *p + 1의 값은 *p의 값에 1을 더한 것이기 때문에 B가 출력된다. 식 *p + 2의 값은 2를 더한 것이기 때문에 C가 출력된다.

    s[0] = 'A'; s[1] = 'B'; s[2] = 'C'; s[3] = '\0';

  이들 문장의 s의 첫 번째부터 세 원소에 ‘A', 'B', 'C' 값이 각각 치환된다. 네 번째 원소는 문자열 끝표시 ’\0'이 치환된다.


A

B

C

\0

                             0   1  2   3  4       99

    p = s;

  배열이름 s는 그 자신이 포인터이다. 우리는 s가 s[0]를 가리키거나, s[0]의 주소로서 배열의 기본주소를 이룬다고 간주한다. 포인터 s는 포인터 변수 p에 치환된다. 여기서 주의해야 하는 것은 비록 s가 포인터이지만, 포인터 변수가 아니고 오히려 포인터 상수라는 것이다. 문장

    s = p;

는 문법에러를 야기시킨다.

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

  s는 문자열형태로 프린트되기 때문에, ABC가 프린트 된다. p는 문자열 형태로 프린트되므로 ABC가 프린트된다. p는 s[0]을 가리키므로, p+1은 s[1]을 가리킨다. 그리고 식 *(p+1)의 값은 p+1이 가리키는 것의 값으로서, s[1]의 값이다. 이 값은 문자열의 형태로 찍힌다. p+1이 s[1]을 가리키므로 연속된 문자들이 \0을 만날 때까지 프린트되어 BC를 출력한다.

    strcpy(ptr1, ptr2)

  strcpy()는 표준 라이브러리 함수로서 char에 대한 두 개의 포인터를 인수로 가진다. 두 번째 인수에 의해 가리켜진 문자열이 첫 번째 인수가 가리키는 위치로 시작되는 기억장소에 복사된다. 이때 널 문자를 포함한 모든 문자가 복사되어진다. 첫 번째 인수가 가리키는 공간이 모든 문자를 복사하기에 충분하다고 가정한다. 이것은 한 문자열을 다른 것으로 복사하는 것이다.

    “\nshe sells sea shells by the seashore"

  프로그램에서 이 상수문자열은 strcpy()의 두 번째 인수로 나타낸다. 다른 문자열처럼 상수문자열도 char에 대한 포인터이다. 이것은 자신의 첫 번째 원소를 가리키는데, 이 경우에는 newline 문자이다.

  상수문자열은 널 문자로 끝난다. 따라서 널 문자열 “ ”도 \0이라는 한 문자를 포함한다.

    strcpy(s, "\nshe sells sea shells by the seashore");

  두 번째 인수문자열이 문자열 s로 복사된다.

    printf("%s", s)

문자열 s는 문자열형태로 출력되어지는데, newline을 시작으로 다음과 같이 출력된다.

    she sells sea sells by the seashore

    p += 17;

  p의 포인터 값이 증가했기 때문에 p는 처음위치에서 17문자를 이동한 것에 대응하는 기억장소를 가리키게 된다. p가 이전에 s[0]의 장소를 가리켰으므로, s[17]의 장소를 가리킨다(이것은 shells 안의 문자 e이다).

    for( ; *p != '\0'; ++p) {

    if(*p == 'e')

      *p = 'E';

        if(*p == ' ');

          *p = '\n';

    }

 

  p가 가리키는 값이 ‘\0’이 아닌 한 for 루프의 본체가 실행된다. 만일 p가 가리키는 값이 e라면 ‘E'로 바뀌고, ’ ‘라면 ’\n'로 바뀐다.

    printf("%s\n\n", s);

  변수 s는 두 개의 newline 문자가 뒤따르는 문자열의 형태로 출력된다.  이전의  for루프가 s의 몇 개 요소의 값을 바꾸었기 때문이다. 다음과 같이 출력된다.

    she sells sea shElls

    by

    thE

    sEashorE


  배열, 문자열, 그리고 포인터들은 밀접한 관계가 있다.

    char   *p, s[100];

과 같은 선언은 char에 대한 포인터로서의 식별자 p와 char형의 100개 원소로 갖는 배열로서의 식별자 s를 생성한다. 배열이름은 그 자체가 포인터이기 때문에 p와 s 둘 다 char에 대한 포인터이다. 그러나 p는 변수 포인터인 반면에, s는 s[0]를 가리키는 상수 포인터이다. 문장

    p = s;

은 문법적으로 옳지만 문장

    s = p;

은 문법에러를 야기시킨다. 두 식

    s[i]와    *(s+i)은 같다.

  즉, 식 s[i]는 배열의 i번째 요소의 값을 갖게 되고 s+i는 s로부터 i번째 문자위치를 가리킨다. 그런데 *연산자는 가리킨 위치의 값을 얻는데 이용되기 때문에 같게 되는 것이다. 같은 형식으로

    *(p+i)와   p[i]도 같다.

  식 ++p는  p가 p+1의 값으로 대치되므로 의미가 있지만, s는 상수 포인터이기 때문에 수식 ++s는 틀리다는 것에 주의하자.

  이 보충강좌에서  마지막 예제로서 다음과 같은 작업을 생각해 보자.

  1. 파일에서 연속적인 단어를 찾는다.

  2. 각 단어에서 소문자를 대문자로 쓴다.

  3. 각 단어의 문자들의 개수를 셈한다.

  4. 관련된 정보를 갖는 단어를 리스트로 인쇄한다.

  다음 프로그램은 이 작업들을 해결하였다. ‘단어’의 정의는 공백으로 분리되어지는 문자들의 문자열이라 하자.

    #include    <stdio.h>

    #define     MAXWORD    100

    main()

    {

    char    *find_next(), word[MAXWORD];

        int      char_cnt = 0, word_cnt = 0, word_length = 0;

        while(find_next(word) != NULL) {

      capitalize(word);

           ++word_cnt;

           word_length = strlen(word);

           char_cnt += word_length;

           printf("\n%12d    %s", word_length, word);

   }

        printf("\n\n%12d characters in %d words\n\n",

            char_cnt, word_cnt);

    }

    char *find_next(word)

    char    word[];

    {

        int    c, i;

        while((c = getchar()) == ' ' || c == '\n' || c == '\t')

           ;  /* 공백을 뛰어 넘김 */

        if(c != EOF) {

           i = 0;

           while(c != ' ' && c != '\n' && c != '\t' && c != EOF) {

            word[i++] = c;

                c = getchar();

            }

           word[i] = '\0';

           return (word);

        }

         else

         return(NULL);

    }

    capitalize(p)

    char     *p;

    {

    for( ; *p != '\0'; ++p)

        if('a' <= *p && *p <= 'z')

          *p += 'A'-'a';

    }

  이 프로그램을 컴파일하고 파일 list_words에 실행가능코드를 기록하였다고 가정하자. 만일 아래와 같은 내용을 갖는 sea_shells라는 파일을 생성하였다고 하자.

    she sells sea shells by the seashore

UNIX 명령어

    list_words < sea_shells

을 수행하면 화면에 다음과 같이 나타난다.

    3    SHE

    5    SELLS

    3    SEA

    6    SHELLS

    2    BY

    3    THE

    8    SEASHORE

   30  characters in 7 words

  단지 몇 가지 새로운 개념이 이 프로그램에 제시되었다.


list_words 프로그램의 분석

    char    *find_next(), word[MAXWORD];

   함수 find_next()는 char에 대한 포인터를 반환하도록 선언되었다. 식별자 word는 MAXWORD 개의 원소를 갖는 배열이다.

    word_length = strlen(word);

  함수 strlen()는 표준 라이브러리에 있다. 이 함수는 char에 대한 포인터를 인수로 문자열의 문자개수를 int형으로 반환한다.

    char *find_next(word)

    char    word[];

    {

      ․․․․․

    }


  이 문장은 함수 find_next()를 정의한다. 머리부의 char *는 컴파일러에게 문자로의 포인터를 반환하는 함수임을 알려준다. 머리부의 두 번째 라인은 char로의 포인터형인 인수 리스트에서 식별자 word를 선언한 것이다. 계속되는 것은 함수 몸체부의 코드이다.

  첫 번째 공백은 통과되고, 만일 문장 c의 값이 EOF가 아니면 공백문자 또는

EOF 문자를 만날 때까지 계속되는 문자들이 word의 원소로 모여진다. 그 시점에서 \0이 문자열의 끝을 표시하기 위해 첨가되며 포인터값 word가 반환된다.

만일 문자 c의 값이 EOF이면 포인터값 널이 반환된다.

    capitalize(p)

    char    *p;

    {

      ․․․․․

    }

  이 문장은 함수 capitalize()를 정의한다. 이것은 디폴트로 int형 값을 반환한다. 그러나 함수의 몸체에 return문이 없기 때문에 아무런 값도 반환되지 않는다. 인수 p는 char에 포인터형으로 선언되었다.


제주삼다수, 2L,... 오뚜기 진라면 매운... 상하목장 유기농 흰... 남양 프렌치카페 카... 고려인삼유통 홍삼 ... 종근당건강 오메가3... 요이치 카링 유무선...