記事内に広告が含まれています

【ワレコのC言語】グローバル変数・定数を複数ファイルで共有する【実践的】

この記事は約13分で読めます。
スポンサーリンク

ワテの場合、プログラミングには最近ではマイクロソフト社のVisual StudioのC#を使う事が多いのだが、昔はC言語を良く使っていた。

その後、C++も使い始めて、最近ではC/C++言語系で開発する場合にはC++を使う事が多い。

しかしながら、この一年半くらいCもC++を殆ど使っていなかったので、ふと昔の自分のソースコードを眺めてみたら、何を書いているのか理解出来ない。

自分の書いたプログラムを自分で理解出来ないのだ。

あかん。

書いている当時は物凄く頭が冴えていたのか、それともその後、頭がボケたのかは分からないが、兎に角さっぱり分からん。

こんな事では、忘れる一方なので、自分の備忘録として、C言語やC++に関するワテ流のプログラミングテクニックをまとめておく事にした。

今回はC言語でプログラミングする場合に、グローバル変数・定数を複数のソースファイルで共有する手法だ。

グローバル変数の宣言の仕方、初期値の与え方などは、幾つかの手法があると思うが、ここで紹介するワテ流の手法は、グローバル変数の宣言も初期値代入も全て一つの共通ヘッダーファイルの中で行うやり方だ。

ワテはこのやり方を昔、同僚の優秀な人に教えて貰った。

それ以来、この手法を使っている。

全てのグローバル変数宣言も初期化も一つのヘッダーファイルの中でやるので、管理も楽だし、何と言っても分かり易い。

まあ、もっと良い方法があるかも知れないが、ワテとしては長年この手法を使っている。

ではその手法を説明しょう。

スポンサーリンク
スポンサーリンク

C言語でmain.cとsub1.cファイルでグローバル変数・定数の共有

まあ、何をしたいかと言うと、下図にその概略を図解してみた。

C言語でプログラム開発を行う場合には、通常は複数のソースコードに分けて作成する。

多い場合だと100ファイル以上になる事もある。

さて、ここではその実例として二つのソースファイル

main.c

sub1.c

と一つのヘッダーファイル

Header.h

があり、その両者でグローバル変数と定数を以下のように宣言して初期化もしたい。

int g_i = 1;
const int c_i=1000;

また、sub1.cソースファイルの中だけで有効なグローバル変数と定数を以下のように宣言して初期化もしたい。

int s_i_sub1 = 2;
const int c_i_sub1=200;

変数名がヘンテコだなあと言う突っ込みがあるかもしれないが、まあそれは気にしないで欲しい。

さて、これらをそれを図にしたものが下図だ。

図 C言語でmain.cとsub1.cファイルでグローバル変数・定数の共有方法(ワテ流)

上の図で青い矢印には深い意味はない。単に赤枠の説明文が関係しているソースコードを指しているだけだ。

要するに、g_i と c_i はプログラム全体で共有されるグローバル変数(グローバルスコープ)。

一方、s_i_sub1 と c_i_sub1 はファイルsub1.c内の関数だけで共有されるスタティック(静的)なグローバル変数(ファイルスコープ)。

と言う事になる。

その三つのファイルを紹介しょう。

Header.hの中身

#pragma once
 
#ifdef    GLOBAL_DEFINE
#define   EXTERN
#define   GLOBAL_VAL(v) = (v)
#define   GLOBAL_ARR(a,b,c,d) = {a,b,c,d}
#else
#define   EXTERN    extern
#define   GLOBAL_VAL(v)
#define   GLOBAL_ARR(a,b,c,d)
#endif
 
// グローバル変数宣言と初期化 
EXTERN int g_i            GLOBAL_VAL(1);
EXTERN int g_iarr[]       GLOBAL_ARR(1, 2, 3, 4);
EXTERN double g_darr[]    GLOBAL_ARR(1.0, 2.0, 3.0, 4.0);
// グローバル定数宣言と初期化
EXTERN const int c_i GLOBAL_VAL(1000);

このヘッダーファイルの各行の意味を先頭から説明しょう。

#pragma once 

プラグマワンスは、多重インクルードを防止するやつだ。いわゆるインクルードガード。

このようにファイルの冒頭で記述しておくと、#include XXXXX で読み込まれるファイルを多重に読み込まない(一回だけ読み込む)と言う宣言だ。

この例では、このプラグマワンスの恩恵は無いけれど、一般にはinclude文も沢山あると、どこで何を読み込んでいるのか、もう訳分からなくなる。

その結果、同じヘッダーファイルを何度も読み込んだり、あるいは再帰的に読み込んでしまうなどの問題が生じる。そう言うのを解決してくれる嬉しいプラグマ文だ。

さて、

