大多数のコンピュータは、メモリ内の個々のビットに直接アクセスするのではなく、8ビットのブロック、すなわちバイト(byte)を最小のアドレス可能なメモリ単位として使用します。マシンレベルのプログラムは、メモリを非常に大きなバイト配列と見なします。この配列は仮想メモリ(virtual memory)と呼ばれます。メモリの各バイトは一意の数字、アドレス(address)によって識別され、すべての可能なアドレスの集合が仮想アドレス空間(virtual address space)と呼ばれます。その名の通り、この仮想アドレス空間は、マシンレベルのプログラムに提示される概念的なイメージに過ぎません。実際の実装(第九章参照)は、動的ランダムアクセスメモリ(DRAM)、フラッシュメモリ、ディスクストレージ、特殊ハードウェア、およびオペレーティングシステムソフトウェアを組み合わせて、プログラムに一貫したバイト配列の外観を提供します。
以降の章では、コンパイラとランタイムシステムがメモリ空間をどのようにプログラムオブジェクト(program object)——プログラムデータ、命令、および制御情報——を格納するより管理しやすい単位に分割するかを説明します。プログラムの異なる部分のストレージを割り当てて管理するための様々なメカニズムが存在します。この管理はすべて仮想アドレス空間内で行われます。例えば、C言語におけるポインタの値(整数、構造体、または他のプログラムオブジェクトを指すかどうかに関わらず)は、常に特定のメモリブロックの先頭バイトの仮想アドレスです。Cコンパイラは各ポインタに関連する型情報も保持しており、ポインタ値の型に基づいて、ポインタが指す位置に格納された値にアクセスするための異なるマシンレベルのコードを生成できます。Cコンパイラがこの型情報を保持しているにもかかわらず、生成される実際のマシンレベルのプログラムにはデータ型に関する情報は含まれません。各プログラムオブジェクトは単にバイトブロックと見なすことができ、プログラム自体はバイトのシーケンスです。
C言語初心者向け:C言語におけるポインタの役割
ポインタはC言語の重要な機能です。データ構造(配列を含む)の要素を参照するメカニズムを提供します。変数と同様に、ポインタも値と型の2つの側面を持っています。その値はオブジェクトの位置を表し、その型はその位置に格納されているオブジェクトの型(整数や浮動小数点数など)を表します。
ポインタを真に理解するには、それらがマシンレベルでどのように表現され、実装されているかを調べる必要があります。これは第三章の主なトピックの一つであり、3.10.1節で詳しく説明します。
2.1.1 十六進数表記法
1バイトは8ビットで構成されます。2進数表記では、その値の範囲は0000000011111111です。10進数整数として見れば、その値の範囲は0255です。これらの表記法はビットパターンの記述にはあまり便利ではありません。2進数表記は冗長すぎ、10進数表記はビットパターンとの相互変換が面倒だからです。代わりに、16を基数とする、または十六進数(hexadecimal)と呼ばれる数値でビットパターンを表現します。十六進数("hex"と略記)は、数字09および文字AFを使用して16の可能な値を表します。図2-2は16の十六進数数字に対応する10進数値と2進数値を示しています。十六進数で書くと、1バイトの値の範囲は00~FFとなります。
C言語では、0xまたは0Xで始まる数字リテラルは十六進数の値と見なされます。文字A~Fは大文字でも小文字でも使用できます。例えば、数値FA1D37Bは0xFA1D37B、0xfa1d37b、あるいは0xFa1D37bのように大小文字を混在させて書くことができます。本書では、Cの表記法を使用して十六進数値を表します。
マシンレベルのプログラムを書く際の一般的なタスクは、ビットパターンの10進数、2進数、十六進数表記間の変換を手動で行うことです。2進数と十六進数の間の変換は比較的簡単で、一度に1つの十六進数数字を変換できます。数字の変換は図2-2に示す表を参考にしてください。簡単な覚え方として、十六進数のA、C、Fに対応する10進数値を覚えておくと便利です。B、D、Eを10進数値に変換するには、これらの値と前3つの値との相対関係を計算するだけで済みます。
例えば、数値0x173A4Cが与えられたとします。各十六進数数字を展開して2進数形式に変換できます。
十六進数: 1 7 3 A 4 C 2進数: 0001 0111 0011 1010 0100 1100
これにより、2進数表現0001 0111 0011 1010 0100 1100が得られます。
逆に、2進数1111001010110110110011が与えられた場合、まず4ビットずつに分割して十六進数に変換できます。ただし、ビットの総数が4の倍数でない場合は、最も左のグループは4ビット未満で、先頭を0で埋める必要があることに注意してください。次に、各4ビットグループを対応する十六進数数字に変換します。
2進数: 11 1100 1010 1101 1011 0011 十六進数: 3 C A D B 3
演習問題2.1 次の数字変換を完了してください。
A. 0x39A7F8を2進数に変換する。 B. 2進数1100 1001 0111 1011を十六進数に変換する。 C. 0xD5E4Cを2進数に変換する。 D. 2進数10 0110 1110 0111 1011 0101を十六進数に変換する。
値xが2の非負整数n乗、つまりx=2ⁿである場合、xを十六進数形式で簡単に書くことができます。xの2進数表現が1の後にn個の0が続くことを覚えておくだけです。十六進数の0は4つの2進数0を表します。したがって、nがi+4j(0≤i≤3)の形式で表される場合、xを開始十六進数数字1(i=0)、2(i=1)、4(i=2)、または8(i=3)の後にj個の十六進数0を続けて書くことができます。例えば、x=2048=2^11の場合、n=11=3+4·2であるため、十六進数表現は0x800となります。
演習問題2.2 次の表の空白を埋めて、2の異なるべき乗の2進数と十六進数表現を示してください。
| n | 2^n(10進数) | 2^n(十六進数) |
|---|---|---|
| 9 | 512 | 0x0200 |
| 19 | ||
| 16384 | ||
| 0x10000 | ||
| 17 | ||
| 32 | ||
| 0x80 |
10進数と十六進数の表記間の変換には、一般的なケースを処理するために乗算または除算が必要です。10進数xを十六進数に変換するには、xを16で繰り返し割り、商qと余りrを得ます(x=q·16+r)。次に、rを十六進数数字で表現し、それを最下位数字として使用し、qに対して同じプロセスを繰り返して残りの数字を得ます。例えば、10進数314156の変換を考えてみましょう:
314156 = 19634·16 + 12 (C) 19634 = 1227·16 + 2 (2) 1227 = 76·16 + 11 (B) 76 = 4·16 + 12 (C) 4 = 0·16 + 4 (4)
これから、十六進数表現は0x4CB2Cであることが読み取れます。
逆に、十六進数を10進数に変換するには、各十六進数数字に対応する16のべき乗を乗算します。例えば、数値0x7AFが与えられた場合、対応する10進数値を計算します。
7 · 16^2 + 10 · 16 + 15 = 7 · 256 + 10 · 16 + 15 = 1792 + 160 + 15 = 1967
演習問題2.3 1バイトは2つの十六進数数字で表すことができます。次の表の欠けている項目を埋めて、異なるバイトパターンの10進数、2進数、および十六進数を示してください。
| 10進数 | 2進数 | 十六進数 |
|---|---|---|
| 0 | 0000 0000 | 0x00 |
| 167 | ||
| 62 | ||
| 188 | ||
| 0011 0111 | ||
| 1000 1000 | ||
| 1111 0011 | ||
| 0x52 | ||
| 0xAC | ||
| 0xE7 |
傍注:10進数と十六進数の変換
大きな数値の10進数と十六進数の間の変換は、コンピュータや電卓に任せるのが最も簡単です。この作業を行うための多くのツールがあります。簡単な方法は、標準的な検索エンジンを利用することです。例えば、次のように検索します:
「0xabcdを10進数に変換する」または「123を十六進数で表現する」。
演習問題2.4 数字を10進数または2進数に変換せずに、次の算術問題を解いてください。答えは十六進数で表してください。ヒント:10進数の加算と減算に使用する方法を、基数16に変更するだけです。
A. 0x503c + 0x8 = B. 0x503c - 0x40 = C. 0x503c + 64 = D. 0x50ea - 0x503c =
2.1.2 ワードデータサイズ
各コンピュータには、ポインタデータの公称サイズ(nominal size)を示すワードサイズ(word size)があります。仮想アドレスはこのようなワードでエンコードされるため、ワードサイズが決定する最も重要なシステムパラメータは、仮想アドレス空間の最大サイズです。つまり、wビットのワードサイズを持つマシンの場合、仮想アドレスの範囲は0~2^w - 1であり、プログラムは最大2^wバイトにアクセスできます。
近年、32ビットワードサイズのマシンから64ビットワードサイズのマシンへの大規模な移行が見られました。これは、大規模な科学およびデータベースアプリケーション向けに設計されたハイエンドマシンで最初に登場し、その後デスクトップやラップトップ、最近ではスマートフォンのプロセッサにも広がっています。32ビットワードサイズは仮想アドレス空間を4ギガバイト(4GBと書かれ、4×10^9バイトをわずかに超えます)に制限します。64ビットへの拡張により、仮想アドレス空間は16エクサバイト(EB)になり、約1.84×10^19バイトになります。
ほとんどの64ビットマシンは、32ビットマシン用にコンパイルされたプログラムも実行できます。これは後方互換性です。したがって、例えば、プログラムprog.cが次の擬似命令でコンパイルされた場合:
linux> gcc -m32 prog.c
このプログラムは32ビットまたは64ビットマシンで正しく実行できます。一方、プログラムが次の擬似命令でコンパイルされた場合:
linux> gcc -m64 prog.c
それは64ビットマシンでのみ実行できます。したがって、プログラムを「32ビットプログラム」または「64ビットプログラム」と呼ぶとき、その違いはプログラムがどのようにコンパイルされたかによって決まり、実行されるマシンのタイプではありません。
コンピュータとコンパイラは、整数や浮動小数点数など、様々な方法でエンコードされた数値形式をサポートしています。例えば、多くのマシンには、単一バイトを処理する命令、2バイト、4バイト、または8バイトの整数を表す命令、そして4バイトと8バイトの浮動小数点数をサポートする命令があります。
C言語は整数と浮動小数点数の多种のデータ形式をサポートしています。図2-3は、C言語の様々なデータ型に割り当てられるバイト数を示しています。(2.2節で、C標準が保証するバイト数と典型的なバイト数の関係について説明します。)一部のデータ型の正確なバイト数は、プログラムがどのようにコンパイルされたかによって異なります。ここでは32ビットおよび64ビットプログラムの典型的な値を示します。整数は符号付き(負の数、ゼロ、正の数を表現可能)または符号なし(非負の数のみを表現可能)のいずれかです。Cのデータ型charは単一のバイトを表します。charという名前は、テキスト文字列の単一の文字を格納するために使用されるという事実に由来しますが、整数値を格納するのにも使用できます。データ型short、int、longは様々なデータサイズを提供できます。64ビットシステム用にコンパイルされていても、データ型intは通常4バイトしかありません。データ型longは32ビットプログラムでは通常4バイト、64ビットプログラムでは8バイトです。
「典型的な」サイズや異なるコンパイラ設定に依存することによる奇妙な動作を避けるために、ISO C99は、データサイズが固定されており、コンパイラやマシン設定に依存しない一連のデータ型を導入しました。その中には、それぞれ4バイトと8バイトであるデータ型int32_tとint64_tが含まれます。固定サイズの整数型を使用することは、プログラマーがデータ表現を正確に制御するための最良の方法です。
ほとんどのデータ型は符号付き数値としてエンコードされます。unsignedキーワードが付いていない限り、または固定サイズのデータ型に対して特定の符号なし宣言が使用されない限りです。データ型charは例外です。ほとんどのコンパイラとマシンはそれらを符号付き数値として扱いますが、C標準はこれを保証しません。代わりに、角括弧で示されているように、プログラマーは符号付き文字の宣言を使用して、それが1バイトの符号付き数値であることを保証する必要があります。ただし、多くの場合、プログラムの動作はデータ型charが符号付きか符号なしかによって異なりません。
C言語は、キーワードの順序やオプションのキーワードを含めるか省略するかに関して、複数の形式を許可しています。例えば、次のすべての宣言は同じ意味です:
unsigned long
unsigned long int
long unsigned
long unsigned int
本書では常に図2-3で示されている形式を使用します。
図2-3はまた、ポインタ(例えば「char*」型として宣言された変数)がプログラムの全ワードサイズを使用することを示しています。ほとんどのマシンは、2つの異なる浮動小数点数形式もサポートしています:単精度(Cではfloatとして宣言)と倍精度(Cではdoubleとして宣言)。これらの形式はそれぞれ4バイトと8バイトを使用します。
C言語初心者向け:ポインタの宣言
任意のデータ型Tに対して、宣言:T *p; は、pが型Tのオブジェクトを指すポインタ変数であることを示します。例えば:char *p; は、char型のオブジェクトを指すポインタを宣言します。
プログラマーは、プログラムを異なるマシンやコンパイラで移植可能にするよう努めるべきです。移植性の1つの側面は、プログラムがデータ型の正確なサイズに依存しないようにすることです。C言語標準は、データ型の数値範囲の下限を設定しています(これについては後で説明します)が、上限は設定していません。1980年頃から2010年頃まで、32ビットマシンと32ビットプログラムが主流の組み合わせだったため、多くのプログラムは図2-3の32ビットプログラムのバイト割り当てを想定して書かれていました。64ビットマシンがますます普及するにつれて、これらのプログラムが新しいマシンに移植されると、多くの隠れたワードサイズへの依存性が現れ、エラーになる可能性があります。例えば、多くのプログラマーは、int型として宣言されたプログラムオブジェクトがポインタを格納するために使用できると想定しています。これはほとんどの32ビットマシンでは正常に動作しますが、64ビットマシンでは問題を引き起こします。
2.1.3 アドレッシングとバイトオーダー
複数バイトにまたがるプログラムオブジェクトの場合、メモリ内でこれらのバイトをどのように並べるかを定義する2つのルールが必要です。ほとんどのマシンでは、複数バイトオブジェクトは連続したバイトシーケンスとして格納され、オブジェクトのアドレスは使用されるバイトの中で最小のアドレスです。例えば、int型の変数xのアドレスが0x100であるとします。つまり、アドレス式&xの値が0x100です。データ型intが32ビット表現であると仮定すると、xの4バイトはメモリの0x100、0x101、0x102、0x103の位置に格納されます。
オブジェクトを表すバイトを並べるには、2つの一般的なルールがあります。wビットの整数を考え、そのビット表現を[x(w-1), x(w-2), ..., x1, x0]とします。ここでx(w-1)は最上位ビット、x0は最下位ビットです。wが8の倍数であると仮定すると、これらのビットはバイトにグループ化できます。最上位バイトにはビット[x(w-1), x(w-2), ..., x(w-8)]が含まれ、最下位バイトにはビット[x7, x6, ..., x0]が含まれます。他のバイトには中間のビットが含まれます。一部のマシンは、メモリ内でオブジェクトを最下位バイトから最上位バイトの順序で格納するのを選択し、他のマシンは最上位バイトから最下位バイトの順序で格納します。前者のルール——最下位バイトを先頭にする方法——はリトルエンディアン(little endian)と呼ばれます。後者のルール——最上位バイトを先頭にする方法——はビッグエンディアン(big endian)と呼ばれます。
int型の変数xがアドレス0x100にあり、その16進数値が0x1234567であるとします。アドレス範囲0x100~0x103のバイトの順序はマシンのタイプに依存します:
注意:ワード0x01234567では、最上位バイトの16進数値は0x01で、最下位バイトの値は0x67です。
ほとんどのIntel互換マシンはリトルエンディアンモードのみを使用します。一方、IBMとOracle(2010年にSun Microsystemsを買収して以来)のほとんどのマシンはビッグエンディアンモードで動作します。注意:ここでは「ほとんど」と述べています。これらのルールは企業の境界に厳密に沿っていません。例えば、IBMとOracleが製造するパーソナルコンピュータはIntel互換プロセッサを使用するため、リトルエンディアンを使用します。多くの新しいマイクロプロセッサはバイエンディアン(bi-endian)です。つまり、ビッグエンディアンまたはリトルエンディアンのマシンとして動作するように構成できます。しかし、実際には、特定のオペレーティングシステムが選択されると、バイトオーダーも固定されます。例えば、多くの携帯電話で使用されるARMマイクロプロセッサは、ハードウェアレベルではリトルエンディアンまたはビッグエンディアンの両方のモードで動作できますが、これらのチップで最も一般的な2つのオペレーティングシステム——Android(Google)とiOS(Apple)——はリトルエンディアンモードでのみ動作します。
驚くことに、どのバイトオーダーが適切かという問題では、人々は非常に感情的になります。実際、「little endian」と「big endian」という用語は、Jonathan Swiftの『ガリヴァー旅行記』(Gulliver's Travels)から来ています。この本では、戦う2つの派閥が、半熟の卵をどちらの端(小端か大端か)から割るべきかで合意に至ることができません。卵の問題と同様に、バイトオーダーの選択には技術的な理由はなく、議論は社会政治的な問題に陥ります。ルールを1つ選んで一貫して適用すれば、どのバイトオーダーを選択するかは任意です。
傍注:「端」の起源
Jonathan Swiftが1726年に書いた、大小端の争いの歴史に関する記述は次のとおりです:
「……ここであなたに話すのは、過去36ヶ月間、LilliputとBlefuscuという2つの大国が絶えず戦っていたことです。戦争の原因は次の通りです:私たちは皆、卵を食べる前に、元来の方法は卵の大きな方の端を割ることでしたが、当代の皇帝の祖父が幼い頃、古法で卵を割った際に指を切ってしまったため、当代の皇帝である彼の父は、臣民全員に卵を食べる際に卵の小さな方の端を割るよう命令し、違反者には重罰を科しました。国民はこの命令に非常に反感を持ちました。歴史は、これにより6回の反乱が起こり、そのうち1人の皇帝が命を落とし、もう1人が王位を失ったことを示しています。これらの反乱のほとんどは、Blefuscuの亡命した大臣たちによって扇動されました。反乱が鎮圧されると、亡命者は常にその帝国に避難場所を求めました。11000人が死ぬことを厭わず、卵の小さな方の端を割ることを拒否したと推定されています。この論争に関する数百冊の大部の著作が出版されましたが、大端派の本は常に禁止されており、法律もその派閥の誰もが役職につくことを禁じていました。」(この段落の翻訳は、オンラインの蒋剣鋒訳『ガリヴァー旅行記』第1巻第4章からのものです。)
彼の時代において、Swiftは英国(Lilliput)とフランス(Blefuscu)間の絶え間ない対立を風刺していました。Danny Cohen、ネットワークプロトコルの初期の開拓者の1人、が初めてこれらの用語をバイトオーダーを指すために使用し、その後この用語は広く受け入れられました。
ほとんどのアプリケーションプログラマーにとって、そのマシンが使用するバイトオーダーは完全に見えません。どのタイプのマシンでコンパイルされたプログラムも同じ結果を得ます。しかし、時にはバイトオーダーが問題になります。まず、異なるタイプのマシン間でバイナリデータをネットワーク経由で送信する場合です。一般的な問題は、リトルエンディアンマシンが生成したデータがビッグエンディアンマシンに送信された場合、またはその逆の場合、受信プログラムがワード内のバイトが逆順になっていることに気づくことです。このような問題を避けるために、ネットワークアプリケーションのコードは、送信側マシンが内部表現をネットワーク標準に変換し、受信側マシンがネットワーク標準をその内部表現に変換するよう、確立されたバイトオーダーのルールに従って記述する必要があります。第11章でこの変換の例を見ることになります。
2番目のケースは、整数データを表すバイトシーケンスを読むときにバイトオーダーが重要になる場合です。これは通常、マシンレベルのプログラムを調査する際に発生します。例として、Intel x86-64プロセッサ向けのマシンレベルコードのテキスト表現を示すファイルから次の行を抽出しました:
4004d3: 01 05 43 0b 20 00 add %eax,0x200b43(%rip)
この行は、逆アセンブラ(disassembler)によって生成されたもので、逆アセンブラは実行可能プログラムファイルが表す命令シーケンスを決定するツールです。第3章でこれらのツールに関する詳細と、このような行を解釈する方法について学びます。現時点では、この行が意味するのは、16進数のバイト列01 05 43 0b 20 00が命令のバイトレベルの表現であり、この命令は、値を加算するもので、その値の格納アドレスは0x200b43に現在のプログラムカウンタの値を加えたものです。このシーケンスの最後の4バイト:43 0b 20 00が右辺の数値です。このようなリトルエンディアンマシンで生成されたマシンレベルのプログラム表現を読むとき、バイトを逆の順序で表示することがよくあります。バイトシーケンスを書く自然な方法は、最下位バイトを左に、最上位バイトを右にすることですが、これは通常、数字を書くときに最上位ビットを左に、最下位ビットを右に書く方法と正反対です。
バイトオーダーが重要になる3番目のケースは、通常の型システムを回避するプログラムを記述する場合です。C言語では、キャスト(cast)または共用体(union)を使用して、オブジェクトを参照するデータ型を、そのオブジェクトが作成されたときに定義されたデータ型とは異なるものにすることができます。ほとんどのアプリケーションプログラミングではこのようなコーディングテクニックは強く推奨されませんが、システムレベルのプログラミングでは非常に有用であり、必須です。
図2-4は、キャストを使用して異なるプログラムオブジェクトのバイト表現にアクセスして表示するCコードを示しています。typedefを使用してデータ型byte_pointerを「unsigned char」型のオブジェクトへのポインタとして定義します。このようなバイトポインタは、各バイトが非負の数と見なされるバイトシーケンスを参照します。最初のルーチンprint_byte_sequenceの入力はバイトシーケンスのアドレスであり、バイト数で示されます。このバイト数はデータ型size_tで定義され、データ構造のサイズを表すための推奨データ型です。print_byte_sequenceは、各バイトを16進数で表示します。Cのフォーマット指定子「%.2x」は、整数が少なくとも2桁の16進数形式で出力されることを示します。
プロセスprint_int、print_float、およびprint_pointerは、それぞれint、float、およびvoid*型のCプログラムオブジェクトのバイト表現を出力するためにprint_byte_sequenceを使用する方法を示しています。それらは、単にパラメータxへのポインタ&xをprint_byte_sequenceに渡し、そのポインタが「unsigned char *」にキャストされることに注意してください。このキャストは、コンパイラに、プログラムがこのポインタを元のデータ型のオブジェクトではなくバイトシーケンスを指していると見なすように指示します。次に、このポインタはオブジェクトが使用する最下位バイトアドレスとして見なされます。
これらのプロセスはC言語のsizeof演算子を使用してオブジェクトが使用するバイト数を決定します。一般的に、式sizeof(T)は、型Tのオブジェクトを格納するために必要なバイト数を返します。固定値ではなくsizeofを使用することは、異なるマシンタイプで移植可能なコードを書くための第一歩です。
図2-5に示すコードをいくつかの異なるマシンで実行した結果を図2-6に示します。使用したマシンは次のとおりです:
linux32:Linuxを実行するIntel IA32プロセッサ。 Windows:Windowsを実行するIntel IA32プロセッサ。 Sun:Solarisを実行するSun Microsystems SPARCプロセッサ。(これらのマシンは現在Oracleによって製造されています。) linux64:Linuxを実行するIntel x86-64プロセッサ。
パラメータ12345の16進数表現は0x00003039です。int型のデータの場合、バイトオーダーを除いて、すべてのマシンで同じ結果が得られます。特に、linux 32、Windows、およびlinux 64では、最下位バイト値0x39が最初に出力されるため、これらはリトルエンディアンマシンであることがわかります。一方、Sunでは最後に表示されるため、Sunはビッグエンディアンマシンであることがわかります。同様に、floatデータのバイトも、バイトオーダーを除いて同じです。一方、ポインタ値は完全に異なります。異なるマシン/オペレーティングシステムの構成は異なるストレージ割り当てルールを使用します。注目すべき特徴は、linux 32、Windows、およびSunのマシンが4バイトのアドレスを使用し、linux 64が8バイトのアドレスを使用することです。
C言語初心者向け:typedefを使用してデータ型に名前を付ける
C言語のtypedef宣言は、データ型に名前を付ける方法を提供します。これは、深くネストされた型宣言は読みにくいので、コードの可読性を大幅に改善できます。
typedefの構文は、変数を宣言する構文と非常によく似ていますが、変数名の代わりに型名を使用します。したがって、図2-4のbyte_pointerの宣言は、変数を「unsigned char *」型として宣言するのと同じ形式です。
例えば、宣言:
typedef int *int_pointer; int_pointer ip;は、型「int_pointer」をintへのポインタとして定義し、その型の変数ipを宣言します。この変数を直接次のように宣言することもできます:
int *ip;
C言語初心者向け:printfを使用したフォーマット出力
printf関数(およびその仲間であるfprintfとsprintf)は、フォーマットの詳細に対してかなりコントロールできる方法で情報を印刷する方法を提供します。最初のパラメータはフォーマット文字列(format string)で、残りのパラメータは印刷する値です。フォーマット文字列内で、「%」で始まる各文字列シーケンスは、次のパラメータをどのようにフォーマットするかを示します。典型的な例には、「%d」は10進数整数を出力し、「%f」は浮動小数点数を出力し、「%c」は文字を出力し、そのエンコードはパラメータによって与えられます。
int32_tなどの固定サイズデータ型のフォーマット指定は、より複雑です。関連する内容は、2.2.3節の傍注を参照してください。
浮動小数点数と整数の両方が数値12345をエンコードしているにもかかわらず、それらは全く異なるバイトパターンを持っていることに気づくでしょう:整数は0x00003039で、浮動小数点数は0x4640E400です。一般的に、これらの2つの形式は異なるエンコード方法を使用します。これらの16進数パターンを2進数形式に拡張し、適切にシフトすると、13ビットの一致するビットシーケンスが見つかります。これは偶然ではありません。浮動小数点数の形式を研究するときに、この例に戻ります。
C言語初心者向け ポインタと配列
関数print_byte_sequence(図2-4)で、ポインタと配列の間の密接な関係を見ることができます。これは3.8節で詳しく説明されます。この関数にはbyte_pointer(unsigned charへのポインタとして定義)型のパラメータstartがありますが、第8行で配列参照start[i]が見られます。C言語では、ポインタを参照するために配列表記を使用でき、同時に配列要素を参照するためにポインタ表記を使用できます。この例では、start[i]は、startが指す位置を開始点としてi番目の位置のバイトを読み取ることを意味します。
C言語初心者向け ポインタの作成と間接参照
図2-4の第13、17、21行で、CおよびC++に固有の2つの操作の使用が見られます。Cの「アドレス取得」演算子&はポインタを作成します。これらの3行で、式&xは変数xが保存されている場所へのポインタを作成します。このポインタの型はxの型に依存するため、これらの3つのポインタの型はそれぞれint*、float*、およびvoidです。(データ型voidは、関連する型情報がない特殊なポインタ型です。)
キャスト演算子は1つのデータ型を別のデータ型に変換できます。したがって、キャスト(byte_pointer)&xは、ポインタ&xが以前何であれ、それが今や「unsigned char」型のデータを指しているポインタであることをコンパイラに伝えます。ここに示されたこれらのキャストは、実際のポインタを変更するものではなく、コンパイラに指されているデータを新しいデータ型として見なすように指示するだけです。
傍注:ASCIIテーブルの生成
man asciiコマンドを実行すると、ASCII文字コードのテーブルを取得できます。
演習問題2.5 print_byte_sequenceの3回の呼び出しを考えてみてください:
int val = 0x87654321;
byte_pointer valp = (byte_pointer) &val;
print_byte_sequence(valp,1); /*A.*/
print_byte_sequence(valp,2); /*B.*/
print_byte_sequence(valp,3); /*C.*/
リトルエンディアンマシンとビッグエンディアンマシンで、各呼び出しの出力値を指定してください。
A. リトルエンディアン: ビッグエンディアン: B. リトルエンディアン: ビッグエンディアン: C. リトルエンディアン: ビッグエンディアン:
演習問題2.6 print_intとprint_floatを使用して、整数3510593の16進数表現が0x00359141で、浮動小数点数3510593.0の16進数表現が0x4A564504であることを確認しました。
A. これら2つの16進数値の2進数表現を書き出してください。 B. これらの2つの2進数文字列の相対位置を移動して、一致するビット数が最大になるようにしてください。一致するビット数はいくつですか? C. 文字列のどの部分が一致しませんか?