선택문
프로그램이 조건에 따라 실행되거나 실행되지 않아야 할 때 필요한 것이 바로 제어문입니다. 제어문은 보통 선택문, 반복문, 분기문으로 구분합니다. 우선 선택문부터 자세히 다뤄 보겠습니다.
if문
if문
if문은 전제인 조건식과 조건을 만족해야 수행되는 실행문으로 구성됩니다. 그중 가장 기본적인 if문은 다음과 같습니다.
if (a > 10)
{
b = a;
}
if 문은 다음과 같이 다양한 모양으로 쓸 수 있습니다.
if (a > 10)
b = a;
if (a > 10) b = a;
그러나 실행할 문장이 두 문장 이상이면 반드시 중괄호로 묶어야 합니다.
if ~ else문
조건을 만족하지 못할 때 수행되는 실행문을 작성할 때는 if ~ else문을 쓰면 됩니다.
if (a >= 10)
{
a = 1;
}
else
{
a = -1;
}
if문을 두 번 사용하면 두 조건식을 모두 검사하므로 한 번 검사할 때보다 실행 속도가 느려질 수 있습니다. 따라서 2개의 실행문 중 하나를 선택하는 경우에는 주저 없이 if ~ else문을 사용하는 것이 좋습니다.
if ~ else if ~ else문
3개 이상의 실행문에서 반드시 하나를 선택하는 경우 if ~ else if ~ else문을 사용합니다.
if (a > 0)
{
b = 1;
}
else if (a == 0)
{
b = 2;
}
else
{
b = 3;
}
이 경우 조건식을 차례로 검사하므로 이전 조건의 결과가 반영됩니다. 따라서 순서가 중요하지 않다면 참이 될 가능성이 많은 조건식을 먼저 사용해 조건식을 검사하는 횟수를 줄이는 것이 좋습니다.
if문 중첩
선행조건이 꼭 필요할 때는 if문 중첩을 사용합니다. if문의 실행문으로 다른 if문이 사용되면 이를 if문 중첩이라고 합니다.
if (a > 10)
{
if (b >= 0)
{
b = 1;
}
else
{
b = -1;
}
}
if문 중첩은 선행조건이 필요한 경우에도 사용하지만 선행조건이 없이도 실행 효율을 위해 의도적으로 중첩해 사용할 수 있습니다.
if (a == 1)
{
printf("1");
}
else if (a == 2)
{
printf("2");
}
else if (a == 3)
{
printf("3");
}
else if (a == 4)
{
printf("4");
}
else
{
printf("5");
}
위와 같이 1부터 하나씩 차례로 검사하는 방법을 다음과 같이 범위를 나누어 검사할 수 있습니다.
if (a <= 3)
{
if (a == 1)
{
printf("1");
}
else if (a == 2)
{
printf("2");
}
else
{
printf("3");
}
}
else
{
if (a == 4)
{
printf("4");
}
else
{
printf("5"):
}
}
이 방법을 분할 정복 기법(devide and conquer)이라고 하는데 비교 항목이 많은 경우 if문을 여러 번 중첩해서 쓰면 실행 시간을 줄이는 데 도움이 됩니다. 그러나 얼핏 보면 선행 조건이 필요하다고 오해할 수 있으며 코드가 읽기 어려워지므로 주의해서 사용해야 합니다.
switch ~ case문
여러 개의 상수 중 조건에 해당하는 하나를 골라 실행하는 것이 switch ~ case문입니다. switch ~ case문의 일반 형식은 다음과 같습니다.
switch ( 조건식 )
{
case 상수식1:
실행문1;
break;
case 상수식2:
실행문2;
break;
default:
실행문3;
break;
}
switch ~ case문을 쓸 때는 다음 2가지 규칙을 꼭 지켜야 합니다.
- 조건식으로 정수식만 사용해야 합니다.
- 기본적으로 case는 break를 포함합니다.
정수식은 정수형 상수나 변수를 쓸 수 있고 수식을 사용할 때는 결괏값이 정수여야 합니다. 또한 case에 사용하는 상수식 역시 정수만 가능합니다.
case와 default는 건너뛸 위치를 표시하는 레이블(label)의 역할을 합니다. 따라서 해당 case에서 실행할 문장이 두 문장 이상이면 순서대로 작성하면 됩니다.
경우에 따라 switch ~ case문은 if문으로 바꾸어도 동일한 결과를 얻을 수 있습니다. 실행결과가 같다면 어떤 구문을 사용할 것인지는 선택의 문제입니다. 다만, switch ~ case문은 정수값에 따라 실행 문장을 선택하기에 좋은 구조며 범위를 검사하는 경우에는 적합하지 않습니다.
break 생략
break는 필요에 따라 생략할 수 있습니다. break를 사용하면 적절한 시점에 블록을 탈출해 필요한 부분만 선택적으로 실행할 수 있습니다. 그러나 반드시 사용해야 하는 것은 아닙니다. 필요에 따라 전부나 일부를 생략할 수 있습니다.
switch (rank)
{
case 1:
m += 300;
case 2:
m += 200;
case 3:
m += 100;
}
위와 같이 작성할 경우 각 case에 break가 없으므로 블록의 끝까지 모든 문장을 실행합니다. 예를 들어 rank의 값이 2일 경우 200이 증가하는 것이 아닌 200 + 100이 되어 총 300이 증가하게 됩니다.
반복문
일정 조건을 만족하는 동안 같은 실행문을 반복하는 게 바로 반복문입니다. 따라서 반복문에는 항상 반복의 조건을 정의하는 조건식과 조건을 만족했을 때 실행하는 문장이 있으며, 다음과 같이 조건식의 위치에 따라 크게 while문, for문, do ~ while문 3가지로 구분할 수 있습니다.

