[C언어 50강] 03강. 첫 프로그램: main 함수, printf, 주석, 컴파일·실행 흐름
처음 C 프로그램을 배울 때 가장 흔한 오해는 “printf만 찍히면 끝”이라는 생각입니다. 하지만 진짜 핵심은 출력 문자열이 아니라, main에서 시작해 컴파일과 링크를 거쳐 실행파일이 동작하는 전체 흐름을 이해하는 데 있습니다. 오늘은 눈에 보이는 코드 한 줄 뒤에서 컴퓨터가 실제로 어떤 단계를 밟는지, 왜 main 시그니처와 반환값이 중요한지, 주석은 어떻게 써야 코드 품질에 도움이 되는지를 개념 중심으로 정리합니다.
핵심 개념
- C 프로그램의 진짜 시작점은
main함수이며, 운영체제는main의 반환값으로 프로그램 성공/실패를 판단한다. printf는 단순 출력 함수가 아니라 형식 문자열(format string) 규칙을 통해 메모리에 있는 값을 해석해 보여주는 인터페이스다.- “코드를 작성한다”는 행위는 곧 전처리→컴파일→어셈블→링크 과정을 거쳐 실행 가능한 바이너리를 만드는 과정이다.
개념 먼저 이해하기
Hello, world를 처음 찍어보는 순간은 즐겁지만, 그다음 단계에서 실력이 갈리는 지점은 “왜 저 코드가 동작했는가”를 설명할 수 있느냐입니다. C언어는 특히 이 설명 능력이 중요합니다. 파이썬처럼 인터프리터가 한 줄씩 읽어 즉시 실행하는 모델과 달리, C는 소스 코드를 기계어에 가까운 실행파일로 변환한 뒤 실행합니다. 이 구조를 모르면 에러 메시지가 나올 때 해석이 막히고, 빌드 문제가 생기면 코드가 맞는지 환경이 틀린지 구분하지 못하게 됩니다.
먼저 main 함수의 역할을 정확히 잡아봅시다. main은 “내가 그냥 정한 함수 이름”이 아니라, 운영체제와 C 런타임이 약속한 진입점(entry point)입니다. 실행파일이 시작되면 내부 초기화 과정을 거친 뒤 결국 main이 호출됩니다. 그래서 int main(void) 또는 int main(int argc, char *argv[]) 같은 표준 형태를 지켜야 합니다. main이 int를 반환하는 이유도 중요합니다. 이 값은 프로그램의 종료 상태를 뜻하고, 셸이나 다른 프로세스가 이를 보고 성공/실패를 판단합니다. return 0;은 관례가 아니라 “정상 종료”라는 계약 신호입니다.
다음으로 printf를 봅시다. 많은 초보자가 printf("%d", x);를 문장처럼 외우지만, 실제로는 “형식 문자열이 뒤따르는 인자들을 어떤 타입으로 해석할지 지시하는 규칙”입니다. 여기서 %d, %f, %c, %s가 타입과 맞지 않으면 경고가 뜨거나, 더 나쁘게는 정의되지 않은 동작(UB)으로 이어질 수 있습니다. 즉 printf는 출력 함수이면서 동시에 타입 일치 감각을 훈련시키는 도구입니다. C에서 타입을 대충 맞추면 운 좋게 보이는 값이 나올 수는 있어도, 그 코드는 신뢰할 수 없습니다.
주석도 개념적으로 다시 볼 필요가 있습니다. 주석은 “코드의 한국어 번역”이 아닙니다. i++; // i를 1 증가 같은 주석은 가치가 거의 없습니다. 좋은 주석은 코드만 봐서는 알기 어려운 의도, 제약, 경계조건, 설계 이유를 남깁니다. 예를 들어 “왜 버퍼 크기를 256으로 고정했는지”, “왜 이 순서로 초기화해야 하는지”, “왜 지금은 최적화 대신 가독성을 택했는지” 같은 맥락이 좋은 주석의 대상입니다. 특히 C는 포인터, 버퍼, I/O 처리에서 의도를 놓치면 버그가 커지기 쉬우므로 주석 품질이 유지보수성을 크게 좌우합니다.
이제 컴파일·실행 흐름을 연결해보겠습니다. C 소스는 대략 네 단계를 거칩니다. (1) 전처리: #include, #define 같은 지시문을 확장합니다. (2) 컴파일: C 코드를 어셈블리로 변환하고 타입/문법을 검사합니다. (3) 어셈블: 어셈블리를 오브젝트 파일(.o)로 변환합니다. (4) 링크: 여러 오브젝트와 라이브러리를 묶어 최종 실행파일을 만듭니다. 이 흐름을 아는 순간, 에러 메시지를 보는 눈이 달라집니다. 예를 들어 undefined reference는 보통 문법 오류가 아니라 링크 단계 문제입니다. 반대로 expected ';'류는 컴파일 단계 문제죠. 단계 구분이 가능하면 문제 해결 속도가 급격히 빨라집니다.
실무 관점에서 가장 유용한 습관은 “작동”이 아니라 “재현 가능한 작동”을 목표로 하는 것입니다. 같은 파일을 같은 옵션으로 빌드했을 때 언제나 같은 결과가 나와야 합니다. 따라서 clang main.c -std=c11 -Wall -Wextra -Wpedantic -o app 같은 기준 명령을 고정해 두는 게 좋습니다. 그리고 경고를 무시하지 말고, 초반부터 경고 0개를 목표로 잡으세요. C에서 경고는 미래 버그의 예고편인 경우가 많습니다.
정리하면, 첫 프로그램에서 진짜 배워야 할 것은 “문자열 출력 방법”이 아닙니다. main이라는 진입 계약, printf의 형식 해석 모델, 주석의 올바른 목적, 빌드 파이프라인의 단계적 사고를 한 번에 익히는 것이 핵심입니다. 이 기반이 있어야 이후 변수, 제어문, 함수, 포인터를 배울 때 단순 암기가 아닌 구조적 이해로 연결됩니다.
기본 사용
예제 1) 가장 작은 표준 시작점
#include <stdio.h>
int main(void) {
printf("Hello, C!\n");
return 0;
}
설명:
int main(void)는 인자를 받지 않는 표준 진입점입니다.\n은 줄바꿈 문자이며, 터미널 출력 버퍼를 눈에 보이게 정리하는 데 도움을 줍니다.return 0;은 정상 종료 신호입니다. 셸에서echo $?로 종료 코드를 확인할 수 있습니다.
예제 2) 형식 문자열과 타입 맞추기
#include <stdio.h>
int main(void) {
int age = 27;
double height = 175.8;
char initial = 'K';
printf("age=%d, height=%.1f, initial=%c\n", age, height, initial);
return 0;
}
설명:
%d는int,%f는 부동소수점(printf에서는double),%c는 문자에 대응합니다.%.1f는 소수점 한 자리까지 출력하라는 뜻입니다.- 형식 지정자와 실제 타입이 맞아야 안전합니다. 불일치는 경고 또는 오동작으로 이어질 수 있습니다.
예제 3) 주석을 “의도 설명”에 쓰는 패턴
#include <stdio.h>
int main(void) {
int retry_limit = 3; // 데모에서는 고정값 사용, 이후 설정파일로 분리 예정
/*
이 반복은 실패한 작업을 즉시 중단하지 않고
최대 3회까지 재시도하려는 의도를 보여준다.
숫자 3 자체보다 "재시도 정책"이 핵심이다.
*/
for (int i = 1; i <= retry_limit; i++) {
printf("try %d\n", i);
}
return 0;
}
설명:
- 주석은 코드가 “무엇을” 하는지보다 “왜 그렇게 설계했는지”를 설명할 때 가치가 큽니다.
- 나중에
retry_limit을 변경할 때도 정책 문맥을 유지할 수 있습니다. - 협업 시 의도 주석이 있으면 코드 리뷰 속도와 정확도가 올라갑니다.
자주 하는 실수
실수 1) void main()을 사용함
- 원인: 인터넷의 오래된 예제를 그대로 따라함.
- 해결: 표준 형태인
int main(void)또는int main(int argc, char *argv[])를 사용한다.
실수 2) printf 형식 지정자와 타입이 맞지 않음
- 원인:
%d,%f,%s를 감으로 사용함. - 해결: 변수 타입과 형식 지정자를 정확히 매칭하고, 컴파일 경고를 반드시 확인한다.
실수 3) 주석을 코드 번역처럼 작성함
- 원인: 주석량이 많으면 좋은 코드라고 착각함.
- 해결: 왜 이 방식이 필요한지, 어떤 제약이 있는지 의도를 설명하는 주석만 남긴다.
실수 4) 컴파일 단계를 모른 채 에러를 한 덩어리로 봄
- 원인: 전처리/컴파일/링크 에러를 구분하지 않음.
- 해결: 에러 문구를 보고 어느 단계 문제인지 먼저 분류한 후 원인을 좁힌다.
실무 패턴
- 첫 파일부터 빌드 명령을 문서화한다:
-std=c11 -Wall -Wextra -Wpedantic권장. - 샘플 코드라도
main반환값을 일관되게 처리한다(성공 0, 실패 비0). - 출력문은 디버깅 도구이기도 하므로, 변수 타입과 포맷을 명시적으로 관리한다.
- 주석은 정책/제약/트레이드오프 중심으로 작성한다.
- 컴파일 로그를 읽을 때는 “어느 단계에서 깨졌는지”를 먼저 판단하는 습관을 갖는다.
오늘의 결론
한 줄 요약: 첫 C 프로그램의 본질은 문자열 출력이 아니라, main 계약·형식 출력·주석 의도·빌드 파이프라인을 하나의 시스템으로 이해하는 데 있다.
연습문제
int main(void)형태로 프로그램을 만들고,return 0;과return 1;의 종료 코드를 셸에서 비교해보세요.int,double,char변수를 선언한 뒤printf형식 지정자를 각각 바꿔가며 컴파일 경고를 관찰하고, 왜 경고가 나는지 설명해보세요.- 짧은 반복문 코드에 “좋은 주석 2개 / 나쁜 주석 2개”를 직접 작성하고, 어떤 주석이 유지보수에 유리한지 비교해보세요.
이전 강의 정답
- 2강 연습문제 1) 예시 빌드 명령
clang src/main.c -o build/app -std=c11 -Wall -Wextra -Wpedantic -O0 -g- 핵심은 컴파일러/표준/경고/디버그 옵션을 고정해 재현성을 확보하는 것
- 2강 연습문제 2) 두 파일 링크 핵심
main.c에서 함수 선언(또는 헤더 포함),util.c에서 함수 정의- 함께 컴파일/링크:
clang src/main.c src/util.c -o build/app ...
- 2강 연습문제 3) 경고 해석 핵심
- 초기화 누락/미사용 변수 경고는 “지금은 돌아가도 나중에 깨질 가능성”을 조기 경고하는 신호
- 경고를 없애는 방향으로 코드 수정하는 습관이 품질의 시작
실습 환경/재현 정보
- 컴파일러: clang 17+ 또는 gcc 13+
- 컴파일 옵션:
-std=c11 -Wall -Wextra -Wpedantic -O0 -g - 실행 환경: macOS/Linux 터미널
- 재현 체크:
clang main.c -o app -std=c11 -Wall -Wextra -Wpedantic -O0 -g./appecho $?로 종료 코드 확인- 포맷 지정자 불일치 실험 시 경고 메시지 캡처 후 원인 정리