C/C++

[C언어와 알고리즘] 제어 흐름 (3)

OSS 2012-08-17 17:04:53 10749
2011


글: 정재준 (rgbi3307@nate.com ) / 커널연구회(www.kernel.bz)   2011-01-06


연재 순서

 1. C언어 소개
 2. 형태, 연산자, 표현
 3. 제어 흐름
 4. 함수와 프로그램 구조
 5. 포인터와 배열
 6. 구조체
 7. 알고리즘 소개
 8. 소팅을 통한 알고리즘 분석
 9. 스택(Stack) 실습
10. 큐(Queue) 실습
11. 리스트(List) 실습
12. 트리(Tree) 실습
13. 해싱(Hash) 실습
14. 알고리즘 설계 및 분석기법
15. 진보된 알고리즘 소개

이 번 연재에서는 C언어의 문장과 블록, if-else, else-if, switch, 루프(while, for, do-while), break와 continue, goto와 labels에 대해서 기술한다. 프로그래밍 언어의 제어 흐름은 계산이 수행되는 순서를 지정한다. 앞에서 몇 가지 기본적인 제어흐름 명령들을 이미 경험했지만, 이번 연재부터 전체적으로 상세히 설명한다.

문장과 블록

문장은 다음과 같이 세미콜론으로 구분된다.

   x = 0;
   i++;
   printf(...);

중괄호 {와 }는 선언들과 문장들을 하나로 묶어 주는데 사용하며, 이렇게 묶여진 블럭은 의미적으로 동일한 것이 된다. 블럭은 {로 시작하여 }로 종료되며 }의 오른쪽 끝에는 세미콜론이 없다.

(실습 소스)



/*
author :    Jung,JaeJoon (rgbi3307@nate.com , http://www.kernel.bz/ )
comments :    문장과블럭
*/
#include <stdio.h>
main ( )
{
    int i = 10;
    {
    int i = 0, x=0;
    i++;
    printf(“i=%d, x=%d₩n”, i, x);
    }
    {
    float x = 3.14;
    printf(“i=%d, x=%f₩n”, i, x);
    }
    printf(“₩nPress any key to end...”);
    getchar();
}
 


(실행 결과)


$ cc -o ch0301 ch0301_block01.c   < 컴파일 및 링크
$ ./ch0301   < 실행
i=1, x=0
i=10, x=3.140000

Press any key to end...
 



if-else

if-else 문장은 조건 판단을 기술한다.  문법은 아래와 같다.

   if (expression)
    statement1
   else
    statement2

여 기서 else 부분은 선택적이다. if 조건이 참(영이 아닌 값)이면 statement1이 수행된다. if 조건이 거짓(영값)이면 else 부분에 있는 statement2가 수행된다. if 조건의 expression은 영이 아닌 값을 참으로 판단하기 때문에

   if (expression)

위의 표현은 아래와 동일하다.

   if (expression != 0)

if 조건을 여러 개 중첩시켜 표현할 수 있으나, 이때 if와 else의 블럭이 어디까지인지 잘 구분해야 한다. 특히 else는 선택적으로 생략 가능하기 때문에 아래와 같은 문장은 의도하지 않은 결과가 나올 수 있다.

   if (n > 0)
    if (a > b)
    z = a;
    else
    z = b;

else가 두 번째 if문의 조건에 걸려 있지만, 첫 번째 if문의 조건에 걸리도록 하려면 아래와 같이 블럭을 명확히 나타내도록 한다.

   if (n > 0) {
    if (a > b)
    z = a;
   }
   else
    z = b;

특히, 아래와 같은 문장은 의도하지 않은 if-else 구문이다. else 문장을 첫 번째 if문에 걸리도록 했지만, 실제적으로는 두 번째 if문에 else 문장이 걸린다.

   if (n > 0)
    for (i = 0; i < n; i++)
    if (s[i] > 0) {
    printf(“...”);
    return i;
    }
   else
    printf(“error n is negative₩n”);

위와 같은 논리적인 문제는 컴파일러가 찾아내지 못하므로 2개 이상으로 중첩된 if문을 사용 할 때는 아래와 같이 블럭을 명확히 표시해 주는 것이 좋다.

   if (n > 0) {
    for (i = 0; i < n; i++) {
    if (s[i] > 0) {
    printf(“...”);
    return i;
    }
    }
   } else {
    printf(“error n is negative₩n”);
   }


else-if

