C语言数据类型

数据是程序的重要组成部分,而数据在计算机中存储、数据的属性以及对数据的处理自然而然成了我们关注和学习的重点部分。下面就C语言数据相关知识做一总结。

基本数据类型
C语言中包括以下几种数据类型:整型、字符型、浮点型、空类型、指针、聚合类型(数组和结构),除此之外的其它更为复杂的类型多是通过这些基本类型的组合而来的。
这里写图片描述

整形

整型包括短整型(short int)、整型(int)、长整型(long int),它们都分为有符号(signed)和无符号(unsigned)两种版本。

听上去“长整型”似乎比“短整型”所能表示的值得范围要长一点,可事实上,这个情况并不一定成立。标准规定:“整型” 应至少和 “短整型” 一样长,而“长整型” 应至少和 “整型” 一样长。

下面是它们能表示的范围(在64位平台下测试值):

数据类型范围
short int-32768 ~ 32767
unsigned short int0 ~ 65535
int-2147483648 ~ 2147483647
unsigned int0 ~ 4294967295
long int-9223372036854775808 ~ 9223372036854775807
unsigned long int0 ~ 18446744073709551615

在头文件 limits.h 中宏定义了上面这些类型的最小值和最大值,它们具体如下:

类型最小值最大值无符号最大值
短整型SHRT_MINSHRT_MAXUSHRT_MAX
整形INT_MININT_MAXUINT_MAX
长整型LONG_MINLONG_MAXULONG_MAX

下面是对这些宏的测试(Ubuntu下):
测试代码:

#include <stdio.h>
#include <limits.h>

int main()
{
    printf("SHRT_MIN: %d\n", SHRT_MIN);//短整型最小值
    printf("SHRT_MAX: %d\n", SHRT_MAX);//短整型最大值
    printf("USHRT_MAX: %u\n", USHRT_MAX);//无符号短整型最大值
    printf("INT_MIN: %d\n", INT_MIN);//整形最小值
    printf("INT_MAX: %d\n", INT_MAX);//整形最大值
    printf("UINT_MAX: %u\n", UINT_MAX);//无符号整形最大值
    printf("LONG_MIN: %ld\n", LONG_MIN);//长整型最小值
    printf("LONG_MAX: %ld\n", LONG_MAX);//长整型最大值
    printf("ULONG_MAX: %lu\n", ULONG_MAX);//无符号长整型最大值

    return 0;
}

输出:
这里写图片描述

字符型

实际上字符型应该被归为整形一类,因为它在本质上除了所占位数比整形少(占8位)之外,没有其他不同。

下面是字符型的范围:

类型范围
signed char-128 ~ 127
unsigned char0 ~ 255

字符型在内存中占 8 个位,对于无符号字符型,8 个位都用来表示数值大小,因此很好理解无符号字符型的范围是:0 ~ 255 (xxxx xxxx有256种排列组合)。对于有符号整型,它的二进制的最高位表示正负,0 表示正, 1表示负, 比如 -1 为 1000 0001,所以 siged char 只有低 7 位表示数值大小。因此有符号字符的范围为:正数:+0(0000 0000) ~ 127(0111 1111),负数:-127(1111 1111) ~ -0(1000 0000),这样表示的话范围就变成了 -127 ~ -0,+0 ~ 127 了,与上面表格中的就不一致了,如何解决这个问题呢?

我们先来复习一下整形数据在计算机内部的存储形式。我们知道,数据在计算机内部是以二进制的 01 序列来存储的,对于无符号数,所有的位都用来表示数值大小,而对于有符号数,最高位表示正负,剩余的位表示数值。而计算机在做减法运算时,会将其转化成加法,比如:1-3 会被转换为 1+(-3),如果直接以转换后的二进制计算的话,结果将是错误的,比如:1+(-3)=-2(0000 0001 + 1000 0011 = 1000 0100)。所以,人们就创造了反码和补码来解决这种问题。
下面是原码反码补码的概念:

  • 原码:原码就是数值的二进制表示,上面提到的都是以原码表示;
  • 反码:规定正数的反码和原码相同,负数的反码是原码除符号位之外其余位全部取反,即0变1,1变0;
  • 补码:补码等于反码加1。

