본문 바로가기

프로그래밍 언어/C

[K.N.K C Programming 정리] Chap 5. Selection Statements

K.N.KING C Programming

안녕하세요 여러분! 오늘은 5단원 분기문을 들고 왔습니다!

바로 들어가봅시다 ㅎㅎ


Chap5. Selection Statements

 

Programmers are not to be measured by their ingenuity and their logic but by the completeness of their case analysis.

 


Intro

C에는 많은 연산자들이 있지만 statements(번역을 어떻게 해야 깔끔할지 고민되서 그냥 원서 표현을 그대로 썼습니다!) 는 몇 가지 밖에 없다. 우리는 지금까지 return 과 expression statement 2개의 statements 를 봐왔다. 나머지의 statements 들은 3가지로 나뉘는데, statements 들이 실행되는 순서에 영향을 미치는지에 따라 나뉜다. 

조건문 - if문과 switch문.

반복문 - while , do, for문 . 

점프문 - break, continue, goto문. 이들은 조건없이(unconditional, 조건문과 대조해서 쓴 듯함) 코드 어딘가 다른 부분에서 실행되게 만든다.

나머지 statements는 복합문(compound statements)으로, 몇개의 statements 들을 한개의 statement로 그룹화합니다.

 


5.1 Logical Expressions

If문을 실행한다고 치자. 그렇다면 if문을 실행하기 전에 표현식이 참인지 거짓인지를 평가해야한다. 예를 들어, if문이 i < j 라는 표현식을 검사하는 if문이라고 하자. i가 j보다 작다면 "참" 값을 나타낼것이다. 

많은 프로그래밍 언어들에서는 i < j 라는 표현식은 "Boolean" 이나 "logical" 타입을 가진다. 이런 타입들은 false와 true라는, 오직 두 가지 값만을 가진다. 그러나 C에서 i < j 와 같은 비교식은 정수값을 도출해낸다. 이를테면 0은 false고 1은 true 처럼 말이다. 이 점을 유의하며 계속 읽어나가자.  

 

 

Relational Operators

C의 관계연산자들은 0(false)과 1(true)를 반환한다는 점만 빼면, 수학에서 우리가 사용하는 <, > 등 과 같다.

 

Symbol Meaning
< less than
> greater than
<= less than or equal to
>= greater than or equal to 

 관계 연산자들은 정수와 소수의 값을 비교하는데 쓰일 수 있으며, 두개를 섞어 쓰는것도 허용된다.

이를테면, 5.6 < 4 라는 표현식은 0 이라는 값을 가지고 1 < 2.5 는 1 이라는 값을 가진다.

또한 산술연산자보다 낮은 우선순위를 가지므로, i + j < k - 1은 (i + j) < (k - 1) 과 동일하다.  또한 관계연산자는 left associative이다. (왼쪽에서 오른쪽으로 연산수행)

i < j < k 라는 표현식은 허용되지만, 아무 의미를 가지지 않는다. 즉 이는 j 가 i 와 k 사잇값이라는걸 의미하지 않는다. 

왜냐하면 관계연산자는 left associative이기 때문에 (i < j) < k 순서로 연산이 수행된다. 

이때 i < j 를 평가한 결과는 0 또는 1 이 도출되고, k 와 이를 비교하게 된다. 

만약 j 가 i 와 k의 사잇값이어야 한다는걸 표현하고 싶다면, i < j && j < k 로 쓰자.(나중에 나온다!)

 

Equality Operators

symbol Meaning
== equal to(같음)
!= not equal to(같지 않음)

관계연산자와 마찬가지로 left associative 이고 결과값으로 0(거짓) 과 1(참)을 가진다.

그러나 동등연산자는 관계연산자보다 낮은 우선순위를 가진다. 즉,

i < j == j < k 는 (i < j) == (j < k) 와 같은 표현이다. 

영리한 프로그래머들이 가끔씩 관계연산자와 동등연산자가 0 과 1을 도출한다는점을 악용하는데, 이를테면

(i >= j) + (i == j) 이런식으로 말이다. (i 가 j 보다 작으면 0 이나오고 크면 1, 같으면 2가 나온다!)

이는 그다지 좋은 생각은 아니다. 프로그램을 이해하기 어렵게 만들기 때문이다. 

 

Logical Operators

 Symbol Meaning
! logical negation(부정)
&& logical and
|| logical or

