第3週 静的変数
INDEX

目標

  • 記憶クラスについて理解する。
  • グローバル変数とローカル変数を使い分けることができる。
  • 静的変数について理解し、他の変数と区別して利用することができる。

予習・復習

 以下のスライドを利用して、予習と復習をしよう。復習では、自分の理解度を確認するために、実際にプログラムを作成し、意図する結果が得られるか確認しよう。
  1. 記憶クラス
  2. 外部変数

本日の講義・演習予定

  1. 記憶クラスと記憶領域
  2. 外部変数
  3. 演習問題
  4. 提出課題
内 容
  1. 記憶クラスと記憶領域
  2. 外部変数

記憶クラスと記憶領域

プログラムのメモリ構成

メモリ構造  ソースコードファイルをコンパイルすることで生成されたプログラムファイル(実行可能ファイル)は、ハードディスク(HDD)やUSBメモリなどの外部記憶装置に格納、保存されます。そのプログラムファイルも、実行時にはメインメモリにロードされ、実行が終了するまでメモリに常駐することになります。
 では、ロードされたプログラムはメインメモリにどのように配置されるのでしょうか。プログラム本体(機械語)と変数はそれぞれ別のメモリ領域に格納されます。また、変数もローカル変数やグローバル変数などその種類によって、配置される領域が決まっています。メモリのどこに配置するかを決めることを記憶クラスと言います。左図にそのメモリ配置の構成図を示します。(ポインタ変数は後期講義に登場します。)

記憶クラス指定子

 変数が記憶領域のどこに配置されるかを決めるのは記憶クラス指定子です。記憶クラス指定子は変数宣言の際に、次のようにデータ型の前に付加します。  
記憶クラス指定子 データ型 変数名 <,変数名,・・・>;

 記憶クラスは、配置される記憶領域を決めるだけではなく、プログラム実行中におけるその変数の有効範囲と記憶寿命を決めることにもなります。  以下に記憶クラス指定子と記憶クラス、記憶寿命の関係を表す一覧表を示します。

記憶クラスと記憶領域
記憶クラス指定子変数の名称記憶領域記憶寿命
auto自動変数スタック領域(自動記憶域)コードブロック内
static静的変数スタティック領域(静的記憶域)プログラム中
extern外部変数(既にある外部変数を示す)プログラム中
registerレジスタ変数レジスタ、またはスタック領域コードブロッックない
※ レジスタ変数は、CPU内部のレジスタと呼ばれる小容量の記憶装置を利用する。アクセス速度が主記憶装置よりも高速であるため、処理速度を優先する処理に利用される。しかし、容量が小さいため、もし、容量を超えて変数宣言された場合は、その部分についてはスタック領域に確保される。

自動変数(auto記憶クラス指定子)

 関数内で記憶クラス指定子を指定しない変数は、自動的に自動変数とみなされます。(今まで関数内で宣言していた変数は自動変数だったわけです。)自動変数はメモリのスタック領域と呼ばれる領域に格納されます。その有効範囲と記憶寿命は宣言されたコードブロック内であり、その変数が利用される間のみ領域が確保されることになります。例えば、ある関数で宣言された自動変数は関数の呼び出しと同時にスタック領域に格納領域が確保され、関数の処理が終了すると同時にその格納領域は消失します。
 自動変数は、変数宣言によってそのデータ型に応じた領域がメモリに確保されますが、その値は明示的な初期化が行われなければ不定となります。すなわち自動的に0で初期化されるといったことは行われません。したがって、必ずその変数を利用する前に値を確定させなければなりません。
 次のプログラムは、main関数から関数func1を繰り返し呼び出してそれぞれの自動変数の値を表示する例です。main関数で定義されている変数iも、関数func1で定義されている変数xも、どちらも自動変数なのでメモリのスタック領域に保存されるます。main関数の自動変数iの記憶寿命は、main関数が処理される間、すなわちプログラム実行中有効で、その値は0、1、2と変化します。しかし、関数func1の自動変数xの記憶寿命は関数func1が処理されている間で、処理が終了するとスタック領域から削除されてしまいます。ですから、呼び出される度に変数xの値は初期値1となります。
automatic_variable.c
#include <stdio.h>

void func1(void);

int main(void)
{
	int i;		//自動変数

	for(i=0; i<3; i++){
		printf("%d: ",i);
		func1();
	}
	return 0;
}