在计算机中,数据都以其补码形式存储。
这里写图片描述
它们之间的运算也以补码形式来进行,现在我们来看看上面那个算式。
1 的补码: 0000 0001
-3的补码:1111 1101 (1000 0011–>1111 1100–>1111 1101)

    0000 0001 ---> 1
+   1111 1101 ---> -3
_____________
    1111 1110 ---> -2

此时的计算结果是正确的。为什么这样计算出的结果是正确的呢?举个例子:
以我们生活中常见的钟表为例,假如此时指针指向 2 点,我想让指针指到7点处,我有两种做法,第一种让指针顺时针走 5 格;第二种让指针逆时针走 7 格,即此处我们可以将 2-7 写成 2+5(-7)。而对于二进制数的运算,计算机正是采用了这种巧妙的方式,将所有的计算都转换为加法。

现在我们来看看字符型范围的问题。
在上面的讨论中,我们发现 0 有两种表示:

  • +0:原码 0000 0000,反码 0000 0000, 补码 0000 0000
  • -0:原码 1000 0000,反码 1111 1111,补码 10000 0000去掉溢出的一位后是0000 0000

+0 = -0 = 0,如果两者都存在的话,显然不合理,因此规定 +0 的那种表示0,而 -0 的这种表示-128。为什么是-128呢?
原因是: 0xxx xxxx 有128中排列组合,刚好对应 0 ~ 127(0000 0000 ~ 0111 1111), 1xxx xxxx 也有128中排列组合,如果对应-0 ~ -127 (1 0000 0000 ~ 1000 0001补码形式)的话,就会出现问题两个问题:

  • 第一:-0 的补码是1 0000 0000 ,显然溢出了;
  • 第二: 综合上述的所有补码的表述形式,发现 1000 0000 这种排列组合没有用到;

对于上面的第一个问题,既然 -0 的补码无法在计算机内以补码的形式存储,那就不存储它了,反正有个 +0(0000 0000) 也就够了;对于第二个问题,规定 1000 0000 作为补码时没有源码,它作为 -128 的补码。
这里写图片描述
这样 -128 和有符号字符型的其它数据之间计算也不会产生错误。比如:
-128 + 127 = -1、-127+(-1)=-128

    1000 0000 --> -128             1000 0001 --> -127
+   0111 1111 --> 127          +   1111 1111 --> -1
_____________                  _____________
    1111 1111 --> -1             1 1000 0000 --> -128 (把溢出的位丢掉)

计算结果正确,这就是用 -0 表示 -128 的好处。

但既然 -128 在内存中只存储低八位,那么在输出时从内存中取出的是 1000 0000,将其转换为原码应该是 -0 才对,为什么打印出的 -128呢,这个是C语言隐式类型转换(整型提升)的原因。C语言中有一个简单的规则:对于长度小于整型的数据类型,在做算术运算时,先将其类型提升为整型,执行完运算后,将结果再截短至原有类型。而此处在输出时应该也做了此类的处理。

对于其它类型,诸如 intdouble 的最大值也是采用同样的方法来处理的。

浮点型

浮点数,顾名思义,就是小数点浮动的数,诸如 4.141592、6.02*10^23这样的数值无法用存储整型的规则存储,第一个并非整数,而第二个数远远超出了当前计算机整型所能表示的范围。因此,计算机采用另一种规则来存储这一类数据。

浮点数包括float、double 和 long double,这些类型分别提供单精度、双精度以及在某些机器上的扩展精度三个版本。ANSI 标准规定 long double 至少和 double 一样长, 而 double 至少和 float 一样长,标准还规定:所有浮点数至少要能容纳 -10^37 ~ 10^37 之间的任何值。

