본문 바로가기
Language

[Programming Language] 5. 문장 수준의 제어 구조

by 삼준 2023. 7. 16.
반응형

직전글

2023.07.15 - [Language] - [Programming Language] 4. 표현식과 배정문

● 서론

프로그램에서 제어의 흐름, 즉 실행 순서를 이해하는 것은 매우 중요함.

앞서 < 4. 표현식과 배정문 > 에서 식 내부의 제어 흐름을 배웠다면, 이번 글에서는 문장 간의 제어 흐름을 배움.

명령형 언어 프로그램에서 계산은 식을 평가하고, 그 결과 값을 변수에 할당함으로써 얻어짐.
그러나, 배정문 만으로 모든 유용한 프로그램을 작성할 수는 없으며, 제어문이 필요함.
모든 알고리즘은 단지 두 개의 제어문만을 갖는 프로그래밍 언어로 작성될 수 있음이 증명되었음.
(1) 양자 택일의 제어 흐름 경로들 가운데서 선택할 수 있는 제어문 = 선택문 (Selection Statement) (a.k.a 조건문)
(2) 일련의 문장들을 반복적으로 실행할 수 있는 제어문 = 반복문 (Iterative Statement)

 

● 선택문(조건문)

프로그램의 두 개 또는 그 이상의 실행 경로들 가운데서 선택할 수 있는 수단을 제공함.
선택문은 두 가지 일반적인 유형으로 구분됨 : (1) 2방향 (If-else문), (2) 다중 선택 (Switch-case문)

- 2방향 선택문

오늘날의 명령형 언어에 속한 2방향 선택문들은 매우 유사함.
일반적인 형식은 다음과 같음.

if 제어식  // 조건을 검사
then 절    // 참일 경우 수행
else 절    // 거짓일 경우 수행

ㄴ 제어식
제어식은 then 예약어(Reserved Word)가 사용되지 않는 경우 괄호 안에 명세됨.

then 예약어가 사용될 때, 괄호의 필요성은 줄어들며, Ruby와 같은 언어에서는 흔히 생략되기도 함.

불리안 테이터 타입을 가지지 않는 C 언어 버전 (C89)에서는, 산술식이 제어식으로 사용됨.
Python과 불리안 타입을 지원하는 C언어 버전 (C99)에서는, 산술식이나 불리안 식이 모두 제어식으로 사
용 가능함.
오늘날의 다른 언어들에서는, 불리안 식만이 제어식으로 사용 가능함.

 

ㄴ 절 형식

오늘날의 많은 언어에서, then과 else절은 단일 문장이나 복합문으로 표현됨

그중 많은 언어들은 중괄호 ( "{", "}" )를 사용하여 복합문을 구성함.

특이하게도 Python에서는 들여 쓰기를 사용하여 복합문을 명세하며, then 예약어 대신에 콜론(":")을 사용함.

 

ㄴ 선택자 중첩

한 선택문이 다른 선택문의 then 절에 중첩될 때, else 절이 어느 if와 연관되어야 하는지에 대한 문제.

// Java
if (sum == 0)     // 1
  if (count == 0) // 2
    result = 0;
  else            // 2번과 짝이 됨
    result = 1;

Python과 F# 언어를 제외하고는 들여 쓰기는 어떠한 의미도 가지지 않음.

많은 다른 명령형 언어들처럼, Java에서 else 절은 항상 가장 가까이 위치한 짝을 갖지 않은 이전의 then 절과 짝이 맺어짐.

따라서, 위의 두 예제에서 else 절은 두 번째 then 절과 짝이 맺어짐.

위의 두 예제에서 else 절이 첫 번째 then 절과 짝이 맺어지기를 원한다면, 다음과 같이 복합문으로 표현해야 함

// Java
if (sum == 0){
  if (count == 0)
    result = 0;
}
else
  result = 1;

C 언어, C++, C#은 선택문 중첩에서 Java와 동일함.

Python은 들여쓰기로 선택자 중첩에서 발생할 수 있는 문제를 해결함.

// Python
if (sum == 0):
  if count == 0 :
    result = 0
else :
  result = 1
// else가 첫 번째 if와 매칭

 

- 다중 선택문

다중 선택문은 임의의 개수의 문장들이나 문장 그룹들 중에서 한 개의 선택을 허용함.

Python, Perl, Lua는 다중 선택문을 가지지 않음.

 

ㄴ C 언어에서의 다중 선택자 구조 Switch-Case (Java, C++, JavaScript에서도 동일함)