다음은 여러가지 판단을 수행해야 할 때 흔히 나타나는 구조이다.

   if (expression)
    statement
   else if (expression)
    statement
   else if (expression)
    statement
   else if (expression)
    statement
   else
    statement

if 는 expression을 위에서 순서대로 판단하다가 참인 것에 해당하는 statement을 수행하고 전체 판단구조를 빠져 나온다. statement는 단일 문장일 수도 있고 중괄호 안의 블록 일 수도 있다.  마지막에 있는 else는 위에 있는 if 조건들을 만족하지 않는 것이 수행된다.
else-if를 잘 활용하는 예제로 binsearch() 함수가 있다. 이 함수는 매개변수로 아래 값을 전달 받는다.

int x: 검색값
int v[]: 검색대상 배열, 순서대로 정렬되어 있음(v[0] <= v[1] <= ... <= v[n-1])
int n: 검색대상 수

즉, n개의 값이 순서대로 배열되어 있는 v에서 x값을 검색한다. 검색에 성공하면 배열에서의 순서 값(색인)을 반환하고 검색하는 값이 없으면 -1을 반환한다.

   int binsearch(int x, int v[], int n)
   {
    int low, high, mid;

    low = 0;
    high = n - 1;
    while (low <= high) {
    mid = (low+high)/2;
    if (x < v[mid])
    high = mid + 1;
    else if (x  > v[mid])
    low = mid + 1;
    else    //발견함
    return mid;
    }
    return -1;   //발견하지 못함(실패)
   }

Switch

switch 제어문은 다방향 결정을 수행하는 문장으로 switch의 괄호 안에 기술된 expression의 상수 값과 일치하는 case 문장으로 분기한다.

   switch (expression) {
    case const-expr: statements;
    case const-expr: statements;
    default: statements;
   }

아래의 switch 문장은 c의 문자 상수 값이 숫자이면 “c is digit”를 출력하고 공백이나 개행 문자, 탭 문자이면 “c is white space”를 출력하고 해당사항 없으면 “c is default”를 출력한다.

 switch (c) {
    case ‘0’: case ‘1’: case ‘2’: case ‘3’: case ‘4’:
    case ‘5’: case ‘6’: case ‘7’: case ‘8’: case ‘9’:
printf(“c is digitn”);
    break;
    case ‘ ‘:
    case ‘₩n’:
    case ‘₩t’:
printf(“c is white space₩n”);
    break;
default:
printf(“c is default₩n”);
break;
}

case에 해당하는 문장들이 실행되다가 break문을 만나면 switch 문장을 즉시 빠져 나온다. 위의 switch 문장 판단 논리를 if문으로 기술하면 아래와 같다.

  if (c >= ‘0’ && c <= ‘9’)
    printf(“c is digit₩n”);
  else if (c == ‘ ‘ || c == ‘₩n’ || c == ‘₩t’)
    printf(“c is white space₩n”);
  else
    printf(“c is default₩n”);

switch와 if는 비슷한 판단 논리를 수행하지만, switch의 조건 값은 상수여야 하는 반면에 if는 관계와 논리연산자를 포함한 다양한 조건식이 올 수 있다는 차이점이 있다.

(실습 소스)

/*
author :    Jung,JaeJoon (rgbi3307@nate.com , http://www.kernel.bz/ )
comments :    switch 문장
*/
#include <stdio.h>
main (  )
{
    char c = ‘8’;
    switch (c) {
    case ‘0’: case ‘1’: case ‘2’: case ‘3’: case ‘4’:
    case ‘5’: case ‘6’: case ‘7’: case ‘8’: case ‘9’:
    printf(“c is digit₩n”);
    break;
    case ‘ ‘:
    case ‘₩n’:
    case ‘₩t’:
    printf(“c is white space₩n”);
    break;
    default :
    printf(“c is default₩n”);
    break;
    }
   c = ‘₩t’;
   if (c >= ‘0’ && c <= ‘9’)
    printf(“c is digit₩n”);
    else if (c == ‘ ‘ || c == ‘₩n’ || c == ‘₩t’)
    printf(“c is white space₩n”);
    else
    printf(“c is default₩n”);
    printf(“₩nPress any key to end...”);
    getchar();
}
 


(실행 결과)


$ cc -o ch0304 ch0304_switch01.c   < 컴파일 및 링크
$ ./ch0304   < 실행
c is digit
c is white space

Press any key to end...
 


루프 - while, for

루프는 문장을 반복 수행하는 제어문으로 C언어는 while과 for를 제공한다. 먼저, while의 문법은 아래와 같다.

   while (expression)
    statement;

