第1章:C言語標準規格の読み方
この章で学ぶこと
- C言語標準規格の歴史的進化
- ISO/IEC 9899の構造と読み方
- 規範的要求と参考情報の違い
- shall, should, may の厳密な意味
- 規格文書の入手と活用法
---
1.1 C言語標準規格の歴史
K&R時代(〜1989年)
C言語は1972年にデニス・リッチーによってベル研究所で開発されました。初期のCには公式な規格がなく、1978年に出版された「The C Programming Language」(通称K&R本)が事実上の標準でした。
K&R Cの特徴:
- 関数プロトタイプなし
intの暗黙宣言- 可変長引数のサポートなし
- 厳密な型チェックの欠如
/* K&R スタイル */
main()
{
printf("Hello\n"); /* 宣言なしでも使用可能 */
}
double avg(a, b)
double a, b; /* パラメータの型は後から宣言 */
{
return (a + b) / 2;
}
ANSI C / C89(1989年)
アメリカ規格協会(ANSI)がC言語を標準化し、ANSI X3.159-1989として公開されました。翌年、国際標準化機構(ISO)がISO/IEC 9899:1990として採用しました(内容はほぼ同一)。
C89の主な追加:
- 関数プロトタイプ
void型constとvolatile修飾子- プリプロセッサの標準化
- 標準ライブラリの規定
/* ANSI C スタイル */
#include <stdio.h>
double avg(double a, double b); /* プロトタイプ */
int main(void)
{
printf("%.2f\n", avg(3.0, 5.0));
return 0;
}
double avg(double a, double b)
{
return (a + b) / 2;
}
C99(1999年)
ISO/IEC 9899:1999は、C言語に大幅な機能追加を行いました。
C99の主な追加:
//コメント- 混合宣言と文
- 可変長配列(VLA)
inline関数_Bool型long long int型restrict修飾子- 指示初期化子
- 複合リテラル
,,
/* C99 スタイル */
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
int main(void)
{
// C++スタイルのコメント
bool flag = true;
int32_t fixed = 42;
for (int i = 0; i < 10; i++) { // 混合宣言
int n = 5;
int arr[n]; // VLA
arr[0] = i;
}
struct point { int x, y; };
struct point p = { .y = 10, .x = 20 }; // 指示初期化子
return 0;
}
C11(2011年)
ISO/IEC 9899:2011は、並行処理とセキュリティに焦点を当てました。
C11の主な追加:
_Atomic型修飾子と_Thread_localストレージクラス_Generic選択- 無名構造体・共用体
static_assert- Bounds checking interfaces(Annex K)
- アラインメント指定(
_Alignas,_Alignof) (オプション)
/* C11 スタイル */
#include <stdatomic.h>
#include <stdalign.h>
_Static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
_Atomic int counter = 0;
struct data {
alignas(16) double values[4];
struct { // 無名構造体
int x, y;
};
};
#define print_type(x) _Generic((x), \
int: "int", \
double: "double", \
default: "unknown")
C17/C18(2018年)
ISO/IEC 9899:2018は、C11の欠陥報告を修正した保守版です。新機能は追加されていませんが、多くの曖昧さが解消されました。
C23(2024年)
最新の規格であるISO/IEC 9899:2024は、多くの新機能を追加しました。
C23の主な追加:
typeofとtypeof_unqualconstexprオブジェクト#embedディレクティブ_BitInt(N)型nullptrとnullptr_ttrueとfalseがキーワードにauto型推論- ラベル前の宣言許可
[[]]属性構文
/* C23 スタイル */
#include <stddef.h>
constexpr int SIZE = 100;
auto ptr = nullptr; // auto型推論
typeof(1 + 2.0) x; // double
_BitInt(128) big_int;
[[maybe_unused]] int unused_var;
[[nodiscard]] int important_function(void);
static const unsigned char icon[] = {
#embed "icon.png"
};
---
1.2 規格文書の構造
ISO/IEC 9899の章構成
C言語規格は以下の章から構成されています:
| 章 | タイトル | 内容 | |----|----------|------| | 1 | Scope | 規格の適用範囲 | | 2 | Normative references | 引用規格 | | 3 | Terms, definitions, and symbols | 用語定義 | | 4 | Conformance | 適合性要件 | | 5 | Environment | 実行環境 | | 6 | Language | 言語仕様(核心部分) | | 7 | Library | 標準ライブラリ |
Annexes(付属書)
- Annex A: Language syntax summary(参考)
- Annex B: Library summary(参考)
- Annex C: Sequence points(参考)
- Annex D: Universal character names(規範)
- Annex E: Implementation limits(参考)
- Annex F: IEC 60559 floating point(規範、条件付き)
- Annex G: IEC 60559 complex(規範、条件付き)
- Annex H: Language independent arithmetic(参考)
- Annex I: Common warnings(参考)
- Annex J: Portability issues(参考)
- Annex K: Bounds-checking interfaces(規範、オプション)
- Annex L: Analyzability(規範、条件付き)
規範的(Normative)と参考(Informative)
規範的(Normative):適合実装が遵守すべき要件。本文と規範的付属書が該当します。
参考(Informative):理解を助けるための補足情報。違反しても適合性には影響しません。
規格文書中の注釈:
NOTE — これは参考情報です
EXAMPLE — これも参考情報です
---
1.3 shall, should, may の意味
shall(〜しなければならない)
shall は絶対的要件を示します。適合実装はこれに従う必要があります。
> The value of an object shall not be accessed by an expression of a type other than the following...
「〜してはならない」は shall not で表現されます。
should(〜すべきである)
should は推奨を示します。従うことが強く推奨されますが、絶対ではありません。
> Implementations should diagnose...
may(〜してもよい)
may は許可を示します。実装はそうしてもしなくてもよいです。
> An implementation may define macros...
制約(Constraints)と意味規則(Semantics)
Constraints セクションの shall 違反は、コンパイル時診断が必要です。
Semantics セクションの shall 違反は、未定義動作になります。
/* Constraints 違反の例 */
int arr[5];
arr = arr; /* 診断が必要:配列は代入不可 */
/* Semantics 違反の例 */
int *p = NULL;
*p = 42; /* 未定義動作:NULLの参照外し */
---
1.4 動作の分類
未定義動作(Undefined Behavior)
規格が何の要件も課さない動作。何が起きても構いません。
> undefined behavior: behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this document imposes no requirements
典型例:
- NULLポインタの参照外し
- 配列境界外アクセス
- 符号付き整数オーバーフロー
- 初期化されていない変数の使用
int arr[5];
int x = arr[10]; /* 未定義動作 */
未規定動作(Unspecified Behavior)
規格が複数の選択肢を許容し、どれが選ばれるか規定しない動作。
> unspecified behavior: behavior, that results from the use of an unspecified value, or other behavior upon which this document provides two or more possibilities and imposes no further requirements
典型例:
- 関数引数の評価順序
sizeofが返す値の型(ただしsize_t)
int f(void) { printf("f\n"); return 1; }
int g(void) { printf("g\n"); return 2; }
int x = f() + g(); /* f()とg()の評価順序は未規定 */
処理系定義動作(Implementation-Defined Behavior)
未規定動作の中で、実装が選択肢を文書化する義務があるもの。
> implementation-defined behavior: unspecified behavior where each implementation documents how the choice is made
典型例:
charが符号付きか符号なしかintのビット数- 右シフトが論理か算術か(負の値の場合)
char c = 128; /* charが符号付きなら負の値になる可能性 */
int x = -1 >> 1; /* 結果は処理系定義 */
ロケール固有動作(Locale-Specific Behavior)
現在のロケールに依存する動作。
> locale-specific behavior: behavior that depends on local conventions of nationality, culture, and language
典型例:
isalpha()の判定strcoll()の照合順序
---
1.5 適合実装の要件
ホスト環境と独立環境
ホスト環境(Hosted):OS上で動作し、完全な標準ライブラリを提供。
独立環境(Freestanding):OS不要。最小限のヘッダのみ保証。
独立環境で保証されるヘッダ:
適合プログラム
厳密適合プログラム:
- 規格で指定された機能のみ使用
- 未規定、未定義、処理系定義動作に依存しない
- 処理系のリソース制限を超えない
適合プログラム:
- 適合実装で受け入れられる
- 処理系定義動作への依存は許可
診断メッセージ
適合実装は、構文規則または制約の違反に対して、少なくとも1つの診断メッセージを出力しなければなりません。
int main(void)
{
int x = "hello"; /* 制約違反:診断必須 */
return 0;
}
---
1.6 規格文書の入手方法
公式規格
ISO/IEC 9899は有料(数百スイスフラン)ですが、以下の方法で内容を確認できます。
最終ドラフト
標準化直前のドラフトは無料で公開されています:
最終ドラフトと公式規格の差異は通常、編集上の修正のみです。
欠陥報告(Defect Reports)
規格の解釈や欠陥に関する公式回答は、WG14のウェブサイトで公開されています。
https://www.open-std.org/jtc1/sc22/wg14/
有用なリソース
---
1.7 規格を読むためのヒント
相互参照の活用
規格は内部で多くの相互参照を含んでいます。
> As specified in 6.2.4, an object has a storage duration...
章番号をたどって定義を確認しましょう。
用語の厳密な理解
日常的な意味と技術的な定義が異なることがあります。
例外に注意
「except」や「unless」を見逃さないようにしましょう。
> An identifier declared in different scopes... may refer to the same object... if one of them has external linkage and the other has no linkage.
Annex J.2の活用
Annex J.2には未定義動作の一覧があり、非常に有用です。
---
1.8 この章のまとめ
学んだこと
次の章の予告
次章では、型システムの仕様レベル理解を学びます。整数昇格、通常の算術型変換、互換型など、規格に基づく厳密な定義を解説します。
---
Column: なぜ規格を読むのか
「動くコード」と「正しいコード」
コンパイルが通り、テストが成功するコードが「正しい」とは限りません。
int main(void)
{
int x = INT_MAX;
x = x + 1; /* 未定義動作 */
if (x < 0)
printf("Overflow detected!\n");
return 0;
}
このコードは多くの環境で「動く」かもしれませんが、規格上は未定義動作です。コンパイラは最適化により予期しないコードを生成する可能性があります。
コンパイラは敵ではない
未定義動作を利用した最適化は、コンパイラのバグではありません。規格に従った正当な動作です。
規格を読むことで:
- コードが本当に正しいか判断できる
- 移植性の問題を予見できる
- コンパイラの動作を予測できる
- 効率的で正確なコードを書ける
プロフェッショナルの条件
「この挙動は保証されているのか?」という問いに、規格を参照して答えられることがプロフェッショナルの条件です。
経験や直感だけでなく、規範的な根拠に基づいてコードを書きましょう。
---
確認問題
問題1
C11で追加された機能を3つ挙げてください。解答
以下のいずれか3つ:
_Atomic型修飾子と_Thread_localストレージクラス_Generic選択- 無名構造体・共用体
static_assert- Bounds checking interfaces(Annex K)
- アラインメント指定(
_Alignas,_Alignof)
問題2
「shall」と「should」の違いは何ですか?解答
- shall: 絶対的要件。適合実装は必ず従わなければならない。
- should: 推奨。従うことが強く推奨されるが、絶対ではない。
問題3
未定義動作と処理系定義動作の違いを説明してください。解答
- 未定義動作: 規格が何の要件も課さない。何が起きてもよい。
- 処理系定義動作: 実装が複数の選択肢から1つを選び、文書化する義務がある。
問題4
Constraints セクションの shall 違反はどうなりますか?解答
コンパイル時に診断メッセージが必須。(適合実装は診断を出力しなければならない)
問題5
C11の無料で入手できる最終ドラフトは?解答
N1570(https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf)
---
次の章では、型システムの仕様レベル理解を学びます。