こんにちは。田原です。

C言語時代から全てのプログラムを一度にまとめてコンパイルするのではなく、機能単位等に分割してコンパイルすることで開発効率が良くなる仕組みがあります。
また、第5回目冒頭の図に記載した4大メモリの1つ静的変数用メモリ(static記憶領域)を使うことで、プログラム実行中有効な変数やプログラムの全体や一部で共有する変数を実現できます。
今回は、これらの有用な機能と4大メモリの3つ目プログラム用メモリについて解説します。

 

1.分割コンパイルとコンパイル単位

まず初めに、グローバル変数の説明に必要なので分割コンパイルについて解説します。

今までのサンプルではソース・ファイルを1つしか使っていませんでしたが、実はC++は複数のソース・ファイルを別々にコンパイルし、後で結合して実行可能なバイナリ形式(exeやdllなど)を生成する機能を持っています。
これは巨大なプログラムをたった1つのファイルに押し込めて開発することが現実的ではないからです。機能毎になど複数のファイルに分割して開発することで、担当している部分だけをコンパイルできるようにする仕組みです。巨大なプログラム開発で威力を発揮します。

その別々にコンパイルする1つのコンパイルのことを「コンパイル単位」と呼びます。
 

1-1.コンパイル単位とヘッダ・ファイルの関係

コンパイル単位に含まれるソース・ファイルは、コンパイル対象の1つのソース・ファイルが決定します。その仕組は、コンバイル対象のソース・ファイルが#include文で他のソース・ファイルをインクルードするのです。その読み込まれたソース・ファイル(ヘッダ・ファイルと呼ばれます。)が更に他のヘッダ・ファイルを#includeすることも多いです。
こうして連鎖的にインクルードされたソース・ファイル全てが、そのコンパイル単位でコンパイルされるソースとなります。

拡張子について
C++ソース・ファイルの拡張子としてcppやhが良く使われますが、これは誰かが定めているわけではなく習慣です。そして、コンパイル対象としては c や cc、ヘッダとしては hpp などもよく見かけます。
拡張子が cpp ノソーフ・ファイルはデフォルトでC++としてコンパイルされ、拡張子が c のソース・ファイルはC言語としてコンパイルする処理系(コンパイラ)が多いです。C言語としてコンパイルサれる時は、C++の機能を使えません。最初の頃はたまにハマリますのでご注意下さい。

さて、インクルードの具体的な振る舞いは#include文のところにインクルードしたファイルをまるっと挿入します。例えば次の2つのファイルがあったとします。

int foo();
int bar();
#include "foo.h"

int foo()
{
    int normal_variable=10;
    ++normal_variable;
    return normal_variable;
}

int bar()
{
    static int static_variable=10;
    ++static_variable;
    return static_variable;
}

foo.cppをコンパイルする場合、foo.cppは最初にプリプロセッサで処理されます。それにより#include等のプリプロセッサ命令が処理されます。そして、その内の#include文は指定されているファイルの内容に展開されます。
そして、そのままコンパイルされるのでプリプロセッサで処理された結果は通常、目に触れることはありません。しかし、コンパイル・オプションを指定して見ることができますので、実際に見てみましょう。

Visual C++の場合、/EPオプションを付けるとプリプロセッサからの出力が表示されます。(コンパイルはされません。)

