定点小数与分数
8 位定点小数
8 位有符号整型的值域是 [-128, 127]
, 8 位定点小数就是将这个整型视为一个分数的分子,这个分数的分母是 21282^{128}2128 。
这样这个分数的值域就是:
[−1,127128]
[-1, \frac{127}{128}]
[−1,128127]
于是就可以表示小于 1 的数了。
分辨率为:
128−1=127=1128
\frac{1}{2^{8-1}}=\frac{1}{2^7}=\frac{1}{128}
28−11=271=1281
所以 8 位定点小数本质上就是一个整型值 int8_t, 只不过计算的时候要记得除以 128, 这样才能得到实际值。
定点小数与分数类
在 C++ 中可以直接设计一个分数类
////// @brief 分数类///class Int64Fraction final :public base::ICanToString{private:int64_t _num = 0;int64_t _den = 1;public:/* #region 构造函数 */////// @brief 默认构造,分子为 0,分母为 1.///constexpr Int64Fraction() = default;////// @brief 从整型值构造。分子为 num, 分母为 1.////// @param num 分子。///template <typename T>requires(std::is_integral_v<T>)constexpr Int64Fraction(T num){_num = num;_den = 1;}////// @brief 通过分子,分母进行构造。/// @param num 分子/// @param den 分母///constexpr Int64Fraction(int64_t num, int64_t den){SetNum(num);if (num == 0){SetDen(1);}else{SetDen(den);}}////// @brief 通过浮点数构造。////// @param value///constexpr Int64Fraction(base::Double const &value){if (value.Value() == 0.0){SetNum(0);SetDen(1);return;}double db = value.Value();// 要保证分数计算过程不溢出,需要保证 factor * db <= INT64_MAX.int64_t factor = INT64_MAX / base::ceil(db);base::Int64Fraction int_part{static_cast<int64_t>(db)};db -= static_cast<double>(int_part);db *= factor;base::Int64Fraction fractional_part{static_cast<int64_t>(db), factor};*this += int_part + fractional_part;}/* #endregion *//* #region 分子分母 */////// @brief 获取分子。////// @return///constexpr int64_t Num() const{return _num;}////// @brief 设置分子。////// @param value///constexpr void SetNum(int64_t value){_num = value;}////// @brief 获取分母。////// @return///constexpr int64_t Den() const{return _den;}////// @brief 设置分母。////// @param value///constexpr void SetDen(int64_t value){if (value == 0){throw std::invalid_argument{"分母不能为 0."};}_den = value;}/* #endregion *//* #region 计算函数 */////// @brief 化简自身。//////constexpr void Simplify(){if (_den == 0){throw std::invalid_argument{"分母不能为 0."};}if (_num == 0){_den = 1;return;}// 分子分母同时除以最大公约数int64_t gcd_value = std::gcd(_num, _den);int64_t scaled_num = _num / gcd_value;int64_t scaled_den = _den / gcd_value;if (scaled_den < 0){// 如果分母小于 0,分子分母同时取相反数scaled_num = -scaled_num;scaled_den = -scaled_den;}_num = scaled_num;_den = scaled_den;}////// @brief 化简后的形式。////// @note 返回化简后的值,不改变自身。////// @return///constexpr Int64Fraction SimplifiedForm() const{base::Int64Fraction ret{*this};ret.Simplify();return ret;}////// @brief 倒数/// @return///constexpr Int64Fraction Reciprocal() const{base::Int64Fraction ret{_den, _num};return ret.SimplifiedForm();}////// @brief 取绝对值。////// @return///constexpr Int64Fraction Abs() const{if (*this < 0){return -*this;}return *this;}////// @brief 向下取整/// @return///constexpr int64_t Floor() const{int64_t ret = Div();if (*this < 0){if (Mod()){ret -= 1;}}else{/* 因为 C++ 除法近 0 截断,所以如果 Div >0 ,本来就是向下取整了,* 不用再额外的操作了。** Div = 0 就更不用说了,也不用什么额外的操作,直接返回 0 就完事了。*/}return ret;}////// @brief 向上取整/// @return///constexpr int64_t Ceil() const{int64_t ret = Div();if (*this > 0){if (Mod()){ret += 1;}}return ret;}////// @brief 获取分子除以分母的值/// @return///constexpr int64_t Div() const{return _num / _den;}////// @brief 获取分子除以分母的余数/// @return///constexpr int64_t Mod() const{return _num % _den;}////// @brief 降低分辨率。////// @param resolution///constexpr void ReduceResolution(base::Int64Fraction const &resolution){if (resolution <= 0){throw std::invalid_argument{CODE_POS_STR + "分辨率不能 <= 0."};}Simplify();if (_den >= resolution._den){// 本分数的分母比 resolution 的分母大,说明本分数的分辨率大于 resolution.//// 首先需要减小本分数的分母,将分辨率降下来。分子分母同时除以一个系数进行截断,// 从而降低分辨率。int64_t multiple = _den / resolution._den;// 首先将分辨率降低到 1 / resolution._den._num /= multiple;_den /= multiple;// 如果 resolution._num > 1, 则还不够,刚才的分辨率降低到 1 / resolution._den 了,// 还要继续降低。_num = _num / resolution._num * resolution._num;}else{// 本分数的分母比 resolution 的分母小。但这不能说明本分数的分辨率小于 resolution,// 因为 resolution 的分子可能较大。//// 将 resolution 的分子分母同时除以一个系数,将 resolution 的分母调整到与本分数的分母// 相等,然后看一下调整后的 resolution 的分子,如果不等于 0, 即没有被截断成 0, 说明原本的// 分子确实较大,大到足以放大 resolution 的大分母所导致的小步长,导致步长很大,分辨率低。// 这种情况下本分数的分辨率才是高于 resolution, 才需要降低分辨率。int64_t multiple = resolution._den / _den;int64_t div = resolution._num / multiple;if (div != 0){_num = _num / div * div;}}Simplify();}/* #endregion */constexpr Int64Fraction operator-() const{Int64Fraction ret{-_num, _den};return ret.SimplifiedForm();}/* #region 四则运算符 */constexpr Int64Fraction operator+(Int64Fraction const &value) const{// 通分后的分母为本对象的分母和 value 的分母的最小公倍数int64_t scaled_den = std::lcm(_den, value.Den());// 通分后的分子为本对象的分子乘上分母所乘的倍数int64_t scaled_num = _num * (scaled_den / _den);int64_t value_scaled_num = value.Num() * (scaled_den / value.Den());Int64Fraction ret{scaled_num + value_scaled_num,scaled_den,};return ret.SimplifiedForm();}constexpr Int64Fraction operator-(Int64Fraction const &value) const{Int64Fraction ret = *this + (-value);return ret.SimplifiedForm();}constexpr Int64Fraction operator*(Int64Fraction const &value) const{base::Int64Fraction ret;ret.SetNum(_num * value.Num());ret.SetDen(_den * value.Den());return ret.SimplifiedForm();}constexpr Int64Fraction operator/(Int64Fraction const &value) const{Int64Fraction ret{*this * value.Reciprocal()};return ret.SimplifiedForm();}/* #endregion *//* #region 自改变四则运算符 */constexpr Int64Fraction &operator+=(Int64Fraction const &value){*this = *this + value;return *this;}constexpr Int64Fraction &operator-=(Int64Fraction const &value){*this = *this - value;return *this;}constexpr Int64Fraction &operator*=(Int64Fraction const &value){*this = *this * value;return *this;}constexpr Int64Fraction &operator/=(Int64Fraction const &value){*this = *this / value;return *this;}/* #endregion */////// @brief 将分数转化为字符串/// @return///virtual std::string ToString() const override{return std::to_string(_num) + " / " + std::to_string(_den);}/* #region 强制转换运算符 */constexpr explicit operator int64_t() const{return Div();}constexpr explicit operator uint64_t() const{return static_cast<uint64_t>(Div());}constexpr explicit operator int32_t() const{return static_cast<int32_t>(Div());}constexpr explicit operator uint32_t() const{return static_cast<uint32_t>(Div());}constexpr explicit operator int16_t() const{return static_cast<int16_t>(Div());}constexpr explicit operator uint16_t() const{return static_cast<uint16_t>(Div());}constexpr explicit operator int8_t() const{return static_cast<int8_t>(Div());}constexpr explicit operator uint8_t() const{return static_cast<uint8_t>(Div());}constexpr explicit operator double() const{base::Int64Fraction copy{*this};double int_part = static_cast<double>(copy.Div());copy -= copy.Div();double fraction_part = static_cast<double>(copy.Num()) / static_cast<double>(copy.Den());return int_part + fraction_part;}/* #endregion *//* #region 比较 */////// @brief 本对象等于 another./// @param another/// @return///constexpr bool operator==(Int64Fraction const &another) const{if (Num() == 0 && another.Num() == 0){/* 2 个分子都为 0 直接返回相等,这样更加安全,避免分子都为 0* 分母不相等时错误地将两个分数判断为不相等。*/return true;}Int64Fraction f1 = SimplifiedForm();Int64Fraction f2 = another.SimplifiedForm();return f1.Num() == f2.Num() && f1.Den() == f2.Den();}////// @brief 本对象大于 another./// @param another/// @return///constexpr bool operator>(Int64Fraction const &another) const{// 先化简,避免分母为负数,然后使用交叉乘法比大小。Int64Fraction f1 = SimplifiedForm();Int64Fraction f2 = another.SimplifiedForm();int64_t num1{f1.Num()};int64_t den1{f1.Den()};int64_t num2{f2.Num()};int64_t den2{f2.Den()};return num1 * den2 > num2 * den1;}////// @brief 本对象小于 another./// @param another/// @return///constexpr bool operator<(Int64Fraction const &another) const{// 先化简,避免分母为负数,然后使用交叉乘法比大小。Int64Fraction f1 = SimplifiedForm();Int64Fraction f2 = another.SimplifiedForm();int64_t num1{f1.Num()};int64_t den1{f1.Den()};int64_t num2{f2.Num()};int64_t den2{f2.Den()};return num1 * den2 < num2 * den1;}////// @brief 本对象大于等于 another.////// @param another/// @return true/// @return false///constexpr bool operator>=(Int64Fraction const &another) const{if (*this == another){return true;}if (*this > another){return true;}return false;}////// @brief 本对象小于等于 another.////// @param another/// @return true/// @return false///constexpr bool operator<=(Int64Fraction const &another) const{if (*this == another){return true;}if (*this < another){return true;}return false;}/* #endregion */};
当一个 int8_t 变量 a 被视为 8 位定点小数,用它来初始化这个分数时就是
base::Int64Fraction frac{a, 128};
即把这个 int8_t 变量 a 作为分数的分子,128 作为分母,得到的分数值就是这个定点小数要表示的值。
视为每一位的权重发生改变了
N 位定点小数
8 位定点小数可以看作是分子是 8 位有符号整型,分母是 28−1=27=1282^{8-1} =2^7=12828−1=27=128 的分数,则 N 位定点小数可以看作是分子是 N 位有符号整型,分母是 2N−12^{N-1}2N−1 的分数。
值域为:
[−1,2N−1−12N−1]
[-1, \frac{2^{N-1}-1}{2^N-1}]
[−1,2N−12N−1−1]
分辨率为:
12N−1
\frac{1}{2^{N-1}}
2N−11
相关的软考例题
例12 的那些选项应该是打错了,实际上这些选项想要表示的应该是 2 的幂,只不过 2 右边的内容没有往 2 的右上角偏,而是错误地与 2 平齐了。
上文说定点小数实际上就是一个补码形式的有符号整型,只不过你要当作它有一个分母存在。N 位的整型共有 2N2^N2N 种不同的值,所以被解释成定点小数后也是有这么多个值,所以选 A.
例13 所谓的定点整数其实就是有符号整型。所以选 B.