while문
while문은 조건식을 먼저 검사하고 조건식이 참인 동안 실행문을 반복합니다.
while (a < 10)
{
a = a * 2;
}
실행을 반복하다 조건식이 거짓이 되었을 때 비로소 while문을 끝내고 while문 이후의 문장을 실행합니다. if문과 동일하게 실행문이 한 문장이면 중괄호를 생략할 수 있습니다.
for문
while문과 비슷하게 for문도 반복문입니다. 다만, for문은 실행문을 원하는 횟수만큼 반복할 때 사용합니다. 예를 들어 a 값을 두 배로 증가시키는 문장을 세 번 반복하는 코드를 for문으로 작성하면 다음과 같습니다.
for (i = 0, i < 3; i++)
{
a = a * 2;
}
위 예제에서 초기식(i = 0)은 변수 i를 초기화하는 부분으로 딱 한 번 실행됩니다. 이어서 조건식을 검사해 결과가 참이면 블록 안으로 들어가 반복할 문장인 a = a * 2를 실행합니다. 반복할 부분을 수행하면 증감식으로 올라가 i 값을 1 증가시키고 다시 조건식을 검사하는 순서로 반복합니다.
for문은 반복할 문장을 실행할 때마다 특정 변수의 값을 하나씩 증가시켜 원하는 횟수가 될 때까지 반복하는 방법을 사용합니다. 따라서 반복 횟수를 제어하는 변수가 필요하며 변수명으로 보통 i, j ,k를 사용합니다.
for문 사용 시 주의할 점은 다음과 같습니다.
- 초기식, 조건식, 증감식은 반복 횟수를 알기 쉽게 작성합니다.
- 반복 횟수를 세는 변수를 반복문 안에서 바꾸지 않는 것이 좋습니다.
대부분 for (i = 0; i < 3; i++)와 같은 형식을 많이 사용하며, 이때 조건식에 사용한 값(3)이 반복횟수가 됩니다.
C99부터는 for (int i = 0; i < 3; i++)처럼 for문의 초기식에서 변수의 선언과 초기화를 동시에 할 수 있습니다.
이때 선언된 변수는 for문 블록 내부에서만 유효한 지역 변수가 됩니다.
초기식, 조건식, 증감식의 위치를 바꾸면 while문으로 바꿀 수도 있습니다.
다만, 초기식, 조건식, 증감식을 한 곳에 모아 놓으면 반복 횟수를 쉽게 알 수 있기 때문에 반복 횟수가 정해진 경우라면 for문을 사용하는 것이 좋습니다.
do ~ while문
while문, for문은 조건식을 먼저 확인하는 반면, do ~ while문은 일단 반복할 문장을 수행한 후에 조건을 검사합니다. 예를 들어 a 값이 10보다 작은 동안 두 배씩 늘리는 문장을 do ~ while문으로 작성하면 다음과 같습니다.
do
{
a = a * 2;
} while (a < 20);
do ~ while문도 다른 반복문과 마친가지로 반복할 문장이 한 문장이면 중괄호를 생략할 수 있습니다. 그리고 마지막 while문에 붙어 있는 세미콜론을 빠뜨리지 않도록 주의해야 합니다.
다른 반복문과는 다르게 do ~ while문은 조건식과 관계없이 반복할 문장을 최소 한 번은 실행합니다.
중첩 반복문 (다중 반복문)
중첩 반복문은 반복문 안에 실행할 문장으로 반복문이 포함된 것입니다.
예를 들어 종이 위에 별을 5개씩 줄지어 3줄을 그린다고 생각해 보겠습니다. 이를 다르게 표현하면 다음과 같이 별 하나를 다섯 번 반복해서 그리는 것을 세 번 반복하는 것과 같습니다.

