15 헬로 월드
먼저 알아야 할 것
돌아보기
10장에서 컴퓨터의 입출력은 “문자가 한 줄씩 흐르는 띠”(스트림)라 했고, 프로그램마다 표준 출력이라는 흐름이 기본으로 주어진다고 했다. 그러면 프로그램이 화면에 글자를 내보낸다는 것은 정확히 무엇을 하는 일인가?
답. 표준 출력이라는 띠에 글자들을 흘려보내는 일이다. 화면은 그 띠의 끝에 붙어 있는 오늘날의 “종이”일 뿐이다(유리 텔레타이프의 후예라는 것을 기억하면 된다). 지금 만날 첫 프로그램이 하는 일이 정확히 이것 — 표준 출력에 인사말 한 줄을 흘려보내는 것 — 이다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 내장 명령과 실행 파일은 왜 갈라야 하는가? 쓰는 쪽에서는 똑같아 보이는데.
- 왜 MSVC 는
/std:c23이 아닌가? - 절반도 이해가 안 된다.
int는 왜 있고,(void)는 무엇이고, 왜 하필 0이 “무사히”인가? - 왜 하필 “Hello, world”인가? 누가 정한 인사인가?
15.1 첫 프로그램
전통에 따라, 첫 프로그램은 세상에 인사를 건넨다.
examples/ch15/hello.c
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
이것은 C언어 문법으로 작성한 일종의 작업 계획서 같은 것으로 소스 코드라고 한다. 이것을 컴파일러라는 번역기를 통해 기계어로 번역하고, 이미 만들어져 있는 미리 작업된 기계어 코드(라이브러리라고 한다)들과 합쳐서 묶어야 우리가 실행할 수 있는 실행 파일이 된다. 그 과정을 알아보고 실제로 첫 프로그램의 결과가 어떠한지 알아보자.
그런데 그 전에 하나가 더 필요하다. 만들어진 실행 파일을 어디서 어떻게 실행하는가 — 그 자리가 터미널이다.
15.2 터미널과 셸 — 프로그램을 실행하는 자리
10장에서 「글자가 한 줄씩 흐르는 띠」를 보았다. 그 띠의 오늘날 창구가 터미널이다. 창을 열면 글자를 칠 수 있고, 친 글자에 무언가가 대답한다.
여기서 두 낱말을 갈라야 한다. 흔히 뭉뚱그려 부르지만 하는 일이 다르다.
| 낱말 | 무엇인가 | 하는 일 |
|---|---|---|
| 터미널(단말) | 글자를 보여 주고 받는 창 | 키보드 입력을 넘겨주고, 나온 글자를 화면에 그린다 |
| 셸(shell) | 그 창 안에서 도는 프로그램 | 내가 친 줄을 읽어 해석하고, 실행하고, 결과를 돌려준다 |
표 15.1
즉 터미널은 유리창, 셸은 그 안의 일꾼이다. 「터미널에 명령을 친다」는 말은 정확히는 「터미널을 통해 셸에게 명령을 친다」는 뜻이다.
15.2.1 어떤 창과 어떤 일꾼이 있는가
| 터미널(창) | 셸(일꾼) | |
|---|---|---|
| 윈도 | Windows Terminal, 옛 콘솔 창 | cmd.exe(명령 프롬프트), PowerShell(powershell / pwsh) |
| 리눅스·유닉스 | GNOME 터미널, Konsole, xterm 등 | bash, zsh, dash, fish 등 |
| macOS | 터미널.app, iTerm2 | zsh(기본), bash |
표 15.2
창은 취향껏 골라도 되고 결과가 같다. 그러나 일꾼이 다르면 문법이 다르다 — 같은 일을 시켜도 적는 방법이 달라진다는 뜻이라, 인터넷에서 명령을 베껴 올 때 「이건 어느 셸의 문법인가」를 먼저 봐야 한다.
지금 어느 일꾼과 이야기하고 있는지 확인하는 방법도 갈린다.
| 셸 | 지금 무엇인지 확인하기 |
|---|---|
bash·zsh | echo $SHELL 또는 echo $0 |
cmd.exe | 프롬프트가 C:\Users\me> 꼴이다 |
| PowerShell | 프롬프트가 PS C:\Users\me> 꼴이다. $PSVersionTable 로도 확인 |
표 15.3
15.2.2 셸이 하는 일 — 읽고, 가르고, 실행한다
셸은 한 줄을 받으면 늘 같은 세 걸음을 밟는다.
- 읽는다 — 엔터까지의 한 줄을 받는다.
- 가른다 — 공백을 기준으로 낱말로 쪼갠다. 첫 낱말이 무엇을 할지이고, 나머지가 인자다.
- 실행한다 — 첫 낱말이 셸이 스스로 아는 일이면 직접 하고, 아니면 그 이름의 실행 파일을 찾아 띄운다.
세 번째 걸음의 「직접 하는 일」이 내장 명령(built-in)이다. 셸 안에 붙박이로 들어 있어서 따로 파일이 없다.
| 하려는 일 | bash·zsh | cmd.exe | PowerShell |
|---|---|---|---|
| 지금 어느 디렉터리인가 | pwd | cd(인자 없이) | Get-Location (pwd) |
| 디렉터리 옮기기 | cd 이름 | cd 이름 | Set-Location (cd) |
| 목록 보기 | ls | dir | Get-ChildItem (ls, dir) |
| 화면에 찍기 | echo 안녕 | echo 안녕 | Write-Output 안녕 (echo) |
| 화면 지우기 | clear | cls | Clear-Host (cls) |
표 15.4
괄호 안은 별칭이다 — PowerShell 은 유닉스에서 오는 사람을 위해 ls·cd 같은 이름을 같은 일에 붙여 두었다. 이름이 같아도 다른 프로그램이라는 것만 기억하면 된다.
문. 내장 명령과 실행 파일은 왜 갈라야 하는가? 쓰는 쪽에서는 똑같아 보이는데.
답. 대개는 몰라도 된다. 갈라야 하는 자리가 셋 있다.
① 찾는 방식이 다르다. 실행 파일은 셸이 경로(PATH)를 뒤져 찾지만, 내장 명령은 찾을 것도 없이 셸 안에 있다. 그래서 PATH 가 망가져도 cd 는 된다.
② 어떤 일은 내장일 수밖에 없다. cd 가 대표다. 디렉터리를 옮기는 것은 셸 자신의 상태를 바꾸는 일인데, 별도 프로그램으로 만들면 그 프로그램의 상태만 바뀌고 끝나 버린다(3장의 프로세스 이야기 그대로다).
③ 이름이 겹칠 수 있다. 같은 이름의 내장 명령과 실행 파일이 있으면 대개 내장이 이긴다. bash 에서 type 이름 을 치면 「이것이 내장인지 파일인지」를 알려 준다.
15.2.3 내가 만든 실행 파일을 실행하기
첫 프로그램을 컴파일하면 실행 파일 하나가 생긴다. 그것을 실행하는 방법이 플랫폼마다 조금 다르고, 여기서 초보자가 가장 자주 막힌다.
| 상황 | 치는 것 |
|---|---|
리눅스·macOS, 지금 디렉터리의 hello | ./hello |
윈도 cmd.exe, 지금 디렉터리의 hello.exe | hello 또는 .\hello.exe |
| 윈도 PowerShell | .\hello.exe (hello 만으로는 안 된다) |
표 15.5
흔한 오해. “리눅스에서 hello 라고 쳤는데 command not found 가 난다”
파일이 없어서가 아니다. 셸이 지금 디렉터리를 뒤지지 않기 때문이다.
셸은 실행 파일을 PATH 에 적힌 디렉터리들에서만 찾는다. 그리고 유닉스 계열은 보안을 위해 지금 디렉터리를 PATH 에 넣지 않는 것이 오랜 관행이다 — 넣어 두면 누가 ls 라는 이름의 나쁜 프로그램을 디렉터리에 심어 두었을 때 그것이 실행되기 때문이다.
그래서 「여기 있는 이것을 실행하라」를 명시적으로 적는다 — ./hello 의 ./ 가 「지금 디렉터리」라는 뜻이다.
PowerShell 도 같은 이유로 .\hello.exe 를 요구한다. 반면 cmd.exe 는 지금 디렉터리를 먼저 보므로 hello 만으로도 된다 — 세 셸의 태도가 여기서 갈린다.
15.2.4 인자 넘기기 — 프로그램에 값을 쥐여 주는 법
셸이 한 줄을 낱말로 가른다고 했다. 첫 낱말 뒤의 나머지가 그대로 프로그램에 전달되고, 그것을 인자(argument)라 부른다.
gcc -Wall -o hello hello.c여기서 gcc 가 실행할 것이고, 뒤의 넷이 인자다. 인자에는 관행적인 갈래가 있다.
| 갈래 | 모양 | 보기 |
|---|---|---|
| 값 | 그냥 낱말 | hello.c — 다룰 파일 이름 |
| 짧은 옵션 | - 하나 + 글자 하나 | -c, -g. 붙여 쓰기도 한다 — -Wall |
| 값이 붙는 옵션 | 옵션 다음 낱말이 그 값 | -o hello — 「출력 이름은 hello」 |
| 긴 옵션 | -- 둘 + 낱말 | --version, --help |
| 옵션의 끝 | -- 만 | 뒤는 전부 값으로 — 파일 이름이 - 로 시작할 때 |
표 15.6
공백이 든 인자는 따옴표로 묶는다. 셸이 공백에서 낱말을 가르기 때문이다.
gcc -o "my program" hello.c # 이름에 공백이 있으면 이렇게이 규칙들은 셸의 규칙이지 C의 규칙이 아니다. C 프로그램이 그 인자를 어떻게 받는지는 main 의 argc·argv 인데, 정식 취급은 53장에서 한다.
플랫폼 노트. 윈도와 유닉스는 인자를 다르게 나눈다
유닉스 계열은 셸이 낱말로 갈라 배열로 넘긴다. 윈도는 문자열 하나를 넘기고 받는 쪽 런타임이 나눈다(3장). 그래서 따옴표와 역슬래시의 처리가 미묘하게 다르고, 「리눅스에서 되던 명령줄이 윈도에서 다르게 잘리는」 일이 생긴다.
지금 단계에서 기억할 것은 하나다 — 경로에 공백이 있으면 따옴표로 묶는다. 윈도의 C:\Program Files\... 가 그 대표적인 자리다.
15.3 명령행에서 컴파일하고 실행하기
이제 앞의 계획서를 실행 파일로 바꿀 차례다. 컴파일러는 여럿이지만 쓰는 모양은 셋으로 줄어든다 — GCC, Clang, 그리고 MSVC 다. 앞의 둘은 옵션 문법이 거의 같고, MSVC 만 다른 계통이다.
15.3.1 GCC (리눅스·macOS·MinGW)
#idx("리눅스") gcc -std=c23 -Wall -Wextra -o hello hello.c
./hello낱말 하나씩 읽으면 이렇다 — gcc 가 부를 프로그램, -std=c23 은 「C23 문법으로 읽어라」, -Wall -Wextra 는 「의심스러운 것을 다 말해 달라」(17장에서 이 둘을 왜 늘 켜는지 다룬다), -o hello 는 「결과 파일 이름은 hello」, 마지막 hello.c 가 재료다.
-o 를 빼면 결과가 a.out 이라는 이름으로 나온다 — 유닉스 초창기부터 내려온 기본값이고, 「assembler output」의 줄임이다.
15.3.2 Clang (macOS 기본, 리눅스·윈도)
clang -std=c23 -Wall -Wextra -o hello hello.c
./helloGCC 와 옵션이 같다. 의도적으로 그렇게 만들어졌기 때문에(18장), 둘 사이를 오갈 때 명령을 고칠 일이 거의 없다. 이 책이 두 컴파일러로 예제를 교차 검증할 수 있는 것도 그 덕이다.
macOS 에서는 gcc 라고 쳐도 실제로는 Clang 이 불리는 경우가 많다 — gcc --version 을 쳐 보면 정체가 드러난다.
15.3.3 MSVC (윈도)
MSVC 는 옵션 문법부터 다르다. 그리고 전용 창에서 실행해야 한다.
cl /std:c17 /W4 hello.c
hello.exe| 하려는 것 | GCC·Clang | MSVC |
|---|---|---|
| 옵션 표시 | - 하나 | / 또는 - (/W4) |
| 표준 고르기 | -std=c23 | /std:c17, /std:c11, /std:clatest |
| 경고 켜기 | -Wall -Wextra | /W4 (/Wall 은 너무 시끄럽다) |
| 출력 이름 | -o hello | /Fe:hello.exe |
| 컴파일만 하기 | -c | /c |
표 15.7
전용 창이 필요한 이유는 MSVC 가 여러 환경 변수(헤더와 라이브러리의 경로)에 기대기 때문이다. 시작 메뉴의 「x64 Native Tools Command Prompt for VS」 를 열면 그 변수들이 설정된 cmd 가 뜬다. 보통의 cmd 에서 cl 을 치면 「내부 또는 외부 명령이 아닙니다」가 나오는데, 파일이 없어서가 아니라 PATH 가 준비되지 않아서다 — 앞 절의 그 이야기다.
문. 왜 MSVC 는 /std:c23 이 아닌가?
답. 이 책을 쓰는 시점에 MSVC 의 C 지원이 c11·c17·clatest 까지이기 때문이다. 마이크로소프트는 오랫동안 C 를 C++ 의 부속으로 다뤄 C99 조차 늦게 채웠고(18장), 지금은 따라오는 중이지만 여전히 GCC·Clang 보다 뒤에 있다.
그래서 이 책의 예제를 윈도에서 돌린다면 셋 중 하나를 고르는 것이 현실적이다 — MinGW-w64 의 GCC, 윈도용 Clang, 또는 WSL 안의 GCC. MSVC 로 가려면 /std:clatest 를 주고, C23 기능(nullptr·constexpr 등)이 걸리는 자리는 각오해야 한다.
플랫폼 노트. 세 컴파일러를 한 줄로 견주면
| GCC | Clang | MSVC | |
|---|---|---|---|
| 어디서 | 리눅스 기본, MinGW 로 윈도 | macOS 기본, 리눅스·윈도 | 윈도 전용 |
| 옵션 계통 | 유닉스식 - | GCC 와 같음 | / 계통 |
| C23 지원 | 넓다 | 넓다 | 따라오는 중 |
| 실행 | ./hello | ./hello | hello.exe |
표 15.8
셋 다 같은 표준을 목표로 하므로 이 책의 예제는 어느 것으로 빌드해도 같은 답을 내야 한다 — 그렇지 않다면 그 자리가 바로 이 책이 말하는 구현 정의나 회색지대다 (12장). 실제로 이 책은 GCC 로 짓고 Clang 으로 교차 검증한다.
15.4 그래서 첫 프로그램은 이렇게 나온다
방금 친 두 줄 — 컴파일하고, 실행하는 — 의 결과가 이것이다.
실행 결과
Hello, world!
이 책의 모든 실행 결과가 그렇듯 장식이 아니라 기계가 실제로 낸 결과다 — 매 빌드마다 예제를 컴파일하고 실행해 그 출력을 이 자리에 싣는다.
15.5 한 줄씩 읽기
돌아가는 것을 보았으니, 이제 그 여섯 줄이 각각 무엇을 말하는지 소리 내어 읽는다.
#include <stdio.h> — 준비물 선언이다. “표준 입출력 도구 상자를 가져다 놓아라”라는 뜻으로, 아래에서 쓸 printf가 그 상자에 들어 있다. 이름 stdio는 standard input/output에서 굵은 글자만 따온 것이다 — 초기 C의 파일 이름 길이 제한 아래에서 이런 압축된 이름이 관행이 됐고, 그래서 표준 헤더 이름 대부분이 짧다.
#으로 시작하는 줄의 정확한 정체는 다음 장(전처리)에서 밝혀진다.
int main(void) — 약속된 출발점이다. 우리가 지금 만드는 것처럼 운영체제 위에서 도는 C 프로그램(호스트 구현)은 main이라는 이름의 함수에서 시작한다. 프로그램을 실행한다는 것은 곧 main을 실행한다는 것이다. 운영체제가 없는 환경에서는 출발점의 이름부터 달라질 수 있는데, 그 이야기는 53장에서 한다. 괄호와 int, void의 뜻은 제4·5부에서 차례로 풀린다.
{ 와 } — 시작과 끝의 담장이다. 담장 안이 main이 할 일의 목록이다.
printf("Hello, world!\n"); — 이 프로그램의 알맹이다. printf라는 도구를 불러서, 따옴표 안의 글자들을 표준 출력의 띠에 흘려보낸다. \n은 10장에서 만난 바로 그 줄바꿈 문자다 — 타자기의 유산이 첫 프로그램에 벌써 들어 있다. 끝의 세미콜론은 문장의 마침표다.
return 0; — 퇴장 인사다. main을 끝내며 “무사히 마쳤다”는 뜻의 0을 운영체제에 알린다.
문. 절반도 이해가 안 된다. int는 왜 있고, (void)는 무엇이고, 왜 하필 0이 “무사히”인가?
답. 지금은 몰라도 되는 것이 맞다 — 그리고 이 책은 그 “몰라도 됨”을 정직하게 관리한다. 이 여섯 줄에서 지금 당장 가져갈 것은 두 가지뿐이다: 지금 만드는 이 호스트 프로그램은 main에서 시작한다는 것, 그리고 printf가 표준 출력에 글자를 흘려보낸다는 것. 나머지는 전부 의도된 외상이다 — 제4부가 이 한 편을 처음부터 끝까지 해부하는 것을 목표로 삼고, 문장과 블록은 19장, printf와 호출은 21–22장, int와 (void)는 23–25장에서 외상을 갚는다. 지금 다 설명하지 않는 것은 게으름이 아니라 순서다.
문. 왜 하필 “Hello, world”인가? 누가 정한 인사인가?
답. 전통이다 — 그리고 족보가 있다. 이 인사는 C의 아버지들 곁에서 태어났다. 브라이언 커니핸이 1970년대 초 벨 연구소의 언어 튜토리얼에서 “hello, world”를 첫 예제로 썼고, 1978년의 K&R 책(12장에서 만날 그 “사실상의 표준”)이 이를 첫 프로그램으로 실으면서 온 세계의 전통이 됐다. 이후 거의 모든 언어의 첫 예제가 이 인사를 따라 한다. 새 언어나 새 환경을 만나면 “헬로 월드부터 띄운다”는 말이 관용구가 됐을 정도다 — 우리도 방금 그 전통에 합류했다.
15.6 그런데, 소스에서 실행까지는 무슨 일이 있었나
방금 우리는 두 개의 물건을 보았다. 사람이 읽고 쓰는 소스 코드 (hello.c)와, 기계가 실행한 결과다. 그런데 4장에서 배웠듯 기계가 실행할 수 있는 것은 기계 명령뿐이고, 소스 코드는 그냥 텍스트 파일 — 9장의 표현으로 말하면 UTF-8로 담긴 글자들의 열 — 이다. 그 사이 어딘가 에서 텍스트가 기계 명령으로 바뀌는 큰 사건이 있었다는 뜻이다.
그 사건의 이름이 컴파일이고, 다음 장이 그 현장을 단계별로 구경한다. 겉에서 보면 명령 한 줄이지만, 속에서는 네 단계의 릴레이가 돈다 — 그중 한 단계에서 우리의 “주문” #include가 풀리고, 마지막 단계에서 printf의 실체가 어디에서 오는지 드러난다.