C++ のプログラムをビルドする際の超基本的な動き
プリプロセッサ, コンパイラ, リンカ
本記事の前提条件は以下の通りです。
- 初心者向け
- とは言っても、何らかのプログラムはそれなりに書けるけど、C とか C++ はちょっと、という人向け
- ざっくり概要しか説明しないので細かいことは気にしないでいただきたい
- Visual Studio 2013 くらい~
- Windowsプログラム (CUI, GUI)
- コードの検証
- 開発環境: Visual Studio 2022, x64, Release ビルド
- 実行環境: Windows 10
- 本記事は上から順番に読む前提となっている
- 「C++」と書いてあるが、本記事の内容としては C でもほぼ同じ
- 「Visual Studio 2013 くらい~」と書いてあるが、本記事の内容だと現在過去未来のどのバージョンでもほぼ同じ
- 「Windowsプログラム (CUI, GUI)」と書いてあるが、本記事の内容だとどの OS でもほぼ同じ
- ただし、エラーコードなどは状況によって異なるので注意
ビルドとは何なのか、最小限の構成で確認
コンパイルに必要なファイルとは?
ソースファイル。以上。
void main()
{
}
実際これだけでコンパイルでき、exeも作成される。
生成された exe を実行しても何も起きないが、実行はできる。
上記ソースコードだと警告が出るかもしれないので以降は下記のように戻り値を int
にするのであしからず。
int main()
{
return 0;
}
main
関数には普通もっと引数がある?まあそういうこともある。
何が起きるか
x64/Release
フォルダ(中間ディレクトリ)の下をみるとごちゃごちゃファイルができているが、ここで注目するのは main.obj
ファイル。
- コンパイラ
cl.exe
によってmain.cpp
がコンパイルされて、main.obj
が作られる -
main.obj
がリンカlink.exe
によってリンク処理されて実行可能なファイル形式であるhogehoge.exe
がつくられる
リンカは指定されたオブジェクトファイルから main
関数を探し、それをエントリポイントとして実行ファイルを作成する。(ちなみにエントリポイントはオプションで変更できたりもする)
なので、下記は「コンパイル」は成功するが、「リンク」は失敗する
int xmain()
{
return 0;
}
// LNK2001: 外部シンボル main は未解決です
// LNK1120: 1件の未解決の外部参照
関数を作って呼び出す
void func()
{
}
int main()
{
func();
return 0;
}
ここでのポイントは、C++ のコンパイラはソースファイルを上から順に読むことしかしないので、必ず先になんらかの定義が必要だということ。
下記はコンパイルエラーとなる。
int main()
{
func(); // C3861: 'func': 識別子が見つかりませんでした
return 0;
}
void func()
{
}
プロトタイプ宣言をする
コンパイラは、関数の実体は気にしない。呼び出しの仕様さえわかっていればコンパイルできる。このため、関数の呼出仕様をプロトタイプ宣言として前方宣言することで下記のように書ける。
void func(); // プロトタイプ宣言
int main()
{
func();
return 0;
}
void func()
{
}
下記のようにするとどうなるか?つまり、プロトタイプ宣言のみで実体がコンパイルされていない場合。
void func(); // プロトタイプ宣言
int main()
{
func();
return 0;
}
これは、「コンパイル」は成功し、「リンク」に失敗する。
ヘッダファイルを使ってみる
「普通 #include <hogehoge.h>とか書かない?」
使ってみる。
void func();
#include "func.h"
int main()
{
func();
return 0;
}
void func()
{
}
#include
とは
コンパイラは、#...
という命令文を見つけるとそれを「プリプロセッサディレクディブ」と認識し、コンパイルの前にプリプロセスを行う。
#include
とは、「その位置に指定したファイルを読み込め」という命令である。
したがって、下記ソースファイルの場合、
#include "func.h"
int main()
{
func();
return 0;
}
void func()
{
}
これをコンパイラに内蔵されたプリプロセッサが処理をすると、下記のようになる。
// プリプロセス後
void func();
int main()
{
func();
return 0;
}
void func()
{
}
コンパイラはこれをコンパイルする。
したがって、これでも問題ない。
void func()
{
}
int main()
{
func();
return 0;
}
#include "func.h"
プロジェクトのプロパティで、[C/C++]-[プリプロセッサ]-[ファイルの前処理]を「はい」にすると処理済みのファイルが中間フォルダに *.i
として出力できる。(※その場合コンパイルはできなくなるのであしからず)
ソースファイルを分割する
「func
関数は別ソースファイルにしないとなんか変じゃない?」
コンパイラもリンカも特に何も気にしないが、人間にはわかりづらいので分けておくことにする。
void func();
void func()
{
}
#include "func.h"
int main()
{
func();
return 0;
}
何が起きるか、再び
この3つのファイルからなるプロジェクトをビルドすると、何が起きるか。
Visual Studio でプロジェクトをビルドした場合の処理を追ってみる。
-
main.cpp
が登録されているので、これをコンパイラに渡す- つまり、
cl.exe main.cpp
を実行する - コンパイラはまずプリプロセスを行う。
#include
を見つけたので、func.h
を所定の位置に読み込む - コンパイルを実行し、
main.obj
を作成する
- つまり、
-
func.cpp
が登録されているので、これをコンパイラに渡す- つまり、
cl.exe func.cpp
を実行する - コンパイラはまずプリプロセスを行うが、このファイルには特に処理するものはない
- コンパイルを実行し、
func.obj
を作成する
- つまり、
- すべてのソースファイルがコンパイルできたので、リンカを実行する
- つまり、
link.exe func.obj main.obj /out:hogehoge.exe
を実行する -
main.obj
が外部シンボルfunc
関数を必要としているので、他のオブジェクトファイルを探す -
func
関数がfunc.obj
に見つかったのでこれをリンクする - exe を作成するために必要なエントリポイントを探す
-
main.obj
にmain
関数が見つかったのでこれをエントリポイントとしてexeを作成する
- つまり、