C 配列の型は奇妙です

2026/05/25 14:05

C 配列の型は奇妙です

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

C 言語の中核となる区別は、配列が関数呼び出し中にポインタに退化するメカニズムにあります。通常、これは重要なサイズ情報を失うことを意味しますが、

sizeof
演算子や
&
オペレーターと組み合わせて使用される場合を除きます。関数の引数において、あらゆる配列型は自動的にポインタとして解釈され、サイズ指定子は破棄され、その結果
sizeof(arr)
sizeof(T)
を返します。標準的な慣行では、配列名はその最初の要素へのポインタ(
&arr[0]
)と扱われますが、
sizeof
および
&
のようなオペレーターは元の配列の身元を保持します。特定の実行長が強制されているより短い配列を渡す場合(例:
char buf[static 8]
)、サイズ指定子は呼び出し時にコンパイラの最適化にのみ有用であるため、未定義の動作を引き起こします。これらのリスクを軽減するために、開発者は単なるポインタではなく「配列へのポインタ型(
T (*)[n]
)」を使用して、関数呼び出し中に長さ情報を保持するようにする事ができます。関数は配列と同様にポインタに退化しますが、関数名を解除参照する(
*fn
)ことで、関数シンボルを直接使用せずに直接呼出しを行うことができます。配列は
&arr[0]
によって退化するのに対し、関数は自動的に宣言位置へのアドレスに変換されます(
&fn
fn
と同等です)。両者とも、
&
オペレーターへの引数として渡された場合、退化しません。高度な解決策としては、「ワイドポインタ」を使用する事があり、これは構造体のように動作し、全サイズなどの追加データを格納することで、コンパイラが関数の境界を越えて長さを追跡できるようにします。これは C++ の
std::vector
に準じるものであり、サイズ指定のない型整合性を維持します。GDB などのツールは、アドレス表現に
@
オペレーターを使用し、メモリアドレスに長さを付与することで、場所表現を配列に変換します(例:
*ptr@2
)。配列を単純なポインタの退化ではなく、値ベースの構造体として捉えるというコンセプトを採用する事により、複雑なシステムにおけるコードの安全性とメンタルモデルを著しく向上させる事が可能です。

本文

C 言語の配列とポインタ:考察と改善案

本記事では、C 言語における配列型の振る舞いに関する奇妙な点について解説し、どのように改善すべきかを提示します。また、関連する話題(関数、@ 演算子など)についても考察を加えます。

1. 技術的な定義と「型崩れ」

配列型とポインタ型の違い

  • T[n]
    の意味
    :メモリ上に連続して配置された長さ
    n
    の要素のシーケンスを表します。
  • T*
    との違い
    :明確に異なる別物の型です。

プログラムでの参照不可避性

  • コード上で
    T[n]
    を直接参照することはできません。
  • 配列を持つ式は、瞬時に
    T*
    (最初の要素へのポインタ)に変換
    されます(配列の自動退化)。
  • 配列インデックス演算子
    arr[ix]
    も、実質的には
    *(arr + ix)
    のような挙動として動作します。

例外:
sizeof
オペレータ

配列がポインタに変換されず、サイズ情報を保持する唯一のケースは

sizeof
です。

int arr[3] = {10, 20, 30};
int *arr_ptr = arr;  // 退化したポインタ型

size_t arr_size   = sizeof(arr);    // サイズ:sizeof(T) × 3
size_t ptr_size   = sizeof(arr_ptr);// 固定サイズ:sizeof(T*)
  • sizeof(arr)
    は配列全体のサイズを返します。
  • sizeof(arr_ptr)
    はポインタのサイズ(通常は 4 または 8 バイト)を返します。

「例外中の例外」:関数への渡り渡し

関数の引数リストにおいて、配列型も実際にはポインタとして解釈されます。この際、サイズ

n
は完全に無視されます。

// 関数内部では sizeof(buf) は配列のサイズではなく、
// ポインタのサイズ (sizeof(char*)) を返します。
size_t foo(char buf[6]) {
    return sizeof(buf); 
}

