グローバル変数 v.s. 構造体

ドーナツグローバル変数を使うべきでない理由で迷っているので。

俺は、グローバル変数はできるかぎり避けるべきだと思っている。なぜなら、グローバル変数よりも構造体のほうが拡張しやすいからだ、と言っても、ピンとこないだろうから、具体的な例でやってみよう。

お題は、あるファイルに含まれている文字数を数えるプログラム。そして、そのあと、'a'を数える機能を追加する。それと、グローバル変数や構造体の例として、名前と値を一つにまとめたカウンタという概念を使ってみる。

グローバル変数

まずは、グローバル変数を使ったバージョン。

#include <stdio.h>
static int num_;
static const char* name_;
void set_name(const char* name){
  name_ = name;
}
void inc_counter(){
  num_++;
}
void print_counter(){
  printf("%s:%d?n",name_,num_);
}

main(){
  FILE* fp = fopen("test.txt","r");
  set_name("char counter");
  while(getc(fp) != EOF){
    inc_counter();
  }
  print_counter();
  
  fclose(fp);
  return 0;
}

プログラムを単純にするために、ファイル名を固定した。一応、実行例をしめしときます。

$ counter
char counter:106

構造体版

次に、構造体を使って書き直します。

#include <stdio.h>
typedef struct{
  const char* name;
  int num;
} Counter;

void set_name(Counter* counter,const char* name){
  counter->name = name;
}
void inc_counter(Counter* counter){
  counter->num++;
}
void print_counter(Counter* counter){
  printf("%s:%d?n",counter->name,counter->num);
}

main(){
  FILE* fp = fopen("test.txt","r");
  Counter counter;
  set_name(&counter,"char counter");
  while(getc(fp) != EOF){
      inc_counter(&counter);
  }
  print_counter(&counter);
  
  fclose(fp);
  return 0;
}

グローバル変数版 機能拡張

ここまでは、構造体を使っても、グローバル変数を使っても、大差はない。というか、むしろグローバル変数を使ったほうが、多少シンプルな気さえする。

で、ここからが本題。今、作ったプログラムに、'a'を数えるカウンタを追加してみる。

グローバル変数の場合は、編集する箇所が非常に広範囲になります。

#include <stdio.h>
static int num_[2];
static const char* name_[2];
void set_name(const char* name,int i){
  name_[i] = name;
}
void inc_counter(int i){
  num_[i]++;
}
void print_counter(int i){
  printf("%s:%d?n",name_[i],num_[i]);
}

main(){
  FILE* fp = fopen("test.txt","r");
  set_name("char counter",0);
  set_name("a counter",1);
  char c;
  while((c = getc(fp)) != EOF){
    inc_counter(0);
    if(c == 'a') inc_counter(1);
  }
  print_counter(0);
  print_counter(1);
  
  fclose(fp);
  return 0;
}

実行すると、以下のようになります。

$ counter
char counter:106
a counter:7

構造体版 機能拡張

構造体の場合は、新しい変数をつくるだけで、ほとんどコードを変更せずに機能を追加できる。

#include <stdio.h>
typedef struct{
  const char* name;
  int num;
} Counter;

void set_name(Counter* counter,const char* name){
  counter->name = name;
}
void inc_counter(Counter* counter){
  counter->num++;
}
void print_counter(Counter* counter){
  printf("%s:%d?n",counter->name,counter->num);
}

main(){
  FILE* fp = fopen("test.txt","r");
  Counter cc; // char counter
  Counter ac; // a counter

  set_name(&cc,"char counter");
  set_name(&ac,"a counter");
  char c;
  while((c = getc(fp)) != EOF){
      inc_counter(&cc);
      if(c == 'a') inc_counter(&ac);
  }
  print_counter(&cc);
  print_counter(&ac);
  fclose(fp);
  return 0;
}

まとめ

プログラミング作法

プログラミング作法

以上の例から分かるように、構造体を使った場合より、グローバル変数を使った場合のほうが修正箇所が広範囲に及びます。そして、修正する場所が広いということは、バグが発生しやすい、発生した場合の原因の特定が難しい、時間がかかるなど、いいことは一つもありません。

というわけで、「グローバル変数よりも、構造体を使うべきだ」と主張させて頂きます。

あと、この話は、プログラミング作法という本に載っていました。

あと、C++の場合、異なるファイルにあるグローバル変数は初期化順序が保証されないという問題があるんだけど、それはまた別の話。(気になるならEffectiveC++でも読みぃ)