C言語ではprintf関数の書式指定を使うことで$16$進法表記を扱うことができますが,標準では$2$進法表記などを扱うことができません。しかし,プログラマであれば$2$進法表記で表示したいタイミングはそこそこの頻度で現れます。そこで今回は$10$進数整数値を一般の$p$進数整数値に基数変換し,それをコンソール上に表示するプログラムを書いていきます!
数学的にアルゴリズムを考える
具体例
まず数学的に一般化をするにあたっては,具体例から考えると良いです。ここでは$p=16$つまり$16$進法に基数変換することを考えてみます。
例1
$10$進法表記における$42$を考えてみましょう。基数変換を考える上で気にすべきは,その数の組成です。まあ言葉にするよりも式を見たほうがわかりやすいですので次式を見てください。
42 = 16 \cdot 2 + 10
このように,除法の原理に従って$42$を$16$で割った商と余りに分解することで$42$を構成する数の組成が調べられるというわけです。これをもう少し一般化しやすいように式変形してみましょう。
16 \cdot 2 + 10 = 16^0 \cdot 10 + 16^1 \cdot 2
$16$の冪乗に関する線型結合で表してみました。こう表記することで$16$進法表記の際,各位の数が係数で一目瞭然になります。すなわち,$16$進法では$10$=Aであることに注意すれば$42$は0x2Aです。
例2
次に,$290$を考えてみましょう。例1のときと同じ要領で$16$の冪乗に関する線型結合の形に変形してみます。
\begin{align}
290 &= 16 \cdot 18 + 2\\
&= 16 \cdot (16 \cdot 1 + 2) + 2\\
&= 16^0 \cdot 2 + 16^1 \cdot 2 + 16^2 \cdot 1\\
\end{align}
よって$290$=0x122です。この例では,1行目の変形の段階ではまだ商が$16$で割り切れる数であるので,このまま$16$進法表記に持ち込むのは少々面倒くさいです。ですから,さらに商の組成を調べるべく1行目から2行目のように変形しているわけです。
一般化
それでは一般化をしてみましょう!$10$進数を$p$進法表記することを考えます。
数式での表現
表記法を以下のように定義しておきます。
任意のn \in \mathbb{N}, (m, p) \in \mathbb{N}^2に対し \quad
\begin{cases}
P(n) &:= (p進法表記におけるn桁目の値)\\
Q(m, p) &:= (mをpで割った商)\\
R(m, p) &:= (mをpで割った余り)\\
\end{cases}
\quad とする.
このとき,先に見た具体例を参考にすれば任意の$10$進数$d$に対して次が成り立ちます。
\begin{align}
P(1) &= R(d, p) \quad \quad \quad \quad \quad \quad ←Qが1-1=0個\\
P(2) &= R( Q( d, p ), p ) \quad \quad \quad \quad ←Qが2-1=1個\\
P(3) &= R( Q( Q( d, p ), p ), p ) \quad \quad←Qが3-1=2個\\
\vdots\\
P(n) &= R( Q( Q( \cdots ( Q( d, p), p ) \cdots , p ), p ), p ) \quad \quad \quad ←Qがn-1個\\
\end{align}
ソースコードに落とし込む
では,数式を元にソースコードを書いていきます。まず簡単に,ループ文を書く前に上の数式をそのままコードにしてみます。
...
// 10進数整数値
int d = 32;
// 基数
int p = 16;
// 桁数に対応する配列のバッファ
int buffer = 1 << 8;
// 各位の値を格納する配列
unsigned char p[buffer];
p[0] = d % p;
int q = d / p;
p[1] = q % p;
q /= p;
p[2] = q % p;
q /= p;
/* 以下同様のコードが続く */
...
このままでは問題がありますね。そうです。コードが永遠に終わりませんからループ文で処理する必要があります。また,$16$進法のようなAなどのアルファベットが各位の値に出てくる基数の場合を考慮しなければなりません。
それぞれ解決策を考えます。前者に関しては,ループ処理自体を別の関数として用意して,buffer
の範囲でループし,あふれるようであれば-1を返すみたいな処理を書けばよいです。また後者に関してですが,print関数の書式指定で"%c"
を指定することによりASCII文字コードを扱うことができるのでそれを応用すれば良いでしょう。
完成したソースコード
上で考えた解決策を元に,全体のコードを書いてみます。文字コードの件は,A以降ASCII文字コードのA以降の文字に対応するように書いています。
#include <stdio.h>
#include <stdlib.h>
unsigned char* convertDecimal(unsigned char* bufferP, int value, int base, unsigned int str_size) {
bufferP += str_size;
*--bufferP = '\0';
for (int i = 0; i < str_size; i++) {
unsigned char digit = (unsigned char)(value % base);
*--bufferP = ( digit < 10 ? '0' : 'A' - 10 ) + digit;
value /= base;
if (value == 0) return bufferP;
}
return NULL;
}
int main(void) {
unsigned int value = 300;
unsigned int base = 16;
unsigned int str_size = 1 << 8;
unsigned char* buffer = (unsigned char*)malloc(str_size);
unsigned char* converted = convertDecimal(buffer, value, base, str_size);
if (converted == NULL) return -1;
printf("%s\n", converted);
free(buffer);
return 0;
}
これをコンパイルし,実行すると以下の結果を得ます。やりたかったことができました!
12C
最後に
一般の$p$進法にする場合,上に書いたASCII文字コードに対応させる案を採用しましたが,他に良い方法があれば是非教えていただきたく存じます!お疲れさまでした!
追記
C言語ではthrow
が実装されていないことにコメントいただいて気づきました。ご指摘いただきありがとうございます、!