switch (식) {
  case 상수식1 : 문장1;
  ...
  case 상수식n : 문장n;
  [ default: 문장n+1; ]
}

"식"과 "상수식"은 이산 타입(Discrete Type) 임. (문자, 정수, 열거 타입을 포함함)

"문장"은 복합문(또는 블록)일 수 있음.
default 세그먼트는 선택적으로 추가 가능하며, 제어식의 표현되지 않는 값들을 위한 것임.

각 세그먼트 끝에서 묵시적 분기를 제공하지 않으므로, break를 사용하여 탈출하도록 지정해야 함.

switch (index) {
  case 1 :
  case 3 : odd += 1;
    sumodd += index;
    break;
  case 2 :
  case 4 : even += 1;
    sumeven += index;
    break;
  default: printf(“Error\n”);
}
// break가 없다면 끝에 항상 “Error”가 출력됨

때때로, 제어가 한 개의 선택 가능 코드 세그먼트로부터 다른 세그먼트로 흘러가는 것을 허용하면 편리함.
예를 들어, 앞의 예제에서 케이스 값 1, 2에 대한 세그먼트는 비어 있어서 제어가 각각 3, 4에 대한 세그먼트로 흘러가는 것을 허용함.

그러나, 매우 복잡하고 이상한 코딩 또한 가능해짐.

// 복잡한 코딩 예시
switch (x) {
  default:
  if (prime(x))
    case 2: case 3: case 5: case 7:
      process_prime(x);
  else
    case 4: case 6: case 8: case 9: case 10:
      process_composite(x);
}

Java의 switch문은 case식이 switch 몸체의 상단 수준을 제외하고는 다른 곳에 나타나는 것을 허용하지 않음으로써, 위와 같은 복잡성을 방지함.

 

ㄴ if를 이용한 다중 선택

많은 상황들에서, switch나 case 구조는 다중 선택을 표현하는데 부적절함.
예를 들어, 선택이 어떤 순서 타입보다는 불리안 식에 기반해서 이루어져야만 할 때, 중첩된 2방향 선택자를 사용하여 다중 선택자를 표현하는 게 나음.
여러 번 중첩되는 2방향 선택자의 낮은 판독성을 완화하기 위해, Python과 같은 언어는 특별하게 확장함. (else-if를 하나의 특수어(elif)로 대체함)

if count < 10 :
  bag1 = True
else :
  if count < 100 :
    bag2 = True
  else :
    if count < 1000 :
      bag3 = True

// 위 아래는 동일한 결과를 가짐

if count < 10 :
  bag1 = True
elif count < 100 :
  bag2 = True
elif count < 1000 :
  bag3 = True

앞 페이지의 예제에서, 각 선택 가능한 문장이 불리안 식에 기반하여 선택되기 때문에, switch 문으로 쉽게 표현되지 않는다는 것에 유의해야 함. 따라서, else-if 문이 switch와 중복된 형식은 아님.

사실, 오늘날의 언어에서, 어떠한 다중 선택자도 if-then-else if 문만큼 일반적이지는 않음.

 

● 반복문

한 문장 또는 문장들의 모임을 0번, 한 번, 또는 그 이상의 횟수로 실행하는 것을 야기하는 문장. 반복문을 흔히 루프 (loop)라고 부름.

- 기본적인 반복문

(1) 계수에 기반한 반복문(for문), (2) 논리에 기반한 반복문(while문)
기본적인 반복문 이외에도, 사용자-정의 반복 제어(새로운 for문)도 대안으로 사용 가능함.

- 반복문 관련 용어

몸체 (Body) : 실행이 반복문에 의해서 제어되는 문장들의 모임
사전-검사 (Pretest) : 루프 몸체가 실행되기 전에 루프 종료에 대한 검사가 일어나는 것을 의미함.
사후-검사 (Posttest) : 루프 몸체가 실행된 후에 루프 종료에 대한 검사가 일어난다는 것을 의미함. (do-while문)

- 계수기-제어 루프

계수 반복 제어문은 루프 변수라 불리는 변수를 가짐. 이 변수에서 계수 값이 유지됨. 또한, 이 문장은 루프 변수의 초기 값, 종료 값, 단계-크기를 가짐. 루프의 초기 값, 종료 값, 단계-크기를 루프 매개변수라고 부름.

C언어 for문 예시

ㄴ C의 for문

for (식_1; 식_2; 식_3)
루프 몸체