在头文件 float.h 中宏定义了这些类型的最小值和最大值。

类型最小值最大值
floatFLT_MINFLT_MAX
doubleDBL_MINDBL_MAX
long doubleLDBL_MINLDB_MAX

下面是对这些宏的测试:
代码:

#include <stdio.h>
#include <limits.h>
#include <float.h>

int main()
{
    printf("FLT_MIN: %f\n", FLT_MIN);
    printf("FLT_MAX: %f\n", FLT_MAX);
    printf("DBL_MIN: %f\n", DBL_MIN);
    printf("DBL_MAX: %f\n", DBL_MAX);
    printf("LDBL_MIN: %Lf\n", LDBL_MIN);
    printf("LDNL_MAX: %Lf\n", LDBL_MAX);
    return 0;
}

结果:
这里写图片描述

可以看到浮点数能表示的范围比整型要大的多的多的多,尤其是 long double。

下面来看浮点数在内存中的存储。
目前所有的C/C++编译器都是采用IEEE所制定的标准浮点格式,即二进制科学表示法:V = (-1)^S * E * 2^M
浮点数的存储分为三个部分:

  • 符号位:S, 0 表示正, 1 表示负;
  • 指数部分:M,实际存储的移码,移码=指数位+127;
  • 尾数部分:E
    比如:4.25 表示为: 100.01 = (-1)^1 * 1.0001 * 2^2,其中符号位、指数位(移码)、位数部分分别为:1、1000 0001(2+127=129=1000 0001)、1.0001。

为什么指数位要以移码的形式存储呢?下面解释一下:
移码是什么,顾名思义,就是在一个数的基础上加个偏移量。对于浮点数的加减计算,怎么做最简单?先比较指数位大小,再移动小数点让两个数的位对应一致,然后将其尾数位(也就是数值)相加,将结果规格化,问题在于如果指数位直接以原码表示的话,在比较浮点数是,需要比较两次符号位:第一次比较数值的符号位,第二次比较指数位的符号位;故给指数位加个偏移量,让负数都变成正数。

单精度(float)在内存中占 32 个bit,其中 1 位符号位,8 位指数位(阶码),23位尾数位,对于尾数位,我们发现它的第一个位(即小数点前的位)总是 1, 既然这样就没必要每次都将其存储下来,故实际上,它能表示的精度变成了24位,24位的二进制数相当于十进制数的多少位呢?我们知道 9 表示成二进制为 1001,也就是说 4 位二进制能表示 1 位十进制,那么 24 位的二进制就可以表示 6 位十进制,因此对于 float, 可以精确到小数点后6位;它的存储方式如下:
这里写图片描述
对于双精度(double),则合单精度相似,它占 64 个 bit, 其中 1 位符号位,11 位指数位(阶码),52 位尾数位,它的存储方式如下:
这里写图片描述

下面是 4.25 在内存的存储形式:
这里写图片描述

空类型

空类型 void 是一种特殊的数据类型,我们无法定义处空类型的变量,因为在定义变量时需要分配内存空间,而空类型占多大的空间呢?没人知道。既然这样,那 void 的存在有什么意义呢?void 有以下几个用处:

  • 第一个是限定函数参数:当函数不需要参数时,可以用void限定:
int fun(void)
{
    ...
}
  • 第二个是限定函数的返回值,当一个函数不需要向调用者返回数值时,可以将返回值声明为 void:
void fun(int a)
{
    ...
}
  • 第三个是空类型的指针,void类型的指针可谓是神通广大,指向任何类型的指针都可以转换为 void* ,而void* 也可以被强转为其它任意类型的指针;因此可以用 void* 作为函数的参数或返回值,这样改函数就可以接收任意类型的指针参数,在实际中,有很多种这样的用法,比如 c语言的 memxxx()...函数族,它们的参数和返回值都是 void*,。
指针类型

指针或许是C语言如此流行至今的一个重要的原因,也是很多程序员欲哭无泪和咬牙切齿的原因。他可以实现很多诸如:list,link,tree 这样的数据结构,也允许你对数据地址的直接访问,这种特性使得C语言可以写出比其它语言效率更高、更紧凑的代码。

那么什么是指针呢?
变量在计算机内存中占据一块地址,而对于每块内存,计算机都会有一个地址编号唯一标识,就像学生宿舍的宿舍号一样。而指针变量存储的就是这些地址编号,你可以获取某个变量所在的那块空间的地址编号,你也可以通过一个地址编号获取某个内存处的内容。

我们很容易将指针和指针所指向的东西搞混淆,事实上,通过生活中的事情,我们恒荣搞明白它俩的含义,就像没人会将宿舍号 218 和 218 宿舍里面的东西高混淆一样。

指针作为C语言的重头角色,我将在后面文章单独讨论。

聚合类型

前面了解到的数据类型同时只能存储一种数据类型,而实际上,很多时候,数据都是以成组的方式存在,比如一个学生(姓名char*、年龄char
、身高float、体重float)。下面的聚合类型则可以将不同类型的数据作为一个整体存储到一起。

  • 结构体(请戳链接),这是我之前总结的结构体知识。

  • 联合体
    联合体和结构体的声明类似,但它们的行为方式却不同。联合体的所有成员引用的是内存中的同一块空间。当你想在不同时刻将不同的数据存储在同一块空间时,就可以用联合体。

union {
    int i;
    char c;
}ic;

32 位机器下 int 占4个字节,而 char 占1一个字节,故 ic 的大小为4个字节,因为他要保证能将所有成员存储下。当 ic 被访问的是 i 的时候,它就被当成整形访问,当 c 被访问时,它就被当成字符型访问。

    ic.i = 1;
    printf("%d\n", ic.c);

由于两个成员共用同一块空间,所以上述的打印结果会是什么呢?我们画个图来看一下:

这里写图片描述
ic 到底是以 a 方式存储呢,还是以 b 方式存储。上述 a 方式,数值的高字节存储于低地址,这种存储方式被称为小端模式,而 b 方式数值的高字节存储在高地址,低字节存储在低地址,这种被称为大端模式。有关大小端的争论,最早来自于《格列夫游记》里一个有趣的故事:

Lilliput和Blefuscu这两个强国在过去的36个月中一直在苦战。战争的原因:大家都知道,吃鸡蛋的时候,原始的方法是打破鸡蛋较大的一端,可以那时的皇帝的祖父由于小时侯吃鸡蛋,按这种方法把手指弄破了,因此他的父亲,就下令,命令所有的子民吃鸡蛋的时候,必须先打破鸡蛋较小的一端,违令者重罚。然后老百姓对此法令极为反感,期间发生了多次叛乱,其中一个皇帝因此送命,另一个丢了王位,产生叛乱的原因就是另一个国家Blefuscu的国王大臣煽动起来的,叛乱平息后,就逃到这个帝国避难。据估计,先后几次有11000余人情愿死也不肯去打破鸡蛋较小的端吃鸡蛋。这个其实讽刺当时英国和法国之间持续的冲突。

理解了大小端,我们再看上面的输出结果。我们常用的 X86 的机器采用的是小端模式,因此,上述结果毫无疑问,是 1;

  • 枚举
    当一个变量仅限几种可能的取值时,就可以将其声明为枚举类型,这种该变量只能取值为已定义的一组中的一个,可以防止用户取无效的值。
enum {
    man,woman
}sex;

则 sex 只能取 manwoman 中的一种。

实际上枚举类型是一个一个的整型值,32 位下,它占 4 个字节,默认情况下,枚举成员的取值从 0 开始,比如上面的 man是0, woman是1,我们也可以给定初始值,像下面这样:

enum {
    man = 3,woman
}sex;

这样,woman 就是4。
下面是测试的结果:
这里写图片描述

有关C语言的基本数据类型就显总结这些。


【作者:果冻 http://blog.csdn.net/jelly_9

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值