#ifdef    GLOBAL_DEFINE
#define   EXTERN
#define   GLOBAL_VAL(v) = (v)
#define   GLOBAL_ARR(a,b,c,d) = {a,b,c,d}
#else
#define   EXTERN    extern
#define   GLOBAL_VAL(v)
#define   GLOBAL_ARR(a,b,c,d)
#endif

この部分が、今回紹介するグローバル変数宣言と初期化の肝になる部分だ。

まあネット上のサンプルプログラムを検索すると、似たような例は沢山あると思うので、多分大昔に誰かが発明した手法だと思うが、その起源はワテは知らない。

少なくとも2000年以前には既にこの手の手法は一般的であった。

それで、上のコードで何をするかと言うと、main.cファイルの中では、

#define GLOBAL_DEFINE
#include "Header.h";

と記述する。

一方、それ以外のファイル(sub1.cなど)では、GLOBAL_DEFINEは定義せずに

#include "Header.h";

とだけ書く。

その結果、main.cではこのHeader.hファイルが展開されると、GLOBAL_DEFINEが定義されているので#ifdefの最初のブロックが有効化される(下図)。

#define   EXTERN
#define   GLOBAL_VAL(v) = (v)
#define   GLOBAL_ARR(a,b,c,d) = {a,b,c,d}

その結果、main.cでは、

// グローバル変数宣言と初期化  
int g_i  = 1; 

int g_iarr[]       = {1, 2, 3, 4}; 
double g_darr[]    = {1.0, 2.0, 3.0, 4.0}; 

// グローバル定数宣言と初期化 
const int c_i = 1000;

と展開される。

これによって、グローバル変数や定数をmain.cで宣言して初期化する事が出来る。

 

一方、sub1.cでは、#ifdefの#elseブロックが有効化されるので、以下のように展開される。

// グローバル変数宣言と初期化  
extern int g_i; 

extern int g_iarr[]; 
extern double g_darr[]; 

// グローバル定数宣言と初期化 
extern const int c_i;

要するに、先頭にexternを付けると同時に、初期値の部分は除去する小細工だ。

ここで登場する変数は既にmain.cで宣言されているので、他のファイル(sub1.cなど)では、それを参照する為に先頭にexternを付けるのだ。

このようにすると上手い具合にグローバル変数やグローバル定数をプロジェクト全体で使う事が出来る。

main.cの中身

#define GLOBAL_DEFINE
#include "Header.h";
#include <stdio.h>
 
int func1(int* pi);
 
int main()
{
    int i = 2;
 
    int result = func1(&i);
    printf("result=%d\n", result);    // 1492
 
    result = func1(&i);
    printf("result=%d\n", result);    // 1192
  
    int iend = sizeof(g_darr) / sizeof(g_darr[0]); // =4
    for (i = 0; i < iend; i++) {
        printf("g_darr[%d]=%20.15f\n", i, g_darr[i]);
    }
     return 0;
}

main関数は、まあ何の変哲もない処理だが、func1()を二回コールしている。

最後に、g_darr配列の中身もプリントしてみた。

深い意味は無い。

sub1.cの中身

 
#include "Header.h"
 
static int s_i_sub1 = 2;
static const int c_i_sub1 = 200;
 
int func1(int* pi)
{
    static int s_i_func1 = 3;
    const int c_i_func1 = 30;
 
    int result
        = c_i*g_i
        + c_i_sub1*s_i_sub1
        + c_i_func1* s_i_func1
        + *pi
        ;
 
    s_i_func1 -= 3;
    s_i_sub1--;
    *pi = -8;
 
    return result;
}

sub1.cでは、先頭にHeader.hのインクルード文があるので、上で説明したグローバル変数・定数が有効化される。

それとは別に、以下の二行がある。

static int s_i_sub1 = 2; 

static const int c_i_sub1 = 200;

この宣言によってこれらの二つの変数と定数はこのsub1.cファイルのみで有効となるグローバル変数定数だ。このようなグローバル変数を俗にファイルスコープと言う。

func1()関数の中の処理には深い意味は無い。

単に数値を掛けたり足したりしているだけなので、皆さんが応用する場合には好きなように書けば良いだろう。

また、以下の部分では

 static int s_i_func1 = 3;
 const int c_i_func1 = 30;

関数func1内でのみ有効な変数や定数を宣言して初期化している。

関数スコープと言うのかな。

 

実行結果

まあ、兎に角実行してみよう。

result=1492
result=1192
g_darr[0]= 1.000000000000000
g_darr[1]= 2.000000000000000
g_darr[2]= 3.000000000000000
g_darr[3]= 4.000000000000000
続行するには何かキーを押してください . . .

Visual Studio2017のC++空プロジェクトを選んで上の三つのファイルを保存する。

拡張子 .c で保管するとそれはC言語のソースと解釈されてCコンパイラーに掛けられる。