void func1(void)
{
	int x=1;		//自動変数
	
	printf("func: x = %d\n", x++);	//自動変数のインクリメント
}

静的変数(static記憶クラス指定子)

 記憶クラス指定子staticが付与され宣言された変数を静的変数と呼びます。静的変数は静的記憶域(スタティック領域)に格納され、その記憶寿命はプログラムの実行中になります。
 静的変数は、自動変数とは異なり明示的に初期化が行われなかった場合、自動的に0で初期化が行われます。
 次のプログラムは、先のプログラムに新たに関数func2を追加したものです。関数func2は変数xが静的変数として宣言されている以外は関数func1と同様に変数xの値をインクリメントして表示するだけです。関数func2の変数xの有効範囲は関数func2の中だけですが、静的変数なのでその記憶寿命はプログラムの実行中となります。したがって、関数func2の処理が終了しても静的領域に格納された値は維持されるため、呼び出される度にその値を1、2、3と増加させていきます。
static_variable.c
#include <stdio.h>

void func1(void);
void func2(void);

int main(void)
{
	int i;		//自動変数

	for(i=0; i<3; i++){
		printf("%d: ",i);
		func1();
		func2();
	}
	return 0;
}

void func1(void)
{
	int x=1;		//自動変数
	
	printf("func: x = %d\n", x++);	//自動変数のインクリメント
}

void func2(void)
{
	static int x=1;		//静的変数
	
	printf("func: x = %d\n", x++);	//静的変数のインクリメント
}

グローバル変数

 グローバル変数とは関数の外で宣言される変数で、その有効範囲はプログラム全体でした。記憶寿命もstaticが付いてはいませんが、変数宣言からプログラムの終了までの間でとなります。すなわち静的変数です。(staticの付いたグローバル変数は別の意味で利用されるので注意が必要です。詳しくは後述する。)
global_variable.c
#include <stdio.h>

int x = 1;		//グローバル変数
void func(void);

int main(void)
{
	int i;		//自動変数

	for(i=0; i<3; i++){
		printf("%d: ",i);
		func();
	}
	return 0;
}

void func(void)
{
	printf("func: x = %d\n", x++);	//静的変数のインクリメント
}

プログラムのメモリ配置

 変数が記憶クラス指定子によってメインメモリにどのように配置されるかは、プログラムを実行させた時のそれぞれの変数のメモリアドレスを知る必要があります。変数のメモリアドレスは、アドレス演算子 &を変数名に付与することで得ることができます。
 次のプログラムは、自動変数がスタック領域に、グローバル変数が静的記憶域に格納されていることを確認するプログラムです。また、main関数と関数funcが配置されるコード域の位置も確認することができます。実行して表示されたメモリ・アドレスからそのメモリ配置図を描いてみてください。
memorymap.c
#include <stdio.h>

char g = 99;	//グローバル変数(静的記憶域)

char func(char x)	//引数(ローカル変数)
{
	char y;		//ローカル変数(自動変数)
	static char z;	//ローカル変数(静的変数)

	y = x * 2;

	puts("--- func memory map ---");
	printf("stack   : x = %d (%p)\n", x,&x);
	printf("stack   : y = %d (%p)\n", y,&y);
	printf("stactic : z = %d (%p)\n", z,&z);

	return y;
}

int main(void)
{
	char a,b,c;	//自動変数(スタック)

	a = 11;
	b = 22;
	c = 33;

	puts("--- main memory map ---");
	printf("code  : main (%p)\n", &main);
	printf("code  : func (%p)\n", &func);
	printf("static: g = %d (%p)\n", g,&g);
	printf("stack : a = %d (%p)\n", a,&a);
	printf("stack : b = %d (%p)\n", b,&b);
	printf("stack : c = %d (%p)\n", c,&c);

	func(a);

	return 0;
}
 ※配置される実際のメモリアドレスはその実行環境によって変わります。

外部変数

 複数のソースコードを横断して、グローバル変数や関数を利用したい場合は外部変数を利用します。外部変数として宣言するためにはグローバル変数にextern指定子を付与します。

他のファイルの変数を利用する

 次の二つのファイルからなるプログラムは、ファイルafile.cで定義されているグローバル変数exaと関数exfunc()をもう一つのファイルbfile.cで利用するために、externを付けて定義している例です。