char msg[6] = "!! ??";
size_t msg_size        = sizeof(msg);     // 正解:6 bytes * sizeof(char)
size_t msg_size_in_fn  = foo(msg);        // 誤り:sizeof(char*) (通常 4 or 8 bytes)

サイズ情報を「強制」することはできるか?

  • char buf[static 8]
    などでサイズを記述しても、単に未定義の動作を引き起こすだけです。
  • これもまた、コンパイラの最適化を援助するためのヒント(restrict などと同様)でしかありません。

2. 関数と配列:第二の「退化」型

C 言語には配列に似た振る舞いを持つが混乱を招かない別の型があります。**「関数」**です。

関数の自動変換

  • 関数値も、即時に変換されて関数ポインタとなります。
  • デリファレンスの特殊性:配列とは異なり、関数を参照する変数を
    *fn
    で記述しても、記号そのものを用いて呼び出すことが可能です。
void foo() {}
(*foo)(); // 同義:foo()

アドレス取得の挙動の違い

対象結果の型説明
配列
arr
&arr
T (*)[n]
配列へのポインタ(退化せず)
関数
fn
&fn
typeof(&fn)
fn
と完全に同等
  • 配列
    arr
    &arr[0]
    へ変換されるため、
    &arr
    は「配列全体へのポインタ」になります。
  • 一方、関数
    fn
    は自動的に正確に
    &fn
    に変換されるため、
    &fn
    fn
    は同一です。
  • 注意点:両者とも引数として
    &
    を使うと退化をしません(
    &arr[0]
    のような動作)。

関数の引数定義における同義性

T fn()
T (*fn)()
は同じ意味を持ちます。後者の形式は前者へ自動的に修正されます(配列がポインタに変換されるのと同様)。

3. 「値として渡す配列」への理想と課題

構造体との類似性と現実

  • 配列型は、すべてのメンバーが同一の型を持つ構造体 (struct) に似ています。
  • しかし、構造体の 2 番目のメンバーへのアドレス取得(シフト配列)はめったになされます。
  • 原因:頭部(先頭要素)をシフトさせた配列もサイズを変えつつ配列であり続けるため、サイズ情報を失いがちです。

コピーによる動作

理想的には

char[5]
を関数に渡す際、実際の 5 つの値が渡されるべきです(構造体のような挙動)。 しかし現在の実装ではコピーが行われることがあります。

int compute(int arr[3]) {
    arr[2] += arr[1];
    // ... 計算処理
    return arr[0] - arr[2];
}

int arr[3] = {10, 20, 30};
int result = compute(arr); 
// arr は変更されない(コピーがなされているため)

ポインタ操作の複雑さ

  • 配列へのポインタは間接参照レベルが一つしかかからないため、
    &arr[0]
    を書く必要があります。
void toggle(bool *flag) {
    *flag = !*flag;
}

bool arr[2] = {true, true};
toggle(&arr[1]); // 明示的に & が必要

メリットとデメリット

  • メリット:言語学習を混乱させない。初心者にとって「関数内で書き込むと外部も変更される」という事実への戸惑いが起きにくい。
  • デメリット:配列が毎回コピーされる場合がある(価値を損なわないが使い分けが必要)。
  • 実装面:コンパイラは目的に応じてポインタを使用する選択肢があり、直感的な語法を維持できる可能性があります。

4.
@
演算子とメモリアドレスによる配列構築

GDB の
@
演算子から学ぶ

GDB(デバッグガール)では、アドレスに長さを持たせて配列として扱うための

@
演算子が存在します。

(gdb) print *at_ix_1      // $1 = 20
(gdb) print *at_ix_1@1    // $2 = {20}
(gdb) print *(at_ix_1 + 1)@2 // $4 = {30, 40}

場所表現 (Place Expression) と左辺値

  • *ptr = 2
    はメモリアドレスを持つ式(左辺値)に対して動作します。
  • 2 = 2
    は値そのものなので代入できません。
  • *ptr@10
    は「最初の要素が
    *ptr
    で残り 9 つの要素を持つ配列」を得る意味を持ちます。

