アルゴリズム

C言語で実装する多倍長演算ライブラリ:オーバーフロー防止の大きな整数計算について解説

この記事では、C言語を用いた多倍長演算ライブラリの実装方法を解説します。

大きな整数計算では、通常の整数型でオーバーフローが発生する可能性があるため、数値を複数のパーツに分割して処理する手法を採用します。

各演算ごとに安全チェックを行う仕組みを取り入れ、信頼性の高い計算が実現できる方法について説明します。

多倍長演算ライブラリの設計

データ構造の定義

多倍長数の表現方法と配列管理

多倍長数は、通常の整数型で表現できる範囲を超える大きな数値を扱うために、各桁を配列に格納する手法で実現します。

たとえば、各要素を1桁ずつ保持する配列を用いる方法や、1要素に複数桁の数値を保持する方法があります。

ここでは、各要素に1桁ずつ保持する方法について説明します。

各配列要素には、最下位桁から順に数値を格納し、配列のサイズは固定または動的に確保することができます。

以下のサンプルコードは、固定長配列による多倍長数の表現例です。

#include <stdio.h>
#include <stdlib.h>
#define MAX_DIGITS 1000  // 配列の最大桁数
// BigInt構造体は多倍長数を表す
typedef struct {
    int digits[MAX_DIGITS];  // 各桁を1要素として格納(0~9)
    int length;              // 数値の桁数
} BigInt;
// 数値をBigIntに変換するサンプル関数
void convertToBigInt(const char *numStr, BigInt *big) {
    int i = 0;
    // 文字列の長さを計算(単純化のためにエラーチェックは省略)
    while(numStr[i] != '\0') {
        i++;
    }
    big->length = i;
    for (int j = 0; j < i; j++) {
        // 最下位桁が配列の先頭になるように格納
        big->digits[j] = numStr[i - j - 1] - '0';
    }
}
int main(void) {
    BigInt num;
    convertToBigInt("123456", &num);
    printf("BigInt length: %d\n", num.length);
    printf("BigInt digits (LSB to MSB): ");
    for (int i = 0; i < num.length; i++) {
        printf("%d ", num.digits[i]);
    }
    printf("\n");
    return 0;
}
BigInt length: 6
BigInt digits (LSB to MSB): 6 5 4 3 2 1

メモリ管理と最適化

固定長配列による実装は扱いやすいですが、数値の桁数が大きく変動する場合には動的メモリ管理を採用することも検討するとよいです。

動的確保を利用することで、不要なメモリ使用量を抑え、必要な場合にのみメモリを割り当てることができます。

メモリ確保失敗のチェックを必ず行い、メモリリークを防ぐために不要になったメモリは適切に解放します。

以下は、動的メモリを利用して多倍長数を管理するサンプルコードです。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// BigIntDyn構造体は動的配列を利用して多倍長数を表す
typedef struct {
    int *digits;  // 動的に確保した配列
    int length;   // 数値の桁数
} BigIntDyn;
// 文字列からBigIntDynへの変換
void convertToBigIntDyn(const char *numStr, BigIntDyn *big) {
    int len = strlen(numStr);
    big->length = len;
    big->digits = (int *)malloc(sizeof(int) * len);
    if (big->digits == NULL) {
        printf("Memory allocation error\n");
        exit(1);
    }
    for (int i = 0; i < len; i++) {
        big->digits[i] = numStr[len - i - 1] - '0';
    }
}
int main(void) {
    BigIntDyn number;
    convertToBigIntDyn("9876543210", &number);
    printf("BigIntDyn length: %d\n", number.length);
    printf("BigIntDyn digits (LSB to MSB): ");
    for (int i = 0; i < number.length; i++) {
        printf("%d ", number.digits[i]);
    }
    printf("\n");
    free(number.digits);
    return 0;
}
BigIntDyn length: 10
BigIntDyn digits (LSB to MSB): 0 1 2 3 4 5 6 7 8 9

オーバーフロー防止機構

キャリー処理の実装

多倍長演算では、各桁の演算結果が基数(ここでは10)を超える場合、上位桁に繰り上げる必要があります。

キャリー処理は、演算後の各桁の値が b(例:b=10)を超える場合に、商を次の桁に加算し、余りをその桁に残す処理です。

アルゴリズムの基本原理は以下の通りです。

各桁の計算結果 r に対して、

carry=r/b,digit=rmodb

と計算します。

以下は、キャリー処理を含む加算処理の一部サンプルコードです。

#include <stdio.h>
#define MAX_DIGITS 100
// BigInt構造体(固定長配列版)
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 2つのBigIntの加算(キャリー処理を実装)
void addBigInts(const BigInt *a, const BigInt *b, BigInt *result) {
    int carry = 0;
    int maxLength = (a->length > b->length) ? a->length : b->length;
    result->length = maxLength;
    for (int i = 0; i < maxLength; i++) {
        int digitA = (i < a->length) ? a->digits[i] : 0;
        int digitB = (i < b->length) ? b->digits[i] : 0;
        int sum = digitA + digitB + carry;
        result->digits[i] = sum % 10;  // 現在の桁
        carry = sum / 10;              // 次桁への繰り上がり
    }
    if (carry != 0) {  // 最後のキャリー処理
        result->digits[maxLength] = carry;
        result->length++;
    }
}
int main(void) {
    BigInt num1 = {{6,5,4}, 3};  // 456として表現(LSBが先頭)
    BigInt num2 = {{4,3,2,1}, 4};  // 1234として表現
    BigInt sum;
    addBigInts(&num1, &num2, &sum);
    printf("和: ");
    for (int i = sum.length - 1; i >= 0; i--) {
        printf("%d", sum.digits[i]);
    }
    printf("\n");
    return 0;
}
和: 1690

演算前後のエラーチェック

演算処理を行う際には、入力データの妥当性や、各演算が正常に実施できたかの検査を行うことが重要です。

エラーチェックとしては、入力文字列が全て数字で構成されているか、桁数が配列の範囲内で収まっているか、演算結果が配列バッファに収まるかなどを検証します。

簡単なエラーチェックの例として、入力文字列の数字チェックを行う関数を以下のサンプルコードで示します。

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
// 入力文字列が数字のみで構成されているか確認する関数
int validateNumber(const char *numStr) {
    for (int i = 0; i < (int)strlen(numStr); i++) {
        if (!isdigit(numStr[i])) {
            return 0;  // 数字以外が含まれている場合はエラー
        }
    }
    return 1;
}
int main(void) {
    const char *input = "123456";
    if (validateNumber(input)) {
        printf("入力文字列は正しい形式です。\n");
    } else {
        printf("入力エラー:数字以外の文字が含まれています。\n");
    }
    return 0;
}
入力文字列は正しい形式です。

演算処理の実装詳細

加算処理

キャリーによる桁上がり対策

加算処理では、各桁の和が 10 以上になる場合に必ずキャリーを伝搬させる必要があります。

この処理では、各桁ごとに足し算を行い、余りと商を計算して次の桁へ反映させる手法を採ります。

サンプルコードでは、先ほど紹介した addBigInts関数を利用して、キャリーの伝搬を正確に行う実装例を示しています。

この手法は、加算処理全体の要となる部分であり、他の演算(乗算など)でも応用可能な基本的なアルゴリズムとなります。

減算処理

借り処理とエラー予防

減算処理では、引かれる桁が被減数の桁より小さい場合、上位桁から借りを行う必要があります。

借り処理を正しく実装することで、桁借りが必要な場合でも正確な結果を算出することができ、また借りができない場合のエラー(負の結果が出る場合など)も検出するようにします。

以下は、2つの多倍長数の減算を行う際の借り処理の実装例です。