while 의 괄호 안에 있는 expression이 참 혹은 영이 아니면 아래에 있는 문장을 수행하고 다시 expression을 평가하고 참이면 계속 반복 수행한다.  즉, expression이 거짓 혹은 영이 될때까지 문장을 반복 수행한다. for문도 while문과 같이 반복 수행하는 제어문이나 아래와 같이 문법이 조금 다르다.

   for (expr1; expr2; expr3)
    statement;

위의 for문은 아래와 같이 while문으로 표현할 수 있다.

   expr1;
   while (expr2) {
    statement;
    expr3;
   }

for 루프의 세가지 구성요소 중에서 expr1과 expr3는 할당이거나 함수 호출일 수 있다. expr2는 관계형 표현이다. 이들은 각각 생략할 수 있지만 세미콜론(;)은 남아 있어야 한다. 아래와 같이 세 개 모두를 생략하면 for는 break나 return을 사용하여 루프를 빠져 나오지 않는 이상 무한히 반복하게 된다.

   for (;;) {
    ...
   }

루프를 사용할 때 while을 사용할 것인지 for를 사용할 것인지는 개인적인 취향이지만, 사용목적에 잘 맞도록 한다. 예를 들면, 아래는 while루프가 더 효율적이다.

   while ((c = getchar()) == ‘ ‘ || c == ‘₩n’ || c = ‘₩t’)
    ;   //white space 문자일 때는 루프를 판복 한다.
반면에 아래처럼 루프 제어변수의 초기값과 반복연산이 있는 경우에는 for루프를 사용하는 것이 더 효율적이다.

   for (i = 0; i < n; i++)
    ...

for 루프를 사용한 예제로써 문자열을 숫자로 변환하는 atoi()라는 함수가 있다. 이 함수는 파라미터로 문자열을 입력 받아서 공백문자(white space)는 건너뛰고 부호를 가져온 뒤 정수형 문자열을 숫자로 변환한다.

   #include <ctype.h>

   int atoi(char s[])
   {
    int i, n, sign;

    for (i = 0; isspace(s[i]); i++) //화이트 스페이스는 건너뛴다
    ;
    sign = (s[i] == ‘-’) ? -1 : 1;
    if (s[i] == ‘+’ || s[i] == ‘-’)    //부호는 건너뛴다
    i++;
    for (n = 0; isdigit(s[i]); i++)
    n = 10 * n + (s[i] - ‘0’);    //정수형 문자를 숫자로 변환
    return sign * n;
   }

for 루프를 여러 개 중첩해서 사용한 예제로 1959년 D. L. Shell에 의해서 알고리즘이 구현된 shellsort() 함수가 있다. 이 함수는 파라미터로 전달받은 정수형 배열 안의 값을 순서대로 정렬하는 역할을 한다.

   //shellsort:  v[0]...v[n-1]들을 순서대로 정렬한다.
   void shellsort(int v[], int n)
   {
    int gap, i, j, temp;

    for (gap = n/2; gap > 0; gap /= 2)
    for (i = gap; i < n; i++)
    for (j=i-gap; j>=0 && v[j]>v[j+gap]; j-=gap) {
    temp = v[j];
    v[j] = v[j+gap];
    v[j+gap] = temp;
    }
   }

C 언어 연산들 중에서 마지막에 콤마(,) 연산자가 있는데 이것은 for 루프의 제어변수에 자주 나타난다. 콤마 연산자는 왼쪽에서 오른쪽으로 연산을 수행할 때 사용한다. 아래는 이것이 사용된 reverse()라는 함수이며 매개변수로 전달받은 문자열의 앞뒤 순서를 바꾸는 역할을 한다.

   #include <string.h>

   void reverse(char s[])
   {
    int c, i, j;

    for (i = 0, j = strlen(s)-1; i < j; i++, j) {
    c = s[i];
    s[i] = s[j];
    s[j] = c;
    }
   }

함 수의 매개변수에 사용된 콤마나 변수 선언시에 사용된 콤마는 연산자가 아니므로 왼쪽에서 오른쪽으로 평가되는 것이 보장되지 않는다. 그러나 위의 for 루프에 사용된 문장들을 콤마 연산자를 아래와 같이 사용하여 한 행에서 동작되도록 할 수 있다.

    for (i = 0, j = strlen(s)-1; i < j; i++, j)
    c = s[i], s[i] = s[j], s[j] = c;

루프 do-while

