読者です 読者をやめる 読者になる 読者になる

ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

【C++】ヘッダファイルとcppファイルの事故らない扱い方(2/2)

書き手:Hidano

前回の続きです。

nut-softwaredevelopper.hatenablog.com

前編では、複数クラスを扱うときに、各クラスごとに1つずつヘッダファイルを用意するルールについて説明しました。

今回は、ヘッダファイルがどんどん増えていった時に注意する点について解説します。

まず大前提として、私たちの書いたプログラミングがどのようにして実行されるのかの概要を把握しましょう。

普段VisualStudioなどで記述しているファイルは「ソースコード」と呼びますよね。厳密には「.cpp」がソースファイル、「.h」がヘッダファイルです。

これらのファイルはこのままではただの文章です。エクスプローラから開いても、VisualStudioやメモ帳などで中身が表示されるだけで、プログラムは実行されません。

このソースコードに書いたプログラムを実行するには、「ビルド」と呼ばれる作業をして、「.exe」ファイルにしないといけません。この.exeファイルのことを「実行(可能)ファイル」と呼びます。

ビルドの作業は、大きく分けて「コンパイル」と「リンク」の二つの工程から成っています。

コンパイル」は、ソースファイル(.cppファイル)を1つずつ解析して、コンピュータが実行可能なオブジェクトファイルに変換する作業、「リンク」はコンパイルされたそれぞれのオブジェクトファイルを統合して、最終的な実行ファイルを生成します。

リンクは今回の説明にはあまり関係ないので省略します。重要なのはコンパイルです。

コンパイルをする機能を持ったソフトの事を「コンパイラ」と呼び、VisualStudioはこのコンパイラを内包しています。LinuxなどのCUI環境でC/C++のプログラミングをしたことのある方は、「gcc」という単語に馴染みがあるかと思います。これもコンパイラのことで、「GNUコンパイラコレクション」の略称となっています。

このコンパイラが複数あるcppファイルを一つずつ解析していくのですが、例えばこんなソースコードを書いたとしましょう。

//Main.cpp
int main(){
    Player player;
    player.Action();
    return 0;
}
//Player.h
class Player{
public:
    void Action();
}
//Player.cpp
void Player::Action(){
    printf("Hello!");
}

一見、実行するとmain関数がPlayerクラスの実態を生成し、Action関数を実行してくれるように見えます。しかし、これはエラーになりますよね。

まずここにはMain.cppとPlayer.cppがありますが、コンパイラがどちらを先に解析するかは分かりません。そして、VisualStudioのコンパイラはプロジェクト内のcppファイルを片端から全て解析してくれますが、我々が指示しない限りヘッダファイルは読み込みません。そのため、Main.cppもしくはPlayer.cppを解析しようとした時に、「Playerクラスなんて定義されてないぞ」とエラーを吐くわけです。Player.cppを読み込んだ時は、ついでに「printfなんて関数は知らんよ」とも言われます。printfが定義されているのは「stdio.h」ですからね。

その為、cppファイルで必要になるクラスや関数が定義されたヘッダファイルを読み込ませるために、お馴染み「#include」を使います。

//Main.cpp
#include"Player.h"
int main(){
    Player player;
    player.Action();
    return 0;
}
//Player.cpp
#include"Player.h"
#include<stdio.h>
void Player::Action(){
    printf("Hello!");
}

余談ですが、#includeでヘッダファイルを指定するときに、ファイル名を「"“」や「<>」で囲いますが、これは「”“」がソースファイルと同じディレクトリ内を意味して、「<>」はあらかじめ用意された別の場所を指定しています。なので自分で作ったヘッダファイルは”“で、stdioなどの最初から用意されていたファイルは<>で囲むようにしてください。

さて、これでMain.cppとPlayer.cppはPlayerクラスの情報を取得できたので、先ほどのエラーは出なくなりました。しかし、このままビルドすると今度は別のエラーとなってしまいます。

コンパイラがMain.cppとPlayer.cppのどちらを先に読み込んだかは分かりませんが、「#include"Player.h"」がそれぞれ1回ずつ、計2回使われていますよね。

このままコンパイルすると、プログラム中にPlayerクラスが2つ存在することになってしまい、どちらを参照すればいいか分からなくなってしまいます。

これを防ぐ方法を「インクルードガード」と呼び、いくつか方法があります。おそらく最も一般的なのは、

//「INCLUDED」部分の名前は任意
#ifndef INCLUDED
#define INCLUDED

