LoginSignup
0
0

[初級] C++: プログラムのビルド(基本)

Last updated at Posted at 2024-05-25

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 でもほぼ同じ
  • ただし、エラーコードなどは状況によって異なるので注意

ビルドとは何なのか、最小限の構成で確認

コンパイルに必要なファイルとは?
ソースファイル。以上。

c++ main.cpp
void main()
{
}

実際これだけでコンパイルでき、exeも作成される。
生成された exe を実行しても何も起きないが、実行はできる。

上記ソースコードだと警告が出るかもしれないので以降は下記のように戻り値を int にするのであしからず。

c++ main.cpp
int main()
{
    return 0;
}

main 関数には普通もっと引数がある?まあそういうこともある。

何が起きるか

x64/Release フォルダ(中間ディレクトリ)の下をみるとごちゃごちゃファイルができているが、ここで注目するのは main.obj ファイル。

  1. コンパイラ cl.exe によって main.cpp がコンパイルされて、main.obj が作られる
  2. main.obj がリンカ link.exe によってリンク処理されて実行可能なファイル形式である hogehoge.exe がつくられる

リンカは指定されたオブジェクトファイルから main 関数を探し、それをエントリポイントとして実行ファイルを作成する。(ちなみにエントリポイントはオプションで変更できたりもする)
なので、下記は「コンパイル」は成功するが、「リンク」は失敗する

c++ main.cpp
int xmain()
{
    return 0;
}

// LNK2001: 外部シンボル main は未解決です
// LNK1120: 1件の未解決の外部参照

関数を作って呼び出す

c++ main.cpp
void func()
{
}

int main()
{
    func();
    return 0;
}

ここでのポイントは、C++ のコンパイラはソースファイルを上から順に読むことしかしないので、必ず先になんらかの定義が必要だということ。
下記はコンパイルエラーとなる。

c++ main.cpp
int main()
{
    func();  // C3861: 'func': 識別子が見つかりませんでした
    return 0;
}
void func()
{
}

プロトタイプ宣言をする

コンパイラは、関数の実体は気にしない。呼び出しの仕様さえわかっていればコンパイルできる。このため、関数の呼出仕様をプロトタイプ宣言として前方宣言することで下記のように書ける。

c++ main.cpp
void func();  // プロトタイプ宣言
int main()
{
    func();
    return 0;
}
void func()
{
}

下記のようにするとどうなるか?つまり、プロトタイプ宣言のみで実体がコンパイルされていない場合。

c++ main.cpp
void func();  // プロトタイプ宣言
int main()
{
    func();
    return 0;
}

これは、「コンパイル」は成功し、「リンク」に失敗する。

ヘッダファイルを使ってみる

「普通 #include <hogehoge.h>とか書かない?」
使ってみる。

c++ func.h
void func();
c++ main.cpp
#include "func.h"
int main()
{
    func();
    return 0;
}
void func()
{
}

#include とは

コンパイラは、#... という命令文を見つけるとそれを「プリプロセッサディレクディブ」と認識し、コンパイルの前にプリプロセスを行う。
#include とは、「その位置に指定したファイルを読み込め」という命令である。
したがって、下記ソースファイルの場合、

c++ main.cpp
#include "func.h"
int main()
{
    func();
    return 0;
}
void func()
{
}

これをコンパイラに内蔵されたプリプロセッサが処理をすると、下記のようになる。

c++ main.cpp
// プリプロセス後

void func();
int main()
{
    func();
    return 0;
}
void func()
{
}

コンパイラはこれをコンパイルする。

したがって、これでも問題ない。

c++ func.h
void func()
{
}
int main()
{
    func();
    return 0;
}
c++ main.cpp
#include "func.h"

プロジェクトのプロパティで、[C/C++]-[プリプロセッサ]-[ファイルの前処理]を「はい」にすると処理済みのファイルが中間フォルダに *.i として出力できる。(※その場合コンパイルはできなくなるのであしからず)

ソースファイルを分割する

func 関数は別ソースファイルにしないとなんか変じゃない?」
コンパイラもリンカも特に何も気にしないが、人間にはわかりづらいので分けておくことにする。

c++ func.h
void func();
c++ func.cpp
void func()
{
}
c++ main.cpp
#include "func.h"
int main()
{
    func();
    return 0;
}

何が起きるか、再び

この3つのファイルからなるプロジェクトをビルドすると、何が起きるか。
Visual Studio でプロジェクトをビルドした場合の処理を追ってみる。

  1. main.cpp が登録されているので、これをコンパイラに渡す
    1. つまり、cl.exe main.cpp を実行する
    2. コンパイラはまずプリプロセスを行う。 #include を見つけたので、func.h を所定の位置に読み込む
    3. コンパイルを実行し、main.obj を作成する
  2. func.cpp が登録されているので、これをコンパイラに渡す
    1. つまり、cl.exe func.cpp を実行する
    2. コンパイラはまずプリプロセスを行うが、このファイルには特に処理するものはない
    3. コンパイルを実行し、func.obj を作成する
  3. すべてのソースファイルがコンパイルできたので、リンカを実行する
    1. つまり、link.exe func.obj main.obj /out:hogehoge.exe を実行する
    2. main.obj が外部シンボル func 関数を必要としているので、他のオブジェクトファイルを探す
    3. func 関数が func.obj に見つかったのでこれをリンクする
    4. exe を作成するために必要なエントリポイントを探す
    5. main.objmain 関数が見つかったのでこれをエントリポイントとしてexeを作成する
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0