#include <stdio.h>
#include <stdlib.h>
#define MAX_DIGITS 100
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 2つのBigIntの減算(borrow処理を実装)
int subtractBigInts(const BigInt *a, const BigInt *b, BigInt *result) {
    if (a->length < b->length) {
        return -1;  // 被減数が小さい場合はエラー
    }
    int borrow = 0;
    result->length = a->length;
    for (int i = 0; i < a->length; i++) {
        int digitA = a->digits[i];
        int digitB = (i < b->length) ? b->digits[i] : 0;
        int sub = digitA - digitB - borrow;
        if (sub < 0) {
            sub += 10;  // 借りを行い、10を加算
            borrow = 1;
        } else {
            borrow = 0;
        }
        result->digits[i] = sub;
    }
    if (borrow != 0) {
        return -1;  // 最終的に借りが残っている場合はエラー
    }
    // 結果の正確な桁数を決定(不要な0を取り除く)
    while(result->length > 1 && result->digits[result->length - 1] == 0) {
        result->length--;
    }
    return 0;
}
int main(void) {
    BigInt num1 = {{6,5,4}, 3};   // 456
    BigInt num2 = {{8,3,1}, 3};     // 138
    BigInt diff;
    if (subtractBigInts(&num1, &num2, &diff) == 0) {
        printf("差: ");
        for (int i = diff.length - 1; i >= 0; i--) {
            printf("%d", diff.digits[i]);
        }
        printf("\n");
    } else {
        printf("減算エラーが発生しました。\n");
    }
    return 0;
}
差: 318

乗算処理

桁あふれ検出とキャリーマネジメント

乗算処理は、各桁ごとの乗算と、その部分乗算結果のキャリー処理を正確に行うことが求められます。

各桁 aibj の積 (ai×bj) を対応する位置に加算し、同時にキャリーを適切に計算する必要があります。

桁あふれ検出は、各計算後にその桁が基数(ここでは10)を超えていないかを確認し、超えている場合は余りと商を分離します。

以下のコードは、乗算処理における基本的な実装例です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DIGITS 200
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 2つのBigIntの乗算処理
void multiplyBigInts(const BigInt *a, const BigInt *b, BigInt *result) {
    // 結果の配列を0で初期化
    memset(result->digits, 0, sizeof(result->digits));
    result->length = a->length + b->length;
    for (int i = 0; i < a->length; i++) {
        int carry = 0;
        for (int j = 0; j < b->length; j++) {
            int index = i + j;
            int prod = a->digits[i] * b->digits[j] + result->digits[index] + carry;
            result->digits[index] = prod % 10;
            carry = prod / 10;
        }
        result->digits[i + b->length] += carry;
    }
    // 結果の桁数を正確に調整
    while(result->length > 1 && result->digits[result->length - 1] == 0) {
        result->length--;
    }
}
int main(void) {
    BigInt num1 = {{6,5,4}, 3};   // 456
    BigInt num2 = {{8,3,1}, 3};     // 138
    BigInt prod;
    multiplyBigInts(&num1, &num2, &prod);
    printf("積: ");
    for (int i = prod.length - 1; i >= 0; i--) {
        printf("%d", prod.digits[i]);
    }
    printf("\n");
    return 0;
}
積: 62928

除算処理

商と余りの計算手法

除算処理では、被除数から除数を何度も引く手法(繰り返し引き算)や、長い除算アルゴリズムを応用して、商と余りを求めます。

一般的なアプローチとしては、左から順に桁を取り出しながら、部分的な被除数に対して除算を実施する方法があります。

この方法では、各ステップで商の桁と余りを計算し、余りに次の桁を付加して再び除算を行います。

以下のコードは、単純な長除算のイメージを示すサンプルです。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DIGITS 100
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 仮の除算処理(シンプルな長除算アルゴリズム:整数値としての扱い)
void divideBigInts(const BigInt *dividend, int divisor, BigInt *quotient, int *remainder) {
    quotient->length = dividend->length;
    *remainder = 0;
    for (int i = dividend->length - 1; i >= 0; i--) {
        int current = *remainder * 10 + dividend->digits[i];
        quotient->digits[i] = current / divisor;
        *remainder = current % divisor;
    }
    // 桁数の調整(先頭の0を除去)
    while(quotient->length > 1 && quotient->digits[quotient->length - 1] == 0) {
        quotient->length--;
    }
}
int main(void) {
    // 被除数の例:1234
    BigInt dividend = {{4,3,2,1}, 4};  // 数字は逆順に格納
    BigInt quotient;
    int remainder;
    divideBigInts(÷nd, 12, "ient, &remainder);
    printf("商: ");
    for (int i = quotient.length - 1; i >= 0; i--) {
        printf("%d", quotient.digits[i]);
    }
    printf("\n余り: %d\n", remainder);
    return 0;
}
商: 102
余り: 10

入出力およびデバッグ機能

数値の入力パースと変換

多倍長数を取り扱う場合、ユーザから入力された文字列を正しくパースし、内部表現に変換することが重要です。

数値文字列を逆順に格納することで、加算・減算などの演算処理が容易になります。

以下のサンプルコードは、文字列から BigInt へ変換する基本的な処理を示しています。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DIGITS 100
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 数値文字列をBigIntに変換する関数
void parseInput(const char *inputStr, BigInt *big) {
    int len = strlen(inputStr);
    big->length = len;
    for (int i = 0; i < len; i++) {
        // 末尾から順に格納する
        big->digits[i] = inputStr[len - i - 1] - '0';
    }
}
int main(void) {
    char inputStr[] = "987654321";
    BigInt number;
    parseInput(inputStr, &number);
    printf("入力されたBigInt: ");
    for (int i = number.length - 1; i >= 0; i--) {
        printf("%d", number.digits[i]);
    }
    printf("\n");
    return 0;
}
入力されたBigInt: 987654321

出力フォーマットの調整

10進法・16進法の対応

多倍長数の計算結果を出力する際には、10進法だけでなく16進法などの形式にも対応できると、デバッグや数値の確認が便利です。

10進法の場合は、桁をそのまま連結して表示し、16進法の場合は各桁を16進数に変換して出力します。

以下は、10進法と16進法に対応した出力サンプルです。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DIGITS 100
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 10進法でBigIntを出力する関数
void printBigIntDec(const BigInt *big) {
    for (int i = big->length - 1; i >= 0; i--) {
        printf("%d", big->digits[i]);
    }
    printf("\n");
}
// 16進法でBigIntを出力する関数(各桁を16進表記に変換)
void printBigIntHex(const BigInt *big) {
    for (int i = big->length - 1; i >= 0; i--) {
        printf("%X", big->digits[i]);
    }
    printf("\n");
}
int main(void) {
    BigInt number = {{1, 10, 15, 3}, 4};  // 内部表現(LSBが先頭)
    printf("10進法表記: ");
    printBigIntDec(&number);
    printf("16進法表記: ");
    printBigIntHex(&number);
    return 0;
}
10進法表記: 3 15 10 1 →  1153(ただし各桁が10進数表現の場合)
16進法表記: 3F A1

上記の出力例は、各桁の値がそのまま出力される例です。

16進法の場合、桁ごとの数値が16進数に変換される点にご注意ください。

エラー管理と例外処理

エラー検知機構の設計

演算エラーの検出手法

多倍長演算では、各演算ステップで正しい結果が得られているかどうかを検証するために、エラーコードやフラグを利用します。

たとえば、加算時に最後の桁でキャリーが残り、結果の桁数が配列サイズを超える場合や、減算で借りが途中まで解決できなかった場合などに、エラーを返す実装が考えられます。

エラーコードは通常、返り値として返すか、グローバル変数で管理する手法が用いられます。

#include <stdio.h>
#include <stdlib.h>
#define MAX_DIGITS 100
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 加算処理において溢れが生じた場合にエラーを返す関数(簡略化した例)
int safeAddBigInts(const BigInt *a, const BigInt *b, BigInt *result) {
    int carry = 0;
    int maxLength = (a->length > b->length) ? a->length : b->length;
    result->length = maxLength;
    for (int i = 0; i < maxLength; i++) {
        int digitA = (i < a->length) ? a->digits[i] : 0;
        int digitB = (i < b->length) ? b->digits[i] : 0;
        int sum = digitA + digitB + carry;
        if (i >= MAX_DIGITS) {
            return -1;  // バッファサイズ超過エラー
        }
        result->digits[i] = sum % 10;
        carry = sum / 10;
    }
    if (carry != 0) {
        if (maxLength >= MAX_DIGITS) {
            return -1;  // 残ったキャリーを格納できない場合のエラー
        }
        result->digits[maxLength] = carry;
        result->length++;
    }
    return 0;  // 正常終了
}
int main(void) {
    BigInt num1 = {{9,9,9}, 3};
    BigInt num2 = {{1}, 1};
    BigInt sum;
    if (safeAddBigInts(&num1, &num2, &sum) == 0) {
        printf("加算結果: ");
        for (int i = sum.length - 1; i >= 0; i--) {
            printf("%d", sum.digits[i]);
        }
        printf("\n");
    } else {
        printf("加算中にエラーが発生しました。\n");
    }
    return 0;
}
加算結果: 1000

ログ出力による情報取得

エラーが発生した際に、どの処理で問題が起きたのかを把握するために、ログ出力を用いるとデバッグが容易になります。

たとえば、エラーコードとともに、そのエラーが発生した関数名や入力値などを出力する仕組みを組み込むことで、問題発生時の対処がしやすくなります。

以下は簡単なログ出力機能を含む例です。

#include <stdio.h>
#include <stdlib.h>
void logError(const char *functionName, const char *message) {
    // シンプルに標準出力へログを出力する(実際の環境ではファイル出力などが望ましい)
    printf("[ERROR] %s: %s\n", functionName, message);
}
int main(void) {
    // サンプルエラー発生時のログ出力例
    logError("safeAddBigInts", "配列サイズを超えるキャリーが発生しました。");
    return 0;
}
[ERROR] safeAddBigInts: 配列サイズを超えるキャリーが発生しました。

ユニットテストと検証

テストケースの設計

境界値テストとオーバーフロー検証

多倍長演算ライブラリの品質を確保するためには、境界値テストとオーバーフロー検証が欠かせません。

入力値が最大または最小の範囲にある場合や、キャリーや借りが連続する場合など、各演算で想定外の挙動が発生しないかどうかをテストします。

ユニットテストでは、以下のようなシナリオを検証します。

  • 加算時に最後の桁でキャリーが発生するケース
  • 減算時に借りが複数回必要となるケース
  • 乗算時に途中で桁あふれが生じるケース
  • 除算時に余りが大きくなるケース

以下は、簡単な境界値テストの例です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DIGITS 100
typedef struct {
    int digits[MAX_DIGITS];
    int length;
} BigInt;
// 加算処理(前述のシンプルな加算関数を再利用)
int safeAddBigInts(const BigInt *a, const BigInt *b, BigInt *result) {
    int carry = 0;
    int maxLength = (a->length > b->length) ? a->length : b->length;
    result->length = maxLength;
    for (int i = 0; i < maxLength; i++) {
        int digitA = (i < a->length) ? a->digits[i] : 0;
        int digitB = (i < b->length) ? b->digits[i] : 0;
        int sum = digitA + digitB + carry;
        if (i >= MAX_DIGITS) {
            return -1;
        }
        result->digits[i] = sum % 10;
        carry = sum / 10;
    }
    if (carry != 0) {
        if (maxLength >= MAX_DIGITS) return -1;
        result->digits[maxLength] = carry;
        result->length++;
    }
    return 0;
}
int main(void) {
    BigInt test1 = {{9,9,9,9}, 4}; // 9999
    BigInt test2 = {{1}, 1};         // 1
    BigInt result;
    if (safeAddBigInts(&test1, &test2, &result) == 0) {
        printf("ユニットテスト結果: ");
        for (int i = result.length - 1; i >= 0; i--) {
            printf("%d", result.digits[i]);
        }
        printf("\n");
    } else {
        printf("テスト中にエラーが発生しました。\n");
    }
    return 0;
}
ユニットテスト結果: 10000

まとめ

この記事では、C言語で多倍長演算ライブラリを実装する際の基本構造、各演算(加算、減算、乗算、除算)におけるキャリーや借り処理の方法、メモリ管理、エラーチェック、デバッグおよびユニットテストの実装例を学ぶことができます。

これにより、大きな整数計算をより安全に、実践的に扱うための手法を習得できる内容となっています。

関連記事

Back to top button
目次へ