> cl /EP foo.cpp
Microsoft(R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

foo.cpp


int foo();
int bar();


int foo()
{
    int normal_variable=10;
    ++normal_variable;
    return normal_variable;
}

int bar()
{
    static int static_variable=10;
    ++static_variable;
    return static_variable;
}

gccの場合、-Eオプションを付けるとプリプロセッサからの出力が表示されます。(VC++同様、コンパイルはされません。)

$ gcc -E foo.cpp
# 1 "foo.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "foo.cpp"
# 1 "foo.h" 1
int foo();
int bar();
# 2 "foo.cpp" 2

int foo()
{
    int normal_variable=10;
    ++normal_variable;
    return normal_variable;
}

int bar()
{
    static int static_variable=10;
    ++static_variable;
    return static_variable;
}

上記どちらともハイライトした行が元々#include "foo.h"があったところです。ここにfoo.hの内容が展開されていることが分かると思います。(意味不明な行も出てますが、それらはコンパイラがエラー・メッセージを生成する時等に必要な情報です。あまり気にしないで下さい。)

このようにC++ではコンパイル対象となるソース・ファイル(拡張子cppがよく使われます)があり、それがコンパイル単位を決定します。そして、そのコンパイル対象のソース・ファイルは全ての必要なヘッダ・ファイルをインクルードして1つのソース・ファイルに纏めてから、コンパイルされます。
 

1-2.分割コンパイルのサンプル

前回の最後のstaticなローカル変数のソースを分割コンパイルしてみます。

foo()関数とbar()関数をfoo.cppへ移動します。それらをmain.cppのmain()関数から使えるようにするため、foo()とbar()の宣言をfoo.hにて用意し、main.cppからインクルードしました。
つまり、foo()とbar()の実体はfoo.cpp、それらを使うための宣言をfoo.hに書きます。
(これは便利なので良く使われる構成ですが、必至ではないです。)

#include "foo.h"

int foo()
{
    int normal_variable=10;
    ++normal_variable;
    return normal_variable;
}

int bar()
{
    static int static_variable=10;
    ++static_variable;
    return static_variable;
}
int foo();
int bar();
#include <iostream>
#include "foo.h"

int main()
{
    std::cout << "foo()=" << foo() << "\n";
    std::cout << "foo()=" << foo() << "\n";
    std::cout << "foo()=" << foo() << "\n";

    std::cout << "bar()=" << bar() << "\n";
    std::cout << "bar()=" << bar() << "\n";
    std::cout << "bar()=" << bar() << "\n";

    return 0;
}
project(separate-compile)

if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc")
else()
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11")
endif()

add_executable(separate-compile main.cpp foo.cpp foo.h)

CMakeLists.txtはadd_executable()で分割コンパイルする対象のソース・ファイルを指定します。
ヘッダ・ファイルは指定してもしなくても構いません。自動的に判定されてコンパイル対象から外されます。(なお、Visual Studioはヘッダ・ファイルをここで指定しておかないとヘッダ・ファイルがソリーションに登録されないようです。)

以上により、main.cppとfoo.cppが、それぞれコンパイルされリンクされて実行形式バイナリが生成されます。

 

2.グローバル変数

ローカル変数は関数の中だけで使うことができました。それに対してグローバル変数は名前の通り全ての関数が使うことができます。
例えばログ出力用の変数など裏方的に多くの関数で使いたいような変数はグローバル変数にすると良いです。
グローバル変数の寿命はstaticなローカル変数と同じくプログラムの開始から終了までです。
 

2-1.単純なサンプル

関数の外で普通に変数を定義すると、それはグローバル変数です。
下記のbazはグローバル変数です。foo()関数、および、main()関数で使っています。

#include <iostream>

int baz=0;

void foo()
{
    std::cout << "foo(0)  baz=" << baz << "\n";
    baz=20;
    std::cout << "foo(1)  baz=" << baz << "\n";
}

int main()
{
    baz=10;
    std::cout << "main(0) baz=" << baz << "\n";
    foo();
    std::cout << "main(1) baz=" << baz << "\n";

    return 0;
}

なお、baz変数を最初に0で初期化してますが、グローバル変数は基本の型の時でも自動的に0初期化されますので、0での初期化は省略可能です。しかし、それを頑張って覚えてもあまりメリットはないので可能な時は明示的に初期化しておくと良いと思います。
 

2-2.分割コンパイルのグローバル変数

グローバル変数の使用宣言時は、externを付けます。また、初期化は記述できません。
以前解説したように関数は定義部分({…})を書かずに ; を付ければ使用宣言となります。

#include <iostream>

int baz=0;

void foo()
{
    std::cout << "foo(0)  baz=" << baz << "\n";
    baz=20;
    std::cout << "foo(1)  baz=" << baz << "\n";
}
#include <iostream>

extern int baz;  // グローバル変数の使用宣言(externが必要)
void foo();      // 関数の使用宣言

int main()
{
    baz=10;
    std::cout << "main(0) baz=" << baz << "\n";
    foo();
    std::cout << "main(1) baz=" << baz << "\n";

    return 0;
}

今回のようにfoo.cppで定義されているグローバル変数や関数を使うのがmain.cppだけなら、上記のようにmain.cppの中に直接使用宣言を書いても良いのですが、bazやfoo()を他のコンパイル単位からも使いたいような場合は多いです。
ですので、一般にはfoo.cppで定義されていて他のコンパイル単位でも使うものは、foo.hで宣言します。
そのために、使用宣言をfoo.hへ移動しmain.cppの使用宣言部分に#include “foo.h”と書きます。

#include <iostream>
#include "foo.h"

int baz=0;

void foo()
{
    std::cout << "foo(0)  baz=" << baz << "\n";
    baz=20;
    std::cout << "foo(1)  baz=" << baz << "\n";
}
extern int baz;  // グローバル変数の使用宣言(externが必要)
void foo();      // 関数の使用宣言
#include <iostream>
#include "foo.h"

int main()
{
    baz=10;
    std::cout << "main(0) baz=" << baz << "\n";
    foo();
    std::cout << "main(1) baz=" << baz << "\n";

    return 0;
}

その際、ついでにfoo.cppでfoo.hをインクルードしておくと便利です。
ここでインクルードしておけば、もしfoo.hと実体の間に不整合があった場合、コンパイラがエラーか警告を発してくれますので、バグを事前に潰せます。

 

2-3.よくやる間違い

よくやる間違いがあります。ヘッダで「使用宣言」ではなく「定義」を書いてしまうことです。

#include <iostream>
#include "foo.h"

void bar()
{
    foo();
}
int baz=0;

void foo()
{
    std::cout << "foo(0)  baz=" << baz << "\n";
    baz=20;
    std::cout << "foo(1)  baz=" << baz << "\n";
}
上記と同じなので省略。

このようにすると、bazとfoo()関数の両方ともmain.cppとfoo.cppにインクルードされて取り込まれるため、両方のコンパイル単位内で定義されます。その結果、各コンパイル単位をリンクする時に多重定義エラーになります。
C++には、ODR(One Definition Rule)と言うルールがあり、原則としてプログラム内(リンクして生成される実行可能形式内)で「定義」は1つと決められています。(ODRは例外が意外に多いので混乱しやすいのです。これについては必要に応じて解説します。)

  foo.obj : error LNK2005: "int baz" (?baz@@3HA) は既に main.obj で定義されています。 
  foo.obj : error LNK2005: "void __cdecl foo(void)" (?foo@@YAXXZ) は既に main.obj で定義されています。
CMakeFiles/global-variable.dir/foo.cpp.o:(.bss+0x0): `baz' が複数定義されています
CMakeFiles/global-variable.dir/main.cpp.o:(.bss+0x0): ここで最初に定義されています
CMakeFiles/global-variable.dir/foo.cpp.o: 関数 `foo()' 内:
foo.cpp:(.text+0x0): `foo()' が複数定義されています
CMakeFiles/global-variable.dir/main.cpp.o:main.cpp:(.text+0x0): ここで最初に定義されています

早速ODRの例外の1つです。
関数はinline指定することができます。inline関数は複数の場所で定義してよいと定められています。

ちなみにinline関数はそれを呼び出した所に直接関数の中身を展開しても良いという指定です。直接展開された時はスタックに戻り先を積む処理が省略されるので少しだけ高速化できます。
なお、同じinline関数の定義がコンパイル単位によって異なると未定義動作を引き起こします。その時、何が起こるかわかりません。鼻から悪魔が飛び出すことも禁止されていないそうです。

#include <iostream>
#include "foo.h"

int baz=0;

void bar()
{
    foo();
}
extern int baz;     // グローバル変数の使用宣言(externが必要)
inline void foo()   // 関数のinline定義
{
    std::cout << "foo(0)  baz=" << baz << "\n";
    baz=20;
    std::cout << "foo(1)  baz=" << baz << "\n";
}
上記と同じなので省略。

 

2-4.staticな?グローバル変数

さて、グローバル変数にも、ローカル変数と同じくstaticを付けることができます。

ローカル変数にstaticを付けるとそのローカル変数の寿命が、元々「定義から{}ブロックの終わりまで」だったのが、「プログラムの開始から終了まで」に伸びました。staticの意味は「静的」でありプログラムの開始から終了までの間に記憶領域が変化しないことから来ています。

ところが、グローバル変数は元々「プログラムの開始から終了まで」が寿命です。更に寿命を延ばすならプログラム終了後も生きているようにすることですが、そのような機能ではありません。グローバル変数をアクセスできる範囲(スコープといいます)が狭くなるだけです。寿命は変わりません。
ローカル変数をアクセスできる範囲(スコープ)は寿命と同じ「定義から{}ブロックの終わりまで」でした。グローバル変数の場合は「定義からそのコンパイル単位の終わりまで」となります。

このようにグローバル変数にstaticを付けた時のstaticは「静的」という意味ではありません。規格の策定者が新たなキーワードの導入を避けるために流用したのではないかと思います。

さて、サンプル・ソースです。foo.cppで定義していたグローバル変数bazをmain.cppへ移動してstaticを付けてみました。static付きなのでbazはmain.cppでのみアクセスでき、foo.cppではアクセスできないため、リンク時に未定義エラーになります。

#include <iostream>
#include "foo.h"

void foo()
{
    baz=20;      // リンク時エラー
}
extern int baz;  // グローバル変数の使用宣言(externが必要)
void foo();      // 関数の使用宣言
#include <iostream>
#include "foo.h"

static int baz=0;

int main()
{
    baz=10;
    std::cout << "main(0) baz=" << baz << "\n";
    foo();
    std::cout << "main(1) baz=" << baz << "\n";

    return 0;
}

更に、もし、別のコンパイル単位で同じ名前同じ型のstaticなグローバル変数を定義した場合、それは別物になります。共有されません。同じ名前で異なる変数は混乱の元ですので、このサンプルのような使い方は必要な場合を除き避けた方が良いです。

#include <iostream>
#include "foo.h"

void foo()
{
    std::cout << "foo(0)  baz=" << baz << "\n";
    baz=20;
    std::cout << "foo(1)  baz=" << baz << "\n";
}
static int baz=100; // グローバル変数の使用宣言(static)
void foo();         // 関数の使用宣言
#include <iostream>
#include "foo.h"

int main()
{
    baz=10;
    std::cout << "main(0) baz=" << baz << "\n";
    foo();
    std::cout << "main(1) baz=" << baz << "\n";

    return 0;
}
main(0) baz=10
foo(0)  baz=100
foo(1)  baz=20
main(1) baz=10

外部リンケージについて:
このように他のコンパイル単位に対して公開しないことを、「外部リンケージを持たない」、もしくは、「内部リンケージを持つ」と表現します。
逆に通常の(staticを付けていない)グローバル変数のように他のコンパイル単位と共有できることを「外部リンケージを持つ」と表現します。(「内部リンケージを持たない」とはあまり言いません。)
「リンケージ」はC/C++の専門用語です。あまり直感的な表現ではなくイメージしづらいのですが、ちらほら見かけますし重要な概念ですので次のことを覚えて下さい。
・他のコンパイル単位と共有できる =外部リンケージを持つ
・他のコンパイル単位と共有できない=外部リンケージを持たない、内部リンケージを持つ

 

3.staticな記憶領域(静的変数用メモリ)について

これまでにプログラムの開始から終了まで有効な変数として以下の変数がでてきました。(他にクラスの中にもう一つ静的変数があります。それはクラスについて解説する時に説明します。)

  • グローバル変数
  • staticを付けたグローバル変数
  • staticなローカル変数

これらはコンパイル時にメモリ割り当てが確定し、プログラムの実行中そのメモリ割当が変化しないため「静的変数」と呼ばれます。そして、これらはプログラムが起動する時に実メモリである「staticな記憶領域」に割り当てられます。

第5回目冒頭の図の「静的変数用メモリ」が「staticな記憶領域」のことです。
また、この図の「スタック用メモリ」には第8回目第9回目で解説した関数の戻り先や通常のローカルが記録されます。
最後の「ヒープ用メモリ」についてはもう少しお待ち下さい。
 

4.プログラム用メモリについて

実は「プログラム用メモリ」のことをすっかり忘れてました。重要では有るのですが、単純な領域ですのでここで解説します。

ここには例えばmain()関数やfoo()関数などの関数のプログラム実体(マシン語のコード群)と定数(例えば、文字列定数”This is foo().”などや数値定数100など)が記録されます。
ここに書き込まれる値はコンパイル時に確定しますのでstaticな記憶領域と同様メモリ領域はプログラムの起動時に行われ、プログラムが終了するまで有効です。
そして、この領域には書き込みができないようなハードウェアを備えているコンピュータも少なくありません。現代のPCはその1つです。そのようなコンピュータでは間違ってプログラム用メモリへ書き込むと不正アクセスでプログラムが強制終了します。

しかし、一部のコンピュータは書き込めてしまいます。間違って書き込むとたいへん痛いです。例えば”test”と言う文字列定数が書かれているその先頭アドレスに’b’を書き込むと、ソース上はどうみても”test”をアクセスしているように見えるのに”best”が出てきます。デバックは地獄をみます。要注意です。

文字列定数”test”の先頭に文字’b’を書き込もうとするプログラムです。PCで実行すると*p='b';で落ちますので、foo()関数は呼ばれません。

#include <iostream>

char* p="test";

void foo()
{
    std::cout << "This is foo().\"n";
}

int main()
{
    std::cout << "p   =" << (void*)p << std::endl;
    std::cout << "&foo=" << &foo << std::endl;
    *p='b';
    foo();
    return 0;
}

あまり深い意味はないのですが、ついでにfoo()関数のアドレスを表示してみてます。
pの指す領域とfoo()のある領域を当講座ではまとめて「プログラム用メモリ」として扱います。
厳密には多少異なりますが、C++を処理系に依存しない範囲で使う分には差はありません。
foo()のある領域へ書き込もうとするとpの場合と同様、プログラムが強制終了します。
もしも、書き込めるような処理系だった場合、foo関数の先頭の機械語コードが書き換わるため、foo()関数を呼び出した時、何が起こるか分かりません。

2017年3月4日修正
上記サンプルプログラムを修正しました。修正前はmain()関数のアドレスを表示してました。C言語では問題ないのですがC++では禁止されていることが分ったためです。
n3337 59ページ最初の行に”The function main shall not be used within a program.”と書かれてます。
C++11の文法と機能(C++11: Syntax and Feature)によると、「main関数は、プログラム中で使われてはならない。「使う」というのは、呼び出すことはもちろん、アドレスやリファレンスを取ることも含まれる。」ということでした。
main関数のアドレスを取った場合の動作を処理系は保証しなくてよいと言う意味です。以前のプログラムもたまたま意図した通りに動作しましたが、意図通りに動作しない処理系が世の中には存在するかも知れません。君子危うきに近寄らずです。

 

4.まとめ

今回はグローバル変数とstaticな記憶領域について主に解説しました。また、グローバル変数と密接に関係する分割コンパイルについても解説しました。

今回の解説の中で最も重要な内容は、「staticな記憶領域」です。第5回目冒頭の図にある下記の4種類のメモリはハードウェア的には全く同じメモリですが、使い方が異なり、それぞれに対して様々な機能をC++は提供しています。

  • プログラム用メモリ
  • 静的変数用メモリ(staticな記憶領域)
  • スタック用メモリ
  • ヒープ用メモリ

今回までにプログラム用メモリ、静的変数用メモリ、スタック用メモリを解説しました。
次回、最後の大物「ヒープ用メモリ」について解説します。お楽しみに。