第7章 基本型
はじめに
int型は整数のみを表すことができる型であるから、小数部をもつ実数を表現することができない。そのため、前章までは、実数を扱うときには、double型を利用してきた。
型によって、表現できる数値の特徴や範囲などが変わる。本章では、C言語が提供する数多くの型を学習する。
7-1 基本型と数
本章の目的は、基本的な型をひとつおり学習することである。本節では、そのための基礎知識として、「数」そのものについて学習する。
算術型と基本型
これまで使ってきたint型やdouble型は、加算や減算などの算術演算が適用できることから、算術型(arithmetic type)と呼ばれる。Fig.7-1に示すように、数多くの型の総称である。
Fig.7-1 主要な算術型
この図は、算術型が、次のように分類されることを示している。
- 整数型(integer type) 整数値を表す型 ※ 広義の整数
- 文字型(character type) 文字を表す型
- int系型(int kind type) 整数を表す型 ※ 狭義の整数
- 列挙型(enumeration type) 限られた整数の集合を表す型
- 浮動小数点型(floating type) 実数値を表す型
なお、文字型とint系型と浮動小数点型は、intやdoubleといったキーワードだけで型名を表せるため、基本型(basic type)と呼ばれる。
Note
基本型ではない算術型が、列挙型である(8-3節で学習する)。なお、厳密に区別する必要がない文脈では、列挙型以外の整数型(文字型とint系型)を「整数型」と呼ぶ。
基数
型の学習に入る前に、整数について学習する。
たとえば、1753という数を考える。みなさんが日常生活で使っている、この数値は10進数である(10進数は、10を基数とする数である)。
ところが、信号のONとOFF、すなわち1と0でデータを表現するコンピュータにとっては、10進数よりも、2を基数とする2進数のほうが便利である。
そのため、ハードウェアを制御するようなプログラムでは、数値は2進数で表したほうが都合よくなる。
とはいえ、2進数は桁数が極めて多くなることから、8進数や16進数による表記が多用される。C言語のプログラムで直接扱えるのは、10進数、8進数、16進数である。
Note
Table 7-1に示すのは、10進数の0~20を、8進数/16進数/2進数で表した一覧である。
Table 7-1 数と基数
10進数
10進数では、0、1、2、3、4、5、6、7、8、9の10種類の数字を使う。
これらをすべて使い切った段階で桁が繰り上がって10となる。その後、2桁で10~99を使い切ると、さらに桁が繰り上がって100となる。
8進数
8進数では、0、1、2、3、4、5、6、7の8種類の数字を使う。
これらを使い切ると、桁が繰り上がって10となる。そして、2桁で10~77までを使い切ると、桁が繰り上がって100となる。
16進数
16進数では、0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、Fの16種類の数字を使う。
これらを使い切ると、繰り上がって10となる。そして、2桁で10~FFまでを使い切ると、さらに桁が繰り上がって100となる。
基数変換
次は、異なる基数間で整数値を変換する方法を学習します。
8進数/16進数/2進数 ⇒ 10進数の変換
10進数の各桁は、10のべき乗の重み(最下位桁から順に10⁰、10¹、10²、…の重み)をもっています。そのため、たとえば1998は、次のように解釈できます。
1998 = 1×10³ + 9×10² + 9×10¹ + 8×10⁰
この考え方を他の基数にそのまま適用すると、10進数への変換が行えます。
2進数の101を10進数に変換
101 = 1 × 2² + 0 × 2¹ + 1 × 2⁰ = 1 × 4 + 0 × 2 + 1 × 1 = 5
8進数の123を10進数に変換
123 = 1 × 8² + 2 × 8¹ + 3 × 8⁰ = 1 × 64 + 2 × 8 + 3 × 1 = 83
16進数の1FDを10進数に変換
1FD = 1 × 16² + F × 16¹ + D × 16⁰ = 1 × 256 + 15 × 16 + 13 × 1 = 509
10進数 ⇒ 2進数/8進数/16進数の変換
10進数を他の基数に変換する学習に進む前に、まずは「10進数を10進数に変換する」方法をFig.7-2を見ながら考えていきます。
Fig.7-2 10進数 → 10進数
10進数を10で割った剰余は、最下位桁の値と一致します。たとえば1962を10で割って得られる剰余は、最下位桁の2です。
さて、ここで行った除算1962 / 10の商196は、1962を右に1桁ずらした値です(最後の桁である2は弾き出されています)。すなわち、10進数の値を10で割ることは、その数値を丸ごと右に1桁ずらすことを意味します。
その196を10で割った剰余の6は、下から2桁目の桁の値です。このときの商である19を10で割ると……。
10進数を10で割った剰余を求め、その商に対して除算を繰り返します。その過程で求められた剰余を逆に並べれば、めでたく変換後の10進数が完成します。
この手順における10をnに置きかえれば、「10進数をn進数に変換する」方法となります。
Note
ある数をnで割ることは、n進数で右に1桁ずらすことに相当するからです。
10進数を2進数に変換
Fig.7-3に示すのは、10進数の57を2進数に変換する具体例です。この数を2で割ると、商は28で剰余は1です。
その商である28を2で割ると、商は14で剰余は0です。
この作業を繰り返して、剰余を逆に並べると、変換後の111001が得られます。
Fig.7-3 10進数 → 2進数
10進数を8進数に変換
10進数を8進数に変換する手順も同様です。0になるまで8で割って、その剰余を並べると完成します。
Fig.7-4に示すように、10進数の57を8進数に変換すると71が得られます。
Fig.7-4 10進数 → 8進数
10進数を16進数に変換
16進数に変換する手順も同様です。0になるまで16で割って、その剰余を並べると完成します。
Fig.7-5に示すように、10進数の57を16進数に変換すると39となります。
Fig.7-5 10進数 → 16進数
Column 7-1 2進数と16進数/8進数の基数変換
Table 7C-1に示すように、4桁の2進数は、1桁の16進数に対応します(すなわち、4ビットで表せる0000~1111は、16進数1桁の0~Fです)。
このことを利用すると、「2進数から16進数への基数変換」と「16進数から2進数への基数変換」が容易に行えます。
たとえば、2進数01111010100111100を16進数に変換するには、4桁ごとに区切って、それぞれを1桁の16進数に置きかえるだけです。
16進数から2進数への変換では、逆の作業を行います(16進数の1桁を4桁の2進数に置きかえます)。
なお、2進数と8進数間の変換も同じ手順で行えます(3桁の2進数が1桁の8進数に対応することを利用します)。
Table 7C-1 2進数と16進数の対応
7-2 整数型と文字型
整数を表すための基本的な型が、文字型を含む整数型です。本節では、これらの型について学習します。
整数型と文字型
本節で学習するのは、文字型(character type)を含む整数型(integer type)です。これは、有限範囲の連続した整数を表す型です。
まず、「有限範囲の連続した整数」について考えます。たとえば、《10個の連続した整数》が必要であれば、
の10個を表すときそうです(もちろん、-4から5までなど、他の範囲も考えられます)。
また、非負の数(0と正の数)のみが必要であれば、
の10個を表すときそうです。この場合、表す値の絶対値は、[a]の約2倍になります。
これら2種類は、用途や目的に応じて、自由に使い分けられるようになっていて、次のように呼ばれます。
[a] 符号付き整数型(signed integer type) 負/0/正を表現する整数型
[b] 符号無し整数型(unsigned integer type) 0/正を表現する整数型
どちらを使うのかは、変数を宣言する際に、signedあるいはunsignedの型指定子(type specifier)を置くことで指定します。ただし、型指定子を与えなければ、符号付き型とみなされるのが基本です。次に示すのが、宣言のパターンです。
int x; // xは符号付きint型(負/0/正を表現)
signed int y; // yは符号付きint型(負/0/正を表現)
unsigned int z; // zは符号無しint型(0/正を表現)
Note
xとyの両方がsigned int型となり、zがunsigned int型となります。
さて、符号付き/無しだけでなく、表現可能な値の範囲によっても、型を使い分けられるようになっています。先ほど、10個の数を表す例を考えましたが、何個の数を表すかによって、右に示す5種類の型の使い分けが可能です。
- char
- short int
- int
- long int
- long long int
それぞれに対して符号付き版と符号無し版とが用意されています(ただし、charについては、signed char型と、"単なる" char型が区別されます)。
Note
宣言時にsignedあるいはunsignedの型指定子を与えなければ、符号付き型とみなされる規則は、②のshort int~⑤のlong long intに適用されます(①のcharには適用されません)。
まとめると、整数型はFig.7-6のように分類されます。
Fig.7-6 整数を表す型の分類
Note
上のほうの型は低い、下のほうの型は高い、と表現されます。なお、signedやunsignedと同様に、shortとlongも型指定子の一種です。
char以外の型名は、複数のキーワードで構成されます。名前が長くなってしまうことから、一部は省略可能です。省略した場合、次の規則に基づいて解釈されます。
- signedあるいはunsignedが省略されたint系型は、signedとみなされる。
- 単なるshortとlongとlong longは、intが省略されたものとみなされる。
- 単なるsignedとunsignedは、(shortやlongやlong longでない)intとみなされる。
Table 7-2に示すのが、型名の一覧であり、各行は、同じ型を表します。
Table 7-2 整数型(文字型・int系型)の名前と短縮名
左端が本来の名前であるフルネームで、右端の赤文字が最も短い表記です。これ以降は、最も短い表記を(原則として)使っていきます。
整数型の使い分け
文字型とint系型を使い分けるには、各型で表現可能な数値の範囲(最小値と最大値)を知る必要があります。それをまとめたのが、Table 7-3です。
Table 7-3 整数型(文字型とint系型)で表現できる値の範囲(標準Cで保証された値)
まずは、次の点をおさえます。
Info
int型は、最も取り扱いが容易で、高速な演算が可能な型である。
この後でも学習しますが、long型やlong long型だと、計算速度が遅くなることがありますし、必要な記憶域(メモリ)も大きくなります。そのため、int系型については、次の指針をとるのが一般的です。
- 基本的にはint型を使う。
- int型で表現不能な大きな数値が必要なときは、long型/long long型を使う。
- ハードウェア制御など、0と正値のみが必要とされる状況では、符号無し型を使う。
ヘッダ
さて、Table 7-3に示しているのは、最低限の範囲であって、ほとんどの処理系では、この表よりも広い範囲の値が表現できます。
そのため、各型で表現できる「最小値」と「最大値」が、
右ページに示すのが、その定義の一例です。
本書で想定する
#define UCHAR_MAX 255U // unsigned charの最大値
#define SCHAR_MIN -128 // signed charの最小値
#define SCHAR_MAX +127 // signed charの最大値
#define CHAR_MIN 0 // charの最小値
#define CHAR_MAX UCHAR_MAX // charの最大値
#define SHRT_MIN -32768 // shortの最小値
#define SHRT_MAX +32767 // shortの最大値
#define INT_MIN -32768 // intの最小値
#define INT_MAX +32767 // intの最大値
#define LONG_MIN -2147483648L // longの最小値
#define LONG_MAX +2147483647L // longの最大値
#define LLONG_MIN -9223372036854775807LL // long longの最小値
#define LLONG_MAX +9223372036854775807LL // long longの最大値
#define USHRT_MAX 65535U // unsigned shortの最大値
#define UINT_MAX 65535U // unsignedの最大値
#define ULONG_MAX 4294967295UL // unsigned longの最大値
#define ULLONG_MAX 18446744073709551615ULL // unsigned long longの最大値
これらのマクロの値を調べれば、みなさんが利用している処理系での、各型の数値の範囲が分かります。List 7-1のプログラムで確認しましょう。
実行すると、みなさんの処理系での各型の表現可能な値の範囲が表示されます。
List 7-1
// 整数型の表現範囲を表示する
#include <stdio.h>
#include <limits.h>
int main(void)
{
puts("本環境での整数型の値の範囲");
printf("char : %d~%d\n", CHAR_MIN , CHAR_MAX);
printf("signed char : %d~%d\n", SCHAR_MIN, SCHAR_MAX);
printf("unsignd char : %d~%d\n", 0 , UCHAR_MAX);
printf("short : %d~%d\n", SHRT_MIN , SHRT_MAX);
printf("int : %d~%d\n", INT_MIN , INT_MAX);
printf("long : %ld~%ld\n", LONG_MIN , LONG_MAX);
printf("long long : %lld~%lld\n", LLONG_MIN, LLONG_MAX);
printf("unsigned short : %u~%u\n", 0U , USHRT_MAX);
printf("unsigned : %u~%u\n", 0U , UINT_MAX);
printf("unsigned long : %lu~%lu\n", 0UL , ULONG_MAX);
printf("unsigned long long : %llu~%llu\n", 0ULL , ULLONG_MAX);
return 0;
}
実行結果一例
本環境での整数型の値の範囲
char : 0~255
signed char : -128~127
unsignd char : 0~255
short : -32768~32767
int : -32768~32767
…以下省略…
Note
表示される値は、処理系や実行環境によって異なります。
本書では、各型で表現可能な範囲は、Table 7-4のようになっていると想定して、学習を進めていきます。
Note
すなわち、"単なるchar型"は、符号無し型と想定します(この後すぐに学習します)。
なお、本書で想定しているものを含めて、多くの処理系で、たとえばshortの表現できる範囲は-32768から32767までというように、Table 7-3よりも負の数が一つ多く表現できます(そのような処理系では、p.200で学習する"2の補数"による表現が用いられています)。
文字型
まずは、文字型を学習しましょう。charは、characterの略であって、文字を格納するための型です。
右に示す3種類の型があることや、signedとunsignedが付かない"単なる"char型が、符号付き型/符号無し型のいずれであるのかが処理系依存であることは、簡単に学習しました。
単なるchar型が、どちらの型であるのかを調べてみましょう。List 7-2に示すのが、そのプログラムです(変換指定%sは、文字列を表示することの指定です)。
List 7-2
// 単なるchar型が符号付き型か符号無し型かを判定
#include <stdio.h>
#include <limits.h>
int main(void)
{
printf("この処理系のchar型は%sです。\n",
CHAR_MIN ? "符号付き型" : "符号無し型");
return 0;
}
実行結果一例
Note
実行結果は、処理系や実行環境によって異なります。本書では、符号無し型と想定しています。
規則により、単なるchar型で表せる範囲は、次のいずれかとなります。
a. 単なるchar型が符号付き型であれば、signed char型と同じ範囲。
b. 単なるchar型が符号無し型であれば、unsigned char型と同じ範囲。
Note
[a]の処理系の
Note
[b]の処理系の
本プログラムでは、CHAR_MINの値が0でないかどうかで判定を行っています。
Note
'C'や'\n'などの文字定数がint型であることを、p.86で学習しました。char型ではないことに注意しましょう(文字に関しては、次章以降でも詳しく学習していきます)。
Column 7-2 charの読み方
charは「チャー」と発音するのが一般的です。characterの略であることから、「キャラ」と発音されることもあるようですが、それは、
① 主として日本人特有の
② カタカナ読み
であることを知っておきましょう。
① 略語と同じ綴りの単語がある場合、その単語の発音を借りるのが一般的です。英語には、「雑用」などの意味をもつchar という単語(発音は[tʃɑː])がありますので、その発音を借ります。単語の途中までを発音するのは、非英語圏的な発想です。
② 単語の途中まで発音するとしても、奇異に感じられるのが、「キャラ」のうの発音です。同様の読み方を他の単語にも適用するのでしたら、integerの略であるintは「イント」となって、floatingの略であるfloatは、「フローティ」となってしまいます。
ちなみに、C++の開発者であるBjarne Stroustrup氏のホームページ内の『Bjarne Stroustrup's C++ Style and Technique FAQ(https://www.stroustrup.com/bs_faq2.html)』には、一般に「キャー」でなく「チャー」と発音されると書かれています(当然ですが、「キャラ」にはまったく言及されていません)。その部分を読んでみましょう:
- How do you pronounce "char"? "char" is usually pronounced "tchar", not "kar". This may seem illogical because "character" is pronounced "ka-rak-ter", but nobody ever accused English pronunciation (not "pronunciation" :-) and spelling of being logical.
日本語訳:一般に、"char" は "キャー" ではなく "チャー" と発音します。"character" は "キャ・ラク・タ" と発音しますから、理屈にあわないように感じられるでしょう。しかし、英語の発音("pronunciation" ではなく "pronunciation" と綴る!)と綴りの論理性は、責められるべきものではありません。
補足:「発音する」という動詞pronounceにはoがある一方で、「発音」という名詞pronunciationにはoがありません。
ビットとCHAR_BIT
コンピュータは0と1のビット(bit)でデータを表現しますので、数値を表現する《箱》の内部は、0と1の並びです(ここでの箱は、変数や定数のことです)。
Note
C言語におけるビットの定義は、次のとおりです。
2種類の値のうちの一つをもちうるオブジェクトを保持するために十分な大きさをもつ実行環境でのデータ記憶域の単位。オブジェクトの個々のビットのアドレスを表現できる必要はない。
ビットがもちうる2種類の値のうちの一方を値0という。ビットの値を値0以外にすることを、"ビットをセットする(set)"という。
さて、char型の構成ビット数(箱の中のビット数)は、『最低でも8ビット』とだけ決められており、具体的なビット数は処理系まかせです。
そのため、そのビット数は、
CHAR_BIT
もしCHAR_BITが8であれば、char型の箱は8ビットで構成される、ということです。その場合、charの内部は、Fig.7-7のようになっています。
Fig.7-7 char型の内部
CHAR_BIT個のビットを並べたものがchar型の1個の箱になる、というイメージです。
Note
文字型で表現できる値の範囲が処理系によって異なるのは、構成ビット数が処理系によって異なるからであることが分かりました。
sizeof演算子
C言語では、char型が占有する《1個の箱の大きさ》が1と定義されていて、それ以外の型の大きさは決まっていません。そのため、Table 7-5のsizeof演算子(sizeof operator)で調べられるようになっています。
sizeof演算子が生成する値は、いわゆるバイト数です。この演算子を利用して、整数型の大きさを表示してみましょう。右ページのList 7-3に示すのが、そのプログラムです。
List 7-3
// 整数型の大きさを表示する
#include <stdio.h>
int main(void)
{
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(short) = %zu\n", sizeof(short));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(long) = %zu\n", sizeof(long));
printf("sizeof(long long) = %zu\n", sizeof(long long));
return 0;
}
実行結果一例
実行結果のイメージをFig.7-8に示しています。
Fig.7-8 整数型の大きさと内部の一例
Note
sizeof(char)は必ず1となりますが、それ以外の値は、処理系や実行環境によって異なります。 ここに示す実行例と図は、CHAR_BITが8であり、sizeof(short)とsizeof(int)の両方が2で、sizeof(long)が4の例です(long longの図は省略しています)。
なお、int系型には、次の関係が成立します。
sizeof(short) ≤ sizeof(int) ≤ sizeof(long) ≤ sizeof(long long)
すなわち、右側の高い型の大きさは、左側の低い型と等しいか、より大きくなります。
Note
処理系によっては、四つすべてが同じ大きさになることも(理論上は)あります。
なお、各型の符号付き版と符号無し版の大きさは同一です。
すなわち、右に示す関係が成立します。
sizeof(short) = sizeof(unsigned short) sizeof(int) = sizeof(unsigned) sizeof(long) = sizeof(unsigned long) sizeof(long long) = sizeof(unsigned long long)
size_t型とtypedef宣言
sizeof演算子が生成するのは、符号無し整数型の値です。ただし、shortからlong longまでの4種類の符号無し整数型の、どの型であるのかは定められていません。
そこで取り入れられているのが、どの型であるのかを
size_t
ここで使われているtypedef宣言(typedef declaration)は、初登場です。まずは、このtypedef宣言について、Fig.7-9を例に理解していきましょう。
Fig.7-9 typedef宣言
typedef宣言は、型の別名/同義語/あだ名を作り出す宣言です。typedefの後ろに置かれたAが既存の型名で、その後ろのBが別名です。すなわち、
型Aに対して、Bというあだ名を与えます!
というニュアンスです。
この宣言によって、Bは別名として振る舞えるようになります。なお、新しく作られた名前は、typedef名と呼ばれます。
重要
typedef宣言は、既存の型に対して、新しい別の名前=typedef名を与える。
typedef宣言は、既存の型に別の名前を与える宣言です。決して、新しい型を作るのではありません。
さて、sizeof演算子が生成する値の型は、unsigned short/unsigned/unsigned long/unsigned long longのいずれかの型です。
ある処理系でsizeof演算子が生成する値の型がunsigned型であれば、そのunsigned型に対して、size_t型という別名をtypedef宣言によって与えます(もしunsigned long型であれば、それに対してsize_t型という別名を与えます)。
こうすることによって、どの処理系でも「sizeof演算子が生成するのはsize_t型」と表現できるようになっているのです。
重要
sizeof演算子が生成する値の型は、(いずれかの)符号無し整数型の同義語となるようにtypedef宣言されたsize_t型である。
なお、size_t型の値をprintf関数で出力する際の書式文字列は、"%zu"とします。
さて、sizeof演算子には、次の2種類の使い方があります。
A. sizeof(型名) ※ ()は必須であって省略できない
B. sizeof 式
大きさを調べる対象が型であれば[A]の形式を利用して、変数や定数や、それらを演算子で結んだ式であれば[B]の形式を利用します。
Note
[B]の形式では、式を囲む()は不要です。ただし、文脈によっては紛らわしくなりますので、本書では、両者に対して()を付けています。
これら両方の形式の動作を確かめましょう。List 7-4に示すのが、そのプログラムです。
List 7-4
// 型や変数の大きさを表示
#include <stdio.h>
int main(void)
{
int a, b;
double x, y;
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(double) = %zu\n", sizeof(double));
printf("sizeof(a) = %zu\n", sizeof(a));
printf("sizeof(x) = %zu\n", sizeof(x));
printf("sizeof(a + b) = %zu\n", sizeof(a + b));
printf("sizeof(a + y) = %zu\n", sizeof(a + y));
printf("sizeof(x + y) = %zu\n", sizeof(x + y));
return 0;
}
実行結果一例
sizeof(int) = 2
sizeof(double) = 8
sizeof(a) = 2
sizeof(x) = 8
sizeof(a + b) = 2 • int + intはint
sizeof(a + y) = 8 • int + doubleはdouble
sizeof(x + y) = 8 • double + doubleはdouble
int型とdouble型、それらの変数や計算結果などの大きさを表示しています。
Note
実行によって表示される値は、処理系や実行環境によって異なります。
Example
次に示す各式の値を表示するプログラムを作成するとともに、各式の値を説明せよ。
sizeof 1 sizeof(unsigned)-1 sizeof n+2
sizeof+1 sizeof(double)-1 sizeof(n+2)
sizeof-1 sizeof((double)-1) sizeof(n+2.0)
ここで、nはint型の変数であるとする。
配列の要素数の求め方
sizeof演算子を応用すると、配列の要素数を計算で求められます。List 7-5に示すプログラムで学習していきましょう。
List 7-5
// 配列の要素数を求める
#include <stdio.h>
int main(void)
{
int a[5];
double x[7];
printf("配列aの要素数=%zu\n", sizeof(a) / sizeof(a[0]));
printf("配列xの要素数=%zu\n", sizeof(x) / sizeof(x[0]));
return 0;
}
実行結果
int[5]型の配列aと、double[7]型の配列xが宣言され、それらの要素数が求められて表示されています。配列aの要素数を求める式sizeof(a) / sizeof(a[0])を、Fig.7-10を見ながら理解していきましょう。
Fig.7-10 配列の要素数
左オペランドsizeof(a)
C言語の規則により、sizeof演算子を配列に適用すると、配列全体の大きさが生成されます(Column 10-3:p.293)。
右オペランドsizeof(a[0])
配列の先頭要素a[0]の大きさ、すなわち要素1個分の大きさが得られます。
配列全体の大きさを、要素の大きさで割ることで、配列の要素数が求められます。
重要
配列aの要素数は、次の式で求められる。
sizeof(a) / sizeof(a[0])
この式は、aの要素の型や大きさに依存することなく、要素数を求めます。公式的に覚えておくとよいでしょう。
Note
念のために確認しましょう。sizeof(int)が2の処理系であれば、10 / 2で5が得られますし、sizeof(int)が4の処理系であれば 20 / 4で5が得られます。
なお、charの配列の要素数はsizeof(a)のみで求められます(要素の大きさが1だからです)。
整数型の内部表現
次は、値が格納される箱の中を探っていきます。箱を構成するビットの意味(ビットと値の関係)は、型によって異なります。
整数型の内部で採用されているのは、純2進記数法(pure binary numeration system)という表現法です。
なお、符号無し整数型と符号付き整数型は、表現が異なりますので、まずは符号無し整数型から学習していきます。
Column 7-3 整数型に関する補足
ここでは、整数型に関して補足学習します。
_Bool型
本文で学習した整数型以外に、_Bool型という0あるいは1の値をもつ型があります。 名前の由来はGeorge Booleというイギリスの数学者・哲学者に由来します。C言語以外の多くのプログラミング言語で採用されている、論理型(真偽型)を模したものです。
なお、
拡張整数型
本文で学習した、charからlong longまでの整数型は、標準整数型(standard integer type)と呼ばれます。処理系独自で整数型を追加してもよいことになっており、そのような整数型は拡張整数型(extended integer type)と呼ばれます。
本文では、整数型のビット数が処理系に依存することや、処理系に依存する情報が
この他にも、
- 幅指定整数型
- 最小幅指定整数型
- 最速最小幅指定整数型
- オブジェクトを指すポインタを保持可能な整数型
- 最大幅整数型
- 幅指定整数型の限界値
- 最小幅指定整数型の限界値
- 最速最小幅指定整数型の限界値
- オブジェクトポインタを保持可能な整数型の限界値
- 最大幅整数型の限界値
- 上記以外の整数型の限界値
符号無し整数の内部表現
符号無し整数の内部は、値を2進数で表し、それをそのままビットに対応させたものです。
ここでは、unsigned型の25を例にとって考えてみます。10進数の25を2進数で表すと11001です。そこで、Fig.7-11に示すように、上位側のビットすべてを0で埋めつくした0000000000011001で表現します。
Fig.7-11 16ビットの符号無し整数における整数値25の表現
Note
ここに示すのは、unsigned型が16ビットである処理系の例です。
Note
nビットの符号無し整数の各ビットを、下位側からB₀、B₁、B₂、…、Bₙ₋₁と表すと、そのビットの並びによって表現される整数値は、次の式で得られます。
Bₙ₋₁ × 2ⁿ⁻¹ + Bₙ₋₂ × 2ⁿ⁻² + … + B₁ × 2¹ + B₀ × 2⁰
たとえば、ビット構成が0000000010101011の整数は、
0×2¹⁵ + 0×2¹⁴ + … + 0×2⁸ + 1×2⁷ + 0×2⁶ + 1×2⁵ + 0×2⁴ + 1×2³ + 0×2² + 1×2¹ + 1×2⁰
であり、その値は10進数での171です。
なお、最下位から pos 個上位に位置する Bpos ビットのことを、「第 pos ビット」と呼びます。
整数型が占有する記憶域のビット数は、多くの処理系で、8、16、32、64、…と、8の倍数です。それらの各ビット数で符号無し整数が表現できる最小値と最大値をまとめたのがTable 7-6です。
Table 7-6 符号無し整数の表現範囲の一例
たとえば、unsigned int型が16ビットであれば、0から65535までの65,536種類の数値が表現できます。右ページのFig.7-12に、その数値とビット構成の対応を示しています。
Fig.7-12 16ビットの符号無し整数の値と内部表現のビット
一般に、nビットの符号無し整数で表現できる数値は、0から2ⁿ - 1までの2ⁿ種類です。
Note
これは、n桁の10進数で、0から10ⁿ - 1までの10ⁿ種類を表現できる(たとえば、3桁までの10進数で、0から999までの1,000種類を表現できる)のと同じ道理です。
Column 7-4 負値のビット構成の求め方
次ページでは、負値の表し方として、3種類の内部表現法を学習します。正値のビット構成から、それに対応する負値のビット構成を求める手順は、単純です。
具体例として、正値5のビット構成から、 それに対応する負値-5のビット構成を求める 手順をFig.7C-1に示しています
Fig.7C-1 負値のビット構成の求め方
[A]符号と絶対値
符号ビットを0から1に変更します。それ以外のビットは変化させません。
[B]1の補数
全ビットを反転します。
[C]2の補数
[B]で求めた1の補数に対して1を加算します。
符号付き整数の内部表現
符号付き整数の内部表現には、2の補数表現、1の補数表現、符号と絶対値表現の3種類があり、どれを採用するのかが処理系にゆだねられています。
3種類の表現法の共通点は、Fig.7-13に示すように、最上位ビットで符号を表すことです。
Fig.7-13 符号付き整数の符号ビット
その符号ビットは、数値が負であれば1とし、非負であれば0とします。
符号ビット以外のビットの意味が、内部表現の種類によって異なります。
右ページのFig.7-14を見ながら理解していきましょう。全ビットが0のパターンから、全ビットが1のパターンまでが順に並べられています。
Fig.7-14 16ビットの符号付き整数の値と内部表現のビット
0と正の値を表現するのが[破線で囲んだ部分]です。この範囲の数値内部表現は、3種類の表現法で共通です(さらに、符号無し整数型とも共通です)。
負の値を表現するのが[破線で囲んだ部分]であり、この範囲が表現法によって異なります。
2の補数表現(2's complement representation)
右ページのTable 7-7に示すように、ビット数がnであれば、-2ⁿ⁻¹から2ⁿ⁻¹ - 1までの値を表せる表現法です。
int型(すなわちsigned int型)が16ビットであれば、-32768~32767の65,536種類を表現できます。図[a]の[破線]内の並びは、先頭から順に-32768から-1までに対応します。
Note
この内部表現での値は、次のようになります。
-Bₙ₋₁ × 2ⁿ⁻¹ + Bₙ₋₂ × 2ⁿ⁻² + … + B₁ × 2¹ + B₀ × 2⁰
1の補数表現(1's complement representation)
右ページのTable 7-8に示すように、ビット数がnであれば、-2ⁿ⁻¹ + 1から2ⁿ⁻¹ - 1までの値を表せる表現法です(2の補数表現よりも1個少なくなります)。
そのため、int型が16ビットであれば、-32767~32767の65,535種類の数を表現でき、図[b]の[破線]内の並びは、先頭から順に-32767~-0に対応します。
Note
この内部表現での値は、次のようになります。
-Bₙ₋₁ × (2ⁿ⁻¹-1) + Bₙ₋₂ × 2ⁿ⁻² + … + B₁ × 2¹ + B₀ × 2⁰
符号と絶対値表現(sign and magnitude representation)
表せる数値の範囲は、1の補数表現と同じです(Table 7-8)。図[c]の[破線]内の並びは、先頭から順に-0~-32767となります。
Note
この内部表現での値は、次のようになります。
(1 - 2 × Bₙ₋₁) × (Bₙ₋₂ × 2ⁿ⁻² + … + B₁ × 2¹ + B₀ × 2⁰)
Table 7-7 符号付き整数型の表現範囲の一例(2の補数)
Table 7-8 符号付き整数型の表現範囲の一例(1の補数/符号と絶対値)
Note
1の補数表現と符号と絶対値表現で"-0"としているビットパターンの扱いも、処理系によって異なります(数値の-0とみなさない処理系もあるため、このパターンを使うべきではありません)。
ビット単位の論理演算
第3章では、論理積と論理和という2種類の論理演算を学習しました。整数型の内部を構成する個々のビットに対して論理演算を行うのが、ここで学習するビット単位の論理演算です。
Table 7-9に示すように、4種類の演算子が提供されます。
Table 7-9 ビット単位の論理演算子
Note
これらの演算子のオペランドは、整数型でなければなりません。浮動小数点型などのオペランドに適用するとエラーが発生します。
なお、~演算子には、補数演算子という通称があります。
これらの演算子で行われる論理演算をまとめたのが、Fig.7-15です。この図が示すように、0を偽、1を真とみなして論理演算が行われます(もしint型が16ビットであれば、オペランドの16ビットすべてに対して、論理演算が適用されます)。
Fig.7-15 ビット単位の論理演算
右ページのList 7-6に示すのは、二つの非負の整数を読み込んで、各種のビット単位の論理演算を行った結果を表示するプログラムです。
実行例で表示されるビットの下位4ビットを取り出したのが、Fig.7-16(右ページ)です。全ビットに対して、論理積や論理和などの論理演算が行われている様子が分かります。
なお、関数print_bitsは、整数xの内部の全ビットを0と1で表示する関数であり、関数int_bitsと関数count_bitsは、その下請け・添え物として利用されている関数です。
これらの関数の中では、ビット単位の論理演算子以外にも、二つの演算子>>と<<が使われています。まずは、これらの演算子を理解していきましょう。
Note
本プログラムは、unsigned型のビット数を調べた上で表示を行います。実行例は、unsigned型が16ビットの環境のものです(unsigned型が32ビットの環境で実行すれば、32桁で表示されます)。
List 7-6
// ビット単位の論理演算
#include <stdio.h>
//--- 整数x中のセットされたビット数を返す ---//
int count_bits(unsigned x)
{
int bits = 0;
while (x) {
if (x & 1U) bits++;
x >>= 1;
}
return bits;
}
//--- unsigned型のビット数を返す ---//
int int_bits(void)
{
return count_bits(~0U);
}
//--- unsigned型のビット内容を表示 ---//
void print_bits(unsigned x)
{
for (int i = int_bits() - 1; i >= 0; i--)
putchar(((x >> i) & 1U) ? '1' : '0');
}
int main(void)
{
unsigned a, b;
printf("非負の整数を二つ入力せよ。\n");
printf("a : "); scanf("%u", &a);
printf("b : "); scanf("%u", &b);
putchar('\n');
printf("a = "); print_bits(a); putchar('\n');
printf("b = "); print_bits(b); putchar('\n');
printf("a & b = "); print_bits(a & b); putchar('\n'); // 論理積
printf("a | b = "); print_bits(a | b); putchar('\n'); // 論理和
printf("a ^ b = "); print_bits(a ^ b); putchar('\n'); // 排他的論理和
printf("~a = "); print_bits(~a); putchar('\n'); // aの1の補数
printf("~b = "); print_bits(~b); putchar('\n'); // bの1の補数
return 0;
}
実行結果一例
非負の整数を二つ入力せよ。
a : 1971□
b : 1237□
a = 0000011110110011
b = 0000010011010101
a & b = 0000010010010001
a | b = 0000011111110111
a ^ b = 0000001101100110
~a = 1111100001001100
~b = 1111101100101010
Fig.7-16 ビット単位の論理演算
シフト演算
シフト演算子
<<
演算子(<<演算子)と>>
演算子(>>演算子)は、整数中の全ビットを左または右にシフトした(ずらした)値を生成する演算子である。なお、両者をまとめて、ビット単位のシフト演算子(bitwise shift operator)と呼ぶ(Table 7-10)。
Table 7-10 ビット単位のシフト演算子
演算子 | 書式 | 意味 |
---|---|---|
<<演算子 | a << b | aをbビット左にシフトする。空いたビットには0を埋める。 |
>>演算子 | a >> b | aをbビット右にシフトする。 |
Note
これらの演算子のオペランドは、整数型でなければならない。
シフト演算子の働きを、右ページのList 7-7を例に学習しよう。符号無し整数値を読み込んで、そのビットを左右にシフトした結果を表示するプログラムである。
Info
関数count_bits、関数int_bits、関数print_bitsは、前ページのList 7-6と同じである。スペースの都合上、関数本体部を省略している。
左シフト x << n
式x << n
は、xの全ビットをnビット左にシフトして、右側(下位側)の空いたビットに0を埋める(Fig.7-17 a)。nが符号無し整数型であれば、シフト結果はx × 2^n
である。
Note
2進数は、各桁が2のべき乗の重みをもっているため、左に1ビットシフトすると、オーバフロー(p.213)しない限り、値は2倍になる。これは、10進数を左に1桁シフトすると、値が10倍になる(たとえば、196を左に1桁シフトすると1960になる)のと同じ理屈である。
右シフト x >> n
式x >> n
は、xの全ビットをnビット右にシフトする。xが符号無し整数型であるか、符号付き整数型の非負値であれば、x ÷ 2^n
の商の整数部がシフト結果である(図b)。
Note
2進数を1ビット右にシフトすると、値は1/2になる。これは、10進数を右に1桁シフトすると、値が1/10になる(たとえば、196を右に1桁シフトすると19になる)のと同じ理屈である。
Fig.7-17 非負の整数に対するシフト演算
List 7-7
// List 7-7 chap07/List0707.c
// unsigned型の値を左右にシフトした値を表示
#include <stdio.h>
int count_bits(unsigned x) { /* List 7-6と同じ */ }
int int_bits(void) { /* List 7-6と同じ */ }
void print_bits(unsigned x) { /* List 7-6と同じ */ }
int main(void)
{
unsigned x, n;
printf("非負の整数:"); scanf("%u", &x);
printf("シフトするビット数:"); scanf("%u", &n);
putchar('\n');
printf("整数 = "); print_bits(x); putchar('\n');
printf("左シフト = "); print_bits(x << n); putchar('\n');
printf("右シフト = "); print_bits(x >> n); putchar('\n');
return 0;
}
実行結果一例
注意
シフトの対象が符号付き整数型で、値が負の場合の演算結果は、処理系に依存します(多くの処理系では、Column 7-5に示す論理シフトあるいは算術シフトのいずれかが行われます)。
プログラムの可読性が損なわれるため、負数のシフトは行うべきではありません。
Column 7-5 論理シフトと算術シフト
論理シフト(logical shift) Fig.7C-2 aに示すように、符号ビットを特別に考慮することなく、まるごとシフトする。負の整数値を右にシフトすると、符号ビットが1から0に変わるため、演算結果は、0または正の値になる。
算術シフト(arithmetic shift) 図bに示すように、最上位の符号ビット以外のビットをシフトして、シフト前の符号ビットで空いたビットを埋めつくす。シフト前後で符号が変わることはありません。 1ビット左にシフトすると値が2倍になって、1ビット右にシフトすると値が1/2になる。
Fig.7C-2 負の整数値の論理シフトと算術シフト
ビット単位の論理演算子とシフト演算子の学習がひとところ終わりました。それでは、List 7-6(p.203)の三つの関数を理解していきましょう。
int count_bits(unsigned x); … 整数x中のセットされたビット数を求める
仮引数xに、セットされた("1"である)ビットが何個あるのかをカウントする関数である。
カウントの手順を、Fig.7-18を見ながら理解していきましょう(この図は、xの値が10の場合を示したものである)。
int count_bits(unsigned x)
{
int bits = 0;
while (x) {
if (x & 1U) bits++;
x >>= 1;
}
return bits;
}
-
xと、1U(最下位ビットのみが1の符号無し整数)との論理積を求めることで、xの最下位ビットが1であるかどうかを判定します。判定の結果、最下位ビットが1であればbitsをインクリメントします。
Info
1Uのuは、整数定数を符号無し整数型にする記号です(p.211で学習します)。xの最下位ビットが1であればx & 1Uは1となり、そうでなければx & 1Uは0となります。
-
調べ終わった最下位ビットを弾き出すために、全ビットを1ビット右にシフトします。
Info
>>= は複合代入演算子ですから、x = x >> 1;と同じ働きをします。
以上の作業を、xの値が0になる(xの全ビットが0になる)まで繰り返すと、セットされたビットの個数が変数bitsに入ります。
Fig.7-18 セットされたビットのカウント
int int_bits(void); … int型/unsigned型のビット数を調べる
関数int_bitsは、int型とunsigned型が何ビットで構成されるのかを調べる関数である。
赤色の~0Uは、全ビットが1であるunsigned型整数(全ビットが0である符号無し整数0Uの全ビットを反転したもの)である。
Fig.7-19 ~0Uを得る
全ビットが1である~0Uを関数count_bitsに渡すことによって、unsigned型のビット数を求めていることが分かりました。
Info
なお、unsigned型とint型のビット数は同じです(p.193)。
void print_bits(unsigned x); … 整数xの全ビット構成を表示
関数print_bitsは、unsigned型整数の最上位ビットから最下位ビットまでの全ビットを、1と0の並びとして表示する関数である。
void print_bits(unsigned x)
{
for (int i = int_bits() - 1; i >= 0; i--)
putchar(((x >> i) & 1U) ? '1' : '0');
}
for文のループ本体中の水色の式に着目しよう。これは、第iビットすなわちBiが1かどうかの判定である。その結果が1であれば'1'と表示し、0であれば'0'と表示する(Fig.7-20)。
Fig.7-20 全ビットの表示
ビット単位の論理演算の応用
ビット単位の論理積、論理和、排他的論理和の各演算は、次の用途で利用できます。
- 論理積 :任意のビットをセット(1にする)
- 論理積 :任意のビットをリセット(0にする)
- 排他的論理和:任意のビットを反転(0を1にして1を0にする)
List 7-8のプログラムで確認しましょう。整数値nの最下位ビットを、セット/リセット/反転した値を表示するプログラムである。
List 7-8
// 最下位ビットのセット/リセット/反転
#include <stdio.h>
int count_bits(unsigned x) { /* List 7-6と同じ */ }
int int_bits(void) { /* List 7-6と同じ */ }
void print_bits(unsigned x) { /* List 7-6と同じ */ }
int main(void)
{
unsigned n;
printf("非負の整数:"); scanf("%u", &n);
putchar('\n');
printf("もとの値 = "); print_bits(n); putchar('\n');
printf("セット = "); print_bits(n | 1U); putchar('\n');
printf("リセット = "); print_bits(n & ~1U); putchar('\n');
printf("反転 = "); print_bits(n ^ 1U); putchar('\n');
return 0;
}
実行例
① 非負の整数:7
もとの値 = 0000000000000111
セット = 0000000000000111
リセット = 0000000000000110
反 転 = 0000000000000110
② 非負の整数:6
もとの値 = 0000000000000110
セット = 0000000000000111
リセット = 0000000000000110
反 転 = 0000000000000111
実行例から分かるように、最下位ビットのみが、セット/リセット/反転されていて、それ以外のビットは、もとの値のまま維持されています。
そうなるのは、次のように演算を行っているからです。
最下位ビット | それ以外のビット | |
---|---|---|
セット:1との論理和 | ⇒ 1 | 0との論理和 ⇒ もとの値 |
リセット:0との論理積 | ⇒ 0 | 1との論理積 ⇒ もとの値 |
反転:1との排他的論理和 | ⇒ もとの値の反転 | 0との排他的論理和 ⇒ もとの値 |
Info
本プログラムでは、nに対して次の演算を行って、その結果を表示しています。 - セット:最下位ビットのみが1で、それ以外が0のビットとの論理和をとる演算 - リセット:最下位ビットのみが0で、それ以外が1のビットとの論理積をとる演算 - 反転:最下位ビットのみが1で、それ以外が0のビットとの排他的論理和をとる演算
Example
符号無し整数を左右にシフトした値が、上位ビットが弾き出されない限り、2のべき乗での乗算や除算の演算結果と一致することを確認するプログラムを作成せよ。
Example
符号無し整数xの全ビットを右にnビット回転した値を返す関数rrotateと、左にnビット回転した値を返す関数lrotateを作成せよ。
※回転とは、最下位ビットと最上位ビットがつながっているとみなしてシフトすることである。たとえば右に5ビット回転した場合は、シフトによって弾き出される下位5ビットを上位にもってくる。
Example
符号無し整数xの第posビットを、セットした値を返す関数set、リセットした値を返す関数reset、反転した値を返す関数inverseを作成せよ。
Example
符号無し整数xの第posビットから第pos + n - 1ビットまでのnコのビットを、セットした値を返す関数set_n、リセットした値を返す関数reset_n、反転した値を返す関数inverse_nを作成せよ。
Column 7-6 論理演算子とビット単位の論理演算子
ビット単位の論理演算子&, |, ~と、第3章で学習した論理演算子&&, ||, !は、見かけと働きが中途半端に似ていますので、混同しないようにしましょう。
そもそも、論理演算とは、真(true)と偽(false)の2値に対する演算であり、論理積/論理和/排他的論理和/否定/否定論理積/否定論理和などの演算があります。
ビット単位の論理演算子&, |, ~, -は、オペランドのすべてのビットに対して、1を真、0を偽とみなして論理演算を行う演算子です。一方、論理演算子&&と||は、0以外の値を真、0を偽とみなして論理演算を行う演算子です。
式5 & 4の評価(2進数)と、式5 && 4の評価(真理値)を比べると、違いがはっきりします。
整数定数
整数定数は、3種類の基数で表せます。Fig.7-21に示すのが、その構文図です。
10進定数(decimal constant) これまで使ってきた10や57といった整数定数は、私たちが日常で使う10進数で表されています。これが、10進定数です。
8進定数(octal constant) 8進定数は、10進定数との見分けが付くように、先頭に0を置いて表記します。そのため、右に示す二つの整数定数は、同じように見えても、まったく異なる値です。
16進定数(hexadecimal constant) 16進定数は、先頭に0xまたは0Xを置いて表記します。10進数の10~15に相当するA~Fは、大文字でも小文字でも構いません。 右に示すのが一例です。
Fig.7-21 整数定数の構文図
整数定数の型
p.189の<limits.h>
の定義例では、一部の整数定数に整数接尾語(integer suffix)と呼ばれるUとLの記号が末尾に付いています。整数接尾語は、次の指示です。
- uおよびU ... その整数定数が符号無しであることを明示する。
- lおよびL ... その整数定数がlongであることを明示する。
- llおよびLL ... その整数定数がlong longであることを明示する。
たとえば、3517Uはunsigned型となり、127569Lはlong型となります。
注意
小文字のlは数字の1と見分けが付きにくいため、大文字のLを使うべきです。 ちなみに、負の数-10は、整数リテラルではありません。整数リテラル10に対して、単項-演算子が適用された式です。
整数定数が具体的にどの型となるかの決定には、右に示す三つの要因が関わります。 - その整数定数の値 - その整数定数に付けられた接尾語 - その処理系における各型の表現範囲
その規則をまとめたものが、Table 7-11とTable 7-12です。スタートは左端の型です。左端の型で表現できれば、その型と解釈され、表現できなければ、"⇒"をたどって、一つ右側の型へと進んでいき、表現できれば、その型となります。
Table 7-11 10進定数の接尾語と型
Table 7-12 8進定数/16進定数の接尾語と型
Info
12Lはlong型で、123LLはlong long型です。また、0x13Uはunsigned型です。 1234567は、int型で表現できるのであればint型となり、表現できなければlong型となります(int型でたとえば1234567を表現できない処理系でも、longであれば確実に表現できます)。
整数の表示
前章までは、printf関数に与える変換指定子として、"%d"を中心に使ってきました。dは、符号付き整数型を10進数で表示するための変換指定子です。
符号無し整数型の値を8進数、10進数、16進数で表示するときは、それぞれoとuとx あるいはXを使います。
Info
oはoctalに由来し、uはunsignedに由来し、xはhexadecimalに由来します。xを使うと小文字a~fで表示され、Xを使うと大文字A~Fで表示されます。
なお、表示するのがlong、あるいはlong longである場合は、変換指定子の直前に、lあるいはllの長さ修飾子を置きます。
Example
List 7-9は、0~65535の整数を、10進/2進/8進/16進で表示するプログラムです。
List 7-9
// 0~65535を10進/2進/8進/16進で表示
#include <stdio.h>
int count_bits(unsigned x) { /* List 7-6と同じ */ }
int int_bits(void) { /* List 7-6と同じ */ }
//--- unsigned型整数xの下位nビットを表示 ---//
void print_nbits(unsigned x, unsigned n)
{
int i = int_bits();
i = (n < i) ? n - 1 : i - 1;
for ( ; i >= 0; i--)
putchar(((x >> i) & 1U) ? '1' : '0');
}
int main(void)
{
for (unsigned i = 0; i <= 65535U; i++) {
printf("%5u ", i);
print_nbits(i, 16);
printf(" %06o %04X\n", i, i);
}
return 0;
}
実行結果
0 0000000000000000 000000 0000
1 0000000000000001 000001 0001
2 0000000000000010 000002 0002
3 0000000000000011 000003 0003
...(中略)...
65532 1111111111111100 177774 FFFC
65533 1111111111111101 177775 FFFD
65534 1111111111111110 177776 FFFE
65535 1111111111111111 177777 FFFF
関数print_nbitsは、unsigned型変数xの下位nビットを表示する関数です。本プログラムでは、下位16ビットを表示しています。
Info
関数print_nbitsは、int型のビット数を超えた値が仮引数nに指定された場合、int型の全ビットを表示します。たとえば、int型が16ビットである処理系でnに32が指定されたとしても、表示するビット数は32ではなく16です。
オーバフローと例外
整数型で表現できる値の範囲が有限であることは既に学習しました。演算結果がその範囲を超えてしまったら、どうなるでしょうか。
符号付き整数型:オーバフローによる例外発生
int型で表現できるのが-32768~32767であるとして、右の演算を行ったらどうなるかを考えましょう。
xとyに代入されるのは、いずれもint型で表現可能な値です。ところが、zに代入されるx + yの演算結果50000は、int型の表現範囲を超えます。このように、演算の結果が、オーバフロー(overflow)すなわち桁あふれなどによって、表現可能な値の範囲を超える場合や、0による除算などによって数学的に定義できない場合は、例外(exception)が発生します。
ただし、例外発生時のプログラムの挙動は、処理系に依存します。プログラムの実行が中断されて強制終了する環境が多いようです。
符号無し整数型:オーバフローしない
次は、符号無し整数型を考えます。unsigned型で表現できるのが0から65535であるとして、右の演算を行います。
実行しても、例外が発生することなく、加算結果67000を65536で割った剰余である1464がzに代入されます。符号無し整数の演算結果が表現可能な範囲を超えた場合、その型で表現できる最大値に1を加えた値で割った剰余が演算結果となるという規則があるからです。
!!! example 例えば、次のようになります。 unsignedで表現できる最大値が65535であるとします。 - 数学的な演算結果が65536であれば、プログラムとしての演算結果は0となる。 - 数学的な演算結果が65537であれば、プログラムとしての演算結果は1となる。 - 数学的な演算結果が65538であれば、プログラムとしての演算結果は2となる。
符号無し整数型の演算では、最小値から最大値までの値が順繰りに使われるため、オーバフローによる例外が発生しないことが分かりました。
重要
符号無し整数型での最大値を超える演算結果は、数学的な演算結果 % (その符号無し整数型で表現できる最大値 + 1)となり、オーバフローは発生しない。
Example
符号無し整数に対する算術演算ではオーバフローが発生せず、上記の重要で示した結果となることを確認するプログラムを作成せよ。
7-3 浮動小数点型
前節で学習した整数型は、小数部をもつ実数値を表すことができません。本節では、実数値を表すのに適した型である浮動小数点型について学習します。
浮動小数点型
実数を表す浮動小数点型(floating point type)には、右に示す3種類があります。 - float - double - long double
Info
型名のfloatは浮動小数点(floating-point)に由来し、doubleは2倍の精度(double precision)に由来します。
List 7-10に示すのは、これらの三つの型の変数に数値を入れて表示するプログラムです。
Note
実行によって表示される値は、処理系によって異なります。
List 7-10
// 浮動小数点型の変数の値を表示
#include <stdio.h>
int main(void)
{
float a = 12345678901234567890.0;
double b = 12345678901234567890.0;
long double c = 12345678901234567890.0;
printf("a = %f\n", a);
printf("b = %f\n", b);
printf("c = %Lf\n", c);
return 0;
}
実行結果一例
a = 12345678901827292718644928432.000000
b = 12345678901234677871719597056.000000
c = 12345678901234577871719597056.000000
実行結果から、変数に入れられた数値が正確に表現されていないことが分かります。そうなるのは、浮動小数点型で表す値が、大きさと精度の両方の制限を受けるからです。
このことを、"たとえ話"で説明すると、次のようになります。
大きさとしては12桁まで表すことができ、精度としては6桁が有効である。
数値1234567890を例に考えていきましょう。
この値は10桁ですから、大きさとしては12桁に収まっています。ところが、精度としては6桁に収まっていません。そこで、左から7桁目を四捨五入すると1234570000となります。
これを数学的な形式で表現したのが、Fig.7-22です。
Fig.7-22 指数と仮数
1.23457は仮数と呼ばれ、9は指数と呼ばれます。仮数の桁数が「精度」に相当し、指数の値が「大きさ」に相当します。
Note
これまでは、たとえ話として10進数で考えてきましたが、実際には、仮数部や指数部は2進数で表現されています。そのため、大きさと精度を"12桁"や"6桁"といった具合に、10進整数でピタリ表現することはできません。
浮動小数点数の内部表現は、Fig.7-23のようになっています。
Fig.7-23 浮動小数点数の内部
指数部のビット数が多ければ大きな数値を表せますし、仮数部のビット数が多ければ精度の高い数値を表せます。
指数部と仮数部に対して、具体的に何ビットを割り当てるのかは、処理系と型に依存します。
三つの型float、double、long doubleは、この並びでの左側の型と同等、もしくは、より大きな"表現範囲"をもちます。
本書では、第2章から、double型を中心に使ってきました。浮動小数点型については、次の指針をとるのが一般的です。
- 基本的にはdouble型を使う。
- 記憶域の節約の必要があればfloat型を使う。
- 計算精度が要求されるときはlong double型を使う。
Column 7-7 小数部をもつ2進数
既に学習したように、10進数の各桁は10のべき乗の重みをもっています。このことは、小数部でも成立します。たとえば、10進数の13.25という値を考えましょう。整数部の1は10^1の、3は10^0で、小数部の2は10^-1、5は10^-2の重みをもちます。
2進数も同様です。2進数の各桁は2のべき乗の重みをもちます。そのため、2進数の小数点以下の桁を10進数と対応させると、Table 7C-2に示す関係となります。
Table 7C-2 2進数と10進数
0.5、0.25、0.125、… の和とならない値は、有限桁の2進数では表現できません。
Example
-
有限桁で表現できる例 10進数の0.75 = 2進数の0.11 ※0.75は0.5と0.25の和
-
有限桁で表現できない例 10進数の0.1 = 2進数の0.00011001…
浮動小数点定数
3.14や57.3のような、実数を表す定数が浮動小数点定数(floating-point constant)です。Fig.7-24に示すのが、浮動小数点定数の構文図です(Column 7-8:p.219)。
Fig.7-24 浮動小数点定数の構文図
整数定数の末尾に接尾語UとLを置けるのと同様に、浮動小数点定数の末尾にも型指定のための浮動小数点接尾語(floating suffix)を置けます。
float型を指定するのがfとFであり、long double型を指定するのがlとLです。なお、接尾語を付けなければdouble型となります。例を示します。
注意
小文字のlは数字の1と見分けが付きにくいため、大文字のLを使うべきです(整数接尾語と同様です)。
構文図が示すように、指数を付けた数学的表記が可能です。例を示します。
また、整数部や小数部を省略することもできます。ただし、すべての部分が省略できるわけではありません。構文図をよく読んで理解しましょう。いくつかの例を示します。
Tip
たとえば、小数点.と小数部の両方を省略した場合は、整数部は省略できません。
Example
float型の変数とdouble型の変数とlong double型の変数にキーボードから数値を読み込んで、その値を表示するプログラムを作成せよ。いろいろな値を入力して、動作を検証すること。
ヘッダ
技術計算などをサポートするために、各種の数学関数が用意されています。それらが宣言されているのは、
List 7-11
// 2点間の距離を求める
#include <math.h>
#include <stdio.h>
//--- 点(x1,y1)と点(x2,y2)の距離を求める ---//
double dist(double x1, double y1, double x2, double y2)
{
return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
int main(void)
{
double x1, y1; // 点1
double x2, y2; // 点2
printf("2点間の距離を求めます。\n");
printf("点1···X座標:"); scanf("%lf", &x1);
printf(" Y座標:"); scanf("%lf", &y1);
printf("点2···X座標:"); scanf("%lf", &x2);
printf(" Y座標:"); scanf("%lf", &y2);
printf("距離は%fです。\n", dist(x1, y1, x2, y2));
return 0;
}
実行例
sqrt
ヘッダ #include
Info
平方根とは、平方すると、もとの値に等しくなる数のことです。換言すると、数 aに対して、b^2がaと等しくなるbのことです。なお、float型の引数を受け取ってfloat型を返却するsqrtf関数、long double型の引数を受け取ってlong double型を返却するsqrtl関数も提供されます。
Example
3種類の浮動小数点型の大きさをsizeof演算子で求めて表示するプログラムを作成せよ。
Example
実数値の面積を読み込んで、その面積をもつ正方形の一辺の長さを求めるプログラムを作成せよ。
繰返しの制御
List 7-12に示すプログラムを考えましょう。float型の変数xの値を、0.0から始めて、1.0になるまで0.01ずつ増やしながら表示するプログラムです。
Note
演算結果がfloat型の精度に依存するため、実行結果は処理系によって異なります。
List 7-12
// 0.0から1.0まで0.01単位で繰り返す
#include <stdio.h>
int main(void)
{
for (float x = 0.0; x <= 1.0; x += 0.01)
printf("x = %f\n", x);
return 0;
}
実行結果一例
最後のxの値が、1.0ではなく0.999999となっています(Fig.7-25 a)。これは、浮動小数点数が、すべての桁の情報を失うことなく表現できるとは限らない(Column 7-7:p.215)からです。100回の加算が行われているため、xには100個分の誤差が累積しています。
制御式の関係演算子<=を、等価演算子!=に変更してみましょう("chp07/List0712a.c")。
そうすると、図bに示すように、1.0を通り越してfor文は延々と繰返しを続けます。というのも、xの値がピッタリ1.0にならないからです。
繰返しの制御を整数で行うように変更したプログラムを、右ページのList 7-13に示します。
Fig.7-25 繰返しの過程で表示される値
List 7-13
// 0.0から1.0まで0.01単位で繰り返す(整数で制御)
#include <stdio.h>
int main(void)
{
float x;
for (int i = 0; i <= 100; i++) {
x = i / 100.0;
printf("x = %f\n", x);
}
return 0;
}
実行結果一例
本プログラムのfor文は、変数iの値を0から始めて100までインクリメントしていきます。繰返しの過程では、毎回変数iを100.0で割る除算を行って、その結果をxとします。
もちろん、xが目的とする実数値をピッタリと表現できるわけではありません。しかし、毎回xの値を求め直すわけであり、誤差が累積しないという点で、List 7-12よりも正確です。
重要
可能な限り、繰返しの判定の基準とする変数は、浮動小数点型でなく整数型とする。誤差が累積しないという点で優れている。
Example
List 7-12のように、float型の変数を0.0から1.0まで0.01ずつ増やしていく様子と、List 7-13のように、int型の変数を0から100までインクリメントした値を100.0で割った値を求める様子を、横に並べて表示するプログラムを作成せよ。
Example
List 7-12とList 7-13のそれぞれを書き換えて、0.0から1.0まで0.01ずつ増やした値すべての累計を求めるプログラムを作成せよ。両者の実行結果に対する考察を行うこと。
Column 7-8 16進浮動小数点定数と16進での出力
p.216で学習した浮動小数点定数は、10進表記の定数です。標準Cの第2版からは、浮動小数点定数は、16進表記も行えます。その形式は"0x整数部.小数部P指数部 浮動小数点接尾語"です。整数部と小数部を16進数で表記して、P以降の指数部は2進数で与えます(10進表記とは異なり、指数部は省略できません)。たとえば、0x1.23P1や0xA.FP1Lなどです。
なお、printf関数に与える変換指定子を"%a"あるいは"%A"とすると、浮動小数点数が16進数で出力されます("chap07/floathex.c")。
7-4 演算と演算子
本節では、演算子の優先順位と結合性について学習します。また、C言語が提供する全演算子の一覧と基本的な型変換の規則を示します。
演算子の優先順位と結合性
これまでに、たくさんの演算子を学習してきました。C言語のすべての演算子をまとめたのが、右ページのTable 7-13です。
優先順位
演算子の一覧表は、優先順位(precedence)が高いほうから順に並べています。
たとえば、乗除を行う*と/が、加減を行う+や-より優先順位が高いのは、私たちが日常生活で使用する数学の規則と同じです。そのため、
は、(a + b) * cではなく、a + (b * c)と解釈されます。すなわち、+のほうが先頭側にあるにもかかわらず、優先順位の高い*の演算が優先されます。
結合性
次に、結合性(associativity)について理解しましょう。
たとえば、二つのオペランドを要する2項演算子を○と表した場合、式 a○b○c が
とみなされるのが左結合の演算子であり、
とみなされるのが右結合の演算子です。このように、同じ優先度の演算子が並んでいるときに、左右どちらの演算が結び付けられるのかを示すのが結合性です。
たとえば、減算を行う2項-演算子は左結合ですから、
です。もしも、右結合だったら、5 - (3 - 1)と解釈され、答えも違うものとなってしまいます。 代入を行う単純代入演算子=は右結合ですから、次のようになります。
Table 7-13 演算子の一覧
優先順位 | 演算子 | 形式 | 名称(通称) | 結合性 |
---|---|---|---|---|
() | x(y) | 関数呼出し演算子 | 左 | |
[] | x[y] | 添字演算子 | 左 | |
1 | . | x . y | .演算子(ドット演算子) | 左 |
-> | x -> y | ->演算子(アロー演算子) | 左 | |
++ | x++ | 後置増分演算子 | 左 | |
-- | y-- | 後置減分演算子 | 左 | |
++ | ++x | 前置増分演算子 | 右 | |
-- | --y | 前置減分演算子 | 右 | |
sizeof | sizeof x | sizeof演算子 | 右 | |
2 | & | &x | 単項&演算子(アドレス演算子) | 右 |
* | *x | 単項*演算子(間接演算子) | 右 | |
+ | +x | 単項+演算子 | 右 | |
- | -x | 単項-演算子 | 右 | |
~ | ~x | ~演算子(補数演算子) | 右 | |
! | !x | 論理否定演算子 | 右 | |
3 | () | (x)y | キャスト演算子 | 右 |
4 | * | x * y | 2項*演算子 | 左 |
/ | x / y | /演算子 | 左 | |
% | x % y | %演算子 | 左 | |
5 | + | x + y | 2項+演算子(加算演算子) | 左 |
- | x - y | 2項-演算子(減算演算子) | 左 | |
6 | << | x << y | <<演算子 | 左 |
>> | x >> y | >>演算子 | 左 | |
7 | < | x < y | <演算子 | 左 |
<= | x <= y | <=演算子 | 左 | |
> | x > y | >演算子 | 左 | |
>= | x >= y | >=演算子 | 左 | |
8 | == | x == y | ==演算子 | 左 |
!= | x != y | !=演算子 | 左 | |
9 | & | x & y | ビット単位のAND演算子 | 左 |
10 | ^ | x ^ y | ビット単位の排他OR演算子 | 左 |
11 | | | x | y | ビット単位のOR演算子 | 左 |
12 | && | x && y | 論理AND演算子 | 左 |
13 | || | x || y | 論理OR演算子 | 左 |
14 | ?: | x ? y : z | 条件演算子 | 右 |
15 | = | x = y | 単純代入演算子 | 右 |
+= -= *= /= %= <<= >>= &= ^= |= | x @= y | 複合代入演算子* | 右 | |
16 | , | x , y | コンマ演算子 | 左 |
*複合代入演算子の形式は、すべて x @= y となる。
型変換の規則
第2章では、型変換について簡単に学習しました。詳しい規則を示しますので、必要なときに参照しましょう(本書では学習しない用語なども使われています)。
整数拡張
int型もしくはunsigned int型を使用してもよい式の中では、『それらの型よりも低い整数型のオブジェクトあるいは式』、『_Bool型、int型、signed int型、unsigned int型のビットフィールド』を使用できる。
いずれの場合も、もとの型のすべての値をint型で表現できるなら、値をint型に変換し、それ以外はunsigned int型に変換する。
Note
整数拡張は、符号を含めてその値を変えない。
符号付き整数型と符号無し整数型
整数型の値を、_Bool型以外の他の整数型に変換する場合、その値が変換後の型で表現可能ならば、値は変化しない。
変換後の型で表現できない場合、変換後の型が符号無し整数型であれば、変換後の型で表現できる最大の数に1加えた数を加えることまたは減じることを、新しい型の範囲に入るまで繰り返すことによって得られる値に変換する。
そうでない場合、すなわち、変換後の型が符号付き整数型であって、値がその型で表現できない場合は、結果が処理系定義の値となるか、あるいは、処理系定義のシグナルを生成するかのいずれかとする。
浮動小数点型と整数型
浮動小数点型の値を、_Bool型以外の整数型に型変換する場合、小数部は切り捨てる。整数部の値が整数型で表現できなければ、その動作は定義されない。
変換する値が変換後の型で正確に表現できれば、その値は変わらない。変換する値が表現しうる値の範囲内にあるが正確に表現できないならば、その値より大きく最も近い表現可能な値、あるいは、その値より小さく最も近い表現可能な値のいずれかを処理系定義の方法で選ぶ。変換する値が表現できる値の範囲外にある場合の動作は定義されない。
浮動小数点型
float型をdouble型もしくはlong double型に拡張する場合、また、double型をlong double型に拡張する場合、その値は変化しない。
double型をfloat型に変換する場合、また、long double型をdouble型もしくはfloat型に変換する場合、変換する値が変換後の型で正確に表現できれば、その値は変わらない。変換する値が表現しうる値の範囲内にあるが正確に表現できないならば、その値より大きく最も近い表現可能な値、あるいは、その値より小さく最も近い表現可能な値のいずれかを処理系定義の方法で選ぶ。変換する値が表現できる値の範囲外にある場合の動作は定義されない。
通常の算術型変換
算術型のオペランドをもつ多くの演算子は、同じ方法で、オペランドの型変換を行って結果の型を決める。型変換は、共通の実数型を決めるために行う。この型が結果の型にもなる。これを通常の算術型変換(usual arithmetic conversion)と呼ぶ。通常の算術変換の規則は、次のとおりとする。
a) 一方のオペランドがlong double型であれば、他方のオペランドをlong double型に型変換する。
b) そうでない場合、一方のオペランドがdouble型であれば、他方のオペランドをdouble型に型変換する。
c) そうでない場合、一方のオペランドがfloat型であれば、他方のオペランドをfloat型に型変換する。
d) そうでない場合、整数拡張を両オペランドに対して行い、拡張後のオペランドに次の規則を適用する。
- 両方のオペランドが同じ型をもつ場合、それ以上の型変換は行わない。
- そうでない場合、両方のオペランドが符号付き整数型をもつ、あるいは両方のオペランドが符号無し整数型をもつならば、整数変換順位の低い方の型を、高い方の型に変換する。
- そうでない場合、符号無し整数型をもつオペランドが、他方のオペランドの整数変換順位より高いまたは等しい順位をもつならば、符号付き整数型をもつオペランドを、符号無し整数型をもつオペランドの型に変換する、
- そうでない場合、符号付き整数型をもつオペランドの型が、符号無し整数型をもつオペランドの型のすべての値を表現できるならば、符号無し整数型をもつオペランドを、符号付き整数型をもつオペランドの型に変換する。
- そうでない場合、両方のオペランドを、符号付き整数型をもつオペランドの型に対応する符号無し整数型に変換する。
浮動小数点型のオペランドの値および式の結果の値を、型が要求する精度や範囲を超えて表現してもよい。ただし、それによって型が変わることはない。
まとめ
Info
- 主要な算術型には、次の型がある。
- 整数型(文字型/int系型/列挙型)
-
浮動小数点型 キーワードだけで型名を表せる文字型とint系型と浮動小数点型は、基本型と呼ばれる。
-
整数型は、有限範囲の連続した整数を表現する型である。符号無し型と符号付き型とがあり、いずれにするのかは、signedあるいはunsignedの型指定子で指定する。 なお、これらの型指定子を明示的に与えない場合は、次のようになる。
- int系型:符号付き型とみなされる。
-
文字型:符号付き型となるか符号無し型となるかは処理系に依存する。
-
int系型には、低いほうから順に、short/int/long/long longの4種類がある。最もよく使われるのがint型であり、プログラムの実行環境において、最も扱いやすくて高速な演算が可能である。
-
処理系依存の値は、
ヘッダで、オブジェクト形式マクロとして定義される。 - 各整数型で表現可能な値の下限値と上限値
-
char型が記憶域上に占有するビット数CHAR_BIT
-
char型が占有する大きさは1と定義される。各型の大きさは、sizeof演算子を使うことで求められる。sizeof演算子が生成するのは、
ヘッダでsize_t型として定義された、符号無し整数型の値である。 -
typedef宣言は、型の同義語を作る宣言である。『typedef A B;』は、既存の型Aに対して、別の名前Bを与える。BはtypedeF名と呼ばれ、型名として振る舞うようになる。
-
整数型の値は、純2進記法で表現される。
-
符号無し整数型の値は、値を2進数で表現したものを、そのままビットに対応させた形式で表現される。
-
符号付き整数型の値は、2の補数表現、1の補数表現、符号と絶対値表現のいずれかで表現される。ビット構成は、正値については符号無し整数と同一である。
-
二つの整数型オペランドのビット単位の論理積、論理和、排他的論理和(論理差)を求める2項演算子は、それぞれ&、|、^であり、整数オペランドの1の補数を求める単項演算子は~である。
-
整数型オペランドの内部を左右にずらすシフト演算子は、<<と>>である。負値のシフトは、原則として行うべきではない。論理シフトと算術シフトのいずれで行われるかが処理系に依存するからである。
-
整数定数は、10進定数/8進定数/16進定数の3種類の基数での表記が行える。また、整数定数の末尾には、次の整数接尾語を付加できる。
- uおよびU ... その整数定数が符号無しであることを明示する。
- lおよびL ... その整数定数がlongであることを明示する。
- llおよびLL ... その整数定数がlong longであることを明示する。 なお、整数定数が何型となるかの決定には、次の三つの要因が関わる。
- その整数定数の値
- その整数定数に付けられた接尾語
-
その処理系における各型の表現範囲
-
符号付き整数型のオーバフローでは、多くの場合、例外が発生する。
-
符号無し整数型の値は、値を2進数で表現したものを、そのままビットに対応させた形式で表現される。オーバフローしても、型で表現できる最大値+1で割った剰余が結果となり、例外は発生しない。
-
浮動小数点型には、float/double/long doubleの3種類がある。
-
浮動小数点定数の末尾にも型指定のための浮動小数点接尾語を置ける。
- fおよびF ... その浮動小数点定数がfloat型であることを明示する。
-
lおよびL ... その浮動小数点定数がlong double型であることを明示する。 これらの接尾語を置かなければdouble型となる。
-
繰返しの判定の基準とする変数は、可能な限り、浮動小数点型でなく整数型とする。誤差が累積しないという点で優れている。
-
各演算子は優先順位が異なる。また、同じ優先度の演算子が並んでいるときに、左右どちらの演算が結び付けられるのかを示すのが結合性である。
-
算術型のオペランドをもつ多くの演算子は、同じ方法で、オペランドの型変換を行って結果の型を決める。型変換は、共通の実数型を決めるために行う。この型が結果の型にもなる。これを通常の算術型変換と呼ぶ。
演習問題
演習問題8-1:基本型のサイズと基数変換表示
問題の説明
C言語の基本型のサイズと基数変換について学習するプログラムを作成してください。以下のタスクを実装してください:
- すべての基本型(整数型と浮動小数点型)のサイズをsizeof演算子を用いて表示する
- 0から15までの整数を10進数、2進数、8進数、16進数で表示する
期待される結果
出力例(32ビットシステムの場合):
【タスク1: 型のサイズ確認】
文字型:
sizeof(char) = 1 バイト
sizeof(signed char) = 1 バイト
sizeof(unsigned char)= 1 バイト
整数型:
sizeof(short) = 2 バイト
sizeof(int) = 4 バイト
sizeof(long) = 4 バイト
sizeof(long long) = 8 バイト
符号無し整数型:
sizeof(unsigned short) = 2 バイト
sizeof(unsigned int) = 4 バイト
sizeof(unsigned long) = 4 バイト
sizeof(unsigned long long)= 8 バイト
浮動小数点型:
sizeof(float) = 4 バイト
sizeof(double) = 8 バイト
sizeof(long double) = 16 バイト
【タスク2: 基数変換表示】
10進数 | 2進数 | 8進数 | 16進数
---------|-----------|-------|--------
0 | 00000000 | 000 | 0
1 | 00000001 | 001 | 1
2 | 00000010 | 002 | 2
3 | 00000011 | 003 | 3
4 | 00000100 | 004 | 4
5 | 00000101 | 005 | 5
6 | 00000110 | 006 | 6
7 | 00000111 | 007 | 7
8 | 00001000 | 010 | 8
9 | 00001001 | 011 | 9
10 | 00001010 | 012 | A
11 | 00001011 | 013 | B
12 | 00001100 | 014 | C
13 | 00001101 | 015 | D
14 | 00001110 | 016 | E
15 | 00001111 | 017 | F
ヒント
- sizeof演算子は、型や変数のサイズをバイト単位で返します
- sizeof演算子の結果の型はsize_t型であり、printf関数で出力する際には%zuを使用します
- 2進数表示には独自の関数が必要です(C言語には2進数用の書式指定子がありません)
print_bits関数の実装方法: 1. 関数のプロトタイプ宣言:
- 関数の引数:
unsigned x
: 2進数で表示する整数値-
int bits
: 表示するビット数(8, 16, 32など) -
関数の実装手順:
- ビットを上位(左)から下位(右)へ順に処理
- シフト演算子(>>)で各ビット位置の値を取得
- ビット単位の論理積演算子(&)で該当ビットが1か0かを判定
-
判定結果に応じて '1' か '0' を表示
-
ビット操作の基本:
x >> i
: xをi桁右にシフト(第iビットが最下位に来る)(x >> i) & 1U
: シフト後の最下位ビットを取り出す(1Uは符号無し整数の1)-
条件演算子
? :
で、該当ビットが1なら '1' を、0なら '0' を出力 -
8進数は%o、16進数は%Xの書式指定子を使います
- CHAR_BITマクロを利用するには
をインクルードする必要があります
演習問題8-2:整数のビット表現と平方根計算
問題の説明
整数のビット表現と実数の平方根計算について学習するプログラムを作成してください。以下のタスクを実装してください:
- ユーザーから入力された整数値のビット表現と解釈方法を表示する
- ユーザーから入力された面積から正方形の一辺と円の半径を計算して表示する
期待される結果
入力例1:
出力例1:
【タスク1: 整数値の内部表現】
入力値: 42
ビット表現: 00000000000000000000000000101010
符号付きintとしての値: 42
符号無しunsignedとしての値: 42
この環境でのint型のビット数: 32
【タスク2: 平方根計算】
面積を入力してください: 100
面積 100.00 の正方形の一辺: 10.000000
面積 100.00 の円の半径: 5.641896
入力例2:
出力例2:
【タスク1: 整数値の内部表現】
入力値: -42
ビット表現: 11111111111111111111111111010110
符号付きintとしての値: -42
符号無しunsignedとしての値: 4294967254
この環境でのint型のビット数: 32
【タスク2: 平方根計算】
面積を入力してください: 100
面積 100.00 の正方形の一辺: 10.000000
面積 100.00 の円の半径: 5.641896
ヒント
- 整数のビット表現を表示するには、問題1で作成した関数を応用できます
- int型のビット数は環境によって異なる場合があります(通常は32ビットか64ビット)
- CHAR_BITマクロは、charの1バイトあたりのビット数を定義しています(通常は8)
- 符号付き整数と符号無し整数の解釈の違いに注目しましょう
- 負の整数は2の補数表現で格納されることが多いです
- 平方根計算にはmath.hヘッダのsqrt関数を使用します
- math.hをインクルードする場合、コンパイル時に数学ライブラリをリンクするため、-lmオプションが必要な場合があります
- 円周率πの値は3.14159265358979323846を使用するか、一部の環境ではM_PI定数が利用できます
int_bits関数と平方根計算の実装方法:
- int_bits関数(int型のビット数を求める関数):
- int型のバイト数(sizeof(int))にcharの1バイトあたりのビット数(CHAR_BIT)を掛けることで、int型の総ビット数を求めます
-
CHAR_BITは
で定義されたマクロで、通常は8です -
平方根計算:
- sqrt関数は
で定義されています - コンパイル時には「-lm」オプションを追加してください(例:
gcc -o program program.c -lm
)
演習問題8-3:特定ビットの操作関数
問題の説明
unsigned型の値xの第posビットを、セット(1にする)・リセット(0にする)・反転(0と1を入れ替える)する関数を作成してください。ビット操作の結果を2進数表示と10進数表示の両方で確認してください。
期待される結果
入力例1:
出力例1:
【ビット表示】
元の値 = 0000000000101010
セット = 0000000000101010
リセット = 0000000000101010
反転 = 0000000000111010
【10進数表示】
元の値 = 42
セット = 42
リセット = 42
反転 = 58
入力例2:
出力例2:
【ビット表示】
元の値 = 0000000000101010
セット = 0000000000101010
リセット = 0000000000101000
反転 = 0000000000101000
【10進数表示】
元の値 = 42
セット = 42
リセット = 40
反転 = 40
ヒント
ビット操作関数の実装方法:
- print_bits関数(2進数表示関数):
- 16ビット(2バイト)表示を想定
- 上位ビットから順に処理(i=15から0まで)
-
シフト演算と論理積で各ビットを取得
-
set関数(ビットをセットする関数):
- 使用する演算子: 論理和(OR)演算子
|
1U << pos
: 第posビットだけが1で他が0のビットパターンを作成-
このパターンとxの論理和を取ると、指定したビットだけが1になる
-
reset関数(ビットをリセットする関数):
- 使用する演算子: 論理積(AND)演算子
&
と論理否定(NOT)演算子~
~(1U << pos)
: 第posビットだけが0で他が1のビットパターンを作成-
このパターンとxの論理積を取ると、指定したビットだけが0になる
-
inverse関数(ビットを反転する関数):
- 使用する演算子: 排他的論理和(XOR)演算子
^
1U << pos
: 第posビットだけが1で他が0のビットパターンを作成-
このパターンとxの排他的論理和を取ると、指定したビットだけが反転する
-
ビット位置の注意点:
- ビット位置は0から始まります(最下位ビットが第0ビット)
- 本プログラムでは16ビット表示を想定しているため、ビット位置は0~15の範囲です
- 入力値の範囲チェックを行い、無効な位置が指定された場合はエラーメッセージを表示します
演習問題8-4:整数の各桁の和
問題の説明
入力された整数の各桁の和を求めるプログラムを作成してください。10進数表記の各桁の和と16進数表記の各桁の和の両方を計算して表示してください。
期待される結果
入力例1:
出力例1:
入力例2:
出力例2:
ヒント
各桁の和を求める関数の実装方法:
- sum_of_digits関数(10進数各桁の和関数):
- 関数の戻り値: 各桁の和(int型)
- 引数: 処理対象の非負整数(unsigned型)
-
実装手順:
- 0の特別処理: 0が入力された場合はそのまま0を返す
- 各桁の抽出: 10で割った余り(%10)で最下位桁を取得
- 次の桁への移動: 10で割った商(/10)で次の桁に進む
- 桁の保存: 桁を配列に逆順で格納
- 正順表示: 配列を逆から辿って元の順序で表示
- 和の計算: 各桁を加算して合計を求める
-
sum_of_hex_digits関数(16進数各桁の和関数):
- 関数の戻り値: 各桁の和(int型)
- 引数: 処理対象の非負整数(unsigned型)
-
実装手順:
- 0の特別処理: 0が入力された場合はそのまま0を返す
- 各桁の抽出: 0xFとの論理積(&0xF)で最下位4ビット(16進数1桁)を取得
- 次の桁への移動: 4ビット右シフト(>>4)で次の桁に進む
- 桁の保存: 桁を配列に逆順で格納
- 正順表示: 配列を逆から辿って元の順序で表示(%X指定子で16進数表示)
- 和の計算: 各桁を加算して合計を求める
-
配列の使用:
- 10進数:
int digits[10];
// 十分な大きさの配列 - 16進数:
int digits[8];
// 十分な大きさの配列 - 配列サイズは最大桁数を想定して確保する
-
countで実際に格納した桁数を記録する
-
桁の処理:
- 桁の抽出と次の桁への移動は、表記法によって異なる手法を使う
- 10進数: 除算と剰余算を使用
- 16進数: ビット操作を使用
- 両方とも共通の処理フローで実装できることに注目する