[C언어 50강] 12강. 반복문 1: for / while 기본 패턴
프로그램은 결국 같은 일을 얼마나 정확하게 반복하느냐의 싸움입니다. 조건문이 “갈림길”을 만든다면, 반복문은 “같은 길을 몇 번 걸을지”를 결정합니다. 이번 강의에서는 for와 while을 문법 암기 대상으로 보지 않고, 반복의 시작 조건·지속 조건·종료 조건을 설계하는 도구로 이해해보겠습니다. 특히 실무에서 자주 문제가 되는 무한 루프, 경계값 오류(off-by-one), 입력 대기 루프를 중심으로 왜 문제가 생기는지까지 설명합니다.
핵심 개념
- 반복문은 “코드를 줄이는 문법”이 아니라 “반복 규칙을 코드로 명시하는 구조”다.
for는 반복 횟수가 비교적 명확할 때,while은 종료 시점이 실행 중 결정될 때 유리하다.- 반복문 버그의 대부분은 문법이 아니라 경계 조건(
i < nvsi <= n)과 상태 갱신 누락에서 발생한다.
개념 먼저 이해하기
초보 단계에서 반복문을 배우면 보통 “for는 숫자 반복, while은 조건 반복” 정도로 요약하고 넘어갑니다. 하지만 실제 코드를 오래 유지보수하다 보면 더 중요한 질문이 생깁니다. “이 반복이 언제 시작되고, 언제 멈추며, 매 회차마다 무엇이 바뀌는가?” 이 세 가지가 불분명하면, 코드는 돌아가더라도 신뢰하기 어려운 코드가 됩니다.
반복문은 본질적으로 상태(state) 전이입니다. 예를 들어 i = 0에서 시작해 매번 i++로 상태를 갱신하고, i < 10인 동안만 동작한다면 우리는 “0부터 9까지 총 10회 실행”이라는 예측 가능한 모델을 가집니다. 이 모델이 명확할수록 디버깅이 쉬워집니다. 반대로 시작값, 종료조건, 갱신식이 서로 다른 의도를 가지면(예: 1부터 시작하는데 i < n인지 i <= n인지 불명확) 프로그램은 작은 테스트에서는 통과해도 실데이터에서 망가집니다.
for문은 반복의 3요소(초기화, 조건, 증감)를 한 줄에 모아 둡니다. 그래서 “반복 제어 변수의 생명주기”를 읽기 쉽습니다. 예를 들어 for (int i = 0; i < n; i++)를 보는 순간, 대부분의 개발자는 이 루프가 인덱스 기반 순회라는 걸 즉시 이해합니다. 즉 for의 강점은 성능이 아니라 의도의 압축 표현입니다.
반면 while은 제어를 더 유연하게 분산합니다. 조건은 헤더에 있지만, 상태 변화는 본문 어디에서든 일어날 수 있습니다. 이 자유도가 장점이자 위험입니다. 입력을 계속 읽다가 특정 값이 나오면 종료하는 패턴, 네트워크 소켓에서 데이터가 들어오는 동안 처리하는 패턴처럼 종료 시점이 외부 상황에 달린 경우 while이 자연스럽습니다. 대신 갱신 코드가 누락되면 무한 루프가 되기 쉽고, continue가 갱신 코드를 건너뛰어 버리는 사고도 자주 납니다.
또 하나 중요한 포인트는 **반복은 계산이 아니라 계약(contract)**이라는 관점입니다. “배열의 모든 원소를 한 번씩 방문한다”, “유효한 입력이 들어올 때까지 재시도한다”, “합계가 임계값을 넘으면 즉시 중단한다” 같은 계약을 코드로 표현한 것이 반복문입니다. 이 계약이 명확하면 팀원이 코드를 읽을 때 버그를 빨리 발견합니다. 예를 들어 배열 길이가 n일 때 i <= n은 계약 위반입니다(마지막 접근은 n-1). 이런 문제는 컴파일러가 잡아주지 못하는 경우가 많아, 개발자가 반복 계약을 스스로 점검해야 합니다.
정리하면 for와 while의 선택 기준은 문법 취향이 아니라 모델링 방식입니다. 반복 횟수와 범위가 명확하면 for, 외부 입력/상태 변화에 따라 멈추면 while이 적합합니다. 그리고 어떤 문법을 쓰든, 시작·조건·갱신·종료를 사람 눈으로 검증 가능한 형태로 작성하는 것이 반복문 설계의 핵심입니다.
기본 사용
예제 1) for 기본 패턴: 배열 합계 구하기
#include <stdio.h>
int main(void) {
int scores[] = {78, 85, 92, 88, 95};
int n = (int)(sizeof(scores) / sizeof(scores[0]));
int sum = 0;
for (int i = 0; i < n; i++) {
sum += scores[i];
}
printf("합계: %d\n", sum);
printf("평균: %.2f\n", (double)sum / n);
return 0;
}
설명:
i = 0에서 시작해i < n동안 반복하므로 정확히n회 실행됩니다.- 배열 인덱스의 유효 범위는
0부터n-1이므로i < n이 안전한 기본형입니다. for헤더만 봐도 반복 계약이 드러나서 코드 리뷰가 쉽습니다.
예제 2) while 기본 패턴: 유효한 입력이 들어올 때까지 재시도
#include <stdio.h>
int main(void) {
int age = -1;
while (age < 0 || age > 130) {
printf("나이를 입력하세요 (0~130): ");
if (scanf("%d", &age) != 1) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
;
}
age = -1;
printf("숫자를 입력해야 합니다.\n");
continue;
}
if (age < 0 || age > 130) {
printf("범위를 벗어났습니다. 다시 입력하세요.\n");
}
}
printf("입력 완료: %d세\n", age);
return 0;
}
설명:
- 종료 시점이 사용자 입력 품질에 따라 달라지므로
while이 자연스럽습니다. - 입력 실패 시 버퍼를 비우지 않으면 같은 실패가 반복되어 루프가 빠져나오지 못할 수 있습니다.
continue를 쓰더라도 상태(age)를 재설정해 다음 반복 조건이 올바르게 계산되도록 해야 합니다.
예제 3) 중단 조건 있는 루프: 누적합이 임계값을 넘으면 종료
#include <stdio.h>
int main(void) {
int data[] = {3, 7, 12, 5, 9, 11};
int n = (int)(sizeof(data) / sizeof(data[0]));
int limit = 20;
int sum = 0;
for (int i = 0; i < n; i++) {
sum += data[i];
printf("i=%d, 현재 합계=%d\n", i, sum);
if (sum >= limit) {
printf("임계값 %d 도달. 반복 종료\n", limit);
break;
}
}
return 0;
}
설명:
- 반복 횟수는 최대
n회지만, 비즈니스 조건 충족 시break로 조기 종료합니다. - “최대 반복 범위”와 “실제 종료 조건”을 분리하면 로직이 명확해집니다.
- 실무에서는 이런 패턴이 성능 최적화(불필요한 반복 방지)와도 연결됩니다.
자주 하는 실수
실수 1) 경계 조건을 잘못 써서 배열 범위를 벗어남
- 문제 코드 예:
for (int i = 0; i <= n; i++) - 원인: 반복 횟수와 마지막 인덱스를 혼동함.
- 해결: 인덱스 순회는 기본적으로
i < n을 습관화하고, 예외 케이스만 의도적으로<=를 사용합니다.
실수 2) while 본문에서 상태 갱신을 빠뜨려 무한 루프가 됨
- 문제 상황:
while (x < 10)인데 본문에서x++가 없음. - 원인: 조건은 작성했지만 상태 전이를 놓침.
- 해결: while 작성 후 “조건에 등장하는 변수들이 본문에서 어떻게 바뀌는지”를 체크리스트로 확인합니다.
실수 3) continue로 인해 필요한 코드가 건너뛰어짐
- 문제 상황:
continue위에만 갱신 코드가 있고 아래에 공통 정리 코드가 있는 경우. - 원인: 루프 흐름 제어를 머릿속으로만 추적함.
- 해결:
continue사용 시 갱신/정리 코드 위치를 재배치하거나, 공통 처리 함수를 분리해 누락을 막습니다.
실무 패턴
- 인덱스 순회 표준화: 팀 컨벤션으로
for (int i = 0; i < n; i++)를 기본형으로 통일하면 리뷰 비용이 줄어듭니다. - 무한 루프 + 명시적 탈출: 이벤트 처리처럼 종료가 외부 조건에 달린 경우
while (1)+break패턴을 쓰되, 탈출 조건을 상단에 배치해 가독성을 확보합니다. - 입력 검증 루프 분리: 입력/검증/업무 로직을 한 함수에 섞지 말고, “유효값을 반환하는 입력 함수”로 분리하면 테스트가 쉬워집니다.
- 반복 변수 최소 스코프: 가능한
for헤더 안에서 변수 선언(int i)하여 오염을 줄입니다. - 로그 기반 디버깅: 반복 버그는 재현이 어려운 경우가 많아, 조건/인덱스/누적값 로그를 찍는 습관이 큰 차이를 만듭니다.
오늘의 결론
반복문은 단순한 문법이 아니라 “상태를 안전하게 전이시키는 계약”입니다. for는 반복 계약을 압축해 보여주고, while은 외부 상황 기반 반복에 강합니다. 어떤 문법을 쓰든 시작값·종료조건·갱신식을 분명히 적으면 반복 버그의 대부분을 초기에 차단할 수 있습니다.
연습문제
- 정수 1부터 100까지의 합을
for로 구하고, 짝수만의 합도 함께 출력해보세요. - 사용자에게 정수를 계속 입력받다가
0이 들어오면 종료하는while프로그램을 작성하세요. 종료 전까지 입력된 양수 개수와 합계를 출력하세요. - 길이 7인 배열에서 최댓값과 최솟값을 찾는 코드를
for로 작성하고, 경계 조건을 왜i = 1부터 시작하는지 설명해보세요.
이전 강의 정답
지난 11강(switch-case) 연습문제 예시 정답입니다.
- 요일 번호 출력
switch(day)로 1~7을 매핑하고default에서 예외 처리하면 됩니다.
- 학점 출력 + 100점(A+) 처리
switch(score / 10)에서case 10을 분리하고, 100점인지 추가 확인하여 A+를 출력합니다.
- 의도적 fall-through 예시
- 여러 case에서 공통 안내문을 출력할 때
break를 일부러 생략할 수 있지만, 반드시 주석으로 의도를 남겨야 합니다.
참고 코드:
#include <stdio.h>
int main(void) {
int day = 3;
switch (day) {
case 1: printf("월\n"); break;
case 2: printf("화\n"); break;
case 3: printf("수\n"); break;
case 4: printf("목\n"); break;
case 5: printf("금\n"); break;
case 6: printf("토\n"); break;
case 7: printf("일\n"); break;
default: printf("잘못된 번호\n"); break;
}
int score = 100;
switch (score / 10) {
case 10:
if (score == 100) printf("A+\n");
else printf("A\n");
break;
case 9: printf("A\n"); break;
case 8: printf("B\n"); break;
default: printf("C 이하\n"); break;
}
return 0;
}
실습 환경/재현 정보
- 컴파일러: Apple clang 17.x 또는 gcc 13+
- 컴파일 옵션:
-Wall -Wextra -Werror -std=c11 - 실행 환경: macOS(arm64), Linux x86_64 터미널
- 재현 체크리스트:
i < n을i <= n으로 바꿨을 때 어떤 문제가 생기는지 확인- while에서 갱신 코드를 주석 처리해 무한 루프를 재현해보기
continue위치를 바꿔 출력/집계 결과가 달라지는지 비교