"식_1"은 초기화를 위한 것으로, for 문이 시작될 때 단지 한 번만 평가됨.
"식_2"는 루프 제어를 위한 것으로, 루프 몸체의 각 실행 전에 평가됨. (C99 이전의 C 언어에서는 산술식이었으며, C99부터는 불리안 식도 가능해짐)
"식_3"은 루프 계수기를 증가시키기 위한 것으로 루프 몸체의 각 실행 후에 평가됨.

루프 몸체는 단일문, 복합문, 또는 널 문장(몸체에 아무것도 적지 않는 경우)이 될 수 있음.

 

C 언어의 for 문에 포함된 모든 식들은 선택적임.
"식_2"가 생략되면 참이라고 고려됨. 따라서, 두 번째 식이 생략되면 무한 루프임.
"식_1"과 "식_3"이 생략되면, 어떠한 가정도 고려되지 않음. 예를 들어 첫 번째 식이 생략되면, 단순히 초기화가 없다는 것을 의미함.
C 언어의 for문은 반드시 계수할 필요가 없다는 것에 유의해야 함.

 

+ 연산 의미론의 기술(Operational Semantic Description)

문장들이 실행되는 순서에 대해 서술한 것.

// 앞선 예제를 어셈블리어 수준으로 변환한 경우
식_1
loop:
  if 식_2 = 0 goto out
  [루프 몸체]
  식_3
  goto loop
out: ...

결국 반복문도 조건문으로 표현됨.

 

C 언어의 for 문에서는 각각의 식이 여러 개의 문장 (다중 문장)을 포함할 수 있음. (여러 개의 루프 변수를 사용할 수 있기 때문)

다중 문장들이 for 문의 단일 식에서 사용될 때, 이들은 콤마(",")로 구분됨.

for (count1 = 0, count2 = 1.0;
  count1 <= 10 && count2 <= 100.0;
  sum = ++count1 + count2, count2 *= 2.5);

위의 예에서, for 문은 반드시 루프몸체를 가질 필요가 없음.

첫 번째와 세 번째 식은 다중 문장들임.

 

+ C 초기 버전과 현재(C99 이후) for문의 차이점

(1) 루프 제어를 위해 산술식 이외에도 불리안식을 사용 가능함.

(2) 첫 번째 식이 변수 정의(ex. int i = 0)를 포함할 수 있음. for 문에 정의된 변수의 영역은 그 정의부터 루프 몸체의 끝까지임.

Java의 for 문은 C++와 같으나, 루프 제어식이 boolean으로 제한된다는 것만 다름.

 

ㄴ Python의 for문

일반적인 형식

for 루프_변수 in 객체 :
  루프 몸체
[else :
  else 절]

루프 변수는 객체에 포함된 값을 루프 몸체의 각 실행에 대하여 한 개씩 할당받음. 객체는 대부분의 경우 범위임.

else 절이 존재할 때, 이 절은 루프가 정상적으로 종료되면 실행됨.

+ range() 함수

Python의 가장 단순한 계수 루프를 위해서 range 함수가 사용됨.
range 함수는 한 개, 두 개, 또는 세 개의 매개변수를 가지며, 주어진 매개변수 범위에 속한 가장 높은 값을 반환하지 않음.
ex) range(5)는 [0, 1, 2, 3, 4]를 반환함 / range(2, 7)는 [2, 3, 4, 5, 6]를 반환함 / range(0, 8, 2)는 [0, 2, 4, 6]를 반환함

- 논리 제어 루프

많은 경우에, 반복 제어는 계수기가 아닌 불리안 식에 기반함 (논리 제어 루프가 편리함)
실제로, 논리 제어 루프는 계수기-제어 루프보다 더 일반적임 (for문은 while문으로 대체가 가능함)
ㄴ C 언어의 사전-검사 및 사후-검사 논리 제어 루프

// 사전-검사 논리 제어 루프
while (제어식)
  루프 몸체


// 연산 의미론적 기술
loop:
if 제어식 값이 거짓이면 goto out
  루프 몸체
goto loop
out: ...

사전-검사 제어 루프에서, 문장이나 문장 세그먼트는 식이 참으로 평가되는 한(=거짓으로 평가될 때까지) 계속 실행함.

// 사후-검사 논리 제어 루프
do
  루프 몸체
while (제어식);


// 연산 의미론적 기술
loop:
  루프 몸체
if 제어식 값이 참이면 goto loop

사후-검사 제어 루프에서, 문장이나 문장 세그먼트는 식이 거짓으로 평가될 때까지(=참으로 평가되는 한) 계속 실행함.

