★ 相關的數據類型定義
在干正事之前,先定義好各種數據類型還是很有必要的,避免在以後的編碼中引起混亂。
uintX X位無符號整形,如uint32表示32位無符號整形
intX X位有符號整形,如int32表示32位有符號整形
基本數據類型定義:
#ifdef _MSC_VER
typedef __int8 int8;
typedef __int16 int16;
typedef __int32 int32;
typedef __int64 int64;
typedef unsigned __int8 uint8;
typedef unsigned __int16 uint16;
typedef unsigned __int32 uint32;
typedef unsigned __int64 uint64;
#else
typedef signed char int8;
typedef signed short int16;
typedef signed int int32;
typedef signed long long int64;
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef unsigned long long uint64;
#endif
這裡的類型定義未必全部都會用上,因為都是從我其他項目直接拿來用的。
布爾類型:
typedef uint8 boolean;
#define TRUE 1
#define FALSE 0
★ 大整數類型的定義
C語言的最長類型是64位,對於超過64位的無符號整形應該如何保存?很簡單,用多個單精度類型存儲即可。
比如1234可以表示成1×1000+2×100+3×10+4×1,於是很容易想到采用十進制,數組的每一位用0到9表示,然後對數字數組按照筆算的原理編寫加減乘除函數,但是這麼做效率會很低的,因為1024比特的大數在十進制下也有三百多位,對於任何一種運算,都需要在兩個有數百個元素的數組空間上多次重循環,還需要許多額外的空間存放計算的進退位標志及中間結果,速度可想而知了,同時,對於一些移位運算,使用十進制會變得非常麻煩,而且速度也會大打折扣。對於程序猿來講,二進制思維是很重要的,在這裡就體現出來了。對於大整數計算,可以采用2^n進制來簡化計算過程(2^n進制實際上就是二進制的縮減表示而已),對於32位的系統,n可以取32,這樣用於表示大整數的數位會大大減少。例如十進制的4294967296可以表示成1,0 (2^32進制),4294967297可以表示成1, 1 (2^32進制),而且處理移位計算會十分方便。
首先定義單精度類型和雙精度類型:
typedef uint32 bn_digit; //定義大整數數位類型,通常情況下長度為操作系統的字長,相當於一個單精度類型。
typedef uint64 bn_udbl; //定義雙字長無符號整形,32位系統下就是64位,相當於一個雙精度類型。
之所以這麼定義,是為了方便移植,對於64位操作系統bn_digit就是64位的無符號整形(這種情況下使用的是2^64進制),此時bn_udbl無定義。
bignum結構定義:
typedef struct
{
int sign; //符號標識,1表示非負整數,-1表示負整數。
size_t alloc; //表示數組dp在增加大小之前的可用數位,alloc數必須是非負整數。
size_t used; //表示數組dp用了多少位來表示一個指定的整數,used數必須是非負整數。
bn_digit *dp; //指針dp指向動態分配的代表指定多精度整數的數組,不足位(alloc-used)全部置0,數組中的數據按照LSB順序存儲(低地址保存低位)
} bignum;
有效的bignum結構:
考慮到效率還有代碼的健壯性,給bignum結構的狀態指定了幾個規則(某些情況下例外):
1. alloc的值為非負整數,也就是說,dp要麼指向一個預先分配有空間的數組,要麼為NULL。
2. used的值為非負整數,且used<=alloc。
3.used的值暗示了數組dp中的第used-1位數不能為0,也就是說以0為首的高位必須被截斷,數組dp中第used個及以上的位置必須置為0。
4. 如果used是0,則sign = 1,此時bignum的值為0或者dp僅僅初始化但沒分配內存。
★ 參數傳遞
在任何庫的開發過程中,都應該盡早確定函數參數傳遞的約定,避免在以後的開發中隨著庫的增大以及復雜度的增加而遇到難題。在我的大整數庫中,采用賦值的方式表示。例如計算兩個大整數a,b的和,結果放在c中,函數原型就是: int bn_add_bn(bignum *c, const bignum *a, const bignum *b); 即c = a + b,函數返回一個整形,用於表示計算時是否遇到錯誤,如內存分配失敗等,返回0表示函數調用正常。
★ 錯誤處理
在大整數算法中,最有可能碰到的問題,就是動態內存分配出錯,當然還有一些其他問題,以後在慢慢討論。對於內存錯誤,只要一出現,後面的計算就無法進行了,應該立即退出並返回錯誤值。在我的大數庫中,有以下的錯誤被定義:
//Error Value
#define BN_MEMORY_ALLOCATE_FAIL -0x0001 //動態內存分配錯誤
#define BN_EXCEED_MAX_LIMB -0x0002 //超出最大數位
#define BN_EXCEED_MAX_BITS -0x0003 //超出最大的比特位
#define BN_NEGATIVE_VALUE_ERROR -0x0004 //負數錯誤
#define BN_INVALID_INPUT -0x0005 //無效輸入
#define BN_DIVISION_BY_ZERO -0x0006 //除以0錯誤
#define BN_BUFFER_TOO_SMALL -0x0007 //緩沖區太小
#define BN_READ_FILE_ERROR -0x0008 //讀文件錯誤
#define BN_WRITE_FILE_ERROR -0x0009 //寫文件錯誤
#define BN_NO_MODULAR_INVERSE -0x000A //模逆不存在
錯誤檢查宏:
#define BN_CHECK(x) if((ret = x) != 0){ goto clean; }
這個宏的作用是用來檢查函數中每一步操作的執行情況,一旦檢測到錯誤(非0的函數返回值),即把函數的返回值置ret為錯誤值x,然後跳轉到函數末尾執行清理操作(通常是臨時變量的動態內存釋放)。這個宏定義裡面使用了goto語句,通常情況下應該是避免使用goto語句,這在大多數情況下是有必要的,然而當所有函數都可能出現調用失敗的時候,使用幾行代碼來處理錯誤就顯得很有意義了。
對於一個大整數計算的函數,通常的形式是這樣的:
int bn_function(bignum *b, const bignum *a)
{
int ret; //如果使用了BN_CHECK宏,ret必須被定義
bignum c;
bn_init(&c); //臨時變量初始化
/**
* Do something here
*/
BN_CHECK(bn_function2(b, a, c)); //BN_CHECK檢查bn_function2的調用情況,如果出錯,直接跳轉到clean後執行清理操作。
clean:
bn_free(&c); //臨時變量內存釋放
return ret; //返回錯誤值,如果無錯誤,ret = 0.
}
★ 總結
本片文章簡單介紹了大整數的相關定義,下一篇就要開始講講最基本的維護算法,主要有大整數的初始化,精度增加,內存釋放等等。