이러면 중첩 반복문입니다. 이를 코드로 작성해보겠습니다.
#include <stdio.h>
int main(void)
{
int i, j;
for (i = 0; i < 3; i++)
{
for (j = 0; J < 5; J++)
{
printf("*");
}
printf("\n");
}
return 0;
}
중첩 반복문을 사용할 때는 반드시 각 반복문이 서로 독립적인 제어 변수를 사용해야 각각 원하는 횟수를 반복할 수 있습니다.
분기문
이번에는 반복문 안에서 사용하는 제어문인 break와 continue 분기문을 살펴보겠습니다.
break
break는 반복문 안에서 반복을 즉시 끝낼 때 사용합니다. 모든 반복문은 조건식이 거짓을 때 반복이 끝납니다. 따라서 반복문 처음에 조건식이 있는 while문이나 for문은 반복문 위에서 반복이 끝나고 마지막에 조건식이 있는 do ~ while문은 반복문 밑에서 반복이 끝납니다. 그런데 예외적으로 반복문 중간에서 임의로 반복을 끝내고 싶을 때는 break를 사용합니다.
break를 사용할 때는 주의할 점이 있습니다. break는 자신을 포함하는 반복문 하나만 벗어난다는 것입니다. 즉, 반복문이 중첩된 경우 가장 안쪽에서 break를 사용하면 break가 포함된 가장 안쪽 반복문 하나만 벗어날 수 있습니다.
무한 반복문 탈출
다음과 같이 반복문의 조건식이 항상 참이면 무한 반복문이 됩니다.
while (1)
{
printf("Infinity");
if (조건문) break;
}
for (;;)
{
printf("Infinity");
if (조건문) break;
}
이러한 무한 반복문은 기본적으로 무한히 반복되므로 반복을 끝낼 때는 조건에 따라 break를 사용합니다.
continue
continue는 반복문의 일부를 건너뜁니다. 반복문 안에서 continue를 사용하면 다음 실행 위치가 반복문의 블록 끝이 됩니다. 이것은 블록을 탈출하는 것은 아닙니다. 따라서 조건에 따라 반복문의 일부를 제외하고 반복할 수 있습니다.
for (i = 0; i <= 100; i++)
{
if ( (i % 3) == 0)
{
continue;
}
sum += i;
}
위 코드에서 continue에 의해서 제어가 블록의 끝으로 이동한 후에는 다시 반복이 시작됩니다. 결국 다음 실행 위치는 증감식이 됩니다.
함수
함수란 기능을 수행하는 코드 단위를 말합니다. 지금까지 사용해 왔던 main 함수가 대표적인 함수입니다. 그리고 printf와 scanf도 함수입니다. printf나 scanf 함수처럼 특정 기능을 미리 약속하고 프로그램에서 바로 사용할 수 있게 구현되어 있는 함수를 표준 라이브러리 함수라고 합니다. printf나 scanf 함수는 예제에 있던 stdio.h 헤더 파일에 포함되어 있습니다.
이처럼 C 언어에서 표준으로 제공하는 함수 외에도 자주 사용하는 코드를 함수로 만들어 필요할 때 사용할 수도 있습니다. 함수를 만드려면 다음의 3가지가 중요합니다:
- 함수 정의: 함수를 실제 코드로 만드는 것으로 함수의 기능을 구현합니다.
- 함수 호출: 함수 호출을 해야지 함수를 사용할 수 있습니다.
- 함수 선언: 프로그램의 상단에서 어떤 함수를 사용할 것이라고 컴파일러에 정보를 주는 역할을 합니다.
함수 정의
앞에서는 main 함수 하나로 프로그램을 만들었습니다. 이제부터는 main 함수와 또 다른 함수가 프로그램의 전체 기능을 나누어 수행하도록 해보겠습니다. 새로운 함수를 만드는 방법도 main 함수를 만드는 방법과 크게 다르지 않습니다. 다만, 함수를 만들기 전에 다음 3가지를 먼저 생각해 보겠습니다:
- 함수의 기능에 맞는 이름은 무엇인가? → 함수명
- 함수가 기능을 수행하는 데 필요한 데이터는 무엇인가? → 매개변수
- 함수가 수행된 후의 결과는 무엇인가? → 반환형
이 질문들이 바로 함수 원형(function prototype)을 이룹니다. 이 내용을 코드 형식으로 쓰면 다음과 같습니다.
반환형 함수명(매개변수1, 매개변수2)
{
// 함수가 수행하는 명령.
}
여기에 함수가 수행할 내용을 중괄호 안에 넣어 주면 함수가 완성됩니다. 이렇게 함수는 만드는 것을 함수 정의라고 합니다.
#include <stdio.h>
int sum(int x, int y);
int main(void)
{
int a = 10, b = 20;
int result;
result = sum(a, b); // result: 30
return 0;
}
int sum(int x, int y)
{
return (x + y);
}
함수를 다른 함수 안에서 정의할 수는 없기 때문에 sum 함수는 main 함수와 별도의 구역에서 만듭니다.
return은 함수의 실행결과를 돌려주는 제어문입니다.
컴파일러는 변수명의 사용 범위를 선언한 블록 내부로 제한하기 때문에 둘 이상의 함수에서 같은 이름의 변수를 선언해도 중복 에러가 발생하지 않습니다. 이 경우 각 변수는 함수별로 독립된 저장 공간을 가지며 함수 내에서만 사용할 수 있습니다.
함수 호출
함수 호출
이렇게 함수는 얼마든지 만들 수 있지만, 만든 함수가 모두 자동으로 실행되는 것은 아닙니다. 함수를 사용하려면 먼저 함수 호출을 해야합니다.
result = sum(a, b);
함수를 호출할 때에는 이름을 사용하며 함수에 필요한 데이터를 괄호 안에 넣어 주는데 이를 인수(argument)라 합니다. 인수는 상수나 변수를 쓸 수 있고 수식을 사용하면 수식의 결괏값이 인수로 쓰입니다.
함수를 호출할 때 이렇게 입력된 인수 a, b의 값은 호출된 함수의 매개변수(parameter)에 복사되어 사용됩니다. 코드에서 sum 함수가 호출되면 main 함수의 실행은 잠시 멈추고 비로소 sum 함수 정의 부분에 있는 코드가 실행됩니다. 이때 인수는 함수의 매개변수에 순서대로 복사되어 sum 함수 안에서 사용됩니다.