앞 에서 학습한 while과 for는 루프의 종료 조건을 상단에서 판단하는 반면에 do-while은 루프의 종료 조건을 하단에서 판단한다. 따라서 do-while은 적어도 한번은 루프를 수행한다. do-while의 문법은 아래와 같다.

   do
    statement;
   while (expression);

statement 가 수행된 다음에 평가된 expression이 참이라면 statement의 수행은 반복된다.  expression이 거짓이면 루프는 종료된다. do-while은 while과 for보다는 적게 사용되지만 이것이 필요할 때가 있다. 숫자를 문자열로 변환하는 아래의 예제(itoa)에 do-while이 사용되었다.  아래의 함수 itoa는 파라미터로 입력 받은 정수 n을 분자열 s[ ]로 변환한다.

   void itoa(int n, char s[])
   {
    int i, sign;

    if ((sign = n) < 0)  //n을 sign에 할당하고 음수이면
    n = -n;    //n이 양수가 되게 한다.
    i = 0;
    do {    //숫자 n의 오른쪽에서부터 문자로 변환한다.
    s[i++] = n % 10 + ‘0’;
    } while ((n /= 10) > 0);
    if (sign < 0)
    s[i++] = ‘-’;
    s[i] = ‘₩0’;
    reverse(s); //숫자 n의 오른쪽 자리에서 문자열을 변환했으므로 자리를 바꾼다.
   }

문자열 s에는 적어도 하나의 문자가 있어야 하기 때문에 do-while을 사용하는 더 효율적이다.  또한 do-while의 몸체가 하나의 문장으로 되어 있어도 while과 혼동되지 않도록 중괄호를 사용하는 것이 바람직하다.

break와 continue

루 프의 상단이나 하단에서 종료 조건을 판단하는 것보다 루프의 몸체 중간에서 루프를 빠져 나오는 것이 필요할 때가 있다. break 문장은 for, while, do 중간에서 루프를 빠져 나오도록 해준다. 아래의 trim 함수 예제에서는 for 루프가 문자열 s의 길이만큼 반복되다가 s에 있는 문자가 공백, 탭, 개행 문자가 아니면 break에 의해서 루프를 빠져 나오도록 하고 있다.

   int trim(char s[])
   {
    int n;

    for (n = strlen(s)-1; n >= 0; n)
    if (s[n] != ‘ ‘ && s[n] != ‘₩t’ && s[n] != ‘₩n’)
    break;
    s[n+1] = ‘₩0’;
    return n;
   }

continue 문은 루프에서 사용되며 실행 순서를 처음으로 이동 시키는데 사용한다. while이나 do에서 사용하면 루프의 종료조건을 판단하는 곳으로 즉시 이동한다. for에 사용하면 제어변수를 증가시키는 곳으로 이동한다. 아래의 예제에 사용된 continue는 a[i]가 음수이면 하단부를 실행하지 않고 for 루프의 제어변수가 증가되는 곳으로 이동하도록 한다.

   for (i = 0; i < n; i++)
    if (a[i] < 0)
    continue;
    ...

(실습 소스)

/*
    author:    Jung,JaeJoon (rgbi3307@nate.com , http://www.kernel.bz/ )
    comments:    break, continue 문장
*/
#include <stdio.h>
main (  )
 {
    int i;
    for (i = 0; i < 10; i++) {
    if (i > 5) break;
    printf (“%d “, i);
    }
    printf (“₩n”);
    for (i = 0; i < 10; i++) {
    if (i < 5) continue;
    printf (“%d “, i);
    }
    printf (“₩n”);
    printf(“₩nPress any key to end...”);
    getchar();
}


(실행 결과)


$ cc -o ch0307 ch0307_break_continue01.c   < 컴파일 및 링크
$ ./ch0307   < 실행
0 1 2 3 4 5
5 6 7 8 9

Press any key to end...
 


goto 와 labels

goto 문은 label의 위치로 실행 순서를 강제적으로 이동시키는 역할을 한다. 실제적으로 goto문은 사용 빈도가 낮지만, 중첩된 루프를 모두 빠져 나오고자 할 때 사용하는 경우가 있다. 아래의 예제처럼 for 루프가 중첩되어 있는 상황에서 예외상황을 처리하기 위해 for 루프를 모두 빠져 나오기 위해서 goto문을 사용한다.

    for ( ... )
    for ( ... ) {
    ...
    if (disaster)
    goto error;
    }
    ...
   error:
    //오류처리문장