afile.c
#include <stdio.h>

int exa;		//グローバル変数

void exfunc(void)
{
	exa = 777;
	printf("afile: exa = %d\n",exa);
}
bfile.c
#include <stdio.h>

extern void exfunc(void);	//別ファイルで定義された関数を利用する
extern int exa;	//別ファイルで定義された変数を利用する:外部変数

int main(void)
{
	exfunc();
	printf("bfile: exa = %d\n", exa);
	return 0;
}

ヘッダファイルで共有する

 他のファイルで利用したい外部変数をヘッダファイルにまとめた例です。ヘッダファイルにまとめておくことで、特定のファイルだけではなく、そのヘッダファイルをインクルードしたファイルでも共有することができる点で、大きなメリットがあります。
headerfile.h
extern int exa;
extern void exfunc(void);
afile.c
#include <stdio.h>
#include "headerfile.h"	//ヘッダファイルの読み込み

void exfunc(void)
{
	exa = 777;
	printf("afile: exa = %d\n",exa);
}
bfile.c
#include <stdio.h>
#include "headerfile.h"	//ヘッダファイルの読み込み

int main(void)
{
	exfunc();
	printf("bfile: exa = %d\n", exa);
	return 0;
}

staticを付けてグローバル変数

 static指定子を付与したグローバル変数は、他のファイルからは見えなくなります。もし、複数のファイルで同じグローバル変数名が使われていたとしても、static指定子を付けておけば別の変数として扱うことができます。
headerfile.h
//external int exa;
extern void exfunc(void);
afile.c
#include <stdio.h>
#include "headerfile.h"	//ヘッダファイルの読み込み

static int exa;		//他のファイルからは見えない

void exfunc(void)
{
	exa = 777;
	printf("exfunc: exa = %d\n",exa);
}
bfile.c
#include <stdio.h>
#include "headerfile.h"	//ヘッダファイルの読み込み

static int exa;		//他のファイルからは見えない

int main(void)
{
	exa = 111;
	exfunc();
	printf("bfile: exa = %d\n", exa);
	return 0;
}
・staticなしグローバル変数 外部リンケージ 別ファイルの同名変数と共有する
・staticありグローバル変数 内部リンケージ 別ファイルの同名変数とは異なる


expand_lessBack to TOP

演習問題

  1. 先のプログラム例memorymap.c で使われている変数の型を char ではなく int型とした時のメモリ配置を確認しなさい。また、なぜ、そのような配置になっているのかchar型の場合と比較して説明しなさい。

  2. 次は、小数点を含むデータを1個づつ追加しながら、追加されたデータの平均値を表示するプログラムです。関数averageAddUp()の空欄を埋めて完成させなさい。
  3. ex3-1.c
    #include <stdio.h>
    
    #include 
    #define N 10
    
    double averageAddUp(double x);
    
    int main(void)
    {
    	double data[N] = { 52.8, 67.3, 58.1, 72.5, 60.2,
    						81.7, 53.2, 55.8, 63.0, 58.4};
    	int i;
    	double avg;
    
    	puts("No.| data | average ");
    	puts("---+------+---------");
    	for(i=0; i<N; i++){
    		printf("%2d %6.1f %7.2f\n", i+1, data[i], averageAddUp(data[i]));
    	}
    
    	return 0;
    }
    
    double averageAddUp(double x)
    {
    	//-----------------------------
    	 ここを埋めて完成させる。
    	//-----------------------------
    
    	return sum/num;		// 合計÷データ数
    }
    

  4. 標準ライブラリを使わずに、0から99までの20個の乱数を得るために自作関数myRand()を作成しました。しかし、期待した結果が得られず同じ値しか表示されませんでした。原因は、関数myrand()の中に間違いが1ヶ所あったためでした。修正して、異なる20個の乱数を表示して下さい。
  5. ex3-2.c
    #include <stdio.h>
    
    int myRand(void);
    
    int main(void)
    {
    	int i;
    
    	for(i=0; i<20; i++){
    		printf("%d\n",myRand());
    	}
    	return 0;
    }
    
    int myRand(void)
    {
    	unsigned int rand = 1; 	
    	rand = rand * 13 + 97;
    	rand %= 99; 
    	return rand;
    }
    

  6. 呼び出されるたびに戻り値の値が交互に整数0と1を繰り返す関数clock()を定義して、main関数から5回呼び出して10101と表示されることを確認しなさい。(ヒント:関数clock()は状態を持ち、その状態を戻り値とします。)

  7. 関数tff()はint型の引数とint型を戻り値する関数です。引数の値が0から1に変化した時のみ状態(戻り値)がそれまでの状態(戻り値)を反転します。そうでなければそれ以前と同じ状態(戻り値)となります。下記の実行例のように先の問題で作成した関数clock()の出力を関数tff()の引数に渡した結果を表示しなさい。関数clock()からの出力は10回分で良い。関数tff()とclock()の初期状態は0であるものとします。
    CLK | TFF
      1 |  1
      0 |  1
      1 |  0
      0 |  0
      1 |  1
      0 |  1
      1 |  0
      0 |  0
      1 |  1
      0 |  1
    

演習問題解答

OPEN ANSWER
  1. 回答 char型のデータ幅は1Byteでint型のデータ幅が4Byteであるため、自動変数のメモリアドレスの間隔が1ではなく4離れている点が異なる。

  2. 回答
    	static double sum = 0.0;
    	static int num = 0;
    
    	sum += x;
    	num++;
    

  3. 回答 unsigned int rand = 1;  に static を追加して宣言する。

expand_lessBack to TOP

今週の確認テストテスト(PDF)

OPEN ANSWER
1.
(1) グローバル変数 ローカル変数
(2) グローバル変数 ローカル変数
(3) ローカル変数
(4) ローカル変数

2.
(1) void
(2) 型名
(3) 実引数 仮引数
(4) 実引数 仮引数
(5) 配列 配列 配列
(6) return文

3.
(1) ○
(2) ○
(3) ○
(4) ○
(5) ○

4.
グローバル変数を利用する方法
#include <stdio.h>

int count=0;	//グローバル変数

void func(int n)
{
	count++;
	printf("% 回\n", count);
}								
							

ローカル変数を利用する方法
#include <stdio.h>

void func(int n)
{
	static int count=0;		//ローカル変数(静的変数)
	
	count++;
	printf("% 回\n", count);
}								
							

本日の提出課題

以下の各プログラムを作成しなさい。ただし、いずれも配列とグローバル変数は利用できないものとします。
  • 課題1
  • 整数0が入力されるまで、繰り返し整数を入力してそれまで入力された整数の平均を求めるプログラム。ただし、追加された整数の個数とその合計を内部に持つ関数avgupを定義して利用すること。関数avgup(int a)は追加する整数を引数aで受け取り、追加されたすべての整数の平均を戻り値とする。
    (実行例 : ideone.comにおいて)
    --- stdin ---
    1 2 3 0
    
    --- stdout ---
    1.0
    1.5
    2.0
    
    		

  • 課題2
  • 1と0を交互に表示するプログラム。ただし、整数1もしくは0の状態値を内部に持ち、呼び出される度にその状態値が0から1もしくは1から0へ変化する関数clockを定義して利用すること。関数clock()は引数を持たず、状態値を戻り値とします。初期状態は0とします。

    (実行例 : ideone.comにおいて)main関数から関数clockを10回呼び出した場合。
    --- stdin ---
    
    --- stdout ---
    1 0 1 0 1 0 1 0 1 0
    		

  • 課題3
  • 次の漸化式の第2項から第10項までを表示するプログラム。ただし、第n項の値を戻り値として、引数なしの関数seqを定義して利用すること。 $$f_n = 3f_{n-1} + 1, \hspace{2em} f_1 = 1$$
    (実行例 : ideone.comにおいて)
    --- stdin ---
    
    --- stdout ---
    4 13 40 121 364 1093 3280 9841 29524 			
    		

  • 課題4(発展課題)
  • 次の漸化式の第3項から第10項までを表示するプログラム。ただし、第n項の値を戻り値として、引数なしの関数fibを定義して利用すること。 $$f_1=1,\hspace{1em} f_2=1,\hspace{1em} f_n = f_{n-2} + f_{n-1} (n \geq 3)$$
    (実行例 : ideone.comにおいて)
    --- stdin ---
    
    --- stdout ---
    2 3 5 8 13 21 34 55 
    			
    		

  • 課題5(発展課題)
  • フリップフロップは、1ビットの情報を保持することのできる論理回路です。フリップフロップにはRSフリップフロップ、Tフリップフロップ、JKフリップフロップなど複数の種類があります。このうちTフリップフロップはトグル(反転)・フリップフロップとも呼ばれ、入力されるクロック信号が0から1へ変化するとその状態は1から0、もしくは0から1へ変化します。このTフリップフロップと同じ動作を関数tff()として実現するプログラムを作成しなさい。
    関数tff()はその状態値(記憶している値)を戻り値として、クロック信号に相当する値を引数として持ちます。引数で0を受け取った後に1を受け取るとその状態値が反転します。
    クロック信号は課題2で作成した関数clock()を利用するものとします。また、関数clock()と関数tff()の初期状態は0であるものとします。
    (実行例 : ideone.comにおいて)
    --- stdin ---
    
    --- stdout ---
    CLK | TFF 
    ----+---- 
      1 |  1
      0 |  1
      1 |  0
      0 |  0
      1 |  1
      0 |  1
      1 |  0
      0 |  0
      1 |  1
      0 |  1		
    		

 株価を評価する指標の一つに移動平均があります。移動平均とは当日から遡ったある一定期間の株価の終値の平均値を言います。日経平均株価終値のある20日分の5日移動平均を求めるプログラムkadai3.cを作成しなさい。但し以下の要件を満たすこと。また、1日目から4日目までの移動平均は無視できるものとします。
