C言語のstatic宣言とextern宣言について

C言語では型を修飾する修飾子(記憶クラス指定子という)にstaticとexternというものがあります。

static宣言

static宣言は関数の外側で行うときと内側で行うときとで意味合いが変わります。関数の外側で行うときは外部変数(グローバル変数とも言う)や関数宣言(関数プロトタイプとも言う)に対してstatic宣言します。以下に、その例を示します。


#include <stdio.h>

static int a;
static void func();

void main() {

	printf("main: a = %d\n", a);
	func();

}

static void func() {

	printf("func: a = %d\n", a);

}

Sample Code.1

この場合、外部変数 a と関数 func() が適用されるスコープはそのソースファイル内に限られます。プログラムソースを分割した場合、別のソースファイルから外部変数 a と関数 func()への参照はできません。関数の外側でstaticを付けることにより、外部変数や関数自体を隠蔽する役割となります。なお、Sample Code.1の実行結果は以下となります。

main: a = 0
func: a = 0

次に関数の内側でstatic宣言をしたときの例を示します。関数の内側なので局所変数(ローカル変数とも言う)に対してstatic宣言を行います。func()の局所変数 b に対してstatic宣言をしています。


#include <stdio.h>

void func();

void main() {

	func();
	printf("----\n");
	func();

}

void func() {

	int a = 0;
	static int b = 0;

	printf("func: a = %d\n", a);
	printf("func: b = %d\n", b);

	a++;
	b++;

}

Sample Code.2

局所変数は関数の呼び出しが終了すれば消滅しますが、staticを宣言することで局所変数は静的変数となり、関数が終了しても値を保持します。Sample Code.2の実行結果は以下となります。

func: a = 0
func: b = 0
----
func: a = 0
func: b = 1

static宣言していない局所変数 a は都度初期化が行われますが、static宣言している局所変数 b は次の関数呼び出しが行われても前の結果を保持していることがわかります。

ここで外部変数、局所変数、静的変数(static宣言した局所変数)の初期化メカニズムについて触れておきます。

外部変数、静的変数の初期化は1回だけプログラムが始まる前に行われます。初期値を指定することもできますが、指定しない場合は自動でゼロに初期化されます。Sample Code.1の実行結果を参照ください。

局所変数の初期化は関数が実行されるたびに行われます。局所変数は初期値を指定するか局所変数に値を代入してから使う必要があります。というのも、局所変数は明示的な初期化がない場合、その値は不定(何が入っているかわからない)となるためです。

[広告]

extern宣言

extern宣言も関数の外側と内側の両方で使われます。staticの場合は関数の外側か内側かで意味合いが変わりましたが、externの場合は同じです。

externはプログラムソースを分割した際に、別のソールファイルに記載された外部変数や関数を宣言しておくときに使います。

外部変数は「宣言」と「定義」をわけており、宣言では変数のメモリ領域の割り当てを行いません。実際にメモリ領域に割り当てるのは定義のほうです。externを付けることで定義ではなく宣言の扱いになります。ソースファイルが複数あるときに外部変数の定義は1つだけでなくてはならず、他の場所ではextern宣言にします。

以下にextern宣言を使用する例を記載します。main.cから、func.cで定義されている外部変数 a 、および、関数 func()を使用したい場合です。main.cの中でextern宣言をしています。


#include <stdio.h>

extern int a;
extern void func();

void main() {

	printf("main.c: a = %d\n", a);
	a++;
	func();

}

Sample Code.3 main.c


#include <stdio.h>

int a;

void func() {

	printf("func.c: a = %d\n", a);

}

Sample Code.3 func.c

extern宣言は関数の内側に書くこともできます。Sample Code.3 main.cを以下のようにしても問題ありません。


#include <stdio.h>

extern void func();

void main() {

	extern int a;
	printf("main.c: a = %d\n", a);
	a++;
	func();

}

Sample Code.4 main.c

この実行結果は以下となります。

main.c: a = 0
func.c: a = 1

ということなんですけれども・・・

実は、Sample Code.3のmain.cの外部変数、および、関数宣言のexternはなくてもコンパイルは通るし、実行結果も正しく出ます。

関数宣言のexternはなくても問題ないです。なぜ不要になったのかはわからないのですが、昔はプログラムソースを分割した際に別のソースファイルに記載された呼び出し先の関数が出来ていない状態でもそのソースファイルのコンパイル(オブジェクトファイルの作成)までは出来るように(明確に)仮で宣言しておいたのでは?と思います。呼び出し先の関数が出来上がったら、リンクして実行形式ファイルを作成したのではないかと。

ここの例では書かなかったのですが、外部関数をextern宣言してヘッダファイルに集めて、それをincludeするという手法は昔からあります。stdio.hなどはそうなっています。

外部変数のexternは付けるべきだと思います。main.cの外部変数 a にextern宣言をしないで、gccに以下のオプションを付けて実行すると警告やエラーとなります。gccがデフォルトで警告やエラーにしていない理由がわからないのですが。。

