前言

计算机要处理的数据(诸如数字,文字,符号,图形,音频,视频等)是以二进制的形式存放在内存中的,我们将八个比特(Bit)称为一个字节(Byte),并将字节作为最小的可操作单元

变量

现实生活中我们会找一个小箱子来存放物品,一来显得不那么凌乱,二来方便以后找到。计算机也是这个道理,我们需要先在内存中找一块区域,规定用它来存放整数,并起一个好记的名字,方便以后查找。这块区域就是“小箱子”,我们可以把整数放进去了。 C语言中这样在内存中找一块区域:


1
int a = 123;

int,它是 Integer 的简写,意思是整数。a 是我们给这块区域起的名字;当然也可以叫其他名字,例如 abc、mn123 等。 这个语句的意思是:在内存中找一块区域,命名为 a,用它来存放整数。

不过int a;仅仅是在内存中找了一块可以保存整数的区域,那么如何将 123、100、999 这样的数字放进去呢? C语言中这样向内存中放整数:
=是一个新符号,它在数学中叫“等于号”,例如 1+2=3,但在C语言中,这个过程叫做赋值。赋值是指把数据放到内存的过程。 把上面的两个语句连起来:
就把 123 放到了一块叫做 a 的内存区域。

a 中的整数不是一成不变的,只要我们需要,随时可以更改。更改的方式就是再次赋值,例如:
第二次赋值,会把第一次的数据覆盖掉,也就是说,a 中最后的值是9999,123、1000 已经不存在了,再也找不回来了。 因为 a 的值可以改变,所以我们给它起了一个形象的名字,叫做变量。
int a;创造了一个变量 a,我们把这个过程叫做变量定义。
a=123;把 123 交给了变量 a,我们把这个过程叫做给变量赋值;

数据类型

内存中的数据有多种解释方式,使用之前必须要确定;上面的int a;就表明,这份数据是整数,不能理解为像素、声音等。

顾名思义,数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义。在C语言中,有多种数据类型,例如:

  • 字符型: char

  • 短整形: short

  • 整形: int

  • 长整型: long

  • 单精度浮点型: float

  • 双精度浮点型: double

  • 无类型: void

连续定义多个变量 为了让程序的书写更加简洁,C语言支持多个变量的连续定义。

数据的长度

所谓数据长度,是指数据占用多少个字节。占用的字节越多,能存储的数据就越多,对于数字来说,值就会更大,反之能存储的数据就有限。 多个数据在内存中是连续存储的,彼此之间没有明显的界限,如果不明确指明数据的长度,计算机就不知道何时存取结束。

数据类型除了指明数据的解释方式,还指明了数据的长度。因为在C语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。 在32位环境中,各种数据类型的长度(字节)一般如下:

  • 字符型char: 1

  • 短整形short: 2

  • 整形int: 4

  • 长整型long: 4

  • 单精度浮点型float: 4

  • 双精度浮点型double: 8

数据是放在内存中的,在内存中存取数据要明确三件事情:数据存储在哪里、数据的长度以及数据的处理方式。 变量名不仅仅是为数据起了一个好记的名字,还告诉我们数据存储在哪里,使用数据时,只要提供变量名即可;而数据类型则指明了数据的长度和处理方式。所以诸如int n;、char c;、float money;这样的形式就确定了数据在内存中的所有要素。 C语言提供的多种数据类型让程序更加灵活和高效,同时也增加了学习成本。而有些编程语言,例如:PHP,JavaScript等,在定义变量时不需要指明数据类型,编译器会根据赋值情况自动推演出数据类型,更加智能。 除了C语言,Java、C++、C#,等在定义变量时也必须指明数据类型,这样的编程语言称为强类型语言。

而PHP、JavaScript等在定义变量时不必指明数据类型,编译系统会自动推演,这样的编程语言称为弱类型语言。 强类型语言一旦确定了数据类型,就不能再赋给其他类型的数据,除非对数据类型进行转换。弱类型语言没有这种限制,一个变量,可以先赋给一个整数,然后再赋给一个字符串。 最后需要说明的是:数据类型只在定义变量时指明,而且必须指明;使用变量时无需再指明,因为此时的数据类型已经确定了。

C语言中的整数(short,int,long)