[要件]
    1. 下記のソースコードを修正・変更することでプログラムを完成させなさい。
    2. 日経平均株価の20日分のデータは、下記のソースコードにある配列dataで与えるものとします。
    3. グローバル変数の追加、およびmain関数への変数の追加はできないものとします。
    4. 戻り値に5日移動平均を、引数に当日分の株価priceとする関数moving_average()を定義して、利用することで移動平均を求めること。
    5. main関数内で実施するテスト結果はすべて OK. と表示されること。

[ソースコード]
#include <stdio.h>
#define PD 5	//移動平均を求める期間

/*  移動平均を求める関数 */
double moving_average(double price)
{
	//------------------------------
	ここを埋めて完成させる。
	//------------------------------
}

int main(void)
{
	double result[] = {0.0, 0.2, 0.6, 1.2, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0};		//検証用データ
	double data[] = {21062.98, 21250.09, 21301.73, 21272.45, 21283.37, 21151.14, 21117.22, 21182.58, 21260.14, 21003.37, 20942.53, 20601.19, 20410.88, 20408.54, 20776.10, 20774.04, 20884.71, 21134.42, 21204.28, 21129.72, 21032.00, 21116.89};		// 日経平均
	int i;
	double avg;

	printf("TEST START ... ");
	for(i=0; i<10; i++){
		if((avg=moving_average(i)) != result[i])
			printf("BAD(%f).\n",avg);
		else
			printf(" OK.");
	}

	printf("\n\n ### Moving Averages ### \n");
	for(i=0; i<20; i++){
		avg = moving_average(data[i]);
		if(i>PD-1)
			printf(" %2dth:  %.2f\n",i,avg);
	}

	return 0;
}

Hint:
 関数moving_averageは、5つの要素からなる配列を持っており、当日から遡って5日分の株価を保持することができます。この関数は呼び出される度に当日の株価の保存先を示す配列の添字を循環させるので、一番古い日の要素は当日の株価で書き換えられます。

  • [提出方法]
    1. ideone.comを利用して作成した課題プログラムのURLをCoursePowerへ提出のこと。
    2. 提出するideone.comの課題プログラムには、実行結果が表示されていること。
    3. プログラムの正当性についてどのようにして検証したか簡単に記すこと。
    4. 詳細はスライド「ideoneの使い方」参照のこと。

  • [評価について]
  • プログラムの内容の評価とは別に以下の要件を満たすことを評価の前提とする。
    1. C言語で記述されていること。
    2. ソースコードファイルのコンパイル結果に、ワーニングやエラーが出力されていないこと。
    3. 所定の実行結果が得られるていること。
    4. ソースコードは、インデントや適当な改行が施された見やすい状態であること。

  • [提出期限]
  • 2023年 7月3 日(月)までとする。ただし、以降の提出も受け付ける。