요약
이 강에서는 프로그램이란 무엇이고 컴파일러는 무엇을 하는지 설명한다. 기계는 메모리에 있는 수에 대한 기초적인 연산만 실행하며, 소스 코드는 실행하기 전에 기계어로 번역되어야 한다. 두 파일로 이루어진 프로그램 하나로 C++에서 이 번역의 세 단계(전처리, 컴파일, 링크)를 차례로 따라가고, 컴파일할 때 일어나는 일과 실행할 때 일어나는 일을 각각의 오류 하나씩을 들어 구별한다. 또 컴파일 언어와 인터프리터 언어를 비교하고, C++의 역사를 C++23 표준과 C++26 표준까지 요약하며, 이 강좌가 가르치는 C++가 어떤 것인지 밝힌다.
학습 목표
- 소스 파일이 실행 파일이 되기까지의 경로를 도식으로 설명한다. 전처리, 컴파일, 링크의 각 단계에 무엇이 들어가고 무엇이 나오는지도 함께 설명한다.
- 어떤 사건이 컴파일, 링크, 실행 가운데 어디에 속하는지 분류한다. 예를 들어 구문 오류, 함수의 누락, 0으로 나누기가 그런 사건이다.
- 오류가 발견되는 시점과 프로그래머가 기계를 얼마나 보는지를 기준으로 컴파일 언어와 인터프리터 언어를 비교한다.
- 언어의 표준들을 시간 순서 속에 자리매기고, 이 강좌가 C++23을 쓰는 이유를 설명한다.
프로그램이란 무엇인가
프로그램은 기계가 실행할 수 있는 명령어의 나열로, 어떤 데이터를 어떤 결과로 바꾸기 위한 것이다. 앞 강인 문제에서 알고리즘으로에서 보았듯이, 프로그램보다 앞서 알고리즘, 곧 문제를 푸는 방법이 있으며, 그것은 사람이 따라갈 수 있도록 쓰인 것이다. 프로그램은 같은 방법을 기계를 위해 쓴 것이다. 기계는 문장도 의도도 이해하지 못하고 기초적인 지시만 이해한다. 메모리의 한 곳에서 다른 곳으로 수를 복사하고, 두 수를 더하고, 둘을 비교하고, 결과에 따라 다른 명령어로 건너뛰는 것 같은 지시이다.
컴퓨터의 메모리는 번호가 매겨진 칸이 길게 늘어선 줄이며, 각 칸에는 0부터 255까지의 수가 하나씩 들어 있다(이것이 바이트이다). 기계가 다루는 모든 것은 그곳에 수로 부호화되어 있다. 데이터도 그렇고, 명령어 자체도 그렇다. 그렇다면 실행 가능한 프로그램은 프로세서가 지시로 해석하는 수의 나열, 곧 기계어이며, 이는 프로세서에 따라 다르다. 데스크톱 컴퓨터에 흔한 x86-64 계열 프로세서와, 휴대전화와 최근의 Mac에 흔한 ARM 프로세서는 같은 수를 이해하지 않는다.
오늘날 기계어로 프로그램을 쓰는 사람은 없다. 사람들은 C++ 같은 고급 언어로 쓰는데, 그 명령어는 수학 기호와 비슷하며, 소스 코드(또는 그냥 소스)라고 부르는 텍스트 파일에 쓴다. 여기서 소스로부터 그와 동등한 기계어를 만들어 내는 번역자가 필요하다는 결론이 나온다. C++에서는 그것이 컴파일러이다.
이것은 C++로 쓴 완전한 프로그램이다. 아직 이해할 필요는 없다. 각 줄은 1.5강에서 설명한다. 그것이 텍스트이고 읽을 수 있다는 것, 그리고 그 아래에 실행할 때 출력한 내용이 나온다는 것만 보면 된다.
// 1.5강에서 한 줄씩 설명한다.#include <print>int main(){ std::println("안녕, 세상"); std::println("2 + 3 = {}", 2 + 3);}안녕, 세상2 + 3 = 5번역의 다른 쪽 끝을 보기 위해, 정수 두 개를 더하기만 하는 함수를 생각해 보자(함수는 5단원에서 배운다). 이것은 완전한 프로그램이 아니다. 실행이 시작되는 지점인 main 함수가 없기 때문이다. 그래도 컴파일러는 이 함수를 따로 번역할 수 있다.
int add(int a, int b){ return a + b;}objdump 프로그램은 GCC 15가 Linux를 쓰는 x86-64 컴퓨터에서 이 함수를 위해 만들어 낸 기계어를 보여 준다. 각 줄에는 명령어의 위치, 16진수로 나타낸 바이트들, 읽기 쉬운 이름이 차례로 나온다.
$ g++ -std=c++23 -O1 -c add.cpp$ objdump -d add.o0000000000000000 <_Z3addii>: 0: f3 0f 1e fa endbr64 4: 8d 04 37 lea (%rdi,%rsi,1),%eax 7: c3 ret정수 덧셈은 여덟 바이트로 줄어들었다. 첫 번째 명령어인 endbr64 명령어는 보안용 표시로, 아무것도 계산하지 않는다. 두 번째인 lea 명령어는 함수가 프로세서의 두 레지스터(%rdi 레지스터와 %rsi 레지스터)로 받은 두 수를 더하고, 결과를 세 번째 레지스터(%eax 레지스터)에 넣는다. 세 번째인 ret 명령어는 함수를 호출한 지점으로 돌아간다. _Z3addii 이름은 컴파일러가 “int 매개변수 두 개를 받는 함수 add”를 부호화한 형태이다.
이 예에서 두 가지를 관찰할 수 있다. 첫째, 기계어에는 a 이름과 b 이름도, 들여쓰기도, 주석도 남지 않는다. 이것들은 모두 소스를 읽는 사람을 위해 있던 것이고, 컴파일러는 어떤 명령어를 쓸지 정하는 데 이것들을 사용했다. 둘째, C++ 프로그래머가 쓴 것은 중개자 없이 몇 안 되는 명령어로 번역된다. 기계에 대한 이 가까움이 C++가 효율적인 이유이며, 이 강좌에서 보게 되듯이 그 위험 가운데 몇 가지의 이유이기도 하다.
콘텐츠를 불러오는 중…