整数是编程中常用的一种数据,C语言通常使用int来定义整数(int 是 integer 的简写)。 在现代操作系统中,int 一般占用 4 个字节(Byte)的内存,共计 32 位(Bit)。如果不考虑正负数,当所有的位都为 1 时它的值最大,为 232-1 = 4,294,967,295 ≈ 43亿,这是一个很大的数,实际开发中很少用到,而诸如 1、99、12098 等较小的数使用频率反而较高。 使用 4 个字节保存较小的整数绰绰有余,会空闲出两三个字节来,这些字节就白白浪费掉了,不能再被其他数据使用。现在个人电脑的内存都比较大了,配置低的也有 4G,浪费一些内存不会带来明显的损失;而在C语言被发明的早期,或者在单片机和嵌入式系统中,内存都是非常稀缺的资源,所有的程序都在尽力节省内存。

反过来说,43 亿虽然已经很大,但要表示全球人口数量还是不够,必须要让整数占用更多的内存,才能表示更大的值,比如占用 6 个字节或者 8 个字节。
int 是基本的整数类型,short 和 long 是在 int 的基础上进行的扩展,short 可以节省内存,long 可以容纳更大的值。 short、int、long 是C语言中常见的整数类型,其中 int 称为整型,short 称为短整型,long 称为长整型。
整型的长度

整形的长度

只有short 的长度是确定的,是两个字节,而 int 和 long 的长度无法确定,在不同的环境下有不同的表现。
一种数据类型占用的字节数,称为该数据类型的长度。例如,short 占用 2 个字节的内存,那么它的长度就是 2。
实际情况也确实如此,C语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:

  • short 至少占用 2 个字节。

  • int 在32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。

  • short 的长度不能大于 int,long 的长度不能小于 int。

sizeof 用来获取某个数据类型或变量所占用的字节数,如果后面跟的是变量名称,那么可以省略( ),如果跟的是数据类型,就必须带上( )。 需要注意的是,sizeof 是C语言中的操作符,不是函数,所以可以不带( )。

C语言中的二进制数、八进制数和十六进制数

  1. 二进制
    二进制由 0 和 1 两个数字组成,使用时必须以0b或0B(不区分大小写)开头,例如:
  2. 八进制
    八进制由 0~7 八个数字组成,使用时必须以0开头(注意是数字 0,不是字母 o),例如:
  3. 十六进制
    十六进制由数字 09、字母 AF 或 a~f(不区分大小写)组成,使用时必须以0x或0X(不区分大小写)开头,例如:
  4. 十进制
    十进制由 0~9 十个数字组成,没有任何前缀,和我们平时的书写格式一样,不再赘述。

二进制数、八进制数和十六进制数的输出

C语言中常用的整数有 short、int 和 long 三种类型,通过 printf 函数,可以将它们以八进制、十进制和十六进制的形式输出。
下表列出了不同类型的整数、以不同进制的形式输出时对应的格式控制符:

进制/数据类型 short int long
八进制 %ho %o %lo
十进制 %hd %d %ld
十六进制 %hx 或者 %hX %x 或者 %X %lx 或者 %lX

十六进制数字的表示用到了英文字母,有大小写之分,要在格式控制符中体现出来:

  • %hx、%x 和 %lx 中的x小写,表明以小写字母的形式输出十六进制数;

  • %hX、%X 和 %lX 中的X大写,表明以大写字母的形式输出十六进制数。
    八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。如果你比较叛逆,想使用大写形式,那么行为是未定义的,请你慎重:

  • 有些编译器支持大写形式,只不过行为和小写形式一样;

  • 有些编译器不支持大写形式,可能会报错,也可能会导致奇怪的输出。

C语言中的正负数及其输出

在数学中,数字有正负之分。在C语言中也是一样,short、int、long 都可以带上正负号,例如:
如果不带正负号,默认就是正数。 符号也是数字的一部分,也要在内存中体现出来。符号只有正负两种情况,用1位就足以表示;C语言规定,把内存的最高位作为符号位。以 int 为例,它占用 32 位的内存,0~30 位表示数值,31 位表示正负号。

short、int、long 中就没有符号位了,所有的位都用来表示数值,正数的取值范围更大了。这也意味着,使用了 unsigned 后只能表示正数,不能再表示负数了。 如果将一个数字分为符号和数值两部分,那么不加 unsigned 的数字称为有符号数,能表示正数和负数,加了 unsigned 的数字称为无符号数,只能表示正数。 请读者注意一个小细节,如果是unsigned int类型,那么可以省略 int ,只写 unsigned

无符号数可以以八进制、十进制和十六进制的形式输出,它们对应的格式控制符分别为:

进制/数据类型 unsigned short unsigned int unsigned long
八进制 %ho %o %lo
十进制 %hu %u %lu
十六进制 %hx 或者 %hX %x 或者 %X %lx 或者 %lX

整数在内存中是如何存储的

  1. 原码
    将一个整数转换成二进制形式,就是其原码。
    例如short a = 6;
    a的原码就是0000 0000 0000 0110;
    更改a的值a = -18,此时a的原码就是1000 0000 0001 0010。
    通俗的理解,原码就是一个整数本来的二进制形式
  2. 反码
    谈到反码,需要将正数和负数区别对待,因为它们的反码不一样。
    对于正数,它的反码就是其原码(原码和反码相同);
    负数的反码就是将原码中除符号位以外的所有位(数值位)取反,也就是0变成1,1变成0.。
    例如short a = 6,a的原码和反码都是0000 0000 0000 0110;
    更改a的值a = -18,此时a的反码是 1111 1111 1110 1101.
  3. 补码
    正数和负数的补码也不一样,也要区别对待。
    对于正数,它的补码就是其原码(原码、反码、补码都相同);
    负数的补码是其反码加 1。
    例如short a = 6;,a 的原码、反码、补码都是0000 0000 0000 0110;
    更改 a 的值a = -18;,此时 a 的补码是1111 1111 1110 1110。
    可以认为,补码是在反码的基础上打了一个补丁,进行了一下修正,所以叫“补码”。
    原码、反码、补码的概念只对负数有实际意义,对于正数,它们都一样。
    最后我们总结一下 6 和 -18 从原码到补码的转换过程:

在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。将补码转换为原码也很简单:先减去 1,再将数值位取反即可。

补码到底是如何简化电路的
假设 6 和 18 都是 short 类型的,现在我们要计算 6 - 18 的结果,根据运算规则,它等价于 6 + (-18)。
如果采用原码计算,那么运算过程为:
6 - 18 = 6 + (-18) = [0000 0000 0000 0110]原 + [1000 0000 0001 0010]原 = [1000 0000 0001 1000]原 = -24
直接用原码表示整数,让符号位也参与运算,对于类似上面的减法来说,结果显然是不正确的。
于是人们开始继续探索,不断试错,后来设计出了反码。下面就演示了反码运算的过程:
6 - 18 = 6 + (-18) = [0000 0000 0000 0110]反 + [1111 1111 1110 1101]反 = [1111 1111 1111 0011]反 = [1000 0000 0000 1100]原 = -12
这样一来,计算结果就正确了。
然而,这样还不算万事大吉,我们不妨将减数和被减数交换一下位置,也就是计算 18 - 6 的结果:
18 - 6 = 18 + (-6) = [0000 0000 0001 0010]反 + [1111 1111 1111 1001]反 = [1 0000 0000 0000 1011]反 = [0000 0000 0000 1011]原 = 11
按照反码计算的结果是 11,而真实的结果应该是 12 才对,它们相差了 1。
加粗的 1 是加法运算过程中的进位,它溢出了,内存容纳不了了,所以直接截掉。
6 - 18 的结果正确,18 - 6 的结果就不正确,相差 1。按照反码来计算,是不是小数减去大数正确,大数减去小数就不对了,始终相差 1 呢?我们不妨再看两个例子,分别是 5 - 13 和 13 - 5。
5 - 13 的运算过程为:
5 - 13 = 5 + (-13) = [0000 0000 0000 0101]原 + [1000 0000 0000 1101]原 = [0000 0000 0000 0101]反 + [1111 1111 1111 0010]反 = [1111 1111 1111 0111]反 = [1000 0000 0000 1000]原 = -8
13 - 5 的运算过程为:
13 - 5 = 13 + (-5) = [0000 0000 0000 1101]原 + [1000 0000 0000 0101]原 = [0000 0000 0000 1101]反 + [1111 1111 1111 1010]反 = [1 0000 0000 0000 0111]反 = [0000 0000 0000 0111]原 = 7
这足以证明,刚才的猜想是正确的:小数减去大数不会有问题,而大数减去小数的就不对了,结果始终相差 1。
相差的这个 1 要进行纠正,但是又不能影响小数减去大数,怎么办呢?于是人们又绞尽脑汁设计出了补码,给反码打了一个“补丁”,终于把相差的 1 给纠正过来了。
下面演示了按照补码计算的过程:
6 - 18 = 6 + (-18) = [0000 0000 0000 0110]补 + [1111 1111 1110 1110]补 = [1111 1111 1111 0100]补 = [1111 1111 1111 0011]反 = [1000 0000 0000 1100]原 = -12
18 - 6 = 18 + (-6) = [0000 0000 0001 0010]补 + [1111 1111 1111 1010]补 = [1 0000 0000 0000 1100]补 = [0000 0000 0000 1100]反 = [0000 0000 0000 1100]原 = 12
5 - 13 = 5 + (-13) = [0000 0000 0000 0101]补 + [1111 1111 1111 0011]补 = [1111 1111 1111 1000]补 = [1000 1111 1111 0111]反 = [1000 0000 0000 1000]原 = -8
13 - 5 = 13 + (-5) = [0000 0000 0000 1101]补 + [1111 1111 1111 1011]补 = [1 0000 0000 0000 1000]补 = [0000 0000 0000 1000]反 = [0000 0000 0000 1000]原 = 8
你看,采用补码的形式正好把相差的 1 纠正过来,也没有影响到小数减去大数,这个“补丁”真是巧妙。
小数减去大数,结果为负数,之前(负数从反码转换为补码要加 1)加上的 1,后来(负数从补码转换为反码要减 1)还要减去,正好抵消掉,所以不会受影响。
而大数减去小数,结果为正数,之前(负数从反码转换为补码要加 1)加上的 1,后来(正数的补码和反码相同,从补码转换为反码不用减 1)就没有再减去,不能抵消掉,这就相当于给计算结果多加了一个 1。

C语言中的小数(float,double)

小数分为整数部分和小数部分,它们由点号.分隔,例如 0.0、75.0、4.023、0.27、-937.198 -0.27 等都是合法的小数,这是最常见的小数形式,我们将它称为十进制形式。 此外,小数也可以采用指数形式,例如 7.25×102、0.0368×105、100.22×10-2、-27.36×10-3 等。任何小数都可以用指数形式来表示。 C语言同时支持以上两种形式的小数。但是在书写时,C语言中的指数形式和数学中的指数形式有所差异。 C语言中小数的指数形式为:aEn 或 aen
a 为尾数部分,是一个十进制数;n 为指数部分,是一个十进制整数;E或e是固定的字符,用于分割尾数部分和指数部分。整个表达式等价于 a×10n。 指数形式的小数举例:

  • 2.1E5 = 2.1×10 5,其中 2.1 是尾数,5 是指数。

  • 3.7E-2 = 3.7×10-2,其中 3.7 是尾数,-2 是指数。

  • 0.5E7 = 0.5×10 7,其中 0.5 是尾数,7 是指数。

小数的输出

小数也可以使用 printf 函数输出,包括十进制形式和指数形式,它们对应的格式控制符分别是:

  • %f 以十进制形式输出 float 类型;

  • %lf 以十进制形式输出 double 类型;

  • %e 以指数形式输出 float 类型,输出结果中的 e 小写;

  • %E 以指数形式输出 float 类型,输出结果中的 E 大写;

  • %le 以指数形式输出 double 类型,输出结果中的 e 小写;

  • %lE 以指数形式输出 double 类型,输出结果中的 E 大写。

数字的后缀

一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是 double 类型。 请看下面的例子:
long a = 100;int b = 294;float x = 52.55;double y = 18.6;
100 和 294 这两个数字默认都是 int 类型的,将 100 赋值给 a,必须先从 int 类型转换为 long 类型,而将 294 赋值给 b 就不用转换了。 52.55 和 18.6 这两个数字默认都是 double 类型的,将 52.55 赋值给 x,必须先从 double 类型转换为 float 类型,而将 18.6 赋值给 y 就不用转换了。 如果不想让数字使用默认的类型,那么可以给数字加上后缀,手动指明类型:

  • 在整数后面紧跟 l 或者 L(不区分大小写)表明该数字是 long 类型;

  • 在小数后面紧跟 f 或者 F(不区分大小写)表明该数字是 float 类型。