2025/04/17 更新
この記事は、自作OS Advent Calendar 2018の 12/6 の記事として書かれました。
2018 年 12 月現在、C が使われている事例が減っていて、C 固有の事情を知っている必要性に乏しいという
事情もありますが、C++ との非互換の仕様で、あまり知られていない仕様の話を書きます。
私が JIS C99 と呼んでいるのは以下の文書です。
JIS X 3010:2003 プログラム言語C/Programming languages -- C 日本規格協会 JSA Webdesk
https://webdesk.jsa.or.jp/books/W11M0090/index/?bunsyo_id=JIS%20X%203010:2003
JIS C99 規格の「6.9.2 外部オブジェクト定義」の 例1. を読むと、C のグローバル変数は、extern の有無に関係なく
外部結合になることが理解できます。
例1. int i1 = 1;//定義,外部結合
int i1; //正しい仮定義,前の定義を参照する
extern int i1; //外部結合をもつ前の定義を参照する
C の規格では、int i1 = 1; のように初期化子を持つことが定義であることに留意する必要があると思います。
グローバル変数を int i1; と初期値なしで書くのは、仮定義と呼ばれています。仮定義は、同じ翻訳単位に
int i1 = 1; があった場合、初期値 1 のグローバル変数となります。
以下は、仮定義の解説文です。C++ には無い C 独自仕様となっています。
以下の文書では未定義の動作とされているようです。規格の本文には、同じ翻訳単位に定義が無い場合の仮定義は
0 で初期化される定義になる一文があり、未定義の動作という記述は無いのですが、現実の処理系だと、多重定義で
エラーになるので、このあたりが未定義の動作と言われる理由なのかもしれません。
C言語分かってなかった (I Do Not Know C) #翻訳 - Qiita
https://qiita.com/yohhoy/items/960ee7a7b502e5c764b4
回答編
1.
int i;
int i = 10;
問:このコードは正しいですか?(変数が2回定義されたというエラーにならないでしょうか?
これは独立したソースファイルであり、関数本体や複合文(compound statement)の一部でないことを思い出してください。)
答:はい。正しいコードです。1行目は、コンパイラが(2行目の)定義を処理した後に"定義"に変化する、
仮定義(tentative definition)と呼ばれるものです。
(訳注:ISO/IEC 9899:1999では"tentative definition"、対応するJIS X 3010:2003では"仮定義"という用語を用います。
異なるコンパイル単位にint i;とint i = 10;をそれぞれ配置すると、
仮定義ではなくなり未定義動作を引き起こします。仮定義はC言語固有の仕様であり、C++言語には引き継がれていません。)
宣言であれば、その識別子を同じオブジェクトと結び付けますが、同じ翻訳単位に定義が無い場合の仮定義は
定義になるので、リンカによって単一の領域にまとめられず、多重定義でエラーになります。
DCL36-C. 矛盾する結合の種類を使用して識別子を宣言しない
https://www.jpcert.or.jp/sc-rules/c-dcl36-c.html
DCL36-C. Do not declare an identifier with conflicting linkage classifications - SEI CERT C Coding Standard - Confluence
https://wiki.sei.cmu.edu/confluence/display/c/DCL36-C.+Do+not+declare+an+identifier+with+conflicting+linkage+classifications
外部結合: 外部結合の識別子は、プログラム全体(つまり、そのプログラムに属するすべての翻訳単位とライブラリ)で
同じオブジェクトまたは関数を表す。リンカはこの識別子を利用できる。外部結合を持つ同じ識別子が再び宣言されると、
リンカはその識別子を同じオブジェクトまたは関数と結び付ける。
古い処理系では、多重定義でエラーにならず、警告もなく、実行可能ファイルが出力されて実行できました。
原則として処理系を信じてはいけないことが理解できると思います。以下の LLVM 6.0.0(Windows 版 Clang)で確認できます。
LLVM Download Page
https://releases.llvm.org/download.html
~$ cat test1.c
#include <stdio.h>
#include <stdlib.h>
extern int i1 = 1;
int t2();
int main()
{
printf("value = %d\n", t2());
return EXIT_SUCCESS;
}
~$ cat test2.c
extern int i1;
int t2()
{
return i1;
}
GCC では警告が出ますが、これはバグ登録されていて対処されていないものです。
~$ gcc test1.c test2.c
test1.c:5:12: warning: ‘i’ initialized and declared ‘extern’
extern int i = 1;
^
~$ ./a.out
value = 1
GCC で警告が消せない問題が対処されないのは、C の規格で決められた奇妙な振る舞いに、開発者が納得
していないからなのかもしれません。
45977 – "warning: 'i' initialized and declared 'extern'" could use a separate warning flag controlling it
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=45977
Clang では警告を無視するためのオプション -Wno-extern-initializer が指定できます。
~$ clang test1.c test2.c -Wno-extern-initializer
~$ ./a.out
value = 1
以下は、今回使用した環境です。
~$ gcc --version
gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
~$ clang --version
clang version 6.0.0-1ubuntu2 (tags/RELEASE_600/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin