CodeGym /コース /C++ SELF /1つのコマンドでのコンパイルとリンク

1つのコマンドでのコンパイルとリンク

C++ SELF
レベル 33, レッスン 1
使用可能

1. なぜ「1つのコマンド」が動くのか

最初に g++ main.cpp -o app のようなコマンドを見ると、コンパイラは「魔法のコンバイン機」―― ソースを入れれば実行ファイルが出てくる、と感じるかもしれません。 実際には内部でいくつかのステップが行われていますが、driver コマンド (g++ や clang++)はあなたの手間を減らすように働きます: ソースのリストを渡して「実行ファイルを作って」と頼めば、各 .cpp をコンパイルし、その後にリンクを実行します。

この「1つのコマンド」は学習時や小さなプロジェクトにとても便利です。頭の中の要素が最小限で済みます。 オブジェクトファイル .o、ビルドステージ、ビルドキャッシュなどの細部をまだ気にする必要はありません。 覚えておくシンプルな考え方は:必要なすべての .cpp をコマンドで列挙する と、期待どおりの実行ファイルが得られる、ということです。

ミステリーを解くために、ビルドを小さなキッチンに例えます。あなたは材料(ソース .cpp)を持ってきて、 シェフ(driver)に料理(プログラム)を作るよう頼みます。シェフは何を切り、何を焼き、何を混ぜるかを決めます。 しかし肉(必要な関数を含む別の .cpp)を持って来忘れると、最終段階で「すみません、料理はできません」と言われます。

以下は driver が「1つのコマンド」を実行するときにだいたい何をしているかの図です:

flowchart TD
    A["コマンド: g++/clang++ + フラグ + .cpp + -o app"] --> B["各 .cpp のコンパイル → 一時的なオブジェクト"]
    B --> C["オブジェクト + 標準ライブラリをリンク"]
    C --> D["完成した実行ファイル (app)"]

2. コマンドの基本形

コマンドラインは日本語の文に似ています:構造が見えないとごちゃごちゃに見えます。 だから我々の目標は「魔法の呪文」を丸暗記するのではなく、パターンを見分けることです。

典型的な形式は次のとおりです:

g++   -std=c++23   [other_flags]   <sources.cpp...>   -o <output_name>

ここで各ブロックの意味を理解することが重要です。コンパイラ(正確には driver)はまずフラグを読み、その後入力ファイルのリストを読み、 そして(あなたが指定していれば)指定した名前で結果を作成します。

これを「文の解析」にまとめると:

部分 例 意味
Driver
g++ / clang++
コンパイルとリンクを管理するコマンド
言語モード
-std=c++23
どの規則で C++ を解釈するか
入力ファイル
main.cpp util.cpp
どの実装がビルドに参加するか
出力名
-o app
生成される実行ファイルの名前

ここで時間を節約する実用的な習慣を2つ紹介します。 第一の習慣:常に -std=c++23 を書くこと、 たとえ「書かなくても動く」ように見えても。第二の習慣:常に -o を書いて、 デフォルトの a.out を増やさないこと。

3. 単一の .cpp を実行ファイルにする

まずはファイルが1つだけの状況から始めましょう。これは自転車の練習を空の駐車場で始めるようなものです。

main.cpp を作ります:

#include <iostream>

int main() {
    std::cout << "Hello from one-file build!\n"; // Hello from one-file build!
    return 0;
}

次にビルドします:

g++ -std=c++23 main.cpp -o app

実行(Linux/macOS では通常こうします):

./app

この段階で重要なのは:1つのコマンドがすべてを行う と実感することです。 コードにエラーがあれば、コンパイラのメッセージは行や列に紐付いて表示されます。つまり「どこを探せばいいか」はほぼ明らかです:コンパイラが座標を示してくれます。

4. 複数の .cpp を1つのコマンドで

プロジェクトが「1ファイル」ではなくなると、新たなタイプのエラーが出てきます:すべて書いたつもりでも、ビルド時に「見つからない」ものが出てきます。 ここでの重要スキルは、.hpp が宣言を提供し、実装は .cpp にあり、実装側の .cpp をコマンドに含めなければリンカは定義を見つけられない、という理解です。

小さなコンソールアプリケーションを続けます。これは簡単な「ミニユーティリティ」(仮に tasker と呼ぶ)で、バージョンを出力したり、数を1増やす処理を行います。 ロジックは別ファイルに置きます。

util.hpp を作ります:

#pragma once

int inc(int x);

util.cpp を作ります:

#include "util.hpp"

int inc(int x) {
    return x + 1;
}

そして main.cpp:

#include <iostream>
#include "util.hpp"

int main() {
    std::cout << inc(41) << '\n'; // 42
    return 0;
}

正しいビルドコマンド(両方の .cpp を列挙しています)は次のとおりです:

g++ -std=c++23 main.cpp util.cpp -o tasker

なぜ #include "util.hpp" が util.cpp を代替しないのか

初心者によくある誤解は、main.cpp がヘッダをインクルードしているなら実装もつながるだろう、というものです。しかし #include は文字通り「ヘッダのテキストをここに挿入する」だけで、ヘッダは通常 関数本体 を含みません(宣言のみです)。そのためコンパイラは main.cpp を正しくコンパイルし、inc の宣言を見て満足します。

その先で起きるのはリンク段階です:どこに 実際の定義 があるかを探す必要があります。 もしビルドコマンドに util.cpp を指定していなければ、定義はビルド対象に存在せず、リンカはエラーを出します。

5. 読みやすいコマンド:-o と引数の順序

フラグ -o:出力名は単なる「見た目」ではない理由

時々 -o は単なる利便性だと考えられがちですが、学習段階では混乱を減らす手段でもあります。

もし -o を指定しなければ、コンパイラはデフォルト名でファイルを作成します。Linux/macOS では通常 a.out、Windows では a.exe(環境による)になります。問題は数日後に複数のプロジェクトで同じ a.out が存在すると、どれを実行しているのかわからなくなることです。すると「出力が違うのはなぜ?」と混乱します。

したがってルールは単純です:毎回出力名を指定する、意味の分かる名前を付けることが望ましいです:

g++ -std=c++23 main.cpp util.cpp -o tasker

デモ用モードなどを表すなら、例えば以下のようにもできます:

g++ -std=c++23 main.cpp util.cpp -o tasker_demo

手作業でビルドしている限り名前付けの規則は混沌を減らす良い習慣です。

引数の順序:コマンドを読みやすくする方法

形式的には g++ や clang++ はかなり自由な引数順を許します。しかし初心者にとってはこれが却って邪魔になります:フラグをファイルの後に書いてしまい、-o をどこに置いたか忘れる、といったことが起こります。

そこで我々は読みやすさを重視したスタイルを導入します。唯一絶対の正解ではありませんが、読みやすい ことを重視します:

g++ -std=c++23 <flags> <all .cpp> -o <output>

つまり最初に driver、次に標準、次にその他のフラグ(あれば)、その後にソースのリスト、最後に出力名、という順番です。

このスタイルの例:

g++ -std=c++23 main.cpp util.cpp -o tasker

ファイルが多い場合は改行とバックスラッシュ(bash/zsh)で分けても良いですが、それはシェル依存の話です。学習ではコマンドを短く明快に保つことが十分です。

6. コンパイラ出力のどこを探すか

この段階で最もよくある状況は、コマンドを実行したら大量のテキストが返ってきてパニックになることです。上や下にスクロールしてコンパイラが自分を嫌っているのではないかと疑い始める人もいます。結論から言うと、コンパイラは感情を持っておらず、ただエラーを報告するだけです。

メッセージを読む基本テクニックはこうです:最初の「本物の」エラーメッセージを探す、ということです。というのも後続の多くは派生的なメッセージだからです。特に C++ では1つの抜けたセミコロンが十数件の奇妙なエラーを誘発することがあります。

コンパイルエラーはどのように見えるか

コンパイルエラーは通常ファイルと行番号を示します。例えばセミコロンを忘れると:

#include <iostream>

int main() {
    std::cout << "Oops!\n"
    return 0;
}

コマンド:

g++ -std=c++23 main.cpp -o app

メッセージはだいたい「expected ‘;’ before ‘return’」のようになり、ほとんどの場合座標が付きます: main.cpp:<line>:<column>。これは問題が ソースのテキスト にあることを示し、コードを修正する必要があります。

実用的な習慣としては:ファイルと位置が示された最初の行を見て、そこからコードの2~3行上を確認する、ということです。多くの場合エラーはコンパイラが「悲鳴を上げた」位置より少し前にあります。

「1つのコマンド」モードでのリンクエラーはどう見えるか

ここでレクチャーの最重要ポイントです。複数ファイルを一度にビルドすると、各ファイルのコンパイルは成功してもリンクで失敗することがあります。

典型例:util.cpp をコマンドに追加し忘れた場合:

g++ -std=c++23 main.cpp -o tasker

コンパイラは main.cpp をコンパイルします(宣言 inc は util.hpp で見えている)が、リンク時に以下のようなメッセージが出ます:

  • どこかに undefined reference to ... というフレーズが出る
  • 近くに関数名、例えば inc(int) が表示される

これは「呼び出しはあるが、定義がビルド対象の中にない」ことを意味します。この段階で重要なのは魔法で直そうとするのではなく、落ち着いて自身に問いかけることです:コマンドにすべての必要な .cpp が列挙されているか? 定義はどのファイルにあるか?

ここでのチェックポイントとして覚えておくと良いのは:undefined reference に関するエラーはほとんどの場合 #include の問題ではありません。リンカが必要なオブジェクトコードを受け取っていない(たとえばある .cpp を渡していない)ことが原因です。

長い出力では最初のメッセージが重要

エラーが大量に出ると、つい最後の行に目がいきがちですが、コンパイラは通常上から順にメッセージを出します。多くの場合、最初の error: が鍵です。

大量の行があるときは、コマンド出力の先頭までスクロールして最初に現れる error: を探しましょう。リンクエラーでは error という語も出ますが、その場合は linker/ld/undefined reference のような特徴的な表示があります。ここでの練習は「ソースの破損」と「結合(リンク)の破損」を区別することです。

7. 実例:2ファイルから tasker を作る

結果のイメージを残すために、最小限のプロジェクト構成と1つのビルドコマンドを確認します。

ファイル構成例:

tasker/
  main.cpp
  util.hpp
  util.cpp

コードは上で示した通りです。最終的なビルドコマンド:

g++ -std=c++23 main.cpp util.cpp -o tasker

実行:

./tasker

正しく作れば、我々の例では 42 が表示されます(inc(41) を出力しているため)。

この例が重要なのは「何か有用なことをする」からではなく、日々の基本的な規律を示す点です: プロジェクト = 複数の .cpp であり、1つのコマンドでビルドする際はすべてを列挙する義務がある、ということです。

8. よくある間違い

誤りその1: main.cpp のみをビルドして、他の部分が「自動的に」取り込まれると期待すること。
これは単一ファイルの成功体験の後に起こりがちです。#include "util.hpp" は「util を接続する」ように見えますが、実際にはヘッダのテキストだけを挿入します。関数定義が util.cpp にあるなら、そのファイルをビルドコマンドで明示する必要があります。そうしないとリンク時に undefined reference が出ます。

誤りその2: -o を使わずに「今ビルドしたもの」と思って実行すること。
出力名を指定しないと a.out(または類似)が生成され、その後別のプロジェクトで別の a.out を作ると混乱します。結果として「コンパイラが無視している」と感じるかもしれませんが、実際には違うファイルを実行しているだけです。

誤りその3: #include を編集してリンクエラーを直そうとすること。
もし undefined reference のようなリンクエラーが出ている場合、通常それは #include を追加することで解決できる問題ではありません。ビルドコマンドに必要な .cpp(またはオブジェクト/ライブラリ)を渡すことが解決策です。現段階では、リンカは「渡された入力だけ」を見て判断することを覚えておいてください。

誤りその4: エラーメッセージの大量出力にパニクって下から順に読むこと。
コンパイラはしばしば連鎖的なエラーを出します。末尾から読むと症状だけを見て根本原因を直しそこねることが多いです。良い習慣は最初の主要なメッセージ(コンパイルならファイル/行付きの error:、リンクなら undefined reference)を特定して、それを直してから残りを見ることです。

誤りその5: -std=c++23 を使わずに「なぜ結果が違うのか」と驚くこと。
本日のテーマはコマンドの形です。標準フラグはビルド契約の一部であり、省略するとコンパイラのデフォルト(コンパイラのバージョンやシステムのポリシーに依存)に委ねられます。その結果、動作が不安定になり、環境が異なる人々の間で「見えない」エラーが発生します。

1
タスク
C++ SELF, レベル 33, レッスン 1
ロック未解除
最初のアプリケーション
最初のアプリケーション
1
タスク
C++ SELF, レベル 33, レッスン 1
ロック未解除
二つのバイナリ
二つのバイナリ
1
タスク
C++ SELF, レベル 33, レッスン 1
ロック未解除
2つのソースファイル
2つのソースファイル
1
タスク
C++ SELF, レベル 33, レッスン 1
ロック未解除
ビルドの診断
ビルドの診断
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION