精华内容
下载资源
问答
  • 整型

    2018-11-01 09:56:42
    C语言学习—整型数据 整型常量 整型常量也叫整常数,在C语言中十进制、八进制和十六进制三种表示方式。 1.十进制整型常量(0-9),表示时没有前缀,以下是合法的十进制整型常量: 5、256、-568、65535、1627 2....

    C语言学习—整型数据

    整型常量

    整型常量也叫整常数,在C语言中十进制、八进制和十六进制三种表示方式。
    1.十进制整型常量(0-9),表示时没有前缀,以下是合法的十进制整型常量:
    5、256、-568、65535、1627
    2.八进制整型常量(0-7),其前缀为0,表示时必须在数字前加0,以下是合法的八进制常量:
    015(13)、0101(65)、0177777
    3.十六进制整型常量(0-9、a-f或A-F),其前缀为0X或0x,以下是合法的十六进制常量:
    0x2A、0xA0、0xFFFF

    内存中的二进制存储
    这里有一个常识:一个字节BYTE=八位bit
    数值是以补码的形式存储:正数的补码是其原码,负数的补码是其绝对值的原码取反加1。例如:
    —14的补码 1111 0010
    绝对值14 0000 1110
    取反 1111 0001
    +1 1111 0010

    在有符号变量里,反码的最高位表示正负,0表示正数,1表示负数。

    整型变量

    整型变量有:标准型(int)、短整型(short int或short)、长整型(long int或者long)以及无符号整型(unsigned int)四种。

    整型变量的定义:类型说明符 变量标识符,变量标识符,…;
    注意:
    1.当一个类型说明符后定义了多个相同类型的变量时,变量标识符之间用“,”隔开;
    2.类型说明符与变量标识符之间至少有一个空格间隔;
    3.所定义的最后一个变量后面以“;”结尾;
    4.变量的定义必须在变量使用之前。

    整型变量所占的字节数与系统和编译器规定有关。在VS2017中查询变量所占字节长度可使用sizeof函数,程序如下:

    #include<stdio.h>
     int main()
    {
    printf("size(int)= %d\n",sizeof(int));
    return 0;
    }
    

    结果如下:size(int) = 4

    在VS2017中,各整型变量的范围和所占字节数如下:
    类型说明符 范围 所占字节数
    int -2E16—2E16-1 4
    short int -2E8—2E8-1 2
    long int -2E16-2E16-1 4
    unsigned int 0-65535 4

    当整型数据超出范围时会产生溢出,此时数据不正确,在使用整型数据时应注意其范围,不能产生溢出。

    展开全文
  • C整型整型溢出

    2020-08-20 22:53:55
    本文以表格的形式讨论了C语言整型数据类型的特点,以及原码、反码、补码和整型取值范围的关系;基于对整型取值范围的认识,对整型溢出的原因做了一个大致分类,也从汇编的层面去探讨了整型“下溢”的本质。

    0 摘要

    本文以表格的形式讨论了C语言整型数据类型的特点,以及原码、反码、补码和整型取值范围的关系;基于对整型取值范围的认识,对整型溢出的原因做了一个大致分类,也从汇编的层面去探讨了整型“下溢”的本质。

    1 整型数据类型

    整型数据类型是C语言中的一种基本类型,其存储的是整数,可分为两大类:有符号整型和无符号整型。

    1.1 位宽、符号和取值范围

    各整型类型的位宽及取值范围如下表所示。

    类型 位宽 最小值 最大值
    char 8 -128 127
    unsigned char 8 0 255
    short 16 -32 768 32 767
    unsigned short 16 0 65 535
    int 32 -2 147 483 648 2 147 483 647
    unsigned int 32 0 4 294 967 295
    long 32 -2 147 483 648 2 147 483 647
    unsigned long 32 0 4 294 967 295
    long long 64 -9 223 372 036 854 775 808 9 223 372 036 854 775 807
    unsigned long long 64 0 18 446 744 073 709 551 615
    • 位宽
      不同的整型类型,有不同的位宽。需要注意的是,不同环境下某些整型类型的位宽定义可能不一样,比如64位Ubuntu17.10+GCC7.2.0编译器的long和64位win7+VS2010的long位宽不一样。16位系统的int和32位系统的int位宽不一样。具体位宽可以通过sizeof()来获知。

    • 符号
      无符号整型和有符号整型在编码上的区别在于“符号位”的有无。下图以8位的整型为例,有符号整型的最高位用作符号位,其余位用来表示数值的绝对值,符号位为1代表这是一个负数,为0表示正数。而无符号数没有符号位,整个内存区间都用来表示数值大小。
      在这里插入图片描述

    • 取值范围
      符号位的有无和位宽的大小决定了整型数据的取值范围。对于位宽为n的无符号整型,其取值范围为:[0, 2n-1],这是显而易见的。对于位宽为n的有符号整型,其取值范围为:[-2n-1, 2n-1-1],为什么是这样的取值范围,后面讨论。

    1.2 原码、反码和补码

    下面的表格是正数、负数以及一些具体数值的原码、反码和补码的关系示意。表格第三列中的符号“~”和第四列中的符号“>>”分别代表“按位取反”和“右移”,跟C语言里的定义一样。

    十进制 原码 反码 补码 右移操作
    正数 符号位 = 0 = 原码 = 反码
    = 原码
    = ~负数补码 + 1
    最高位补0
    负数 符号位 = 1 符号位不变,
    其余各位取反
    = 反码 + 1
    = ~正数补码 + 1
    最高位补1
    4 0000 0100 0000 0100 0000 0100 4 >> 1 = 0000 0010补码 = 2
    -4 1000 0100 1111 1011 1111 1100 -4 >> 1 = 1111 1110补码 = -2
    1 0000 0001 0000 0001 0000 0001 1 >> 1 = 0000 0000补码 = 0
    -1 1000 0001 1111 1110 1111 1111 -1 >> 1 = 1111 1111补码 = -1
    • 原码
      原码就是“最高位表示符号,其余位表示数值的绝对值”的编码。

    • 反码
      相对原码而言,正数的编码不变,负数的编码除符号位外所有位都按位取反,就是反码。

    • 补码
      相对反码而言,正数的编码不变,负数的编码加1,就是补码。这是计算机存储数值的编码方式。

    1.3 再论有符号数取值范围

    前面说到,位宽为n的有符号整型,其取值范围为:[-2n-1, 2n-1-1]。以char型为例,则其取值范围为[-128, 127]。为什么取值范围是[-128, 127],而不是[-127, 128]呢?

    我们知道原码非常直观而容易理解,但是这种编码在计算机上进行运算是比较复杂的——需要先判断正负,再进行加减。相比之下,反码的运算则比较简单,因为反码让符号位也参与了运算(省去了符号位的判断),让减法可以用加法来表示(省去了专门的减法电路),比如4-4,可以表示为4+(-4),即0000 0100反码+1111 1011反码=1111 1111反码=1000 0000原码=0

    但反码有个问题:由于0的原码有两个:“正零”00000000原码和“负零”10000000原码,导致反码也有两个0:“正零”00000000反码和“负零”11111111反码

    如下表所示,原码和反码的“零”都有两种编码。

    十进制 原码 反码
    -127 1111 1111 1000 0000
    -126 1111 1110 1000 0001
    -2 1000 0010 1111 1101
    -1 1000 0001 1111 1110
    -0 1000 0000 1111 1111
    0 0000 0000 0000 0000
    1 0000 0001 0000 0001
    2 0000 0010 0000 0010
    126 0111 1110 0111 1110
    127 0111 1111 0111 1111

    为了统一两个0,约定正数的编码跟原码、反码一样,负数用其反码+1来表示——也就是我们说的补码,这样原来的“负零”就可以表示为:1111 1111反码 + 1 = 0000 0000补码,而“正零”还是不变:0000 0000原码 = 0000 0000反码 = 0000 0000补码

    于是零编码就统一成了0000 0000补码,而且还腾出了一个编码1000 000补码,对比下表中右边两列,补码相当于把反码整体往上挪了一个位置。

    十进制 原码 反码 补码
    1000 0000
    -127 1111 1111 1000 0000 1000 0001
    -126 1111 1110 1000 0001 1000 0010
    -2 1000 0010 1111 1101 1111 1110
    -1 1000 0001 1111 1110 1111 1111
    -0 1000 0000 1111 1111 0000 0000
    0 0000 0000 0000 0000 0000 0000
    1 0000 0001 0000 0001 0000 0001
    2 0000 0010 0000 0010 0000 0010
    126 0111 1110 0111 1110 0111 1110
    127 0111 1111 0111 1111 0111 1111

    多出的编码1000 000补码就这样放着吗?当然不是,它正好可以用来表示十进制数-128,因为-128 = -127 - 1 = -127 + (-1) = 1000 0001补码 + 1111 1111补码 = 1000 000补码,不过-128比较特殊——只有补码,没有原码和反码。这就是为什么char型的取值范围是[-128, 127],而不是[-127, 128]。

    最后,下面是我们完整的取值范围对照表。

    十进制 原码 反码 补码
    128 1000 0000
    -127 1111 1111 1000 0000 1000 0001
    -126 1111 1110 1000 0001 1000 0010
    -2 1000 0010 1111 1101 1111 1110
    -1 1000 0001 1111 1110 1111 1111
    -0 1000 0000 1111 1111 0000 0000
    0 0000 0000 0000 0000 0000 0000
    1 0000 0001 0000 0001 0000 0001
    2 0000 0010 0000 0010 0000 0010
    126 0111 1110 0111 1110 0111 1110
    127 0111 1111 0111 1111 0111 1111

    2 整型溢出

    2.1 何为整型溢出

    当用一个整型变量存放一个超出它可表示范围的数值时,就会发生溢出,称之为整型溢出。

    有符号整型的溢出是未定义行为,溢出结果没有约定俗成的标准,由编译器决定,是不可预测的。无符号数的溢出是有定义的:对于位宽为n的无符号整型,其溢出结果是正确值对2n取模,举个例子:

    unsigned char a;
    a = 300;				//溢出
    printf("%d\n", a);		//a = 300 mod 2^8 = 300 mod 256 = 44
    

    导致整型溢出的原因都有哪些?由于符号位的有无和位宽的大小决定了整型数据的取值范围,那么,整型溢出的原因基本可分为两种:

    • 宽度问题导致的溢出
    • 符号问题导致的溢出

    2.2 宽度问题导致的溢出

    往一个整型存入比它的位宽更宽的数时所产生的溢出,这是典型的整型溢出,很好理解。原因一般有两种,一种是直接的赋值,另一种是算数运算。

    • 赋值
      看到下面例子中的赋值操作。
    #include "stdio.h"
    void main(void)
    {
    	unsigned int ui;
    	unsigned short us;
        unsigned char uc;
        
        ui = 0x12345678;
        us = ui;
        uc = ui;
        
        printf("ui = 0x%08x\n", ui);
        printf("us = 0x%08x\n", us);
        printf("uc = 0x%08x\n", uc);
    }
    

    由于us和uc太短,装不下ui的0x12345678,结果被“截断”,输出如下:

    ui = 0x12345678
    us = 0x00005678
    uc = 0x00000078
    
    • 算术运算
      本质上来说,算术运算中发生的溢出,也是一种赋值时发生的溢出,只是被赋值的对象是一个看不到的临时变量罢了。看下面的例子。
    #include "stdio.h"
    void main(void)
    {
        unsigned int a = 0xffffffff;
        printf("a = 0x%x\n", a);
        printf("a + 1 = 0x%x\n", a + 1);
    }
    

    其输出如下:

    a = 0xffffffff
    a + 1 = 0x0
    

    a+1的正确结果应该是0x100000000,但因为存放该值的临时变量太短,被截断成了0x00000000。

    算术运算发生的溢出,有一种可能不易发现:中间结果的溢出。

    #include "stdio.h"
    void main()
    {
        int a = 1000000;
        int b;
        
        b = a * a / a;
        printf("b = %d\n", b);
    }
    

    输出如下:

    b = -727
    

    上面例子的最终结果是不正确的,其原因在于,在第一步计算a*a时就已发生溢出,导致了最终的错误,这还只是一个简单的例子,如果是一个有很多步的复杂算术运算,整型溢出将藏得很深。

    2.3 符号问题导致的溢出

    根据前面对取值范围以及补码的讨论,一个十六进制数0xFFFFFFFF,若解释成有符号数,是十进制的-1,若解释成无符号数,则是十进制的4294967295。也就是说,同样的内存数据,对内存数据的解释不一样,得到的数值可能相差十万八千里。所以一个有符号数如果转换成无符号数(或者反过来),将导致数值的突变,从而导致错误。

    下面是一个由于符号问题导致溢出的例子。

    #include "stdio.h"
    
    int copy_something(char *buf, int len)
    {
    	#define MAX_LEN 256
    	char mybuf[MAX_LEN];
    	
    	// do something...
    
    	if(len > MAX_LEN) {				// <---- [1]
    		return -1;
    	}
    	return memcpy(mybuf, buf, len);	// <---- [2]
    }
    
    void main()
    {
    	char buffer[512];
        copy_something(buffer, -1);
    }
    

    当对len传入一个负数,比如-1,将通过位置[1]的检查,len在位置[2]被转成unsigned int,从而变成一个很大的正数,拷贝了超出长度限制的数据到mybuf,使得mybuf后面的内存被覆盖。

    2.4 整型“下溢”

    一些文章根据溢出的方向,把整型溢出分为“上溢”(overflow)和“下溢”(underflow),“上溢”是指数值大于整型可表示的最大值时发生的溢出,“下溢”则是数值小于整型可表示的最小值时发生的溢出。也有一些文章认为下溢是浮点数的概念,整型没有“下溢”。

    那么该如何理解整型“下溢”?先看下面的代码。

    #include "stdafx.h"
    int main(void)
    {
    	unsigned int a = 0;
    	int b = 0;
    
    	a = a - 1;
    	b = b - 1;
    
    	if(a > 0) {
    		a = 0;
    	}
    
    	if(b > 0) {
    		b = 0;
    	}
    
    	return 0;
    }
    

    无符号整型a和有符号整型b的初始值都是0,两者都减一,然后分别与0比较大小。a发生了“下溢”,其值变成了一个很大的正数4294967295,因此第一个if判断为真。b没有“下溢”,其值为-1,因此第二个if判断为假。

    前面我们讨论过,计算机使用补码可以让符号位参与运算,让减法可以用加法来表示。因此,上面代码的两个减法运算可表示为:

    a = a + (-1) = 0 + 0xFFFFFFFF = 0xFFFFFFFF;
    b = b + (-1) = 0 + 0xFFFFFFFF = 0xFFFFFFFF;
    

    两者的运算过程完全一样,十六进制结果也一样,只是由于a和b的类型不一样:a是unsigned int型,b是int型,因此a被解释成4294967295,而b被解释成-1。

    上面代码在VS2010下的反汇编如下。看到15行和17行,a和b的初始化汇编代码是一样的,而20-22行中a的减一和赋值,和24-26行中b的减一和赋值也是一样的。那么问题来了,既然a和b的十六进制值都一样,它们又是如何根据类型解释成不同的十进制值呢?答案在30和37行。30行的jbe是用于无符号数的小于等于跳转指令,而37行jle是用于有符号数的小于等于跳转指令——汇编代码根据a和b类型的不同,使用了不同的比较跳转指令,实现了对相同十六进制值的不同解释。

    #include "stdafx.h"
    int main(void)
    {
    00981350  push        ebp  
    00981351  mov         ebp,esp  
    00981353  sub         esp,0D8h  
    00981359  push        ebx  
    0098135A  push        esi  
    0098135B  push        edi  
    0098135C  lea         edi,[ebp-0D8h]  
    00981362  mov         ecx,36h  
    00981367  mov         eax,0CCCCCCCCh  
    0098136C  rep stos    dword ptr es:[edi]  
    	unsigned int a = 0;
    0098136E  mov         dword ptr [a],0  
    	int b = 0;
    00981375  mov         dword ptr [b],0  
    
    	a = a - 1;
    0098137C  mov         eax,dword ptr [a]  
    0098137F  sub         eax,1  
    00981382  mov         dword ptr [a],eax  
    	b = b - 1;
    00981385  mov         eax,dword ptr [b]  
    00981388  sub         eax,1  
    0098138B  mov         dword ptr [b],eax  
    
    	if(a > 0) {
    0098138E  cmp         dword ptr [a],0  
    00981392  jbe         main+4Bh (98139Bh)  
    		a = 0;
    00981394  mov         dword ptr [a],0  
    	}
    
    	if(b > 0) {
    0098139B  cmp         dword ptr [b],0  
    0098139F  jle         main+58h (9813A8h)  
    		b = 0;
    009813A1  mov         dword ptr [b],0  
    	}
    
    	return 0;
    009813A8  xor         eax,eax  
    }
    

    所以,从底层角度来看,整型“下溢”跟“上溢”是一样的,都是十六进制数之间的运算赋值,其结果再根据具体的类型来解释:结果太长了?截掉。有符号数?最高位拿来表示正负。无符号数?所有位都拿来表示数值。所以说,整型没有“下溢”。

    如果从上层角度来看,“下溢”就是十进制数值的变化超出了变量所能表示的最小值,向下溢出了。也并非不可。

    2.5 避免整型溢出

    整型溢出问题,轻则导致运算结果出错,重则导致内存被覆盖、缓冲区溢出、被黑客攻击、系统崩溃。

    避免整型溢出的一般方向有两个,就是避免前面所说的宽度问题和符号问题。

    对于宽度问题:

    • 避免将一个“更长”的变量赋值给“更短”的变量
    • 考虑参与运算的变量可能取到的最大值,即当变量都以最大值参与运算时,运算的结果或中间结果,是否在变量的取值范围内

    对于符号问题:

    • 避免有符号整型和无符号整型之间的运算,比较,赋值

    想了解更多整型溢出例子和避免方法,可参考coolshell的 C语言的整型溢出问题

    由于笔者水平有限,文章若存在纰漏,烦请指出。



    参考:

    展开全文
  • 为什么Python中整型不会溢出

    千次阅读 多人点赞 2018-10-30 11:04:15
    在python2时代,整型有 int 类型和 long 长整型,长整型不存在溢出问题,即可以存放任意大小的整数。在python3后,统一使用了长整型。这也是吸引科研人员的一部分了,适合大数据运算,不会溢出,也不会有其他...

    前言

    本次分析基于 CPython 解释器,python3.x版本

    在python2时代,整型有 int 类型和 long 长整型,长整型不存在溢出问题,即可以存放任意大小的整数。在python3后,统一使用了长整型。这也是吸引科研人员的一部分了,适合大数据运算,不会溢出,也不会有其他语言那样还分短整型,整型,长整型...因此python就降低其他行业的学习门槛了。

    那么,不溢出的整型实现上是否可行呢?

    不溢出的整型的可行性

    尽管在 C 语言中,整型所表示的大小是有范围的,但是 python 代码是保存到文本文件中的,也就是说,python代码中并不是一下子就转化成 C 语言的整型的,我们需要重新定义一种数据结构来表示和存储我们新的“整型”。

    怎么来存储呢,既然我们要表示任意大小,那就得用动态的可变长的结构,显然,数组的形式能够胜任:

    [longintrepr.h]
    struct _longobject {
        PyObject_VAR_HEAD
        int *ob_digit;
    };

     

    长整型的保存形式

    长整型在python内部是用一个 int 数组( ob_digit[n] )保存值的. 待存储的数值的低位信息放于低位下标, 高位信息放于高下标.比如要保存 123456789 较大的数字,但我们的int只能保存3位(假设):

    ob_digit[0] = 789;
    ob_digit[1] = 456;
    ob_digit[2] = 123;

    低索引保存的是地位,那么每个 int 元素保存多大的数合适?有同学会认为数组中每个int存放它的上限(2^31 - 1),这样表示大数时,数组长度更短,更省空间。但是,空间确实是更省了,但操作会代码麻烦,比方大数做乘积操作,由于元素之间存在乘法溢出问题,又得多考虑一种溢出的情况。

    怎么来改进呢?在长整型的 ob_digit 中元素理论上可以保存的int类型有 32 位,但是我们只保存 15 位,这样元素之间的乘积就可以只用 int 类型保存即可, 结果做位移操作就能得到尾部和进位 carry 了,定义位移长度为 15

    #define PyLong_SHIFT  15
    #define PyLong_BASE ((digit)1 << PyLong_SHIFT)
    #define PyLong_MASK ((digit)(PyLong_BASE - 1))

    PyLong_MASK 也就是 0b111111111111111 ,通过与它做位运算  的操作就能得到低位数。

    有了这种存放方式,在内存空间允许的情况下,我们就可以存放任意大小的数字了。

     

    长整型的运算

    加法与乘法运算都可以使用我们小学的竖式计算方法,例如对于加法运算:

        ob_digit[2] ob_digit[1] ob_digit[0]
    加数a   23 934 543
    加数b +   454 632
    结果z   24 389 175

    为方便理解,表格展示的是数组中每个元素保存的是 3 位十进制数,计算结果保存在变量z中,那么 z 的数组最多只要 size_a + 1 的空间(两个加数中数组较大的元素个数 + 1),因此对于加法运算,可以这样来处理:

    [longobject.c]
    static PyLongObject * x_add(PyLongObject *a, PyLongObject *b) {
        int size_a = len(a), size_b = len(b);
        PyLongObject *z;
        int i;
        int carry = 0; // 进位
    
        // 确保a是两个加数中较大的一个
        if (size_a < size_b) {
            // 交换两个加数
            swap(a, b);
            swap(&size_a, &size_b);
        }
    
        z = _PyLong_New(size_a + 1);  // 申请一个能容纳size_a+1个元素的长整型对象
        for (i = 0; i < size_b; ++i) {
            carry += a->ob_digit[i] + b->ob_digit[i];
            z->ob_digit[i] = carry & PyLong_MASK;   // 掩码
            carry >>= PyLong_SHIFT;                 // 移除低15位, 得到进位
        }
        for (; i < size_a; ++i) {                   // 单独处理a中高位数字
            carry += a->ob_digit[i];
            z->ob_digit[i] = carry & PyLong_MASK;
            carry >>= PyLong_SHIFT;
        }
        z->ob_digit[i] = carry;
        return long_normalize(z);                   // 整理元素个数
    
    }

    这部分的过程就是,先将两个加数中长度较长的作为第一个加数,再为用于保存结果的 z 申请空间,两个加数从数组从低位向高位计算,处理结果的进位,将结果的低 15 位赋值给 z 相应的位置。最后的 long_normalize(z)是一个整理函数,因为我们 z 申请了 a_size + 1 的空间,但不意味着 z 会全部用到,因此这个函数会做一些调整,去掉多余的空间,数组长度调整至正确的数量,若不方便理解,附录将给出更利于理解的python代码。

    竖式计算不是按个位十位来计算的吗,为什么这边用整个元素?

    竖式计算方法适用与任何进制的数字,我们可以这样来理解,这是一个 32768 (2的15次方) 进制的,那么就可以把数组索引为 0 的元素当做是 “个位”,索引 1 的元素当做是 “十位”。

    乘法运算

    乘法运算一样可以用竖式的计算方式,两个乘数相乘,存放结果的 z 的元素个数为 size_a + size_b 即可:

      操作     ob_digit[2] ob_digit[1] ob_digit[0]
    乘数a       23 934 543
    乘数b *       454 632
    结果z     15 126 631 176
        10 866 282 522  
    结果z   10 881 409 153 176

    这里需要主意的是,当乘数 b 用索引 i 的元素进行计算时,结果 z 也是从 i 索引开始保存。先创建 z 并初始化为 0,这 z 上做累加操作,加法运算则可以利用前面的 x_add 函数:

    // 为方便理解,会与cpython中源码部分稍有不同
    static PyLongObject * x_mul(PyLongObject *a, PyLongObject *b)
    {
        int size_a = len(a), size_b = len(b);
        PyLongObject *z = _PyLong_New(size_a + size_b);
        memset(z->ob_digit, 0, len(z) * sizeof(int)); // z 的数组清 0
    
        for (i = 0; i < size_b; ++i) {
            int carry = 0;          // 用一个int保存元素之间的乘法结果
            int f = b->ob_digit[i]; // 当前乘数b的元素
    
            // 创建一个临时变量,保存当前元素的计算结果,用于累加
            PyLongObject *temp = _PyLong_New(size_a + size_b);
            memset(temp->ob_digit, 0, len(temp) * sizeof(int)); // temp 的数组清 0
    
            int pz = i; // 存放到临时变量的低位
    
            for (j = 0; j < size_a; ++j) {
                carry = f * a[j] + carry;
                temp[pz] = carry & PyLong_MASK;  // 取低15位
                carry = carry >> PyLong_SHIFT;  // 保留进位
                pz ++;
            }
            if (carry){     //  处理进位
                carry += temp[pz];
                temp[pz] = carry & PyLong_MASK;
                carry = carry >> PyLong_SHIFT;
            }
            if (carry){
                temp[pz] += carry & PyLong_MASK;
            }
            temp = long_normalize(temp);
            z = x_add(z, temp);
        }
    
        return z
    
    }

    这大致就是乘法的处理过程,竖式乘法的复杂度是n^2,当数字非常大的时候(数组元素个数超过 70 个)时,python会选择性能更好,更高效的 Karatsuba multiplication 乘法运算方式,这种的算法复杂度是 3nlog3≈3n1.585,当然这种计算方法已经不是今天讨论的内容了。有兴趣的小伙伴可以去了解下。

    总结

    要想支持任意大小的整数运算,首先要找到适合存放整数的方式,本篇介绍了用 int 数组来存放,当然也可以用字符串来存储。找到合适的数据结构后,要重新定义整型的所有运算操作,本篇虽然只介绍了加法和乘法的处理过程,但其实还需要做很多的工作诸如减法,除法,位运算,取模,取余等。

    python代码以文本形式存放,因此最后,还需要一个将字符串形式的数字转换成这种整型结构:

    [longobject.c]
    PyObject * PyLong_FromString(const char *str, char **pend, int base)
    {
    }

    这部分不是本篇的重点,有兴趣的同学可以看看这个转换的过程。

    参考

    附录

    
    # 例子中的表格中,数组元素最多存放3位整数,因此这边设置1000
    # 对应的取低位与取高位也就变成对 1000 取模和取余操作
    PyLong_SHIFT = 1000
    PyLong_MASK = 999
    
    # 以15位长度的二进制
    # PyLong_SHIFT = 15
    # PyLong_MASK = (1 << 15) - 1
    
    def long_normalize(num):
        """
        去掉多余的空间,调整数组的到正确的长度
        eg: [176, 631, 0, 0]  ==>  [176, 631]
        :param num:
        :return:
        """
        end = len(num)
        while end >= 1:
            if num[end - 1] != 0:
                break
            end -= 1
    
        num = num[:end]
        return num
    
    def x_add(a, b):
        size_a = len(a)
        size_b = len(b)
        carry = 0
    
        # 确保 a 是两个加数较大的,较大指的是元素的个数
        if size_a < size_b:
            size_a, size_b = size_b, size_a
            a, b = b, a
    
        z = [0] * (size_a + 1)
        i = 0
        while i < size_b:
            carry += a[i] + b[i]
            z[i] = carry % PyLong_SHIFT
            carry //= PyLong_SHIFT
            i += 1
    
        while i < size_a:
            carry += a[i]
            z[i] = carry % PyLong_SHIFT
            carry //= PyLong_SHIFT
            i += 1
        z[i] = carry
    
        # 去掉多余的空间,数组长度调整至正确的数量
        z = long_normalize(z)
    
        return z
    
    def x_mul(a, b):
        size_a = len(a)
        size_b = len(b)
        z = [0] * (size_a + size_b)
    
        for i in range(size_b):
            carry = 0
            f = b[i]
    
            # 创建一个临时变量
            temp = [0] * (size_a + size_b)
            pz = i
            for j in range(size_a):
                carry += f * a[j]
                temp[pz] = carry % PyLong_SHIFT
                carry //= PyLong_SHIFT
                pz += 1
    
            if carry:    # 处理进位
                carry += temp[pz]
                temp[pz] = carry % PyLong_SHIFT
                carry //= PyLong_SHIFT
                pz += 1
    
            if carry:
                temp[pz] += carry % PyLong_SHIFT
            temp = long_normalize(temp)
            z = x_add(z, temp)   # 累加
    
        return z
    
    a = [543, 934, 23]
    b = [632, 454]
    print(x_add(a, b))
    print(x_mul(a, b))

     

    本文由 hongweipeng 创作

    展开全文
  • 整型提升

    2020-11-05 10:05:30
    用大白话说就是:C语言中字节数少于整型字节数的数据类型在进行整型运算时,该类型的数据会被默认转为整型数据。 其中,该类型的数据被转化为整型数据的过程就称为整型提升。 为什么要进行整型提升? 为什么会发生...

    整型提升

    什么是整型提升?

    首先,我们应该知道这一点:C语言中整型运算总是至少缺省整型类型的精度来进行的。

    这句话什么意思呢?用大白话说就是:C语言中字节数少于整型字节数的数据类型在进行整型运算时,该类型的数据会被默认转为整型数据。

    其中,该类型的数据被转化为整型数据的过程就称为整型提升

    为什么要进行整型提升?

    为什么会发生整型提升呢?这是由计算机体系结构决定的,我们都知道计算机中的计算都由CPU完成,具体来说是由CPU中的运算器(ALU)完成的。而ALU一般在被设计时,其操作对象——操作数的字节大小被要求至少是int的字节数。此时,我们还要明晰一个事情,那就是数据在被运算时,数据并不是直接在ALU上存储的,而是存储在CPU的寄存器(register)中,而通用寄存器的字节大小与ALU操作数的字节大小保持一致。

    这又怎样?空间大又不是不能存储字节数少的数据。是的,话没错,能存,但问题在于我是4个字节的空间(大房间里有四个小房子),你是少于4个字节的数据(不能完全占据整个大房间),我到底该把你安排在哪个小房间里?我把你随便放进去的话,我要用你时又要在4个小房间里查找你到底在哪些房间里,这样势必会让我的效率变低。所以,你在住进来时,不好意思,你至少得把自己变成4个字节的,我再把你直接放进去,用你时我也不用查你在哪,我找到大房间就找到了你。

    可能这会有人会问,那我多于4个字节怎么办?不要担心,你多于4个字节,大不了我给你再安排一间大房子,你一个人住两间总该够了。在64位平台,C语言非自定义数据类型的字节数只有4种(1,2,4,8)。两间大房子妥妥把你舒舒服服住好喽!

    再看非自定义数据类型的字节数取值可能,我们发现只有(1,2)在进行整型运算时,会发生整型提升。而这两个数据类型对应的便是charshort

    说来说去,我们一句话就可以给它总结了:C语言中,表达式中的字符和短整型操作数在使用之前会发生整形提升,变为普通整型

    再来回顾一下问题:为什么要进行整型提升?这是由计算机体系结构决定的!

    怎么进行整型提升?

    大白话:怎么将少于4个字节的数据变成4个字节的数据?

    规则!规则!规则!:整型提升是按照变量的类型来进行提升的。具体点如下:

    a. 如果是无符号数,则高位直接补0;
    b. 如果是有符号数,则高位全补符号位。
    

    对!你没有看错,我也没有说错,就这么两句话!

    举个栗子:

    声明一点:任何数据在计算机中进行存储时,都是它的二进制形态,再精确点就是以补码形态进行存储。

    char c = -1;
    

    c变量的空间里存储着-1的补码(1111 1111),因为char只有一个字节,所以仅有8个比特位。
    这个char定义时没说unsigned char吧?所以此char有符号char.
    那它在进行整型提升时,用规则b。(1111 1111)被char修饰为有符号数,所以它的最高位为符号位!符号位为1,则它进行整型提升时,高位补3个字节的1。
    即(1111 1111 1111 1111 1111 1111 1111 1111

    再来看第二个栗子:

    char c = 1;
    

    c变量的空间里存储着1的补码(0000 0001),因为char只有一个字节,所以仅有8个比特位。
    这个char定义时没说unsigned char吧?所以此char有符号char.
    那它在进行整型提升时,用规则b。(0000 0001)被char修饰为有符号数,所以它的最高位为符号位!符号位为0,则它进行整型提升时,高位补3个字节的0。
    那我对你进行整型提升时,高位补2个字节的0就就行了。
    即(0000 0000 0000 0000 0000 0000 0000 0001

    再来看栗子:

    unsigned short a = -1;
    

    a变量的空间里存储着-1的补码(1111 1111 1111 1111),因为short有两个字节,所以有16个比特位。
    这个short定义时说了unsigned short吧?所以此short有符号short.
    那它在进行整型提升时,用规则a。我管你原来是什么数!现在我就知道你是无符号数!
    那我对你进行整型提升时,我只管高位补2个字节的0就行了。
    即(0000 0000 0000 0000 1111 1111 1111 1111
    今天就是天王老子来了,只要你是无符号数!我照样只管高位补0,把你补成4个字节就行。

    注意注意!注意!

    不要觉得charshort在存储时,就发生了整型提升!发生这个整型提升的前提是它们参与整型运算!!而且整型运算结束后,4个字节的数据将发生截断,再返回值。也就是说,运算完成后,CPU返回值的字节数仍为这个数据原本类型的字节数,而不是提升后的字节数。截断的规则是留低位弃高位。

    理解没理解的关键在于你现在知道整型提升的数据在哪存着吗?

    CPU的寄存器!!!!!寄存器!

    你懂了?上道例题考察你一下!

    int main()
    {
    	char a = 0xb6;
    	short b = 0xb600;
    	int c = 0xb6000000;
    	if(a==0xb6)
    		printf("a");
    	if(b==0xb600)
    		printf("b");
    	if(c==0xb6000000)
    		printf("c");
    	return 0;
    }
    

    你猜程序打印结果是啥?

    abc? ab? ac? bc? b? c?
    

    容我问一句!==在哪里进行?它是逻辑运算,在CPU上进行,所以左右两边的数据在哪?对!寄存器register! 所以数据要不要发生整型提升?要的!

    咱们来剖析一下代码

    if(a==0xb6)
    

    ==左边的a是char型,所以a是有符号数,发生整型提升,a为0xffffffb6;
    ==左边的0xb6是字面常量,是有符号数,写完整是0x000000b6
    相等吗?不相等!条件不成立,不打印!

    if(b==0xb600)
    

    ==左边的b是short型,所以b是有符号数,发生整型提升,b为0xffffb600;
    ==左边的0xb6是字面常量,是有符号数,写完整是0x0000b600
    相等吗?不相等!条件不成立,不打印!

    if(c==0xb6000000)
    

    ==左边的c是int型,右边4个字节数,要发生整型提升吗?不要!
    条件成立。
    所以程序运行结果就是 c
    result

    嘿嘿嘿

    懂了?
    来,思考一下,结果是啥

    	char a = 255;
    	unsigned char b = -1;
    
    	printf("a = %u\n", a);
    	printf("b = %u\n", b);
    

    result2
    再送一句话:
    初始化或赋值时只关心该变量提供的空间;
    提取内容时要先看数据的类型。

    第一句话:不管你定义啥类型变量,我只需要等号右边数据的补码放进你的空间就行
    第二句话:当你要用这个变量时,要先看该变量的定义类型,再看需不需要整型提升,需要的话,怎么提升,提升完了再看读取这个数据的类型。
    result3

    展开全文
  • Java整型变量和整型常量

    千次阅读 2020-01-26 00:01:32
    1 整型分类 2 Java 语言整型常量的四种表示形式 3 长整型常数的声明 1 整型分类 整型用于表示没有小数部分的数值,它允许是负数。整型的范围与运行Java代码的机器无关,这正是Java程序具有很强移植能力的原因之...
  • 整型变量

    千次阅读 2020-02-28 18:32:54
    1、整型变量用于存放整型数据。根据数值的表示范围整型可以为整型(int)、短整型(short)、长整型(long)三种。 2、这三种整型都默认为有符号型(signed),有符号型即可以是正数、负数和0。也可以根据需要,将...
  • C语言的整型溢出问题 整数溢出 int、long int 、long long int 占用字节疑问 《C和指针》中写过:long与int:标准只规定long不小于int的长度,int不小于short的长度。 double与int类型的存储机制不同,long int...
  • pb表示整型数组: 用pb整型数组获取json整型数组 ​​​​
  • C++ - 将长整型数转换为字符串

    万次阅读 多人点赞 2019-02-25 14:24:28
    分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请点击http://www.captainbed.net /* * Created by Chimomo */ #include &lt;... int i =...
  • 整型常量和整型变量

    千次阅读 2019-04-28 03:09:01
    * 整形常量和整型变量 * byte(1个字节)、short(2个字节)、int(4个字节)、long(8个字节) * java语言整型常量的四种标识形式 * 十进制整数:如:99、-500、0 * 八进制整数:要求一0开头,如:015 * 十六...
  • 展开全部Java的整型最大值e69da5e6ba9062616964757a686964616f31333337396166/长整型和短整型最大值及表示法定义代码:publicclassDamon{publicstaticvoidmain(Stringargs[]){longlong_max=java.lang.Long.MAX_VALUE...
  • 整型数据

    2020-04-30 13:53:07
    1、整型数据的分类 (1)基本整型(int型) 编译系统分配给int型数据2个字节或4个字节(由具体的C编译系统自行决定)。如Turbo C 2.0为每一个整型数据分配2个字节(16个二进制位),而Visual C++为每一个整型数据...
  • 整型常量

    千次阅读 2020-02-28 18:19:10
    整型常量的三种表示形式: 十进制整数:由数字0~9和正负号表示。 八进制整数:由数字0开头,后跟数字0~7表示。 十六进制整数:由0x或0X开头,后跟0~9,a~f,A~F表示。 3.3.19.1 vs2013查看整型常量 (1)代码...
  • 整型指针与整型数组指针

    千次阅读 2018-01-30 22:07:55
    整型指针与整型数组指针 int arr[rows][colums]; %二维整型数组 int *pInt; %整型指针 int (*pArr)[colums]; %整型数组指针 { pInt = arr[0]; %指向第“0”行第“0”个元素的整型指针 pArr++; %pInt依次指向第...
  • 整型溢出

    2018-08-05 13:51:21
    什么是整型溢出 C语言的整型问题相信大家并不陌生了。对于整型溢出,分为无符号整型溢出和有符号整型溢出。 对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说...
  • 整型溢出漏洞

    万次阅读 2020-03-02 11:53:19
    整型在计算机系统中的相关知识 0x02. 上界溢出和下界溢出 上界溢出: 下界溢出: 0x03.漏洞利用 0x01.整型在计算机系统中的相关知识 在计算机中,整数类型分为无符号整数和有符号整数 两种。 有符号整数会在...
  • 10进制整型转16进制整型

    千次阅读 多人点赞 2016-06-13 14:46:46
    探究10进制整型转16进制整型  1).16进制字符串与整型的互换:   String str="1A";  int t=Integer.parseInt(str, 16); //将16进制的字符串转换为16进制整型; 注:整型包括各种进制,如2进制,8进制,10进制,16...
  • 在有符号整型和无符号整型的比较中,自动将有符号整型数转换为无符号整型,之后对s和u进行比较。 更多详细的转换规则搜一搜:C语言类型自动转换
  • C++ - 编写一个从字符串转变成长整型的函数

    万次阅读 多人点赞 2019-02-19 19:51:19
    分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请点击http://www.captainbed.net /* * Created by Chimomo */ #include&...long convertStrToLong(...
  • 文章目录`limits.h` 的不足有符号整型的编码计算方法需要注意的地方以 int 传递 short取模位移 limits.h 的不足 通过库 limits.h 中的常量,我们可以得知绝大多数整型的范围。但是其中并没有 long long 类型的取值...
  • 整型与无符号整型相加

    千次阅读 2017-08-24 09:01:13
    整型与无符号整型相加结果怎么算??  定义两个变量i,j.  int i = -20;  unsigned int j = 10;  int m = i + j;  cout  cout  为什么会这样?  因为计算机在处理不同类型数相加时,哪个能表示更大的数...
  • 我下图代码第五行和第九行分别定义了一个整型变量和一个整型常量: static final int number1 = 512; static int number3 = 545; Java程序员都知道两者的区别。 下面我们就用javap将.class文件反编译出来然后...
  • 浅析无符号整型和有符号整型

    千次阅读 多人点赞 2017-11-04 17:20:00
    整型包括长整型(long),短整形(short),整型(int),字符型(char)。众所周知,char是字符类型。但其实char也是一个比较古怪的整型,长度为1字节 (默认是无符号类型)。 然后我们来解释一下有符号数和无符号...
  • C++有符号整型和无符号整型的区别 C++中的整型分为有符号类型整型和无符号类型整型,区别如下。 在有符号类型中:最左边的位是符号位,余下的位是数值位。符号位为1,代表负数;符号位为0,则为正数。例如,0100 ...
  • Java 中字符串转整型整型转字符串 1.字符串转整型Java代码,字符串为纯数字的情况下,调用Integer的静态方法parseInt或者valueOfJava代码,如果单个字符或字符串,需要切开转化为char字符再转化,否则按照上述方法...
  • int a = 1; double b = a / 255; std::cout<<b<<std::endl;

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 96,062
精华内容 38,424
关键字:

整型