コンテンツへスキップ
Topos Uranos

要約

このレッスンでは、プログラムとは何か、コンパイラは何をするのかを説明する。機械はメモリ上の数に対する初歩的な演算しか実行せず、ソースコードは実行の前に機械語へ翻訳されなければならない。二つのファイルからなるプログラムを使って、C++ におけるこの翻訳の三つの段階(前処理、コンパイル、リンク)をたどり、コンパイル時に起こることと実行時に起こることを、それぞれのエラーを一つずつ挙げて区別する。さらに、コンパイル言語とインタプリタ言語を比較し、C++ の歴史を規格 C++23 と C++26 まで概観し、このコースが教えるのがどのような C++ なのかを明確にする。

学習目標

  1. ソースファイルが実行ファイルになるまでの道筋を、図を用いて説明できる。前処理、コンパイル、リンクの各段階について、何が入り何が出るかも含めて説明できる。
  2. ある出来事が、コンパイル、リンク、実行のどれに属するかを分類できる。たとえば構文エラー、関数の欠落、ゼロによる除算である。
  3. コンパイル言語とインタプリタ言語を、エラーが見つかる時点と、プログラムを書く人に機械がどれだけ見えるかという観点から比較できる。
  4. 言語の規格を時間の流れの中に位置づけ、このコースが C++23 を使う理由を説明できる。

プログラムとは何か

プログラムとは、機械が実行できる命令の列であり、あるデータをある結果へ変換するためのものである。前回のレッスン 問題からアルゴリズムへで見たように、プログラムの前にはアルゴリズム、すなわち問題を解く方法があり、それは人が従えるように書かれている。プログラムとは同じ方法を機械のために書いたものである。機械は文も意図も理解せず、理解するのは初歩的な指令だけである。メモリのある場所から別の場所へ数をコピーする、二つの数を足す、それらを比較する、結果に応じて別の命令へ飛ぶ、といった指令である。

コンピュータのメモリは番号の付いた区画が長く並んだ列であり、各区画には 0 から 255 までの数が一つ入っている(これがバイトである)。機械が扱うものはすべて、そこに数として符号化されている。データも、命令そのものもそうである。そうであるならば、実行可能なプログラムとは、プロセッサが指令として解釈する数の列、すなわち機械語であり、それはプロセッサに依存する。デスクトップ・コンピュータで一般的な x86-64 系列のプロセッサと、電話や最近の Mac で一般的な ARM のプロセッサとでは、理解する数が同じではない。

今日、機械語でプログラムを書く人はいない。書くのは高水準言語、たとえば C++ であり、その命令は数学の記法に似ている。それをソースコード(あるいは単にソース)と呼ばれるテキストファイルに書く。このことから、ソースから等価な機械語を生成する翻訳者が必要になる。C++ では、それがコンパイラである。

例一つのプログラムと、機械が実行するもの

これは C++ の完全なプログラムである。今はまだ理解しなくてよい。各行はレッスン 1.5 で説明する。テキストであること、読めること、そしてその下に実行時に書き出したものが表示されていることを見れば十分である。

C++ のプログラム
// レッスン 1.5 で 1 行ずつ説明する。#include <print>​int main(){    std::println("こんにちは、世界");    std::println("2 + 3 = {}", 2 + 3);}
出力
こんにちは、世界2 + 3 = 5

翻訳のもう一方の端を見るために、二つの整数を足すだけの関数を考えよう(関数は第 5 単元で学ぶ)。これは完全なプログラムではない。実行が始まる地点である main がないからである。それでもコンパイラはこれを単独で翻訳できる。

C++ のコード
int add(int a, int b){    return a + b;}

プログラム objdump は、Linux の x86-64 コンピュータ上で GCC 15 がこの関数のために生成した機械語を表示する。各行に、命令の位置、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++ の効率の理由であり、コースの中で見ていくように、その危険のいくつかの理由でもある。