논리 연산자들은 그 결과로 0 또는 1을 도출해낸다. 피연산자들은 아마 그 값으로 0 또는 1을 갖겠지만, 사실 이는 필수가 아니다.  논리연산자들은 0이 아닌 피연산자들은 참값(true)으로 다루고, 0 인 피연산자는 거짓값(false)으로 다룬다.

(즉 false는 0, true는 0이 아닌 것으로 생각하면 편하다.)

몇가지 예를 들면,

  • !expr 은 expr이 0이면 1 이다.
  • 표현식 expr1 && expr2 은 expr1과 expr2 둘 다 0 이 아닌값을 가지면 1을 도출한다. 
  • expr1 || expr2 는 expr1 과 expr2 둘 중 하나라도 0이 아닌값을 가지면 1을 도출한다.

&& 과 || 모두 피연산자에 대해 "short - circuit" evaluation 을 수행한다. 무슨 말이냐면, 우선 왼쪽 피연산자를 평가하고,  그 후 오른쪽 피연산자를 평가한다. 이때, 만약 왼쪽 피연산자만 평가(evaluate)해서 전체 표현식의 값을 추론할 수 있으면 오른쪽 피연산자는 평가되지 않는다는것이다. 다음 예를 살펴보자.

 

(i != 0) && (j / i > 0) 

 

여기서 전체 표현식의 값을 알기 위해 우리는 우선 표현식 (i != 0) 을 평가해야한다. i가 만약 0과 같지 않다면 우리는

전체 표현식의 값을 알기 위해 (j / i > 0) 을 평가한다. 그러나 만약 i 가 0과 같다면 전체 표현식은 분명히 거짓일것이고, 우리는 (j / i > 0) 을 평가할 필요가 없다. short circuit의 장점이 여기서 나온다. 즉, 분모가 0 인경우로 나누는걸 방지해준다. 그런데 short circuit이 장점만 갖고 있는것은 아닌데, 이를테면

 

i > 0 && ++j > 0 

 

위 표현식의 경우 j의 값이 증가하지 않을 수도 있다! 왜냐하면 i > 0 이 false인 경우 ++j > 0 은 평가되지 않기 때문이다. 이럴 경우 ++j >0 && i > 0 과 같이 둘의 순서를 바꿈으로써 해결가능하다.

 


5.2 The if Statemnet

if문은 표현식의 값을 평가함으로써 프로그램이 두가지 대안을 선택할 수 있게 한다. 

 

if  (  expression  )  statement

 

if문이 실행되면 소괄호 안의 표현식이 실행되는데 만약 표현식의 값이 0이 아니라면 (C언어는 true로 해석) 소괄호 바깥의 statement가 실행된다. 

어떤 변수의 값이 특정 범위 내에 들어오는지 검사하고 싶을 경우 사용되는 C언어의 관용구는 다음과 같다. 

 

if(0 <= i && i < n) ...

 

만약 반대인 경우를 조사하고 싶으면,

 

if(0 > i || i >= n) ...

 

&&대신 ||를 사용했음에 유의하자.

 

Compound Statemnets

if문에서 statement 가 한개라면 

 

if  (  expression  )  statement

 

이렇게 썼지만, 만약 여러개라면 복합문(compound statements) 을 사용하도록 한다. 복합문은 다음과 같은 형태를 가진다.

 

{ statements }

 

중괄호를 statements 여러개에 둘러쌈으로써 우리는 컴파일러에게 이것을 마치 하나의 statement로 다루게 할 수 있다. 

 

The else Clause

if  (  expression  )  statement  else  statement

 

else문은 소괄호 안의 표현식이 0일때 실행된다. 

이 때 else문의 위치를 if문과 정렬할때, 중괄호를 적극적으로 사용하는건 아주 좋은 태도다.

if (i > j) {
	if (i > k)
    	max = i;
    else
    	max = k;
}

이것보다,

if (i > j) {
	if (i > k) {
    }	max = i;
    else {
    	max = k;
    }
}

 

이렇게 굳이 중괄호를 쓰지 않아도 되는 상황일지라도 중괄호로 if와 else를 구분해주는건 표현식에 소괄호를 써주는것과 마찬가지인 일이다. 둘 다 프로그램을 더 읽기 쉽게 만들고 컴파일러가 우리가 쓴 의도대로 프로그램을 실행시킬 수 있게 하는 테크닉이다.

 