C 言語への適用案

この演算子を C に導入すれば、以下のような自然なスライシングが可能になります。

// 配列のスライシング(部分的な切り出し)
int iota[4] = {0, 1, 2, 3};
int one_two[2] = iota[1]@2; // 先頭から 2 要素

// ワイドポインタ的なアプローチ
char arr[4] = {'x', 'y', 'z', 'w'};
char *arr_ptr = &arr[0];
char arr_again[4] = *arr_ptr@4; 

デメリットと残課題

  • 先頭をシフトする際、新しい長さを明示的に指定する必要があります(例:
    &arr[1]@1
    )。
  • サイズ推論のための特殊な構文を導入するには、3 つのアプローチがあります:
    1. arr + 1
      のような記述を許可する。
    2. 特殊な構文(例:
      arr[1]@...
      )で長さを自動推論させる。
    3. 新しいカスタム演算子(例:
      arr +@ 1
      )を導入する。

現時点では C を再設計する必要はないため、具体的な推奨は留保します。

5.
->
演算子の考察と好み

構文の任意性

->
演算子も
@
演算子と同様に、ポインタと場所表現 (lvalue) の扱いは若干任意です。

現状

  • ptr->foo
    (*ptr).foo
    と同じ意味(参照が含まれている)。
  • アドレス取得:
    &ptr->foo
    (定義すべきではなかったかもしれない)。
  • ネスト:
    ptr->foo.bar

代替案 (
ptr->foo->bar
)

  • もし
    ptr->foo
    を明確に「ポインタ」と見なせる構造にすれば、ネストは
    ptr->foo->bar
    と書けます。
  • 個人的な好み
    ptr->foo->bar
    を好みます。
    • 理由:これは「ポインタの領域内で動作しており、コンパイラが単一のオフセットを適用するだけ」であることを反映していると感じるから。
    • 一方で
      ptr->foo.bar
      は場所表現、デリファレンス、アドレス取得の相互作用を反映しています(少し背徳的ですが)。

同じ日のほかのニュース

一覧に戻る →

2026/05/26 5:41

いくつかの興味深い現代風ピクセルフォント

## Japanese Translation: Vercel による Geist Pixel は、新しさ重視のベクトルフォントから、プロフェッショナルな生産環境に適合した堅牢で機能的なタイポグラフィシステムへの転換を象徴する。アンドリュー・グリーソン氏の Analog Mono(低基準線問題を解決)、ジョセフ・ファチュラ氏の Two Slice(読みやすい 2 ピクセル高のベクトルフォント)、および古谷由美氏の Coral Pixels(ノスタルジックなサブピクセルレンダリングによるフレアを包含)など、過去のデザインは特定の美的特徴や歴史的真似に焦点を合わせていたのに対し、Geist Pixel は重要な生産上の課題に取り組む。ビューポート間での一貫したスケーリングを保証し、対立するタイポグラフィ指標を解決するとともに、文字形式以外の領域(キアニング、メタデータ、追加のグリフ、垂直指標など)において「目に見えない本業」として多大な努力を投入している。ユーザー体験を劣化させる可能性のあるリスクの高い新奇品ではなく、Geist Pixel は広範なタイポグラフィエコシステムにおける信頼性の高いシステムツールおよび拡張機能として振る舞う。この進化は、現代的インターフェースに必要な本質的なタイポグラフィ的堅牢性を保ちながら、画面上で本物らしいテクスチャを維持することを可能にする新たな業界標準を確立する。 ## Text to translate: Improved summary: Geist Pixel by Vercel marks a shift from novelty vector fonts to a rigorous, functional typography system built for professional production. Unlike earlier designs—such as Andrew Gleeson’s Analog Mono (fixing low baseline issues), Joseph Fatula’s Two Slice (a 2‑pixel tall readable vector font), and Kumiko Yoshida’s Coral Pixels (incorporating nostalgic subpixel rendering fringing)—which focus on specific aesthetic quirks or historical replication, Geist Pixel addresses critical production challenges. It ensures consistent scaling across viewports, resolves conflicting typographic metrics, and includes significant “invisible hard work” beyond letterforms in areas like kerning, metadata, extra glyphs, and vertical metrics. Rather than being a risky novelty that can degrade user experience, Geist Pixel acts as a reliable system tool and extension within a broader typographic ecosystem. This evolution establishes a new industry standard where pixel fonts maintain authentic visual texture while preserving the essential typographic rigor required for modern interfaces.