もし拡張子 .cpp で保管するとC++の文法で記述されていると解釈されてC++コンパイラーに掛けられる。

デバッグ無しで開始(CTRL+F5)を押すと実行出来る。

デフォルト設定では、実行してConsoleウインドウが開いて、直ぐに閉じてしまうので、何が表示されたのか分からない。

それを回避する為には、main関数末尾にgetchar()を挿入するなどの小細工手法が一般的だが、まあそれでも良いのだが、Visual Studioの場合には、プロジェクトのプロパティ画面を開いて、

リンカー

  システム

    サブシステム:コンソール(/SUBSYSTEM:CONSOLE)

に変更すると、デバッグ無し実行(CTRL+F5)では勝手にコンソールウインドウが閉じないように出来る。

その結果、上の実行例のように「続行するには何かキーを押してください . . .」が表示される。

 

以上で、ワテが良く使っているグローバル変数・定数の宣言と初期化手法の紹介は終わる。

どうでしょうか?

ここまでの説明でお分かり頂けましたでしょうか?

もし良く分からないとなると、ワテの説明が悪いのかも知れない。

なので、補足説明しておくと以下の通り。

要するに何がしたいのかと言うと

C言語でグローバル変数を定義して初期化して使うには、main.cで宣言と初期化をして、それ以外の

sub1.c

sub2.c

sub3.c

・・・

などの多数のサブファイルでは、それらの変数の先頭にexternを付けて宣言する必要がある。

その時には、初期値を与えては行けない。

もしサブファイルの中で同じく初期値を与えてしまうと、Visual Studio 2017の場合には以下のエラーが出る。

重大度レベル コード

説明 プロジェクト ファイル 行 抑制状態

エラー LNK2005 _g_i は既に main.obj で定義されています。

(ファイルパスは省略)\sub1.obj 

今回紹介した手法では、それらの問題を回避してグローバル変数や定数の宣言と初期化を行う事が出来るやり方なのだ。

可変引数マクロ __VA_ARGS__ を使えば何個でも行ける

当記事では、四つの要素を持つ配列に対して初期値を与える手法を紹介した。

その後、

可変引数マクロ __VA_ARGS__ 

と言うのが有る事を知った。

それを使えば何個でも行けるのだ。

#ifdef    GLOBAL_DEFINE
#define   EXTERN
#define   GLOBAL_VAL(v) = (v)  // これや
#define   GLOBAL_ARR(a,b,c,d) = {a,b,c,d}  // これを使わなくても
#define   INITIALIZE(...)   = __VA_ARGS__  // これひとつで行ける
#else
#define   EXTERN    extern
#define   GLOBAL_VAL(v)
#define   GLOBAL_ARR(a,b,c,d)
#define   INITIALIZE(...)  
#endif

と定義すれば、数字でも配列でも行ける。

EXTERN int    g_i         INITIALIZE(1);
EXTERN double g_darr[]    INITIALIZE({ 1.0, 2.0, 3.0, 4.0 });

知らなんだ。

まとめ

この記事では、C言語のプログラミングでワテが昔から使っているグローバル変数やグローバル定数の宣言と初期化の手法を紹介した。

GLOBAL_DEFINE方式と呼んでも良いかもしれない。

大昔から割とよく使われている手法なので、ワテ以外にも大勢の人が使っている手法だ。

またこの例では要素数4個の配列変数に初期値を与える小細工も使っている。

でも、今なら

可変引数マクロ __VA_ARGS__ 

を使えば5個でも10個でも、任意個数の引数を与えられるので、それを使うほうが良いだろう。

 

ワテとしては、今回紹介したGLOBAL_DEFINE方式は以下の理由で気に入っている。

グローバル変数もグローバル定数も一個のヘッダーファイルの中で管理出来るので分かり易いからだ。

逆に言うと、物凄く沢山のソースファイルがあるプロジェクト、例えば100個のソースファイルがあるとして、それらが全て共通ヘッダーファイル Header.h をインクルードすると、ヘッダーファイルの一箇所でも変更すると、全部のソースファイルが再コンパイルの対象となる。

なので、ビルドに長い時間が掛かる問題が出る。

従って、闇雲にHeader.hを全部のソースファイルでインクルードするのでは無くて、グローバル変数を使うソースファイルだけでHeader.hのインクルードを行うようにするのが良いだろう。

ちなみに表示される数字は以下の通り。

result=1492  コロンブス、西インド諸島に到達
result=1192  「いい国作ろう鎌倉幕府」と昔は覚えたのだが今は1185なのか⁉

深い意味は無い。

スポンサーリンク
コメント募集

この記事に関して何か質問とか補足など有りましたら、このページ下部にあるコメント欄からお知らせ下さい。

C#C/C++Visual Studio
スポンサーリンク
シェアする
warekoをフォローする
スポンサーリンク

コメント