label은 변수명과 동일한 형태이며 끝에 콜론(:)을 붙인다. label의 영역은 함수 내부이다. 아래의 예제는 goto문을 사용한 경우와 goto문을 사용하지 않고도 동일한 제어를 할 수 있는 상황을 보여주고 있다.

    for (i = 0; i < n; i++)
    for (j = 0; j < m; j++)
    if (a[i] == b[j])
    goto found;
    ...
   found:
    ...

위에서 사용한 goto문을 아래와 같이 found라는 변수를 사용하여 없앨 수 있다.

   found = 0;
   for (i = 0; i < n && !found; i++)
    for (j = 0; j < m && !found; j++)
    if (a[i] == b[j])
    found = 1;
   if (found)
    ...
   else
    ...

goto문을 많이 사용하면 코드의 제어가 복잡해지기 때문에 되도록이면 goto문을 사용하지 않는 것이 좋다.

(실습 소스)

/*
    author: Jung,JaeJoon (rgbi3307@nate.com , http://www.kernel.bz/ )
    comments:    goto 문장
*/
#include <stdio.h>
main (  )
{
    int i, j, found;
    int a[] = {1, 3, 5, 7, 9, 10};
    int b[] = {2, 4, 6, 8, 10};

    //goto문으로 중첩된 루프 탈출
    for (i = 0; i < 10; i++)
    for (j = 0; j < 10; j++) {
    printf(“%d “, j);
    if (j > 5)
    goto Error;
    }
    printf(“₩nThis is the end of loop.₩n”);
    Error:
    printf(“₩nThis is the Error label.₩n₩n”);

    //goto문을 사용하여 분기
    for (i = 0; i < 6; i++)
    for (j = 0; j < 5; j++)
    if (a[i] == b[j])
    goto Found;
  printf(“This is the end of loop.₩n”);
    Found:
    printf(“Found(%d,%d)₩n”, i, j);

    //goto문을 사용하지 않고 분기
    found = 0;
    for (i = 0; i < 6 && !found; i++)
    for (j = 0; j < 5 && !found; j++)
    if (a[i] == b[j])
    found = 1;
    printf(“₩nThis is the end of loop.₩n”);
    if (found)
    printf(“found(%d,%d)₩n”, i, j);
    else
    printf(“Does not found(%d,%d)₩n”, i, j);

    printf(“₩nPress any key to end...”);
    getchar();
}
 


(실행 결과)

$ cc -o ch0308 ch0308_goto01.c   < 컴파일 및 링크
$ ./ch0308   < 실행
0 1 2 3 4 5 6
This is the Error label.

Found(5,4)

This is the end of loop.
found(6,5)

Press any key to end...
 



맺음말

이 번 연재에서는 C언어의 문장과 블록, if-else, else-if, switch, 루프(while, for, do-while), break와 continue, goto와 labels에 대해서 설명했다. 이것들을 사용하여 제어흐름을 코딩 할 수 있으므로 C언어 프로그래밍에 대한 기본적인 준비는 되었다. 다음 연재에서는 C언어 함수와 프로그램 구조를 기술할 예정이다. 이것은 C언어 프로그래밍을 풍부하게 할 수 있는 배경 지식이 되므로 독자 여러분들의 꾸준한 관심 있기를 바란다.

필 / 자 / 소 / 개

(정 재준) 필자는 학창시절 마이크로프로세서 제어 기술을 배웠고, 10여년동안 쌓아온 IT관련 개발 경험을 바탕으로 “오라클실무활용SQL튜닝(혜지원)” 책을 집필하고, “월간임베디드월드” 잡지에 다수의 글을 기고하였다.  서울대병원 전산실에서 데이터베이스 관련 일을 하면서 학창시절부터 꾸준히 해온 리눅스 연구도 계속하고 있다.  특히, 스탠포드대학교의 John L. Hennessy 교수의 저서 “Computer Organization and Design” 책을 읽고 깊은 감명을 받았으며, 컴퓨터구조와 자료구조 및 알고리즘 효율성 연구를 통한 기술서적 집필에 노력하고 있다.  또한, 온라인 상에서 커널연구회 (http://www.kernel.bz/ )라는 웹사이트를 운영하며 관련기술들을 공유하고 있다.


※ 본 내용은 (주)테크월드(http://www.embeddedworld.co.kr)의 저작권 동의에 의해 공유되고 있습니다.
Copyright ⓒ
Techworld, Inc. 무단전재 및 재배포 금지

맨 위로
맨 위로