Java의 while 문과 do-while 문은 C와 C++와 유사하지만, 차이점으로 제어식이 불리언 타입이어야 함.

 

- 사용자-지정 루프 제어 메커니즘

프로그래머가 루프 제어를 위한 위치를 루프 몸체의 처음이나 끝이 아닌 곳을 선택하는 것이 편리할 때가 있음.
이 때문에, 여러 언어들은 사용자-지정 루프 제어를 위한 구문적 메커니즘(Syntactic Mechanism)을 제공함.
보통 그러한 루프는 무한 루프의 구조를 가지지만, 사용자-지정 탈출을 포함함.

 

ㄴ C, C++, Python 등의 언어
무조건적이면서 레이블이 없는 탈출 (break)를 제공함.

break 문은 가장 가까운 루프를 탈출하는 제어문임.

while (sum < 1000) {
  getnext(value);
  if (value < 0) break;
  sum += value;
}
// 음수 값은 루프를 종료

continue 문은 아래쪽 루프 문장들을 건너뛰는 제어문임.

while (sum < 1000) {
  getnext(value);
  if (value < 0) continue;
  sum += value;
}
// 음수 값은 sum에 더하지 않음


ㄴ Java 등의 언어
무조건적이면서 레이블을 갖는 탈출 (break)를 제공하며, 레이블 없이도 사용 가능함.

outerLoop:
for (row = 0; row < numRows; row++)
  for (col = 0; col < numCols; col++) {
    sum += mat[row][col];
    if (sum > 1000.0)
      break outerLoop;
}
// break outerLoop;는 for문 두 개를 모두 탈출하게 만듦

 

 

- 데이터 구조에 기반한 반복

ㄴ 일반적인 데이터-기반 반복문

(1) 사용자-정의 데이터 구조 또는 (2) 사용자-정의 함수(반복자)를 사용하여, 그 구조의 원소들에 대하여 반복하게 만듦.

따라서 데이터-기반 반복문을 사용자-정의 반복문라고도 함.

 

ㄴ 반복자(Iterator)

각 반복의 시작 시점에서 호출됨.
호출될 때마다 특정 데이터 구조로부터 어떤 특정 순서로 한 개의 원소를 반환함.
+ C 언어에서 사용자-정의 반복문으로 for 문을 활용하는 예

프로그램이 데이터 노드들을 포함하는 사용자-정의 이진 트리를 가지며, 각 노드의 데이터가 어떤 특정 순서로 처리되어야 한다고 가정함.
트리 루트가 root 변수로 가리켜지고, traverse() 함수는 매개변수가 요구된 순서로 트리의 다음 번째 원소를 가리키도록 설정하는 함수라고 가정하면, 반복자는 traverse() 함수가 됨.

for (ptr = root; ptr != null; ptr = traverse(ptr)) {
  ...
}

사용자-정의 반복문은 객체-지향 프로그래밍에서 더욱 중요해짐. 객체-지향 프로그래밍들이 데이터 구조, 특히 데이터 모임들에 대한 추상 데이터 타입을 상시적으로 사용하기 때문임. (추상 데이터 타입은 나중에 다룰 예정)
이 경우, 사용자-정의 반복문과 그 반복자가 데이터 추상화의 작성자에 의해 제공되어야 함.

+ Java에서 향상된 for 문 (Java 5.0 이후)
이 for 문은 배열의 값들에 대해서 또는 Iterable 인터페이스를 구현하는 집합체(Collection)의 객체들에 대해서 반복하는 과정을 단순화시킴.

for (String myElement : myList) { ... }
// myList라는 이름의 ArrayList 인스턴스에서,
// 모든 원소들에 대하여 반복하면서 각 원소를 myElement로 설정

이 새로운 문장은 예약어가 for이기는 하지만 "foreach"라고 읽음

 

● 무조건 분기

실행 제어를 프로그램상의 명세된 위치로 이동시키는 명령문으로, goto 문을 의미함.
무조건 분기문인 goto 문은 프로그램 문장들의 실행 흐름을 제어하기 위한 강력한 문장이지만, 무분별하게 사용하면 심각한 문제를 초래할 수 있음.
따라서 몇몇 언어들(Java, Python 등)은 goto 없이 설계되었음. C는 goto 를 지원함.

- 단점

프로그램을 판독하기 어렵게 만들어 신뢰할 수 없게 만듦.
유지 보수 비용도 많이 듦.

반응형

댓글