The "Dangling else" Problem

다음과 같은 코드가 있다고 하자. 

if (y != 0)
	if (x != 0)
    	result = x / y;
else
	printf("Error : y is equal to 0\n");

이 경우 else는 어느 if와 짝을 맞추는 걸까? 인덴트만 보면 바깥쪽 if문과 짝을 이루는것 같다. 

그러나 C에서 else문은 이미 else문과 짝지어지지 않은 가장 가까운 if문과 짝을 이루게 된다

(else clause belongs to the nearest if statement that hasn't already been paired with an else)

따라서 위에 언급했듯, 중괄호를 써서 구분짓는 것을 습관화하자. 

 

Conditional Expressions

 

C의 if문은 조건에 따라 프로그램이 둘 중 한 가지의 선택지를 고르게한다. C는 여기서 그치지 않고 특정 조건의 값에 따라 둘 중 한개의 값을 도출해내는 연산자(operator) 를 지원한다. 

바로 conditional operator, 즉 조건연산자인데 ? 와 : 로 이루어진다. 

 

expr1 ? expr2 : expr3

 

expr1, expr2, expr3 모두 어떠한 타입도 올 수 있다. 3개의 피연산자가 필요하다는 점에서 매우 유니크한 연산자이다. 

이 점때문에 삼항연산자라고 불리기도 한다. 

expr1 ? expr2 : expr3 는 해석할때 "if expr1 then expr2 else expr3" 라고 해석하면 편하다. 

expr1 이 가장먼저 평가되고, 만약 그 값이 0이 아니면, 즉 참이면 expr2가 평가된다. 그리고 expr2의 값이 전체 표현식의 결과값이 된다. 만약 expr1이 0이면, 즉 거짓이면 전체 표현식의 값은 expr3가 된다. 

삼항연산자는 대입연산자를 제외하고 우선순위가 꼴찌이다. 따라서 다음과 같이 사용할 수 도있다.

 

k = i > j ? i : j;

 

삼항연산자는 프로그램을 간결하게 만들어주지만, 이해하기 힘들게 만들기 때문에 사실 안쓰는게 상책이다. 

그럼에도 불구하고 쓰고 싶을때가 생긴다. 언제냐면, 

 

if(i > j)
    return i;
else
    return j;

 

이를

 

return i > j ? i : j;

 

한 문장으로 바꿀 수 있다. 또한 printf("%d\n", i > j ? i : j); 이렇게도 쓸 수 있다. 

 

Boolean values in C89

많은 세월동안 C에는 적절한 Boolean type이 없었다. C89표준에도 Boolean type에 대해 정의된 바가 없다.

많은 프로그램들이 true나 false를 저장할 변수를 필요로 하기 때문에 이런 누락은 조금 거슬린다. 

이러한 C89 의 한계점을 극복하기 위한 한가지 방법은, int 타입 변수를 정하고 0 이나 1을 할당하는 것이다. 

 

int flag;

flag = 0;
...
flag = 1;

 

이런식으로 말이다.

그러나 이런 방식은 프로그램의 가독성에 그다지 기여하지 않는다. 오히려 헷갈리게 할 수 있다!

왜냐하면 변수 flag 에 0과 1같은 Boolean 값들만 저장되는게 보장되지 않을 뿐더러 처음 보는 사람이 보면 0 과 1이 false와 true를 의미하는건지도 모를수 있기 때문이다.

이러한 문제를 해결하기 위해 C89 프로그래머들은 매크로를 사용했는데, 이를테면 다음과 같다.

 

#define TRUE 1
#define FALSE 0

 

이제 flag 에 값을 할당하는것은 좀 더 자연스러운 모양새를 띈다. 

 

flag = FALSE;
...
flag = TRUE;

 

이런식으로 말이다.  flag 가 true임을 검사하기 위해 우리는

 

if (flag = TRUE) ... //혹은

if (flag) ...

if(!flag) ...

 

이렇게 쓸 수 있다. 여기서 두번째 방식이 더 낫다고 볼 수 있는데, 우선 좀 더 간결하게 쓸 수 있고 flag 값이 0과 1 말고 다른값을 가지더라도 여전히 정상적으로 작동하기 때문이다. 

 

여기서 한 발 더 나아가 아예 타입을 매크로로 지정할 수 도 있다.

 

#define BOOL int

 

이렇게 말이다. 이제 BOOL 은 Boolean 변수를 선언할때 int 대신 쓰일 수 있다.

 

BOOL flag; // declares Boolean type variable flag.

 

이런식으로 말이다. 이제 flag 가 보통의 int 타입 변수와 다르다는 것이 명확해졌을 뿐더러 Boolean 타입을 나타낸다는것을 알 수 있다. 물론 컴파일러는 flag를 int 타입 변수로 다룰것이다.

이후의 장에서 우리는 C89에서 boolean타입을 어떻게 type definitions 와 enumerations 로 나타내는지를 배울것이다. 

 

Boolean Values in C99

장기간에 걸친 Boolean 타입의 부재는 C99에 와서야 해결되었다.

C99에서는 _Bool 타입을 제공한다. C99에서 Boolean 타입 변수는 

 

_Bool flag;

 

와 같이 선언될 수 있다.(이때 _Bool은 부호없는 정수형이다. 알고만 넘어가자. 즉, _Bool 은 int타입이 변장한것이다.)

그러나 _Bool  타입 변수는 오직 0과 1만 할당받을 수 있다. 만약 0이 아닌 수를 할당하려한다면 변수의 값이 1로 저장된다. 

 

flag = 5; // flag is assigned 1

 

C99는 _Bool 타입을 정의함과 더불어 새로운 헤더파일인 <stdbool.h> 을 제공한다.

이 헤더를 사용하면 우리는 다음과 같이 선언할 수 있다.

 

bool flag; // same as _Bool flag; (헤더내에서 매크로를 사용함)

 

또한 stdbool.h 헤더파일은 true 와 false의 매크로를 제공하는데, 각각 1과 0 을 나타낸다.

 

flag = false;
...
flag = true;

 

이렇게 선언할 수 있다. <stdbool.h> 는 아주 유용한 헤더파일이다! 

 


5.3 The switch Statement

프로그래밍을 하다보면 종종 한 표현식의 값을 여러 시리즈의 값들과 비교해야 할 상황에 맞닥뜨린다. 

이 때 계단식 if문(cascaded if) 을 생각해 볼 수 있지만, 계단식 if문과 동일한 기능을 하는 switch문을 떠올려 볼 수 있다. 

다음 코드를 보자.

switch (grade) {
	case 4 : printf("Excellent");
    		 break;
	case 3 : printf("Good");
    		 break;
    case 2 : printf("Average");
    		 break;
    case 1 : printf("Poor");
    		 break;
    case 0 : printf("Failing");
    		 break;
    default : printf("Illegal grade");
    		 break;             
}             

위 switch문이 실행되면, 우선 변수 grade의 값이 4,3,2,1,0 에 대해 검사된다. 만약 4와 일치하면 Excellent 를 출력할것이다.  그리고 break문에 의해 switch 문의 바깥으로 이동할것이다. 만약 grade 의 값과 일치하는 case가 없다면 default 케이스가 실행된다.

스위치문은 계단식 if문보다 읽기가 편하다. 게다가, 스위치문은 if문보다 빠르다. 

많은 경우 switch 문은 다음과 같은 형식을 지닌다.

 

 

switch  (  expression  )  {

    case constant-expression  :  statements

    ....

    case constant-expression  :  statements

    default : statements

}

 

 

switch 문은 꽤 복잡하다! 구성요소들을 하나씩 살펴보자.

  • 제어식(controlling expression) : switch 문 바로다음에는 소괄호 안에 정수 표현식이 와야한다. 이 때 문자형(Characters)도 올 수 있다. 왜냐면 C에서는 문자형도 정수로 처리하기 때문이다. 실수와 문자열은 올 수 없다. 
  • 케이스 라벨(case labels: 각 케이스는 '상수 표현식'을 필요로 한다. 여기서 상수 표현식이란, 보통의 표현식과 다른 점이 있는데, 바로 변수와 함수호출은 올 수 없다는 것이다. 이를테면, 5 또는 5 + 10은 상수표현식이 맞지만 n + 10 같은 경우 n이 매크로가 아니라면 상수표현식이 될 수 없다. 케이스 라벨 안의 상수표현식은 반드시 정수가 되야 한다. (문자형도 가능함 - 정수로 처리하니까)
  • 구문들(Statements) : 중괄호가 오지 않는다는것을 유의하라! 보통 각 그룹의 마지막 구문은 break 문이다.

default문은 없어도 된다. 만약 default문이 없는데 해당 case에도 없는 경우라면 그냥 switch문을 벗어난다.

오직 한개의 상수표현식이 case 뒤에 와야 한다. 그런데 몇개의 case 들은 같은 구문들이 온다면 한꺼번에 적을 수도있다. 이를테면,

 

switch (grade) {
	case 4:
    case 3:
    case 2:
    case 1:  printf("Passing");
    		 break;
    case 0:  printf("Failing");
    		 break;
    default: printf("Illegal grade");
    		 break;

이렇게 쓸 수 있다. 여기서 공간을 더 줄이기 위해 다음과 같이 쓸 수 도 있다.

 

switch (grade) {
	case 4: case 3: case 2: case 1:  
    		 printf("Passing");
    		 break;
    case 0:  printf("Failing");
    		 break;
    default: printf("Illegal grade");
    		 break;

 

The Role of the break Statement

이제 break문의 정체에 대해 조금 더 알아보자! 

break문을 실행시키는 것은 프로그램이 switch 문을 "탈출(break)" 할 수 있게 한다. 자연스레 switch문 다음 구문들이 실행된다.  우리가 break문을 필요로 하는것은 switch문이 "계산된 점프(computed jump)" 라는 사실과 관련이 있다. 

제어식(controlling expression)이 평가될때, switch문 다음 소괄호에 있는 변수의 값과 일치하는 case로 말그대로 "점프" 한다. 이 떄, 해당 케이스가 끝까지 실행되고 나면, 다음 케이스의 첫번째 구문이 실행된다. 케이스 라벨은 사실 점프할 위치를 표시한 것(marker) 그 이상 그 이하도 아니다. 케이스 라벨은 무시된다! 따라서 break문이 없다면 계속해서 아래로 내려가며 해당 케이스의 아래에 위치한 모든 케이스가 실행될것이다. 

 

다음 switch문을 보자.

 

switch (grade) {
	case 4 : printf("Excellent");
	case 3 : printf("Good");
    case 2 : printf("Average");
    case 1 : printf("Poor");
    case 0 : printf("Failing");
    default : printf("Illegal grade");
}            

 

이 경우 grade가 3이라면, 출력되는 결과는 다음과 같다.

 

GoodAveragePoorFailingIllegal grade

 

break문을 생략함으로써 의도적으로 다음 case를 실행시키는건 아주 드문 일이기 때문에,

만약 정말 그런 의도로 코드를 작성한다면 다음과 같이 주석으로 의도를 표시하도록 하자.

 

switch (grade) {
	case 4: case 3: case 2: case 1:  
    		 printf("Passing");
    		 /* FALL THROUGH */
    case 0:  printf("Failing");
    		 break;
    default: printf("Illegal grade");
    		 break;
}

 

이러한 주석이 없는 경우 다른 사람이 여러분의 코드를 보고 이를 에러라고 여기고 break를 넣는 불상사가 일어날 수 있다. switch문의 마지막 case는 break를 필요로 하지않지만(왜 안필요한지 알겠지? 그렇지?) 마음가짐을 모든 case에는 break를 넣는다고 마음먹으면 아주 편하다. 혹시 나중에 case를 추가할 일이 생길지도 모르고 말이다.


Q&A

 

Q : 삼항연산자에서 int와 float 값을 사용하는 경우 전체 표현식의 결과값은 무슨 타입인가요? (i > 0 ? i : f)

A : 결론부터 말하면 float 타입이야! 만약 i > 0 이 true 면 결과값은 float 타입으로 형변환을 끝낸 i 의 값이야.

 

Q : 왜 C99 타입은 Boolean type에 더 적절한 이름을 사용하지 않았나요?

A : _Bool 은 사실 그다지 우아한 작명은 아니야. 그렇지? bool 이나 boolean 같은 걸 사용하지 못한 이유는 이미 기존에 존재하던 C 프로그램들이 이 정의를 이미 사용하고 있었기 때문에 그래. 이러면 C99로 업데이트 하는 순간 기존 코드들은 못 쓰게 될 가능성이 있거든. (사실 이거 저도 이해 못했습니다. King 선생님 무슨말인가요 ㅠㅠ 아시는 분 댓글로 남겨주세용 ㅎㅎ)

 


후후 ㅎㅎ

하루하루 써가는 보람이 있네요! 읽어주셔서 감사합니다~

그럼 모두 안녕히~ :)