2026/05/23 2:17

Adobe と Microsoft を飛び越えてGitで管理する書籍製作パイプラインを作成しました

## 日本語訳: 著者は、新規の形式付けをソフトウェア工学上のタスクとして扱い、Adobe InDesign などの高価なライセンスに依存する脆弱な専用ファイルから、オープンでプレーンテキスト形式のアートファクトへの移行を行うことで、自己出版の自動化を目指している。以前は Microsoft Word と Adobe InDesign を用いて印刷物を制作しており、Calibre を使って Kindle 版への変換を試みても品質が不足していた上、LibreOffice のアップデートにもかかわらず高品質なタイポグラフィを達成できていなかった。今回の移行では LaTeX と自作の Python スクリプトを採用し、電子書籍版および印刷版双方で高品質なテキストを提供すると同時に、Adobe InDesign などの高額ライセンスへの依存度を低減させている。 最も重要な点として、Standard Ebooks のガイドラインを採用することで、厳格なスタイルマニュアルとコマンドラインツールが不可欠な「リンター」として機能し、コードの品質を自動的に検証してデジタル上のエラーを未然に防ぐ。最終出版である『サルデーニャ公(Prince of Savoy)』により、Git を用いたバージョン管理に基づく開発へのピボットが完了した。今後、プロジェクトでは汎用的なスクリプトを活用し、Open Document XML をそのままクリーンな XHTML と LaTeX にマッピングする手法を採用する。この方法は、著者にとって持続可能で再現可能なアプローチを提供し、脆弱なバイナリ形式を意味論的データ構造に置き換えることで、高価なソフトウェアへの依存関係を持たずに長期的な互換性を促進する。

2026/05/26 14:57

予兆的な再会

## Japanese Translation: 学術的な集会で、著者は同世代の多くが大型言語モデル(LLM)による知識労働の人間的側面の喪失に対して広く不安を抱いているのに対し、以前の高齢世代が直面した恐怖とは対照的だと指摘した。この感情は、ウェスリアン大学の工学プロジェクトのために構築され、後にブラウン大学 CS の卒業生アダム・レビエンタールによってメンテナンスされた 1992 年のネットワーク接続型テトリス「BattleTris」の復活という具体的な成功と鮮明な対比を形成していた。長年にわたり、グリッド構成を変更する特定の武器を含むこのレガシーコードベースは、元の 32 ビット Solaris ビルドに影響を与えたことのない現代システム上でクラッシュに見舞われていた。最近、「スパイ」兵器によって開始された試合では、バッファ過負荷によりスタックのスマッushing の検出エラーが発生した:`sendBoard` 関数は 4 バイト(`sizeof(int)`)しか割り当てていないが、8 バイト(`sizeof(unsigned long)`)を書き込み、結果として現代の 64 ビット Linux システム上で 1114 バイトの過負荷を引き起こしていた。 多くの専門家の圈で現在恐れている LLM クロードを使用することで、チームは割り当てと書き込み操作間のこの特定の不一致を特定した。これらの AI の洞察に基づいたターゲットされた修正を適用することで、彼らはゲームを成功裏に移植し再構築し、20 年間クラッシュせずにもう一度元の著者たちにプレイさせることができた。この成功は、LLM が歴史的なデジタルアーティファクトの保存において脅威ではなく有益なパートナーであることを示す強力な証拠であり、現在の不安を引き起こすその技術自体が、複雑なレガシーシステムのデバッグを効果的に支援し、古いプロジェクトの継続的な関連性を確保することを可能にすることを明らかにしている。