class Player{
public:
    void Action();
}

#endif

とするやり方です。

「#ifndef」は「INCLUDED」という任意の単語が定義されているかを確認し、定義されていなければ「#endif」までを読み込み、もし定義されていたら読み飛ばします。

「#ifndef」は「if not define」のことで、他に存在する「#ifdef」(if define)とは処理が逆になりますから注意してください。

こうすることで、最初にPlayer.hがインクルードされた時だけPlayerクラスの定義を行い、二度目以降の「#include “Player.h"」で二重に定義されることはなくなりました。

ただ、このやり方はコードを3行も使う上、「INCLUDED」などの定義するワードが重複すると不味い為、多くのコンパイラでは別のキーワードが使えるようになっています。

#pragma once

class Player{
public:
    void Action();
}

これなら冒頭に1行書くだけで済みますね。ちなみに、最近のVisualStudioではヘッダファイルを作った時点で自動的に1行目に書いてくれていたりします。

さて、ここまでで重複してインクルードされる事態を防ぐ「インクルードガード」について説明しました。

しかしこの方法も万能ではなく、プログラムが複雑になってくると思わぬエラーが起こったりします。ちょっと再現が面倒なので詳細は省略しますが、ヘッダファイルの中で他のヘッダファイルを複数インクルードしているとき(クラスが継承されたり、メンバに別のクラスのポインタを持つなど)に、参照エラーが発生することがあります。

これはインクルード文の順番を変える、ヘッダファイルにプロトタイプ宣言を付けるなどの方法で解決できますが、エラーが出るたびに一々調べて回るのも面倒ですし時間の無駄ですよね。

そんな時に便利なのが、プリコンパイル済みヘッダーの利用です。

プリコンパイル済みヘッダーとは、コンパイラコンパイルを始めるとき、事前に読み込んでおくようにと指定するヘッダーファイルのことです。

このプリコンパイル済みヘッダーとして「stdafx.h」を通常のヘッダファイルと同じように作成します。

この「stdafx.h」という名前ですが、本当はどんな名前でもいいんです。私は「Pch.h」とした方が分かりやすいと思います。

しかし、VisualStudioが推奨する名前が「stdafx」なので、ここではそれに従っています。

ヘッダファイルを作成したら、その中でプロジェクトに使っているヘッダファイルを全部インクルードさせます。

//stdafx.h

#pragma once

//例
#include"GameBase.h"
#include"Input.h"
#include"GameObject.h"
#include"Player.h"
#include"Enemy.h"
#include"Bullet.h"

ヘッダファイルを作成したら、今度はそれに対応するcppファイルを作成します。

インクルード文しか書いていないのだから実装部を書くcppファイルを用意する必要はないじゃないかと私も思ったのですが、コンパイラの都合で用意しないといけないそうです。

//stdafx.cpp

#include"stdafx.h"

あとは各cppファイルの冒頭を全て「#include"stdafx.h"」に書き直します。面倒くさい場合はVisualStudioのプロジェクトの設定から一括してインクルードするように設定できますが、後々トラブルを避けるためにも頑張ってコピペした方がいいかもしれません。

最後に、VisualStudioのプロジェクト>「[プロジェクト名]のプロパティ」>C/C++>「プリコンパイル済みヘッダー」を選択し、設定画面の「プリコンパイル済みヘッダー」を「作成」に、「プリコンパイル済みヘッダー ファイル」を「stdafx.h(違う名前で作った場合はその名前)」に設定します。

以上で作業は終わりです。もしかするとstdafx.hの中でインクルードする順番を入れ替えなければならないかもしれませんが、1度修正すればそれ以降は不要です。継承したクラスを別々に書いている場合は、基底クラスの方を上に書くのがコツです。

もちろん、今後新しいクラスを作ってヘッダファイルが増えたときは、stdafx.hファイルも更新するのを忘れないようにしましょう。

ちょっと後半が長くなりましたが、ヘッダファイルとcppファイルの扱いについて解説してきました。

一応注釈すると、本来プリコンパイル済みヘッダーはコンパイルを効率化させるために用いるものであり、インクルードエラーの解決法として採用するのは邪道と言われるかもしれません。

しかしソースコードの中身と関係ないコンパイルエラーで立ち止まってしまい、作りかけのプロジェクトを放棄するような事態に陥るよりはずっとマシだと思いますので、今後プログラミングをする上で是非活用して欲しいと思います。