【C言語】浮動小数点型を自作関数で出力する

はじめに

こんな情報に辿り着いてしまった酔狂なみなさんこんにちは。
私は42Tokyoというプログラマー養成スクールで学んでいる者です。

課題にはありませんが、浮動小数点型の出力について、どんな戦略で解決したかをざっくり説明します。

なお以降の説明ではfloat型(4バイト)を用います。しかし、関数はdouble型(8バイト)で作り始めるのがおすすめです。
*float型の関数を作る必要性がない理由の説明は省略

あと一言、
Don't Trust, Verify.

これが分かればあなたの戦略が立てられる

私がとった戦略を説明する前に、内部表現とは、内部表現の利用方法、簡単な実例を解説します。

この見出しが理解できれば、以降の内容を読まなくてもあなたなりの戦略を作れると思います。

まず内部表現について


wikiから拝借

float型は32ビット(4バイト)で構成されています。
その内訳は、
1ビットの符号部(sign)
8ビットの指数部(exponent)
23ビットの仮数部(fraction)
で計32ビットです。

”内部表現”でつまづいた人用→https://www.youtube.com/watch?v=SUNv3QqwcZw

内部表現をどのように利用するか

では内部表現(ビット)がどのように数値化されるのか?次見出しと合わせて読んでください。

まず、このビットの並びは、2進数を以下の形式で表すことに用いられています(少し回りくどい表現をしました)。

符号部を入れ忘れた…

というのも、実はビットから読み取れる情報は赤色の部分だけです。
青字になっている「2」と「×」と「1.」は内部表現にはでてきません(省略されています)。浮動小数点型とはこういうもの、という決め事の範疇なのでしょう。

  1. 符号部(sign):
    1ビット目の符号部は、数値を画面出力するときに(+)−記号をつけるフラグとして機能しています。1ならマイナスです。

  2. 指数部(exponent):
    2のn乗のn部分にあたる。仮数部のPoint位置を決める値。

  3. 仮数部(fraction):
    指数を適用して標準形式にしたあとで、Pointより左が整数に、右が小数になる。

2, 3の意味は次の実例を見ればわかりやすいと思います。

簡単な実例で内部表現の利用方法を解説

実例に沿って、あなたも2進数から10進数への変換計算を手書きでやってみてほしい

バイアスという表現が唐突に出てきましたが、ここはIEEE745のルールだと思って流して理解でいいかなと思います。

個人的に理解で混乱したところは、exponentはさっさと10進数に変換して利用するのに対して、fractionは2進数のまましばらく利用するところです。いま扱おうのしてるのって2進数?10進数?あれ?みたいな。
※もちろん、2進数をどうやって10進数にするかも大混乱でした

さて、ここまでが理解できれば、あなたなりの戦略を立てることができるはずです

以降はここまでが理解できた前提で、「プログラムでどのように実現するか」の一例です。

私の戦略

以下はポイントポイントで、つまづいた時の参考にでもしてください。

構造体

以降のコードでは構造体変数を利用しているので、まず載せておきます。
説明は省略。

#define SIZEFLOAT_INT 40
#define SIZEFLOAT_DECI 150

//To understand internal representation of float type. use union(share memory type).
typedef union
{
	float			value;
	unsigned int	bit_float;
}	t_ufloat;

typedef struct
{
	char	sign;
	char	exponent[8];
	char	fraction[23];
	int		exp_int; //指数部のn乗部分を数値型にしたもの
	char	integer_bin[129];// 2進数表現でPointより左側(整数になる部分)を格納
	char	decimals_bin[151]; // 同上にPointより右側を格納
	char	rev_integer[SIZEFLOAT_INT]; //整数は逆順に格納した(例:123は321と格納)
	char	nor_decimals[SIZEFLOAT_DECI];
} t_sfloat;

内部表現(ビット)を取得し配列に挿入する


0と1で構成される内部表現

ここまで読み進めた方は、内部表現を取得したくなっているはず。

ここで新たに必要になる知識は「シフト演算子」「1U(double型の場合はちょっと変わる)」「ビット演算子(特に&演算子)」それと「union型」です。

内部表現の取得の概要は

  1. (必要に応じてunion型で目的の数値とunsigned型の数値をメモリ共有する)

  2. unsigned型の数値(変数)と1Uを&演算子で比較して、最下位ビットが1か0かを判定する

  3. シフト演算子で数値をビットシフトする。

  4. 2と3を変数のビット数分くり返す。

ビット表現を取得する方法の参考書籍は、「新・明解C言語 入門編 (明解シリーズ)」の旧版です(新版に載っているかはわからない)。

以下のコードは、内部表現を1ビットづつ取得して、配列に格納する関数です。capture_float_bis関数は第一引数にunsigned型を受け取っていますが、これはunionでメモリを共有しているfloat型の値が元になっています。(unionについては記事後方に補足あり)

//以下2つの関数はビットシフトの理解用.floatが32ビットという結果を返してるだけ
int	count_unint_bits(unsigned x)
{
	int	count;

	count = 0;
	while (x)
	{
		if (x & 1U)
		{
			count++;
			x >>= 1;
		}
	}
	return (count);
}

int	int_bits(void)
{
	return (count_unint_bits(~0U)); //double型の時はここにハマりポイントあり
}

//capture float bits to struct
t_sfloat	*capture_float_bits(unsigned x, t_sfloat *s_fnum)
{
	int	i;
	int	j;
	int	bits;

	i = 0;
	j = 0;
	bits = int_bits();
	while (i < 1)
	{
		if ((x >> (bits - i - 1)) & 1U)
			s_fnum->sign = '1';
		else
			s_fnum->sign = '0';
		i++;
	}
	j = 0;
	while (i < 9)
	{
		if ((x >> (bits - i - 1)) & 1U)
			s_fnum->exponent[j++] = '1';
		else
			s_fnum->exponent[j++] = '0';
		i++;
	}
	j = 0;
	while (i < bits)
	{
		if ((x >> (bits - i - 1)) & 1U)
			s_fnum->fraction[j++] = '1';
		else
			s_fnum->fraction[j++] = '0';
		i++;
	}
	return (s_fnum);
}

指数部(exponent)を10進数にする

指数部は最大でも「11111111」なので、10進数に直すと255までしかない。
配列の中身が0か1かを読み取り、その時の配列の位置に応じてn乗する関数n乗した値を足し合わせる関数を作りました。

そして結果にバイアス(−127)を加える。最終的な結果はint型にでも格納する。

//Convert binary array(that means integer) to decimal integer.
size_t	binary_to_size(char *binstr, size_t len_binstr)
{
	int		i;
	int		result;

	result = 0;
	i = 0;
	while (len_binstr--)
	{
		if (binstr[len_binstr] == '1')
			result = result + power_num(2, i);
		i++;
	}
	return(result);
}
//構造体の以下変数に保存しました
	int		exp_int; //指数部のn乗部分を数値型にしたもの

仮数部に指数をかけて標準形式にして配列へ保存

さて指数部を10進数にした結果が得られたので、仮数部にかけ(乗算し)ましょう。
ポイントは、仮数部は「1.」が省略されていることです。

例えば、
①仮数部の配列が「0101110」、指数部(10進数)が「5」なら、標準形式にした結果は「101011.10」です。
②仮数部の配列が「0101110」、指数部(10進数)が「-3」なら、標準形式にした結果は「0.0010101110」です。

以前の説明でいうこの部分

10進数への変換説明は次項で行いますが、少し補足しておきます。
①の場合は「101011」を10進数整数に、「0.10」を10進数小数にそれぞれ変換します。②の場合も同様ですが整数部分は「0」なので、10進数に変換しても0ですね。

私は、得られた結果のPointより左と右をそれぞれ配列に保存しました。
左は128、右は150が最大必要になるはずです。(nan, infになる指数部の値0、255を除外、かつ末端NULL分を考慮しない場合)

//構造体の以下変数に格納しました。
	char	integer_bin[129];// 2進数表現でPointより左側(整数になる部分)を格納
	char	decimals_bin[150]; // 同上にPointより右側を格納

Pointより左側を10進数整数にする

さて、10進数への変換はちょっとめんどくさかったです。
「考えつかなくはないけどめんどくさい方法」でやりました。

float型で表せる整数部分の最大値は39桁もあります。
計算過程は以下です。
(2進数) 2^127 * 1.11111111111111111111111
= (10進数) 2^127 * 1.9999999999999997779554
= (10進数) 3.0428… * 10^39

数値型で最大の値を格納できるlong long型(10^18)でも足りません。
なので10進数に変換した整数値はchar配列に格納しました。
(*変換の過程も数値型は利用せずchar配列でした)

このとき、例えば2進数で「11000(10進数では24)」が与えられたときには、「10000(10進数では2^4 = 16)」+「1000(〃 2^3 = 8)」というように分けて計算するようにしました。

つまり、以下2つの関数を準備しました。
・2のn乗(最大はn=127)の結果を配列に格納して返却する関数(全ての計算過程をchar配列で行う) →上記の例でいう16や8が得られる
・別の配列に格納された数字どうしを加算する関数 → 上記の例でいう16と8を加算して24を得る
これでやっと整数部分が出力できる状態になります。

余談ですが10進数に変換する計算過程では、配列の格納は逆順(つまり16という数字は「61」の順)で配列に格納しました。
これは、個人的に配列形式での数字計算をやりやすくするためです。

//2のn乗の結果を配列に格納して返却する関数。結果は逆順になっています。
//return power of 2. Result is provided by reversed char array.
char	*power_2_revary(char *result, size_t power)
{
	int	i;
	int	flag;
	int	temp;

	result[0] = '1';
	result[1] = '\0';
	if (power == 0)
		return (result);
	while (power--)
	{
		i = 0;
		flag = 0;
		while (result[i])
		{
			temp = (result[i] - '0') * 2;
			result[i] = (temp % 10) + '0';
			if (flag == 1)
			{
				result[i] += 1;
				flag = 0;
			}
			if (temp > 9)
				flag = 1;
			i++;
		}
		if (temp > 9)
		{
			result[i] = (temp / 10) + '0';
			result[i + 1] = '\0';
		}
		else
			result[i] = '\0';
	}
	return (result);
}
//2つの数字を加算した最終的な結果は、構造体の以下変数に保存しました。
	char	rev_integer[SIZEFLOAT_INT];

Pointより右側を10進数小数にする

整数部分と同じように、小数も「考えつかなくはないけどめんどくさい方法」でやりました。

・2の-n乗(最大はn=126)の結果を配列に格納して返却する関数
・配列に格納された数字どうしを加算する関数

//Return power of 1/2(zero point five). Result is only after zero point(not include 0.).
char	*power_point5_ary(char *result, size_t power)
{
	int	i;
	int	flag;
	int	temp;

	if (power == 0)
		return (NULL);
	result[0] = '5';
	result[1] = '\0';
	if (power == 1)
		return (result);
	power--;
	while (power--)
	{
		i = 0;
		flag = 0;
		while (result[i])
		{
			temp = (result[i] - '0');
			if (flag == 1)
			{
				temp += 10;
				flag = 0;
			}
			result[i] = (temp / 2) + '0';
			if ((temp % 2) == 1)
				flag = 1;
			i++;
		}
		result[i] = '5';
		result[i + 1] = '\0';
	}
	return (result);
}

*2^-0の結果(=1)を返す機能はありません

ということで、整数と小数が得られたので、それぞれ出力すれば完成ですね。printf関数でいうオプションへの対応も頑張ってください。

敗退記録

もともと”42Tokyo生の記事”を見て作り始めたのですが、ビットシフトあたりの考え方に頭が追いつかず、オリジナリティー満載で作ることにしました。

前提知識

ここまで説明を端折ってきた用語などについて少しだけ補足しておきます。

2進数

避けることはできません。2進数については以下から学んでください。
2進数を10進数に変換する方法
小数点を含む2進数を10進数に変換する方法

共用体(union型)

複数の変数型で同じメモリ領域を共有する、という不思議な宣言。

typedef union
{
	float			fvalue;
	unsigned int	uivalue;
}	t_ufloat;

例えば上記のように定義して、代入と結果出力すると以下のような結果が得られます。

t_ufloat a;

a.fvalue = 0.05;
printf("%f\n",a.fvalue);
printf("%d\n",a.uivalue);

//出力結果
0.050000
1028443341

uivalueには値を代入していないのに出力することができましたね。これはメモリを共有しているfvalueにデータ(float型で0.05となるビットの並び)が入力されていたから、です。
残りの疑問は自分で調べてみてください。

ビットシフト

自分の知識整理のため、後日追記を予定。

構造体

必須の知識ではありませんが、知っておけば変数宣言が減ったり、たくさんある変数の扱いが楽になるかもしれません。

double型

ここまでfloat型で説明してきましたが、double型はなにが違うのか?を以下にざっくり書いておきます。

double型は64ビット(8バイト)で構成される。
その内訳は、
1ビットの符号部
11ビットの仮数部
52ビットの仮数部
で計64ビットです。

バイアスは1023です。
このため10進数にした時、整数の最大の桁数は308桁、小数の最大の桁数は1074桁になります。

今回はここまで!がんば!


いいなと思ったら応援しよう!