-Xlinker --warn-commonオプションを付けると警告となる。


$ gcc main.c func.c -Xlinker --warn-common
/tmp/cc6pxmVg.o: 警告: 共通シンボル `a' が重複しています
/tmp/ccq7NGE1.o: 警告: 前の共通シンボルはここです

-fno-commonオプションを付けるとコンパイルエラーとなる。


$ gcc main.c func.c -fno-common
/tmp/ccZAWnT1.o:(.bss+0x0): `a' が重複して定義されています
/tmp/cc7mLKXX.o:(.bss+0x0): ここで最初に定義されています
collect2: error: ld returned 1 exit status

C言語のポインタ変数にはどうして型が必要なのか?

C言語のポインタ変数はメモリ上のアドレス位置を格納する変数です。アドレス位置を格納する場合、ポインタ変数には32bit OSでは4バイト、64bit OSでは8バイトが割り当てられます。これは、32bit OSの場合は2の32乗(約4ギガバイト)までのメモリを扱える(制御できる)ということになります。64bit OSの場合は2の64乗(約18エクサバイト)です。

本題に入る前に以下のプログラムを実行してみて実際の型のサイズを確認してみます。ポインタ変数のサイズの確認ですが、char型、int型、float型、double型のサイズも表示するようにしています。コンパイル、および、実行した環境は64bit Linux(ubuntu)です。


#include <stdio.h>

int main( void )
{
	char  *charp;
	int  *intp;
	float  *floatp;
	double  *doublep;
	void  *voidp;

	printf( "整数型のサイズ\n" );
	printf( "sizeof( char ) = %d\n", (int)sizeof( char ));
	printf( "sizeof( short ) = %d\n", (int)sizeof( short ));
	printf( "sizeof( int ) = %d\n", (int)sizeof( int ));
	printf( "sizeof( long ) = %d\n\n", (int)sizeof( long ));

	printf( "浮動小数点型のサイズ\n" );
	printf( "sizeof( float ) = %d\n", (int)sizeof( float ));
	printf( "sizeof( double ) = %d\n", (int)sizeof( double ));
	printf( "sizeof( long double ) = %d\n\n", (int)sizeof( long double ));

	printf( "ポインタのサイズ\n" );
	printf( "sizeof( charp ) = %d\n", (int)sizeof( charp ));
	printf( "sizeof( intp ) = %d\n", (int)sizeof( intp ));
	printf( "sizeof( floatp ) = %d\n", (int)sizeof( floatp ));
	printf( "sizeof( doublep ) = %d\n", (int)sizeof( doublep ));
	printf( "sizeof( voidp ) = %d\n", (int)sizeof( voidp ));

	return 0;

}

実行した結果は以下です。

整数型のサイズ
sizeof( char ) = 1
sizeof( short ) = 2
sizeof( int ) = 4
sizeof( long ) = 8

浮動小数点型のサイズ
sizeof( float ) = 4
sizeof( double ) = 8
sizeof( long double ) = 16

ポインタのサイズ
sizeof( charp ) = 8
sizeof( intp ) = 8
sizeof( floatp ) = 8
sizeof( doublep ) = 8
sizeof( voidp ) = 8

ここから本題に入ります。ポインタ変数はメモリ上のアドレス位置を格納できれば良いので8バイト分を確保すれば済みます。わざわざchar型のポインタ変数、int型のポインタ変数などと分ける必要があるのでしょうか?

これについては僕が思っている答えを書いておきます。ポインタ変数に格納されているアドレス位置はその変数の開始位置であって、それ以上の情報はありません。実際にプログラムが変数を取り出すには変数の開始位置と終了位置(開始位置からどれだけの領域を確保しているか)の情報が必要です。以下に図で示します。

ポインタは変数の開始位置を指し示していますが、アドレス位置だけでは変数が何バイト分の領域を要しているかはわかりません。この「何バイト分の領域か?」を示すのがポインタ変数の型となります。char型であれば開始位置から1バイト分、int型であれば開始位置から4バイト分となります。ポインタ変数に型を指定することで、ポインタを使用して参照する変数への値の代入や書き換えが可能になると考えます。

C言語のポインタは配列と強い関係性があります。配列の先頭位置をポインタ変数に格納してそれに整数を加算すると、配列の要素を数え上げることになります。

void型のポインタについても書いておきます。void型のポインタは、char、int、float、doubleのどの型のポインタでも格納することができます。ですが、void型のポインタは「開始位置から何バイトを保持しているのか?」の情報を持っていないので、void型のポインタを使用して変数へのアクセス(値の参照や代入など)はできません。

最後に。
C言語を学ぶ上でバイブルと呼ばれている本です。初心者向けの本を一通り読み終えた後に読むと理解が一層深まります(初心者が読むには少し難しい)。かなり昔の本ですが、Cの思想や考え方などは今読んでもとても参考になります。