6.12 識別子
6.12.2 基本概念
変数や関数の定義に関わる基本的な概念として,「宣言」,「定義」,「翻訳単位」,「スコープ」,「寿 命」,「リンケージ」を説明していくが,はじめにそれらの用語の意味をきちんと定義しておこう.
40確かに処理系が識別子がどの名前空間に属するかを区別するのは易しい. しまし,違う名前空間に属する,同じ文字列からなる識 別子をむやみやたらに多用すると,プログラマにとっては混乱の元になり,自分自身の書いたコードでさえ,何が書いてあるかわから なくなるので,そのようなことはやってはいけない.
6.12.2.1 定義と宣言
これまでは, オブジェクトや関数の「定義」・「宣言」という言葉を曖昧なまま利用してきた. ここで, そ れらの言葉を正しく理解しよう. 宣言(declaration)とは, 識別子の組の解釈および属性を指定すること である. 識別子によって名前付けられたオブジェクトまたは関数のために記憶域の確保を引き起こす宣言 を定義(definition)という41.
つまり,オブジェクトや関数の「定義」とは「宣言」の中に含まれている. オブジェクトの「宣言」を行 う場合には, オブジェクトは型(type)とともに宣言されなくてはならない. また, 必要であれば記憶クラ ス指定子や型修飾子を伴って宣言される. 関数の「宣言」を行う場合には,関数は戻り値の型,仮引数の型 とともに宣言されなくてはならない42. また,必要であれば記憶クラス指定子を伴って宣言される.
このように「定義」と「宣言」を定義すると,どれが識別子の「定義」で,どれが「宣言」かわからなく なるのだが,一つのオブジェクトや関数に対して「定義」はただ一度だけである. 関数の「定義」は関数本 体とともに宣言された時に行われる. それ以外のものはすべて「定義」を伴わない「宣言」である. オブ ジェクトの「定義」は通常extern指定子を伴わない「宣言」で行われる43. extern宣言の詳細について は後に解説する.
なお,変数の宣言と同時に初期化を行うことが出来るが,初期化を行うと,その宣言は定義とみなされる.
Example 6.12.1
extern int sum(int, int) ; <==== これは宣言(定義にはならない)
extern int x ; <==== これは宣言(定義になるかどうかは,
他のプログラムファイルに依存する)
int main(int argc, char **argv) <==== これは定義 {
int a ; <==== これは定義
....
}
int sum(int a, int b) <==== これは定義 {
int c ; <==== これは定義
}
Example 6.12.2
extern int x = 1 ;
と書くと,「extern宣言と初期化を同時にしているけどいい?」なんて警告が出される. 初期化をする場 合には,extern宣言は書かない方が良い. (というよりも,extern宣言の趣旨とは矛盾する.)
41ANSI規格書によれば,ファイルスコープのオブジェクトを,初期化子を使わず,記憶クラス指定子なしかstaticで宣言する場
合を,仮宣言(tentative definition)と呼んでいる. これは,オブジェクトコードのリンク時に最終的にリンケージが決定される
ことを意味している. (cf. [3, X 3010 6.7.2, p. 1924])
42正しくは, traditionalな形式の宣言も許されている.すなわち,仮引数の型を伴わない宣言も許されるが,バグを引き起こす原因 となる.
# 何でこんなのを許す仕様を残しておいたのだろう?
43つまり,これまでオブジェクトを宣言してきたものは,すべてオブジェクトの定義となっている. オブジェクトの宣言にすべて
externをつけてもエラーとはならない.リンク時にそれらの宣言のうちのいずれか一つを「定義」とみなす.これが「リンケージ」
というやつ.
# 要するに,extern宣言とはきちんと使わなければ,「メチャメチャ」になるものの典型的なものである.
Example 6.12.3 2つのファイルからなるプログラムで以下のようなことをすると,リンク時にエラーと なる. (分割コンパイルに関しては, Section 6.13 を参照.)
/* file1.c */
int x = 1 ;
/* file2.c */
extern int x = 1 ; これはxという識別子が2ヶ所で定義されていることがエラーの原因となる.
6.12.2.2 翻訳単位
Cにおいて翻訳単位とは,プログラムのファイル1個づつを指す44. Cではプログララムを複数のファイ ルに分割し,ファイルごとにコンパイルを行い,リンカでそれぞれのオブジェクトコードを結び付けること が出来る.
6.12.2.3 スコープ
オブジェクトや関数のスコープ(scope,日本語では有効範囲という)とは,そのオブジェクトや関数の識 別子をプログラム中のどこから見えるか(可視(visible)かどうか)を示す概念である. スコープには次の 4種類がある.
1. ファイル・スコープ 2. 関数・スコープ 3. ブロック・スコープ,
4. 関数プロトタイプ・スコープ.
スコープは,変数を宣言する場所から決定される. 変数を宣言できる場所は以下のいずれかである.
1. どの関数にも含まれない部分. 正しい言い方では「どのブロックにも,仮引数ならびよりも外側にあ らわれる時」. この場合は「ファイル・スコープ」となる.
スコープは翻訳単位の終了,すなわちファイルの終了によって終了する.
2. 「関数・スコープ」となる場合は2通りあり,
• 関数定義の仮引数.
• 関数の先頭部分.
この場合,スコープは関数ブロックの終了によって終了する.
3. ブロックに入った直後45. これは「ブロック・スコープ」となり,対応する}の出現で終了する46. 4. 関数プロトタイプ宣言の仮引数. この場合,「関数プロトタイプ・スコープ」となり, スコープはそ
の宣言内のみとなる.
44より正確には,プリプロセッシングを終了した翻訳フェーズにおけるファイルが翻訳単位となる.
45これは余り利用しないが,有効に利用できる場合がある. 変数を関数内で極めて局所化したい場合に利用することがある. これ
は,デバック(debug)を行う場合などで利用することがある.恒久的なコードでこの手法を用いると,思わぬ変数の隠蔽が起きる可
能性があるので,デバック時などの一時的なコードにのみ用いる方がよいだろう.
46正確には,上の「関数・スコープ」は「ブロック・スコープ」の一部であり, ANSI規格に定める「関数・スコープ」とは,goto 文にあらわれるラベル名だけが適用対象であり,このラベル名は関数内のどこからでも参照可能である.なお,ラベル名の識別子は構 文の出現とともに暗黙に宣言される.
このノートでは「関数・スコープ」と「ブロック・スコープ」を便宜上区別しよう.
Example 6.12.4 これらの実際の例は以下の通りである.
#include <stdio.h>
int i ; /* ファイルスコープ */
extern int sum(int a, int b) ; /* 関数プロトタイプスコープ */
int main(int argc,char **argv) /* 関数スコープ */
{
int j ; /* 関数スコープ */
{
int k ; /* ブロックスコープ */
k = 0 ; }
return 0 ; }
int l ; /* ファイルスコープ */
識別子は(ラベル名を除いて)すべて宣言以後でないと可視でないことに注意する. したがって, Example
6.12.4の例の識別子lのスコープを,ファイル全体にしたい場合には,以下の例のいずれかに書き直す必要
がある.
Example 6.12.5 Example 6.12.4 の識別子lのスコープをファイル全体にする.
#include <stdio.h>
int i, l ;
int main(int argc,char **argv) {
int j ; j = 0 ; {
int k ; k = 0 ; }
return 0 ; }
#include <stdio.h>
int i ;
extern int l ;
int main(int argc,char **argv) {
int j ; j = 0 ; {
int k ; k = 0 ; }
return 0 ; }
int l ;
右の例では,lの定義の位置を変えずに,extern宣言を用いてスコープを拡大した. しかし,このような例 は「奇妙な書き方」であり,わざわざこのようなことをする必要はない47.
Example 6.12.6 文法上許されない変数宣言の例.
47要するに, Example 6.12.4のint lの宣言は,文法上可能というだけのことである.
int main(int argc,char **argv) {
int j ; j = 0 ;
int k ; /* この宣言は文法上許されない */
k = 0 ; return 0 ; }
この例におけるint k の宣言は文の後に書かれているため,文法エラーとなる. 複文内で宣言は文のな らびの前に書かなければならない.
6.12.2.3.1 識別子の隠蔽 次のような例では,識別子の可視性はどうなるのであろうか?
int i ;
int main(int argc, char **argv) {
int i ; {
int i ; }
}
このようにプログラム中に同じ名前空間に属する識別子が複数あり,プログラム中のある点において,それ らのうちのいくつかが可視であるとき,その点において見えている識別子は,スコープの最も小さいものと なる. したがって他の識別子は見えなくなる. これを識別子の隠蔽と呼ぶ.
int i ; <= この識別子の示すオブジェクトを i0 としよう /* この点では識別子 i は, オブジェクト i0 を参照する */
int main(int argc, char **argv) {
int i ; <= この識別子の示すオブジェクトを i1 としよう /* この点では識別子 i は, オブジェクト i1 を参照する */
{
int i ; <= この識別子の示すオブジェクトを i2 としよう /* この点では識別子 i は, オブジェクト i2 を参照する */
}
/* この点では識別子 i は, オブジェクト i1 を参照する */
}
/* この点では識別子 i は, オブジェクト i0 を参照する */
6.12.2.3.2 関数名のスコープ 関数名は通常はファイル・スコープを持つが, 次のような例では関数宣
言がブロックスコープとなる.
int main(int argc,char **argv) {
int a ;
extern int foo(int) ; foo(a) ;
return 0 ; }
double foo(int a) {
...
}
この場合,関数fooの関数プロトタイプ宣言は,main関数内の関数スコープとなる.
6.12.2.4 寿命
オブジェクトの寿命とは,正しくは記憶域期間(storage duration)とは,プログラム実行状態において, そのオブジェクトの記憶域が存在する期間を指す. Cにおける記憶域期間は静的 (static)と自動(auto) の2種類がある. 寿命という概念は記憶域と関わる概念であるので,識別子に対する概念ではなく,オブジェ クトに対する概念である.
オブジェクトが静的であるとは,プログラム実行の開始から終了までの期間,そのオブジェクトの記憶域 が記憶領域内に存在することをいう. オブジェクトが自動であるとは,プログラム実行中のある期間にのみ, そのオブジェクトの記憶域が記憶領域内に存在することをいう.
オブジェクトが静的かどうかは, その宣言方法に依存する. ファイルスコープを持つと宣言されたオブ ジェクトは,必ず静的である. 一方,ブロックスコープと関数スコープを持つと宣言されたオブジェクトは, デフォールトでは自動であり,その記憶域は,その関数の実行開始から実行終了までで, 記憶領域内に存在 し, その関数の実行期間以外は記憶領域内には存在しない. ブロックスコープと関数スコープを持つオブ ジェクトを静的にするには,記憶クラス指定子staticをつけて宣言する.
なお,関数プロトタイプ宣言内で宣言された仮引数宣言は,定義ではないため,寿命とは無関係である.
Example 6.12.7 オブジェクトの寿命を見てみよう.