c/c 之数据类型 -凯发k8官方网
tips:
1. 本人当初学习c/c 的记录。
2. 资源很多都是来自网上的,如有凯发k8官方网的版权请及时告知!
3. 可能会有些错误。如果看到,希望能指出,以此共勉!
ansi c99标准中规定的数据类型如下图所示。
说明:
- 同一行类型的语义相同,即可以相互替代。
- long float类型与double相同,故在c99标准中没有此类型。
- 部分编译器也提供了unsigned float和unsigned double,最好不要使用,以免影响程序的可移植性。
- int默认是signed,所以int, signed, signed int三者等价。其它unsigned的情况类似。char默认情况不确定。
c语言中数据类型的长度
ansi c99标准中定义了两类(四个)类型修饰符:long/short和unsigned/signed。c99标准规定,long类型不能比变通类型短,short类型不能比普通类型长。而unsigned与signed的区别在实现上是有无符号的区别,而是使用上是取值范围的区别,两者表示范围相同,但前者全是正数,后者关于0对称。
说明:
- long/short可以修饰int,long还可以修饰double。
- unsigned/signed可以修饰int、char,不可以修饰浮点型。
- int长度是机器的字长,short int是半个字长,long int是一个或两个字长。
- unsigned/signed长度与普通类型一样,只是表示区间不同。
c语言中数据类型的转换
类型转换分为显示和隐式两种,前者比较简单,这里只讲后者。下面是c99标准中给出的各种类型对象的基本转换规则:
- 枚举常量: 转换成int,如超出int范围,则转成long int等
- 浮点型:
- 如果转成整类型,只保留整数部分,如果超出整型表示范围,则转换错误;
- 如果向上转成double/long double,值不变;
- 如果向下转成float/double等,如果能用float/double表示,则正常,如果超出表示范围,则转换错误,而如果在表示范围内,但精度降低,则要依赖于编译器的处理了
整型: short int/char/枚举类型/位类型都可转换成int,如果超出int表示范围,则提升到unsigned int。
对于二元运算符中的普通算术运算转换,c99标准给出了如下图所示的转换规则:
说明:对于unsigned char和unsigned short int的整型提升问题,c99标准给出“保值”的转换方法:方法是将unsigned char和unsigned short int转换成int,如果超出表示范围,则转成unsigned int。
- 对于表格中第五行,long int与unsigned int的情况,在vc6.0没有这样实现,是直接转成unsigned int。
面向对象编程(oop)的本质是设计并扩展自己的数据类型。c 兼容c的数据类型,又稍有区别。当然,下面的数据类型说明多数同样适用于c类型,部分不同于c的地方将单独指出。
基本类型
基本类型主要就是整型和浮点型,同时对这两种进行了多种变形。(c 11新增了bool类型,兼容c99)
整型
基本整型包括:char、short、int、long和c 11中新增的long long(兼容c99)。其中每种类型都有有符号版和无符号版。
1. 每种数据类型都有一定的数据范围,不同的系统可能有不能的范围。c 采用灵活的标准,确保了数据类型的最小长度(c语言可能不同):
- short 至少16位
- int 至少与short一样长
- long 至少32位,且至少与int一样长
- long long 至少64位,且至少与long一样长
在头文件climits(旧limits.h)中包含了关于整型限制的信息。如下图(vs2010):
数据类型的范围是怎么算出来的。(c及c ) - 计算机中数据都是以二进制存储;
- 二进制可以由不同的编码(原码、补码、反码)表示,计算机统一采用补码表示。
- 计算机中的正负号,0表示正数,1表示负数
以2字节(16位)有符号类型来说:
范围: (10进制)-32768到32767;(16进制)8000到7fff;(2进制补码)1000,0000,0000 ,0000到0111,1111,1111,1111
原码:最高位为符号位,其余位与正常二进制表示方法一致;
原码表示范围:
最大为0111111111111111 = 2^15-1 = 32767
最小为1111111111111111 = -2^15-1 = -32767
0和-0:
0:0000000000000000
-0:1000000000000000
即正零与负零表示方法不同。也就意味着:原码能表示的有符号数范围是:-32767~-0和0~32767
补码: 正数补码与原码相同,负数补码需要把除符号位以外的原码取反加1
补码表示范围:
最大为0111111111111111 = 2^15-1 = 32767
最小为1000000000000001 = 原码:1111111111111111的符号不变其余取反为:1000000000000000,再加1为:1000000000000001
0和-0:
0:0000000000000000 // 与原码相同
-0:0000000000000000 // 原码:1000000000000000的符号不变其余取反为:1111111111111111,再加1为:0000000000000000(进位舍掉)
也就是正0和负0在补码系统中的编码是一样的。也就是补码会比原码多一个编码出来,这个编码就是1000000000000000。因为任何一个原码都不可能在转成补码时变成1000000000000000。所以,人为规定1000000000000000这个补码编码为-32768。所以,补码系统中,范围是-23768~32767。
- c 中,对于变量的赋值更加灵活,原来用于结构体和数组的赋值,现在对于单个变量也是可以的。
- c 如何确定整型常数的类型
- 对于有字符修饰的整型数字,根据字符来判断类型。例如:123l,则为long;123ul为unsigned long。
- 对于没有符号修饰的十进制数字,c 总是采用int、unsigned int、long、unsigned long、long long (没有short)中能够存储该数的最小类型表示。
- 对于没有符号修饰的十六进制或者八进制,则总是以对应的无符号类型表示。
浮点型
c 基本浮点型包括:float、double、long double。unsigned和signed不能修饰浮点型
c/c 对于浮点型有效位的规定:
- float至少32位
- double 至少48位,且不少于float
- long double 至少和double一样多
注意:默认情况下,浮点常数字为double型,例如程序中直接写1.0,其被当做double型数字。
在头文件cfloat(旧float.h)中包含了关于浮点型限制的信息(有些系统没有提供该文件)。如下图:
从上表中可看到,在vs2010中,double的有效位数为15,float的有效位数为6
浮点数的存储
c/c 编译器都是按照ieee的浮点数表示法,即一种科学计数法,用符号、指数和尾数来表示,底数为2。也就是把浮点数表示为尾数乘以2的指数次方再添加上符号的形式。因为科学技术法 a×bm的形式,a介于1~10,而浮点数表示法中,a始终为1,所以在最终的表示结果中,这个1被略去。即:尾数二进制最高位的1不要
具体规格是:
float | 1 | 8 | 23 | 32 |
double | 1 | 11 | 52 | 64 |
下面通过例子来解释上面的表示规格:
- 38414.4表示为double:
- 分开整数和小数部分,整数化为16进制,0x960e;小数部分为:0.4=0.5×0 0.25×1 0.125×1 …… 0.5×(1 or 0)/n ……。//实际上这永远算不完!
- 有的小数可以穷尽,有的是永远不会穷尽的,此时只需要提取出各项的系数,即011……,这些项的和加上整数部分共53位就可以了。正如上面所言的,最高为不变的1可以省略(归一化),最终是53-1=52位。
- 38414.4可以表示为1001011000001110.0110011001100110011001100110011001100b。
- 用科学计数法表示为1.0010110000011100110011001100110011001100110011001100×215。
- 然后计算阶码,阶码共11位,可以表示-1024~1023,因为指数可以为负数,规定先加上1023变为非负数(指数偏移),上面的15表示为15 1023=1038,二进制为10000001110。符号位,0为正,1为负。所以最终结果是
- 0 10000001110 0010110000011100110011001100110011001100110011001100
- 颜色与上表对应。
- 3490593表示为float:
- 3490593的浮点数为3490593.0。
- 整数化为二进制,为1101010100001100100001b,即1.101010100001100100001×221,由于float的尾数有23位,需要补0。即1.10101010000110010000100×221。
- 计算阶码时,类似double的表示,阶码共8位,表示的范围是-128~127,为了方便,加上127,上面的21表示为21 127=148=10010100b。最终结果是:
- 0 10010100 10101010000110010000100
- 颜色与上表对应。
- 0.5的二进制表示:
- 上面给出了0.4的二进制表示的计算方法:
- 0.4 = 0.5×0 0.25×1 0.125×1 …… 0.5×(1 or 0)/n ……。
- 它是无穷尽的,直到精度合适了为止。
- 然而对于有的数来说,是有穷的,比如0.5=1×0.5。整数部分为0,小数部分为0.1,所以0.5的二进制形式是0.1,即1.0 × 2-1。
- 计算阶码时,用127 (-1)=126=b1111110b。所以最终结果是:
- 0 01111110 00000000000000000000000
- 颜色与上表对应。
- -12.5的二进制浮点表示:
- 整数部分为12,即1100b;小数部分为0.5,即0.1b,即1100.10000000000000000000,即1.10010000000000000000000 × 23。
- 计算阶码,3 127=130,即10000010b,所以最终结果是:
- 1 10000010 10010000000000000000000
- 颜色与上表对应。
- 逆向求取,1011 1101 0100 0000 0000 0000 0000 0000转为十进制:
- 1011 1101 0100 0000 0000 0000 0000 0000为:
- 1 01111010 10000000000000000000000
- 所以该数为-1.10000000000000000000000 × 201111010-127=-5 = -0.000011b = 0.046875
详细见http://blog.163.com/yql_bl/blog/static/847851692008112013117685/
有了以上知识,那么printf(“%f”,10/3);的结果是什么?结果是0.0000
10/3的结果无疑应该是3,但是,我们却要求printf按照浮点数来去这个数,通过以上我们知道,整数和浮点数的存储方式是不一样的。
整型数3在内存存储如下:
0000 0000 0000 0000 0000 0000 0000 0011
但是现在我们要用浮点数的方式来解析这32位数字。按照浮点数方式:
0000 0000 0000 0000 0000 0000 0000 0011
上面红色是符号为0,表示正数;蓝色的是指数位,结果为0,但是这儿要注意的一点是指数在存储的时候是进行过偏移的,所以这儿要剪掉127,所以指数为-127。最后的紫色是尾数,结果是2^(-22) 2^(-23),但是也要注意一点是,尾数在进行存储的时候是归一化过的,小数点前面其实有个1,所以最后尾数是1 2^(-22) 2^(-23)。所以最后的浮点数是:[1 2^(-22) 2^(-23)]*2^(-127)转化为可读数字就是5.87747385606e-39 ,这个数就非常小了,所以显示的时候就是0.000000啦。
数据类型转换
隐式类型转换
在某些情况下,c 将自动对数据类型进行转换:
- 不同数据类型之间的赋值 例如:int a = 1.0;目标类型是被赋值对象的类型。
- 算数表达式中存在不同数据类型的数运算,例如int a = 1;double b = 2.0; int c = a b;
- 将一个表达式作为实参传递给函数调用,此时形参和实参类型不一致:目标转换类型为形参的类型
- 从一个函数返回一个表达式,表达式类型与返回类型不一致:目标转换类型为函数的返回类型
c 11表达中,不同数据进行运算时的校验规则(与c语言稍有区别): - 如果其中有一个数为long double,那么另一个就被转换为long double
- 否则,如果有一个数double,那么另一个就被转换为double
- 否则,如果有一个数为float,那么另一个数就被转换为float
- 否则,否则说明操作数都是整型,执行整型提升
- 如果两个操作数同是有符号或同是无符号,这转换为等级较高的类型进行运算
- 如果一个有符号一个无符号,且无符号类型级别高,这转换无符号数运算
- 否则,如果有符号可以表示所有无符号取值,这转换为有符号类型运算
- 否则,将两个数都转换为有符号数的无符号版本运算
显示类型转换
(typename)value; // c语言风格 typename(value); // c 风格此外,c 还提供了四个关键字来实现转换,与传统强制转换相比,其转换更加严格
static_cast显示对浮点数进行强制转换时,规则如下:
较大浮点转较小浮点,例如double转float精度降低,如果超出float范围,结果不确定浮点型转整型 | 小数被省略,如果超出float范围,结果不确定 |
较大整型转较小整型,例如long转int | 如果超出int范围,通常只复制右边的值 |
潜在的数据转换问题
复合数据类型由基本数据类型组成,c 中类就是一种符合数据类型。此外数组、字符串、结构体、共同体、枚举、指针和自由存储空间都作为复合数据类型
数组(c/c )
如果只对数组部分赋值,后面的默认为零。
不能将一个数组赋值给另一个数组。
指针和二维数组(c/c )
指针
首先,指针也是一个变量,其在内存中一般占用4个字节。比较特殊的是,指针变量中存放的值是一个地址。例如:
int *p;
这里,定义了一个指针,编译器在内存中拿出4个字节,名字叫p,里面存放一个4字节的地址。对于未初始化的指针,其值是随机的,很危险!
常见的指针操作:*与 、–
指针和引用的联系与区别(仅c )
(1)指针是一个实体,而引用仅是个别名;
(2)引用使用时无需解引用(*),指针需要解引用;
(3)引用只能在定义时被初始化一次,之后不可变;指针可变;
(4)引用没有 const,指针有 const;
(5)引用不能为空,指针可以为空;
(6)“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
(7)指针和引用的自增( )运算意义不一样;
(8)从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
在说明指针的时候,有必要额外说明一下二维数组。
二维数组
有很多地方说数组就是指针,这是错误的一种说法。这两者是不同的数据结构。其实,在c/c 中没有所谓的二维数组,书面表达就是数组的数组。为了表述方便才叫它二维数组。二维数组在概念上是二维的,即其下标在两个方向上变化,下标变量在数组中的位置也处于一个平面之中,而不是像一维数组只是一个向量。但是,实际的硬件存储器却是连续编址的,也就是说存储器单元是按一维线性排列的。如何在一维存储器中存放二维数组,可有两种方式:一种是按行排放, 即放完一行之后顺次放入第二行。另一种是按列排放, 即放完一列之后再顺次放入第二列。
在c语言中,二维数组是按行排列的。即,先存放a[0]行,再存放a[1]行,最后存放a[n]行。每行中的元素也是依次存放。例如对数组a[5][3]赋值两种方式(结果完全相同):
- 按行分段赋值可写为:
int a[5][3]={ {80,75,92}, {61,65,71}, {59,63,70}, {85,87,90}, {76,77,85} }; - 按行连续赋值可写为:
int a[5][3]={ 80,75,92,61,65,71,59,63,70,85,87,90,76,77,85};
注意:
- 可以只对部分元素赋初值,未赋初值的元素自动取0值。
- 如果对全部元素赋初值,则第一维的长度可以不给出,例如:int a[][3]={1,2,3,4,5,6,7,8,9};
二维数组一维化
我们可以用一个指向int型的指针变量来访问这个数组,下面的代码是将数组一维化(以上面的a数组为例):
int *p = a[0]; // 这样就可以用 p 访问每个元素了 p[3] // 第三个元素 *(p 3) // 这个 = p[3]这样就实现了将二维数组一维化,通过p访问的是每个元素,而不是行
数组指针和指针数组
指针数组: 指针数组就是个数组,只不过元素是指针。定义方式如:int *p[3]; 表示三个指针,分别为:p[0]、p[1]、p[2]
数组指针: 指向数组的指针。定义方式如:int (*p)[3]; 表示 p指向的是一个数组元素为int类型并且数组元素的个数为3的一个指针。
上例中,parr是个数组指针,每次 1是移动一行,不是一个元素。比如说,parr 1代表的现在指针已经指向第一行元素了(0行开始),而要取得指针所指的对象,就要用到解引用运算符,所以(parr 1)就代表第一行数组,是整个这一行元素就取到了,那现在要取这一行的第二个元素,只须将指针再移动两个元素,即*(iarr 1) 2,这样就指向了这个元素的地址,再解引用取得元素的值即可。
也许我们应该这样来数组指针:
int (*)[10] p2;
int (*)[10]是指针类型,p2 是指针变量。这样看起来的确不错,不过就是样子有些别扭。其实数组指针的原型确实就是这样子的,只不过为了方便与好看把指针变量p2 前移了而已。
既然这样,那问题就来了。现在再来看看下面的代码:
上面对p3 和p4 的使用,哪个正确呢?p3 1 的值会是什么?p4 1 的值又会是什么?
毫无疑问,p3 和p4 都是数组指针,指向的是整个数组。&a 是整个数组的首地址,a是数组首元素的首地址,其值相同但意义不同。在c 语言里,赋值符号“=”号两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。p3 这个定义的“=”号两边的数据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。在visual c 6.0 上给出如下警告:
warning c4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。
还好,这里虽然给出了警告,但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过我仍然警告你别这么用。
但是如果修改一下代码,把数组大小改小点,会有什么问题?p3 1 和p4 1 的值又是多少呢?
或把数组大小改大点:
int main() {char a[5]={'a','b','c','d'};char (*p3)[10] = &a;char (*p4)[10] = a;return 0; }测试结果:把数组大小改变,都会编译不通过。
地址的强制转换
以下,以x86 windows为例
#include下面分析上面的数据结果
ptr1: a为数组名,那么&a 1不是增一个int,而是(int*)(a的地址 sizeof(a)),因此ptr1指向了数组结尾的第一个字节。
可以这样理解:不管是增1还是减1,这里的1都是sizeof(类型),上面对a取地址,可认为此时类型为int a[4],这里增的是sizeof(a)
ptr2: 任何数值一旦被强制转换,其类型就改变了。这里实际上就是将地址a,转换为了数,然后 1,把转换后的数再次转换为地址。如下图:
字符串
c 有两种风格的字符串,一种是c语言风格的,一种是c 语言风格string。
c语言风格字符串
c语言风格的字符串以空字符结尾,空字符被写作\0,ascii码为0。c不会检查字符串长度是否越界。
对于c风格的字符串操作一般通过库函数来实现,在头文件string.h中(c cstring)包换大量字符串操作的函数。
要保证目的字符串可以容纳原字符串,否则,编译不会出错,但是运行时,会出现错误:stack around the variable ‘xxx’ was corrupted.
c 字符串 string
ios/ansi c 98标准添加了string类,使用者可以直接将它作为一种数据类型来用,定义字符串变量(对象)。要使用string类必须包含头文件string,而且string位于std命名空间中。
详细的使用方法见文件: 双击图标查看!
结构体
无论在c还是c 中,结构体都是很常用的一种数据类型。结构体名,用作结构体类型的标志,它又称结构体标记。大括号内是该结构体中的成员列表,又称为域表。
结构体的内存对齐
结构体内存分配的原则:编译器按照成员列表顺序一个接一个地给每个成员分配内存。只有当存储成员需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间。如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。此外,合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在vc/c 和gnu gcc中都是默认是4字节对齐。
结构体字节对齐的细节和具体编译器实现相关,但一般而言满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
位域(位段)
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,c语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。定义方式如下:
struct 位域结构名 { 类型说明符 位域名:位域长度 }; 例: struct bs {int a:8; // 8个二进制位int b:2; // 2个二进制位int c:6; // 6个二进制位 };位域需要遵循以下规则:
1. 位域的长度不能大于数据类型本身的长度,比如int类型就能超过32位二进位。有其他人说是不能超过8位,我在我的机子上是可以实现int :32的位域长度的。
2. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的
3. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
4. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
5. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,vc6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),dev-c 和gcc都采取压缩方式;
6. 如果位域字段之间穿插着非位域字段,则不进行压缩
7. 整个结构体的总大小为最宽基本类型成员大小的整数倍
8. c99规定int、unsigned int和bool可以作为位域类型
系统会先为结构体成员按照对齐方式分配空间和填塞(padding),然后对变量进行位域操作。
举例如下:
#include如上代码,执行结果如下:
分析:
高位 00110100 00110011 00110001 00110000 低位
‘4’ ‘3’ ‘1’ ‘0’ // 以上二进制位字符的ascii码
其中d.a和d.b共同占用低位一个字节(00110000), d.a : 10000, d.b : 001
然后,int 是有符号的。所以d.a对应的数为11111111 11111111 11111111 11110000;d.b对应的二进制为10000000 00000000 00000000 00000001
同理,如果int a:5改为了int a:16,此时,d.a对应的值就是10000000 00000000 00110001 00110000
共同体(联合体)
共同体是一种数据格式,它能够存储不同的数据类型,但同时只能存储一种。
匿名共同体:定义时,直接省去共同体的名称,但这里一般同时定义一个对象,因为没有名字以后就没法定义了!除非是放在其他结构里面,可以不定义对象。
关于共同体的嵌套
注:结构体与联合体有何区别?
枚举
1、枚举值默认从零开始,后面的比前面的增加1
2、c早期版本规定,枚举赋值必须是int型,现在该限制被取消了,赋值可以是long、long long
3、可以定义具有相同值的枚举值
在c 98中enum变量的实际大小由编译器决定,只要能够保存enum的成员即可,而在将要发布的新的c 0x中,可以指定enum的实际实现类型,如实现为int类型。
enum month:int{ jan, feb, …, dec }
其他类型:自由存储
单独说明
总结
- 上一篇: c/c 之 c发展史及 各标准特性说
- 下一篇: c/c 之常用关键字