함수 반환
호출된 함수의 실행이 끝나고 값을 반환할 때 return문을 사용합니다.

sum 함수에서 x와 y를 더하고 그 값을 함수가 호출된 곳으로 반환합니다. 이후 main 함수는 반환된 값을 대입 연산으로 result에 저장하고 잠시 중단했던 함수의 나머지 부분을 실행합니다.
컴파일러는 함수를 호출할 때 반환값을 저장할 공간을 미리 준비해 둡니다. 이 공간은 컴파일러가 별도로 확보하는 공간이며 식별할 수 있는 이름이 없으므로 계속 사용할 수 없습니다. 대신에 저장된 반환값을 대입 연산으로 다른 변수에 복사하거나 수식에 바로 사용하는 것은 가능합니다.
함수 선언
함수 선언은 컴파일러가 새로 만든 함수를 인식할 수 있도록 알리는 역할을 합니다.
int sum(int x, int y);
선언 방법은 간단합니다. 그저 함수 원형에 세미콜론을 붙이면 됩니다.
함수 선언은 main 함수 앞에 하며 다음처럼 매개변수 이름은 생략할 수 있습니다.
int sum(int, int);
함수 선언이 별도로 필요한 이유
- 함수 선언에서 반환값의 형태를 확인합니다.
- 함수의 호출 형식에 문제가 없는지 검사합니다.
컴파일러는 컴파일할 때 함수를 호출한 자리에 반환값과 같은 형태의 저장 공간을 준비합니다. 즉, 정수를 반환하면 호출한 자리에 int형 공간을 확보하고 실수를 반환하면 double형 공간을 확보합니다. 따라서 함수를 호출하기 전에 선언을 통해 반환형을 미리 컴파일러에 알릴 필요가 있습니다. 물론 함수 정의에서도 반환형을 확인할 수 있으므로 다음과 같이 함수 호출 이전에 함수를 정의하는 방법도 있습니다.
#include <stdio.h>
int sum(int x, int y)
{
...
}
int main(void)
{
result = sum(a, b);
...
return 0;
}
위와 같이 main 함수 앞에 sum 함수를 정의해도 실행 순서는 변하지 않습니다. 프로그램은 항상 main 함수부터 시작하며 그 이후에는 호출 순서에 따라 실행되기 때문입니다.
함수를 미리 선언하지 않으려면 항상 호출하는 함수 앞에 정의해야 하는데, 이때 여러 함수 간에 호출 관계가 엉켜 있다면 순서에 맞게 정의하는 일이 쉽지 않습니다. 따라서 필요한 함수를 main 함수 밑에 차례로 만들고 main 함수 앞에는 모든 함수를 선언해 함수의 종류와 원형을 한눈에 파악하고 자유롭게호출할 수 있도록 작성하는 것이 좋습니다.
함수를 호출하는 위치보다 앞선 위치에 함수를 정의하거나 선언하지 않아도 프로그램이 문제 없이 실행될 수 있습니다. 이 경우 컴파일러는 호출 함수의 반환형을 int형으로 간주해 컴파일합니다.
보통 컴파일러는 정의나 선언 없이 호출이 먼저 나오는 경우 경고 메시지로 알려 줍니다.
여러 가지 함수 유형
함수 원형은 반환형, 함수명, 매개변수로 이루어진다고 배웠습니다. 하지만 언제나 함수에 반환형과 매개변수가 있는 건 아닙니다.
매개변수가 없는 함수
매개변수가 없는 함수를 살펴보겠습니다. 예를 들어 키보드로 수를 입력하면 양수를 반환하는 함수를 만든다고 할 때 호출한 함수로부터 값을 받을 필요가 없으므로 매개변수도 필요 없습니다. 이때 함수의 매개변수 자리에는 void를 사용합니다.
#include <stdio.h>
int get_num(void);
int main(void)
{
int result;
result = get_num();
return 0;
}
int get_num(void)
{
int num;
printf("양수 입력: ");
scanf("%d", &num);
return num;
}
반환값이 없는 함수
함수는 기능에 따라 형태가 결정됩니다. 데이터를 받아서 단지 화면에 출력하는 함수라면 반환값이 필요 없습니다.
예를 들어 문자와 숫자를 인수로 받으면 문자를 숫자만큼 화면에 출력하는함수를 생각해 보겠습니다. 화면에 출력한 내용이 함수가 수행한 결과이므로 호출한 곳으로 특별히 값을 반환할 필요가 없습니다. 매개변수의 빈자리에 void를 사용했던 것처럼 반환형의 자리에 void를 사용합니다.
#include <stdio.h>
void print_char(char ch, int count);
int main(void)
{
print_char('@', 5);
return 0;
}
void print_char(char ch, int count)
{
int i;
for (i = 0; i < count; i++)
{
printf("%c", ch);
}
return;
}
return의 역할이 값을 돌려주는 일인데, 돌려줄 값이 없으므로 자연스럽게 혼자 쓰입니다. 심지어 return문 자체를 생략하는 것도 가능합니다. return문이 없어도 함수의 코드를 모두 수행하면 호출한 곳으로 자동으로 돌아갑니다.
반환형이 void인 함수는 컴파일러가 반환값이 없다고 가정해 호출한 위치에 반환값을 저장할 공간을 준비하지 않습니다.
매개변수와 반환값이 모두 없는 함수
매개변수와 반환값이 모두 없는 함수도 있습니다. 예를 들어 일정한 문자열을 여러 번 출력하는 함수라면 매개변수와 반환값이 모두 필요 없습니다. 이때는 함수의 매개변수와 반환형에 모두 void를 씁니다.
#include <stdio.h>
void print_line(void);
int main(void)
{
print_line();
printf("이름 수량 가격");
print_line();
return 0;
}
void print_line(void)
{
int i;
for (i = 0; i < 30; i++)
{
printf("-");
}
printf("\n");
}
재귀호출 함수
함수는 보통 다른 함수를 호출하는데 재귀호출 함수(recursive call function)는 자기 자신을 호출합니다.
#include <stdio.h>
void fruit(void);
int main(void)
{
fruit();
return 0;
}
void fruit(void)
{
printf("apple\n");
fruit();
}
함수가 모든 명령을 수행하면 자동으로 반환하는데 반환 전에 자신을 호출하므로 처음부터 다시 시작합니다. 즉, 이렇게 계속 호출하면 함수는 끝나지 않고 apple을 계속 출력하다가 종료됩니다.
함수는 호출만으로도 일정 크기의 메모리를 사용하므로 무한 호출하면 프로그램 하나가 쓸 수 있는 메로리(해당 프로세스에 할당된 스택 메모리)를 모두 사용하게 되어 메모리 부족으로 강제 종료됩니다.
따라서 컴파일러는 컴파일 과정에서 경고 메시지를 띄워 알려 줍니다.
이렇게 중간에 종료되는 건 정상적인 종료가 아닙니다. 따라서 정상적으로 종료하기 위해서는 반복 고리를 끊을 수 있는 조건식을 반드시 포함해야 합니다.
재귀호출과 반복문의 차이점
재귀호출과 반복문은 같은 거 아닐까? 라고 생각할 수 있습니다. 다음 예시를 통해 비교해보겠습니다.
#include <stdio.h>
void fruit(int count);
int main(void)
{
fruit(1);
return 0;
}
void fruit(int count)
{
printf("apple\n");
if (count ==3) return;
fruit(count + 1);
printf("jam\n");
}
위 코드를 실행해보면 아래와 같은 결과가 나옵니다.
apple
apple
apple
jam
jam
다음 그림과 같이 재귀호출 함수의 경우 최초 호출한 곳이 아니라 이전에 호출했던 곳으로 돌아갑니다.

결국 재귀호출은 하나의 함수에서 코드를 반복 실행하는 듯하지만, 실제로는 새로운 함수를 실행하는 것과 같습니다.
재귀호출 함수는 경우에 따라 복잡한 반복문을 간단히 표현할 수 있으나 코드 읽기가 쉽지 않고 반복 호출되면서 메모리를 사용하므로 제한적으로 쓰는 것이 좋습니다.
'Lang > C' 카테고리의 다른 글
| C (6) - 문자와 문자열 (2) | 2025.08.18 |
|---|---|
| C (5) - 배열과 포인터 ② (2) | 2025.08.15 |
| C (4) - 배열과 포인터 ① (3) | 2025.08.11 |
| C (2) - 연산자 (3) | 2025.08.03 |
| C (1) - 상수와 변수 (1) | 2025.07.08 |