3.基础数据类型
参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
****:C语音深入剖析班(国嵌 唐老师主讲)
数据在内存中的存储
计算机要处理的信息是多种多样的,如数字、文字、符号、图形、音频、视频等,这些信息在人们的眼里是不同的。但对于计算机来说,它们在内存中都是一样的,都是以二进制的形式来表示。
数据是放在内存中的,而运算时在寄存器中进行的
#include <stdio.h>
#include <stdlib.h>
void main()
{
int a = 10,b = 20,c;
printf("a=%x,b=%x,c=%x\n", &a, &b, &c);
_asm //嵌入汇编语句
{
mov eax, a; //把a的值放到eax寄存器中
add eax, b; //把b的值与eax中的值进行运算,并把运算的结果放到eax存器中
mov c, eax;
}
printf("c = %d\,c = %x\n", c,&c);
system("pause");
}
寄存器初始化的状态
mov eax.a;
add eax,b;
运行完后
一般情况下我们不一个一个bit的算,而是将8个bit看做一个单位,即使表示很小的数,例如 1,也需要8个,也就是 00000001。
单位换算:
- 1Byte = 8 Bit
- 1KB = 1024Byte = 210Byte
- 1MB = 1024KB = 220Byte
- 1GB = 1024MB = 230Byte
- 1TB = 1024GB = 240Byte
- 1PB = 1024TB = 250Byte
- 1EB = 1024PB = 260Byte
在内存中没有abc这样的字符,也没有gif、jpg这样的图片,只有0和1两个数字,计算机也只认识0和1。所以,计算机使用二进制,而不是我们熟悉的十进制,写入内存中的数据,都会被转换成0和1的组合。
变量
现实生活中我们会找一个小箱子来存放物品,一来显得不那么凌乱,二来方便以后找到。计算机也是这个道理,我们需要先在内存中找一块区域,规定用它来存放整数,并起一个好记的名字,方便以后查找。这块区域就是“小箱子”,我们可以把整数放进去了。
C语言中这样在内存中找一块区域:
int a;
这个语句的意思是:在内存中找一块区域,命名为 a,用它来存放整数。
不过 int a;仅仅是在内存中找了一块可以保存整数的区域,那么如何将 123、100、999 这样的数字放进去呢?
C语言中这样向内存中放整数:
a=123;
=
是一个新符号,它在数学中叫“等于号”,例如 1+2=3,但在C语言中,这个过程叫做赋值(Assign)。赋值是指把数据放到内存的过程。
把上面的两个语句连起来:
int a;
a=123;
就把 123 放到了一块叫做 a 的内存区域。你也可以写成一个语句:
int a=123;
a 中的整数不是一成不变的,只要我们需要,随时可以更改。更改的方式就是再次赋值,例如:
int a=123;
a=1000;
a=9999;
第二次赋值,会把第一次的数据覆盖(擦除)掉,也就是说,a 中最后的值是9999,123、1000 已经不存在了,再也找不回来了。
因为 a 的值可以改变,所以我们给它起了一个形象的名字,叫做变量(Variable)。
int a;
创造了一个变量 a,我们把这个过程叫做变量定义。
a=123;
把 123 交给了变量 a,我们把这个过程叫做给变量赋值;又因为是第一次赋值,也称变量的初始化,或者赋初值。
你可以先定义变量,再初始化,例如:
int abc;
abc=999;
也可以在定义的同时进行初始化,例如:
int abc=999;
这两种方式是等价的。
变量声明的意义
为什么要先声明变量?为什么要指定变量的名字和对应的数据类型?
因为
- 建立变量符号表
- 通过声明变量,编译器可以建立变量符号表,这样程序中用了多少变量,每个变量的类型是什么。编译器非常清楚,是否使用了没有声明的变量,编译器在编译期间就可以发现。
- 变量的数据类型指示系统分配多少内存空间
- 变量的数据类型指示了系统如何解释存储空间中的值
- 变量的数据类型确定了该变量的取值范围
- 不同的数据类型有不同的操作方式
数据类型
数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别,那么,00010000 该理解为数字16呢,还是图像中某个像素的颜色呢,还是要发出某个声音呢?如果没有特别指明,我们并不知道。
也就是说,内存中的数据有多种解释方式,使用之前必须要确定;上面的
int a;
就表明,这份数据是整数,不能理解为像素、声音等。int 有一个专业的称呼,叫做数据类型(Data Type)。
顾名思义,数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义。
在C语言中,有多种数据类型,例如:
说 明 | 字符型 | 短整型 | 整型 | 长整型 | 单精度浮点型 | 双精度浮点型 | 无类型 |
---|---|---|---|---|---|---|---|
数据类型 | char | short | int | long | float | double | void |
连续定义多个变量
为了让程序的书写更加简洁,C语言支持多个变量的连续定义,例如:
int a, b, c;
float m = 10.9, n = 20.56;
char p, q = '@';
连续定义的多个变量以逗号,分隔,并且要拥有相同的数据类型;变量可以初始化,也可以不初始化。
数据的长度
所谓数据长度(Length),是指数据占用多少个字节。占用的字节越多,能存储的数据就越多,对于数字来说,值就会更大,反之能存储的数据就有限。这个数据的长度也就相当于存数据的盒子的大小,定义不同的数据类型也就相当于拿不同大小的盒子来存东西。
多个数据在内存中是连续存储的,彼此之间没有明显的界限,如果不明确指明数据的长度,计算机就不知道何时存取结束。例如我们保存了一个整数 1000,它占用4个字节的内存,而读取时却认为它占用3个字节或5个字节,这显然是不正确的。
所以,在定义变量时还要指明数据的长度。而这恰恰是数据类型的另外一个作用。数据类型除了指明数据的解释方式,还指明了数据的长度。
因为在C语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。
在32位环境中,各种数据类型的长度一般如下:
说 明 | 字符型 | 短整型 | 整型 | 长整型 | 单精度浮点型 | 双精度浮点型 |
---|---|---|---|---|---|---|
数据类型 | char | short | int | long | float | double |
长 度 | 1 | 2 | 4 | 4 | 4 | 8 |
数据是放在内存中的,在内存中存取数据要明确三件事情:数据存储在哪里、数据的长度以及数据的处理方式。
变量名不仅仅是为数据起了一个好记的名字,还告诉我们数据存储在哪里,使用数据时,只要提供变量名即可;而数据类型则指明了数据的长度和处理方式。所以诸如int n;
、char c;
、float money;
这样的形式就确定了数据在内存中的所有要素。
数据类型只在定义变量时指明,而且必须指明;使用变量时无需再指明,因为此时的数据类型已经确定了。
数据是放在内存中的,在内存中存取数据要明确三件事情:数据存储在哪里,数据的长度以及数据的处理方式
C语言中的整数(short,int,long)
C语言通常使用int
来定义证书(int
是integer
的简写),在现代操作系统中,int 一般占用 4 个字节(Byte)的内存,共计 32 位(Bit)。如果不考虑正负数,当所有的位都为 1 时它的值最大,为 2^32
-1 = 4,294,967,295 ≈ 43亿,这是一个很大的数,实际开发中很少用到,而诸如 1、99、12098 等较小的数使用频率反而较高。
使用 4 个字节保存较小的整数绰绰有余,会空闲出两三个字节来,这些字节就白白浪费掉了,不能再被其他数据使用。现在个人电脑的内存都比较大了,配置低的也有 2G,浪费一些内存不会带来明显的损失;而在C语言被发明的早期,或者在单片机和嵌入式系统中,内存都是非常稀缺的资源,所有的程序都在尽力节省内存。
反过来说,43 亿虽然已经很大,但要表示全球人口数量还是不够,必须要让整数占用更多的内存,才能表示更大的值,比如占用 6 个字节或者 8 个字节。让整数占用更少的内存可以在 int 前边加short,让整数占用更多的内存可以在 int 前边加 long,例如:
short int a = 10;
short int b, c = 99;
long int m = 102023;
long int n, p = 562131;
这样 a、b、c 只占用 2 个字节的内存,而 m、n、p 可能会占用 8 个字节的内存。
也可以将 int 省略,只写 short 和 long,如下所示:
short a = 10;
short b, c = 99;
long m = 102023;
long n, p = 562131;
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。
总结起来,它们的长度(所占字节数)关系为:
2 ≤ short ≤ int ≤ long
这就意味着,short 并不一定真的”短“,long 也并不一定真的”长“,它们有可能和 int 占用相同的字节数。
在 16 位环境下,short 的长度为 2 个字节,int 也为 2 个字节,long 为 4 个字节。
对于 32 位的 Windows、Linux 和 Mac OS,short 的长度为 2 个字节,int 为 4 个字节,long 也为 4 个字节。
在 64 位环境下,不同的操作系统会有不同的结果,如下所示:
操作系统 | short | int | long |
---|---|---|---|
Win64(64位 Windows) | 2 | 4 | 4 |
类Unix系统(包括 Unix、Linux、Mac OS、BSD、Solaris 等) | 2 | 4 | 8 |
C语言中的二进制数、八进制数和十六进制数
一个数字默认就是十进制的,表示一个十进制数字不需要任何特殊的格式。但是,表示一个二进制、八进制或者十六进制数字就不一样了,为了和十进制数字区分开来,必须采用某种特殊的写法,具体来说,就是在数字前面加上特定的字符,也就是加前缀。
二进制
二进制由 0 和 1 两个数字组成,使用时必须以0b
或0B
(不区分大小写)开头
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 合法的二进制
int a = 0b101; //换算成十进制为5
int b = -0b110010; //换算成十进制为 -50
int c = 0B100001; //换算成十进制为 33
//非法的二进制
int m = 101010; //无前缀0B,相当于十进制
int n = 0B410; //4不是有效的二进制数,在编译时编译器会报错
system("pause");
}
标准的C语言并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字。换句话说,并不是所有的编译器都支持二进制数字,只有一部分编译器支持,并且跟编译器的版本有关系。
下面是实际测试的结果:
- Visual C++ 6.0 不支持。
- Visual Studio 2015 支持,但是 Visual Studio 2010 不支持;可以认为,高版本的 Visual Studio 支持二进制数字,低版本的 Visual Studio 不支持。
- GCC 4.8.2 支持,但是 GCC 3.4.5 不支持;可以认为,高版本的 GCC 支持二进制数字,低版本的 GCC 不支持。
- LLVM/Clang 支持(内嵌于 Mac OS 下的 Xcode 中)。
八进制
八进制由 0~7 八个数字组成,使用时必须以0
开头(注意是数字 0,不是字母 o)
#include <stdio.h>
#include <stdlib.h>
int main()
{
//合法的八进制
int a = 015; //换算成十进制为13
int b = -0101; //换算成十进制为-65
int c = 0177777; //换算成十进制为65535
//非法的八进制
int m = 256; //五前缀0,相当于十进制
int n = 03A2; //A不是有效的八进制数
system("pause");
}
十六进制
十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,使用时必须以0x
或0X
(不区分大小写)开头
#include <stdio.h>
#include <stdlib.h>
int main()
{
//合法的十六进制
int a = 0X2A; //换算成十进制为 42
int b = -0XA0; //换算成十进制为 -160
int c = 0xffff; //换算成十进制为 65535
//非法的十六进制
int m = 5A; //没有前缀 0X或0x,是一个无效数字
int n = 0X3H; //H不是有效的十六进制数字
}
C语言中的正负数
short、int、long 都可以带上正负号,例如:
//负数
short a1 = -10;
short a2 = -0x2dc9; //十六进制
//正数
int b1 = +10;
int b2 = +0174; //八进制
int b3 = 22910;
//负数和正数相加
long c = (-9) + (+12);
如果不带正负号,默认就是正数。
符号也是数字的一部分,也要在内存中体现出来。符号只有正负两种情况,用1位(Bit)就足以表示;C语言规定,把内存的最高位作为符号位。以 int 为例,它占用 32 位的内存,0~30 位表示数值,31 位表示正负号
C语言规定,在符号位中,用 0 表示正数,用 1 表示负数。例如 int 类型的 -10 和 +16 在内存中的表示如下:
short、int 和 long 类型默认都是带符号位的,符号位以外的内存才是数值位。如果只考虑正数,那么各种类型能表示的数值范围(取值范围)就比原来小了一半。
如果不希望设置符号位,可以在数据类型前面加上 unsigned 关键字,例如:
unsigned short a = 12;
unsigned int b = 1002;
unsigned long c = 9892320;
这样,short、int、long 中就没有符号位了,所有的位都用来表示数值,正数的取值范围更大了。这也意味着,使用了 unsigned 后只能表示正数,不能再表示负数了。
如果将一个数字分为符号和数值两部分,那么不加 unsigned 的数字称为有符号数,能表示正数和负数,加了 unsigned 的数字称为无符号数,只能表示正数。
如果是unsigned int
类型,那么可以省略 int ,只写 unsigned,例如:
unsigned n = 100;
它等价于
unsigned int n = 100;
C语言整数的取值范围以及数值溢出
在现代操作系统中,short、int、long 的长度分别是 2、4、4 或者 8,它们只能存储有限的数值,当数值过大或者过小时,超出的部分会被直接截掉,数值就不能正确存储了,我们将这种现象称为溢出(Overflow)。
溢出的简单理解就是,向木桶里面倒入了过量的水,木桶盛不了了,水就流出来了。
要想知道数值什么时候溢出,就得先知道各种整数类型的取值范围。
无符号数的取值范围
计算无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。
以 unsigned char 类型为例,它的长度是 1,占用 8 位的内存,所有位都置为 1 时,它的值为 2^8 - 1 = 255,所有位都置为 0 时,它的值很显然为 0。由此可得,unsigned char 类型的取值范围是 0~255。
char 是一个字符类型,是用来存放字符的,但是它同时也是一个整数类型,也可以用来存放整数
unsigned char | unsigned short | unsigned int(4字节) | unsigned long(8字节) | |
---|---|---|---|---|
最小值 | 0 | 0 | 0 | 0 |
最大值 | 2^8 - 1 = 255 | 2^16 - 1 = 65,535 ≈ 6.5万 | 2^32 - 1 = 4,294,967,295 ≈ 42亿 | 2^64 - 1 ≈ 1.84×1019 |
有符号数的取值范围
有符号数以补码的形式存储,计算取值范围也要从补码入手。我们以 char 类型为例,从下表中找出它的取值范围:
补码 | 反码 | 原码 | 值 |
---|---|---|---|
1111 1111 | 1111 1110 | 1000 0001 | -1 |
1111 1110 | 1111 1101 | 1000 0010 | -2 |
1111 1101 | 1111 1100 | 1000 0011 | -3 |
…… | …… | …… | …… |
1000 0011 | 1000 0010 | 1111 1101 | -125 |
1000 0010 | 1000 0001 | 1111 1110 | -126 |
1000 0001 | 1000 0000 | 1111 1111 | -127 |
1000 0000 | – | – | -128 |
0111 1111 | 0111 1111 | 0111 1111 | 127 |
0111 1110 | 0111 1110 | 0111 1110 | 126 |
0111 1101 | 0111 1101 | 0111 1101 | 125 |
…… | …… | …… | …… |
0000 0010 | 0000 0010 | 0000 0010 | 2 |
0000 0001 | 0000 0001 | 0000 0001 | 1 |
0000 0000 | 0000 0000 | 0000 0000 | 0 |
我们按照从大到小的顺序将补码罗列出来,很容易发现最大值和最小值。
如果按照传统的由补码计算原码的方法,那么 1000 0000 是无法计算的,因为计算反码时要减去 1,1000 0000 需要向高位借位,而高位是符号位,不能借出去,所以这就很矛盾。
是不是该把 1000 0000 作为无效的补码直接丢弃呢?然而,作为无效值就不如作为特殊值,这样还能多存储一个数字。计算机规定,1000 0000 这个特殊的补码就表示 -128。
为什么偏偏是 -128 而不是其它的数字呢?
首先,-128 使得 char 类型的取值范围保持连贯,中间没有“空隙”。
其次,我们再按照“传统”的方法计算一下 -128 的补码:
- -128 的数值位的原码是 1000 0000,共八位,而 char 的数值位只有七位,所以最高位的 1 会覆盖符号位,数值位剩下 000 0000。最终,-128 的原码为 1000 0000。
- 接着很容易计算出反码,为 1111 1111。
- 反码转换为补码时,数值位要加上 1,变为 1000 0000,而 char 的数值位只有七位,所以最高位的 1 会再次覆盖符号位,数值位剩下 000 0000。最终求得的 -128 的补码是 1000 0000。
-128 从原码转换到补码的过程中,符号位被 1 覆盖了两次,而负数的符号位本来就是 1,被 1 覆盖多少次也不会影响到数字的符号。
你看,虽然从 1000 0000 这个补码推算不出 -128,但是从 -128 却能推算出 1000 0000 这个补码,这么多么的奇妙,-128 这个特殊值选得恰到好处。
负数在存储之前要先转换为补码,“从 -128 推算出补码 1000 0000”这一点非常重要,这意味着 -128 能够正确地转换为补码,或者说能够正确的存储。
关于零值和最小值
仔细观察上表可以发现,在 char 的取值范围内只有一个零值,没有+0
和-0
的区别,并且多存储了一个特殊值,就是 -128,这也是采用补码的另外两个小小的优势。如果直接采用原码存储,那么
0000 0000
和1000 0000
将分别表示+0
和-0
,这样在取值范围内就存在两个相同的值,多此一举。另外,虽然最大值没有变,仍然是 127,但是最小值却变了,只能存储到 -127,不能存储 -128 了,因为 -128 的原码为 1000 0000,这个位置已经被-0
占用了。
按照上面的方法,我们可以计算出所有有符号数的取值范围(括号内为假设的长度):
char | short | int(4个字节) | long(8个字节) | |
---|---|---|---|---|
最小值 | -2^7 = -128 | -2^15 = -32,768 ≈ -3.2万 | -2^31 = -2,147,483,648 ≈ -21亿 | -2^63 ≈ -9.22×1018 |
最大值 | 2^7 - 1= 127 | 2^15 - 1 = 32,767 ≈ 3.2万 | 2^31 - 1 = 2,147,483,647 ≈ 21亿 | 2^63 - 1≈ 9.22×1018 |
数值溢出
char、short、int、long 的长度是有限的,当数值过大或者过小时,有限的几个字节就不能表示了,就会发生溢出。发生溢出时,输出结果往往会变得奇怪
#include <stdio.h>
#include <stdlib.h>
int main()
{
unsigned int a = 0x100000000;
int b = 0xffffffff;
printf("a=%u,b=%d\n", a, b);
system("pause");
return 0;
}
结果
a=0,b=-1
变量 a 为 unsigned int 类型,长度为 4 个字节,能表示的最大值为 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,占用33位,已超出 a 所能表示的最大值,所以发生了溢出,导致最高位的 1 被截去,剩下的 32 位都是0。也就是说,a 被存储到内存后就变成了 0,printf 从内存中读取到的也是 0。
变量 b 是 int 类型的有符号数,在内存中以补码的形式存储。0xffffffff 的数值位的原码为 1111 1111 …… 1111 1111,共 32 位,而 int 类型的数值位只有 31 位,所以最高位的 1 会覆盖符号位,数值位只留下 31 个 1,所以 b 的原码为:
1111 1111 …… 1111 1111
这也是 b 在内存中的存储形式。
当 printf 读取到 b 时,由于最高位是 1,所以会被判定为负数,要从补码转换为原码:
[1111 1111 …… 1111 1111]补
= [1111 1111 …… 1111 1110]反
= [1000 0000 …… 0000 0001]原
= -1
最终 b 的输出结果为 -1。
_bool形数据
#include <stdio.h>
#include <stdbool.h>
void main()
{
_Bool b1,b2;
b1 = false; //不成立
b2 = true; //成立
printf("b1 = %d\n", b1);
printf("size = %d\n", sizeof(b2));//占一个字符
printf("b3 = %d\n", b2 + 3); //实质上还是0和1
system("pause");
}
C语言中的小数(float,double)
小数分为整数部分和小数部分,它们由点号.
分隔,例如 0.0、75.0、4.023、0.27、-937.198 -0.27 等都是合法的小数,这是最常见的小数形式,我们将它称为十进制形式。
此外,小数也可以采用指数形式,例如 7.25×10^2
、0.0368×10^5
、100.22×10^-2
、-27.36×10^-3
等。任何小数都可以用指数形式来表示。
C语言同时支持以上两种形式的小数。但是在书写时,C语言中的指数形式和数学中的指数形式有所差异。
C语言中小数的指数形式为:aEn
或 aen
a 为尾数部分,是一个十进制数;n 为指数部分,是一个十进制整数;E
或e
是固定的字符,用于分割尾数部分和指数部分。整个表达式等价于 a×10^n。
指数形式的小数举例:
- 2.1E5 = 2.1×105,其中 2.1 是尾数,5 是指数。
- 3.7E-2 = 3.7×10-2,其中 3.7 是尾数,-2 是指数。
- 0.5E7 = 0.5×107,其中 0.5 是尾数,7 是指数。
C语言中常用的小数有两种类型,分别是 float 或 double;float 称为单精度浮点型
,double 称为双精度浮点型
。
小数的长度是固定的,float 始终占用4个字节,double 始终占用8个字节。
C语言中还有long double这个数据类型,但这个数据类型占用的字节数为: long double >= double 具体占多少个字节是看具体的编译器来决定的,可以用sizeof来进行查看
小数的输出
小数也可以使用 printf 函数输出,包括十进制形式和指数形式,它们对应的格式控制符分别是:
- %f 以十进制形式输出 float 类型;
- %lf 以十进制形式输出 double 类型;
- %e 以指数形式输出 float 类型,输出结果中的 e 小写;
- %E 以指数形式输出 float 类型,输出结果中的 E 大写;
- %le 以指数形式输出 double 类型,输出结果中的 e 小写;
- %lE 以指数形式输出 double 类型,输出结果中的 E 大写。
#include <stdio.h>
#include <stdlib.h>
int main()
{
float a = 0.302;
float b = 128.101;
double c = 123;
float d = 112.64E3;
double e = 0.7623e-2;
float f = 1.23002398;
printf("a=%e \nb=%f \nc=%lf \nd=%lE \ne=%lf \nf=%f\n", a, b, c, d, e, f);
system("pause");
return 0;
}
结果
a=3.020000e-01
b=128.100998
c=123.000000
d=1.126400E+05
e=0.007623
f=1.230024
对代码的说明:
-
%f 和 %lf 默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。
-
将整数赋值给 float 变量时会变成小数。
-
以指数形式输出小数时,输出结果为科学计数法;也就是说,尾数部分的取值为:0 ≤ 尾数 < 10。
-
b 的输出结果让人费解,才三位小数,为什么不能精确输出,而是输出一个近似值呢?这和小数在内存中的存储形式有关,很多简单的小数压根不能精确存储,所以也就不能精确输出
另外,小数还有一种更加智能的输出方式,就是使用%g
。%g 会对比小数的十进制形式和指数形式,以最短的方式来输出小数,让输出结果更加简练。所谓最短,就是输出结果占用最少的字符。
%g 使用示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float a = 0.00001;
float b = 30000000;
float c = 12.84;
float d = 1.229338455;
printf("a=%g \nb=%g \nc=%g \nd=%g\n", a, b, c, d);
system("pause");
return 0;
}
运行结果
a=1e-05
b=3e+07
c=12.84
d=1.22934
对各个小数的分析:
- a 的十进制形式是 0.00001,占用七个字符的位置,a 的指数形式是 1e-05,占用五个字符的位置,指数形式较短,所以以指数的形式输出。
- b 的十进制形式是 30000000,占用八个字符的位置,b 的指数形式是 3e+07,占用五个字符的位置,指数形式较短,所以以指数的形式输出。
- c 的十进制形式是 12.84,占用五个字符的位置,c 的指数形式是 1.284e+01,占用九个字符的位置,十进制形式较短,所以以十进制的形式输出。
- d 的十进制形式是 1.22934,占用七个字符的位置,d 的指数形式是 1.22934e+00,占用十一个字符的位置,十进制形式较短,所以以十进制的形式输出。
注意的两点是:
- %g 默认最多保留六位有效数字,包括整数部分和小数部分;%f 和 %e 默认保留六位小数,只包括小数部分。
- %g 不会在最后强加 0 来凑够有效数字的位数,而 %f 和 %e 会在最后强加 0 来凑够小数部分的位数。
总之,%g 要以最短的方式来输出小数,并且小数部分表现很自然,不会强加零,比 %f 和 %e 更有弹性,这在大部分情况下是符合用户习惯的。
除了 %g,还有 %lg、%G、%lG:
- %g 和 %lg 分别用来输出 float 类型和 double 类型,并且当以指数形式输出时,
e
小写。 - %G 和 %lG 也分别用来输出 float 类型和 double 类型,只是当以指数形式输出时,
E
大写。
数字的后缀
一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是 double 类型。
long a = 100;
int b = 294;
float x = 52.55;
float 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 类型。
long a = 100l;
int b = 294;
short c = 32L;
float x = 52.55f;
double y = 18.6F;
float z = 0.02;
加上后缀,虽然数字的类型变了,但这并不意味着该数字只能赋值给指定的类型,它仍然能够赋值给其他的类型,只要进行了一下类型转换就可以了。
小数和整数相互赋值
在C语言中,整数和小数之间可以相互赋值:
- 将一个整数赋值给小数类型,在小数点后面加 0 就可以,加几个都无所谓。
- 将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值。注意是直接丢掉小数部分,而不是按照四舍五入取近似值。
#include <stdio.h>
#include <stdlib.h>
int main()
{
float f = 251;
int w = 19.427;
int x = 92.78;
int y = 0.52;
int z = -87.27;
printf("f = %f,w = %d,x = %d,y = %d,z = %d\n", f, w, x, y, z);
system("pause");
return 0;
}
运行结果
f = 251.000000,w = 19,x = 92,y = 0,z = -87
由于将小数赋值给整数类型时会“失真”,所以编译器一般会给出警告,让大家引起注意。
C语言处理字符
处理英文字符
字符串是多个字符的集合,它们由" "
包围,例如"http://c.biancheng.net"
、"你好"
。字符串中的字符在内存中按照次序、紧挨着排列,整个字符串占用一块连续的内存。
当然,字符串也可以只包含一个字符,例如"A"
、"6"
;不过为了操作方便,我们一般使用专门的字符类型来处理。
字符类型是 char,它的长度是 1,只能容纳 ASCII 码表中的字符,也就是英文字符。
要想处理汉语、日语、韩语等英文之外的字符,就得使用其他的字符类型,char 是做不到的
字符的表示与字符串是不一样的
字符类型由单引号' '
包围,字符串由双引号" "
包围。
给 char 类型的变量赋值:
#include <stdio.h>
#include <stdlib.h>
int main()
{
//正确的写法
char a = '1';
char b = '$';
char c = 'X';
char d = ' '; //空格也是一个字符
//错误的写法
char x = '中';
char y = 'A'; //A 是一个全角字符
char z = "t"; //字符类型应该由单引号包围
printf("a = %c\nb = %c\nc = %c\nd = %c\nx = %c\ny = %c\nz = %c\n", a, b, c, d, x, y, z);
system("pause");
return 0;
}
结果:
a = 1
b = $
c = X
d =
x = ?
y = ?
z = 0
说明:在字符集中,全角字符和半角字符对应的编号(或者说编码值)不同,是两个字符;ASCII 编码只定义了半角字符,没有定义全角字符。
字符的输出
输出 char 类型的字符有两种方法,分别是:
- 使用专门的字符输出函数
putchar
; - 使用通用的格式化输出函数
printf
,char
对应的格式控制符是%c
。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char a = '1';
char b = '$';
char c = 'X';
char d = ' ';
//使用putchar输出
putchar(a);
putchar(b);
putchar(c);
putchar(d);
putchar('\n');
//使用printf输出
printf("%c %c %c\n", a, b, c);
system("pause");
return 0;
}
运行结果
1$X
1 $ X
putchar
函数每次只能输出一个字符,输出多个字符需要调用多次。
字符与整数
计算机在存储字符时并不是真的要存储字符实体,而是存储该字符在字符集中的编号(也可以叫编码值)。对于 char 类型来说,它实际上存储的就是字符的 ASCII 码。
无论在哪个字符集中,字符编号都是一个整数;从这个角度考虑,字符类型和整数类型本质上没有什么区别。
我们可以给字符类型赋值一个整数,或者以整数的形式输出字符类型。反过来,也可以给整数类型赋值一个字符,或者以字符的形式输出整数类型。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char a = 'E';
char b = 70;
char c = 71;
int d = 'H';
printf("a: %c,%d\n", a, a);
printf("b: %c,%d\n", b, b);
printf("c: %c,%d\n", c, c);
printf("d: %c,%d\n", d, d);
system("pause");
return 0;
}
运行结果:
a: E,69
b: F,70
c: G,71
d: H,72
在 ASCII 码表中,字符 ‘E’、‘F’、‘G’、‘H’ 对应的编号分别是 69、70、71、72。
a、b、c、d 实际上存储的都是整数:
- 当给 a、d 赋值一个字符时,字符会先转换成 ASCII 码再存储;
- 当给 b、c 赋值一个整数时,不需要任何转换,直接存储就可以;
- 当以 %c 输出 a、b、c、d 时,会根据 ASCII 码表将整数转换成对应的字符;
- 当以 %d 输出 a、b、c、d 时,不需要任何转换,直接输出就可以。
可以说,是 ASCII 码表将英文字符和整数关联了起来。
中文字的存储
正确地存储中文字符需要解决两个问题。
1) 足够长的数据类型
char 只能处理 ASCII 编码中的英文字符,是因为 char 类型太短,只有一个字节,容纳不下我大中华几万个汉字,要想处理中文字符,必须得使用更长的数据类型。
一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数,所以我们可以用整数类型来存储一个字符,比如 unsigned short、unsigned int、unsigned long 等。
2) 选择包含中文的字符集
C语言规定,对于汉语、日语、韩语等 ASCII 编码之外的单个字符,也就是专门的字符类型,要使用宽字符的编码方式。常见的宽字符编码有 UTF-16
和UTF-32
,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。
为了解决这个问题,C语言推出了一种新的类型,叫做 wchar_t
。w 是 wide 的首字母,t 是 type 的首字符,wchar_t
的意思就是宽字符类型。wchar_t
的长度由编译器决定:
- 在微软编译器下,它的长度是 2,等价于 unsigned short;
- 在
GCC
、LLVM/Clang
下,它的长度是 4,等价于 unsigned int。
wchar_t
类型位于<wchar.h>
头文件中,它使得代码在具有良好移植性的同时,也节省了不少内存,以后我们就用它来存储宽字符。
要想使用宽字符的编码方式,就得加上L
前缀,例如L'A'
、L'9'
、L'中'
、L'国'
、L'。'。
注意,加上L
前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。
#include <wchar.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'中'; //中文汉字
wchar_t d = L'国'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'ヅ'; //日文片假名
wchar_t g = L'♥'; //特殊符号
wchar_t h = L'༄'; //藏文
printf("a = %c\nb = %c\nc = %c\nd = %c\ne = %c\nf = %c\ng = %c\nh = %c\n", a, b, c, d, e, f, g, h);
system("pause");
return 0;
}
在以后的编程中,我们将不加L
前缀的字符称为窄字符,将加上L
前缀的字符称为宽字符。窄字符使用 ASCII 编码,宽字符使用 UTF-16
或者UTF-32
编码。
宽字节的输出
putchar
、printf
只能输出不加L
前缀的窄字符,对加了L
前缀的宽字符无能为力,我们必须使用 <wchar.h>
头文件中的宽字符输出函数,它们分别是putwchar
和wprintf
:
-
putwchar
函数专门用来输出一个宽字符,它和putchar
的用法类似; -
wprintf
是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串(稍后讲解)。宽字符对应的格式控制符为%lc
。
另外,在输出宽字符之前还要使用 setlocale
函数进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。由于大家基础还不够,关于本地化设置的内容我们不再展开讲解,请大家先记住这种写法。
如果希望设置为中文简体环境,在 Windows 下请写作:
setlocale(LC_ALL, "zh-CN");
在 Linux 和 Mac OS 下请写作:
setlocale(LC_ALL, "zh_CN");
setlocale
函数位于<locale.h>
头文件中,我们必须引入它。
下面的代码完整地演示了宽字符的输出:
#include <wchar.h>
#include <locale.h>
#include <stdlib.h>
int main()
{
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'中'; //中文汉字
wchar_t d = L'国'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'ヅ'; //日文片假名
wchar_t g = L'♥'; //特殊符号
wchar_t h = L'༄'; //藏文
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用专门的putwchar输出宽字符
putwchar(a);
putwchar(b);
putwchar(c);
putwchar(d);
putwchar(e);
putwchar(f);
putwchar(g);
putwchar(h);
putwchar(L'\n'); //只能使用宽字符
//使用通用的wprintf输出字符
wprintf(
L"Wide chars%lc %lc %lc %lc %lc %lc %lc %lc\n", //必须使用宽字符串
a, b, c, d, e, f, g, h
);
system("pause");
return 0;
}
运行结果:
A9中国。ヅ♥༄
Wide chars: A 9 中 国 。 ヅ ♥ ༄
宽字符串
给字符串加上L
前缀就变成了宽字符串,它包含的每个字符都是宽字符,一律采用 UTF-16
或者 UTF-32
编码。输出宽字符串可以使用<wchar.h>
头文件中的 wprintf
函数,对应的格式控制符是%ls
。
#include <wchar.h>
#include <locale.h>
int main(){
wchar_t web_url[] = L"www.baidu.com";
wchar_t *web_name = L"百度一下你就知道";
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用通用的 wprintf 输出宽字符
wprintf(L"web_url: %ls \nweb_name: %ls\n", web_url, web_name);
return 0;
}
运行结果:
web_url: www.baidu.com
web_name: 百度一下你就知道
转义字符
字符集(Character Set)为每个字符分配了唯一的编号,我们不妨将它称为编码值。在C语言中,一个字符除了可以用它的实体(也就是真正的字符)表示,还可以用编码值表示。这种使用编码值来间接地表示字符的方式称为转义字符(Escape Character)。
转义字符以\
或者\x
开头,以\
开头表示后跟八进制形式的编码值,以\x
开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
字符 1、2、3、a、b、c 对应的 ASCII 码的八进制形式分别是 61、62、63、141、142、143,十六进制形式分别是 31、32、33、61、62、63。下面的例子演示了转义字符的用法:
char a = '\61'; //字符1
char b = '\141'; //字符a
char c = '\x31'; //字符1
char d = '\x61'; //字符a
char *str1 = "\x31\x32\x33\x61\x62\x63"; //字符串"123abc"
char *str2 = "\61\62\63\141\142\143"; //字符串"123abc"
char *str3 = "The string is: \61\62\63\x61\x62\x63" //混用八进制和十六进制形式
转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。
#include <stdio.h>
#include <stdlib.h>
int main()
{
puts("\x68\164\164\x70://www.nihao.\x6e\145\x74");
system("pause");
return 0;
}
运行结果
http://www.nihao.net
转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:
- 八进制形式的转义字符最多后跟三个数字,也即
\ddd
,最大取值是\177
; - 十六进制形式的转义字符最多后跟两个数字,也即
\xdd
,最大取值是\7f
。
超出范围的转义字符的行为是未定义的,有的编译器会将编码值直接输出,有的编译器会报错。
对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符,它们都是看不见的,不能在显示器上显示,甚至无法从键盘输入,只能用转义字符的形式来表示。不过,直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言又定义了简写方式,完整的列表如下:
转义字符 | 意义 | ASCII码值(十进制) |
---|---|---|
\a | 响铃(BEL) | 007 |
\b | 退格(BS) ,将当前位置移到前一列 | 008 |
\f | 换页(FF),将当前位置移到下页开头 | 012 |
\n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
\r | 回车(CR) ,将当前位置移到本行开头 | 013 |
\t | 水平制表(HT) | 009 |
\v | 垂直制表(VT) | 011 |
’ | 单引号 | 039 |
" | 双引号 | 034 |
\ | 反斜杠 | 092 |
\n
和\t
是最常用的两个转义字符:
-
\n
用来换行,让文本从下一行的开头输出,前面的章节中已经多次使用; -
\t
用来占位,一般相当于四个空格,或者 tab 键的功能。
单引号、双引号、反斜杠是特殊的字符,不能直接表示:
- 单引号是字符类型的开头和结尾,要使用
\'
表示,也即'\''
; - 双引号是字符串的开头和结尾,要使用
\"
表示,也即"abc\"123"
; - 反斜杠是转义字符的开头,要使用
\\
表示,也即'\\'
,或者"abc\\123"
。
#include <stdio.h>
int main(){
puts("C\tC++\tJava\n\"C\" first appeared!");
return 0;
}
运行结果:
C C++ Java
“C” first appeared!
char short 类型提升
char
,short
字符,无论有无符号,在表达式都会转换为int
或者unsigned int
类型
#include <stdio.h>
#include <stdlib.h>
void main()
{
char ch = 10;
short sh = 256;
printf("ch = %d,sh = %d ch + sh = %d", sizeof(ch), sizeof(sh), sizeof(ch + sh));
system("pause");
}
结果
ch = 1,sh = 2 ch + sh = 4
#include <stdio.h>
#include <stdlib.h>
void main()
{
char ch1 = 10;
char ch2 = 127;
printf("ch1 = %d,ch2 = %d ch1 + ch2 = %d", sizeof(ch1), sizeof(ch2), sizeof(ch1 + ch2));
system("pause");
}
结果
ch1 = 1,ch2 = 1 ch1 + ch2 = 4
跨平台移植的整数
不同平台,不同编译器,同样的一个整数数据类型,可能大小不一样
int 16位的情况是2个字节,32位是4个字节
long 64位 linux是8个字节,windows 32,64位都是4个字节
为了解决不同平台,不同的编译器,同样的一个整数数据类型,可能大小不一样的问题就有了stdint.h的头文件,只要支持C99的编译器都可以使用这个头文件,使不同平台的数据类型都能一
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
void main()
{
int64_t num = 123;
printf("%d,%d\n", sizeof(num), num);
system("pause");
}
结果
8,123
空类型变量的问题
在变量的类型中有一个类型是void
类型,也就是空类型,这个类型一般用在函数中的,表明该函数为空类型的函数,也就是没有返回值的函数,但如果用在变量上,那么就会报错
void a; //非法使用void类型,void表示空类型,使用在变量这里表示的就是任何类型,也就会报错,因为编译器不知道该分配多少内存,所以会报错
#include <stdio.h>
#include <stdlib.h>
void main()
{
void a;
system("pause");
}
编译会出错
C语言中的几个中重要概念
标识符
定义变量时,我们使用了诸如 a、abc、mn123 这样的名字,它们都是程序员自己起的,一般能够表达出变量的作用,这叫做标识符(Identifier)。
标识符就是程序员自己起的名字,除了变量名,后面还会讲到函数名、宏名、结构体名等,它们都是标识符。不过,名字也不能随便起,要遵守规范;C语言规定,标识符只能由字母(A~Z, az)、数字(09)和下划线(_)组成,并且第一个字符必须是字母或下划线,不能是数字。
以下是合法的标识符:
a, x, x3, BOOK_1, sum5
以下是非法的标识符:
- 3s 不能以数字开头
- sT 出现非法字符
- -3x 不能以减号(-)开头
- bowy-1 出现非法字符减号(-)
在使用标识符时还必须注意以下几点:
- C语言虽然不限制标识符的长度,但是它受到不同编译器的限制,同时也受到操作系统的限制。例如在某个编译器中规定标识符前128位有效,当两个标识符前128位相同时,则被认为是同一个标识符。
- 在标识符中,大小写是有区别的,例如 BOOK 和 book 是两个不同的标识符。
- 标识符虽然可由程序员随意定义,但标识符是用于标识某个量的符号,因此,命名应尽量有相应的意义,以便于阅读和理解,作到“顾名思义”。
关键字
关键字(Keywords)
是由C语言规定的具有特定意义的字符串,通常也称为保留字,例如 int、char、long、float、unsigned 等。我们定义的标识符不能与关键字相同,否则会出现错误。
你也可以将关键字理解为具有特殊含义的标识符,它们已经被系统使用,我们不能再使用了。
标准C语言中一共规定了32个关键字
注释
注释(Comments)可以出现在代码中的任何位置,用来向用户提示或解释代码的含义。程序编译时,会忽略注释,不做任何处理,就好像它不存在一样。
C语言支持单行注释和多行注释:
- 单行注释以
//
开头,直到本行末尾(不能换行); - 多行注释以
/*
开头,以*/
结尾,注释内容可以有一行或多行。
一个使用注释的例子:
/*
Powered by: www.baidu.com
Author: 啦啦啦啦
Date: 2019-3-12
*/
#include <stdio.h>
int main()
{
/* puts 会在末尾自动添加换行符 */
puts("http://www.baidu.com");
printf("百度一下你就知道\n"); //printf要手动添加换行符
return 0;
}
运行结果:
http://www.baidu.com
百度一下你就知道
在调试程序的过程中可以将暂时将不使用的语句注释掉,使编译器跳过不作处理,待调试结束后再去掉注释。
需要注意的是,多行注释不能嵌套使用。例如下面的注释是错误的:
/*百度一下/*你就*/知道*/
而下面的注释是正确的:
/*百度一下你就知道*/ /*www.baidu.com*/
表达式(Expression)和语句(Statement)
表达式(Expression)和语句(Statement)的概念在C语言中并没有明确的定义:
- 表达式可以看做一个计算的公式,往往由数据、变量、运算符等组成,例如
3*4+5
、a=c=d
等,表达式的结果必定是一个值; - 语句的范围更加广泛,不一定是计算,不一定有值,可以是某个操作、某个函数、选择结构、循环等。
赶紧划重点:
- 表达式必须有一个执行结果,这个结果必须是一个值,例如
3*4+5
的结果 17,a=c=d=10
的结果是 10,printf("hello")
的结果是 5(printf 的返回值是成功打印的字符的个数)。 - 以分号
;
结束的往往称为语句,而不是表达式,例如3*4+5;
、a=c=d;
等。
数据类型转换
数据类型转换就是将数据(变量、数值、表达式的结果等)从一种类型转换为另一种类型。
自动类型转换
自动类型转换就是编译器默默地、隐式地、偷偷地进行的数据类型转换,这种转换不需要程序员干预,会自动发生。
- 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如:
float f = 100;
100 是 int 类型的数据,需要先转换为 float 类型才能赋值给变量 f。再如:
int n = f;
f 是 float 类型的数据,需要先转换为 int 类型才能赋值给变量 n。
在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低;所以说,自动类型转换并不一定是安全的。对于不安全的类型转换,编译器一般会给出警告。
- 在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
- 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
- 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
- char 和 short 参与运算时,必须先转换成 int 类型。
自动类型转换示例
#include <stdio.h>
#include <stdlib.h>
int main()
{
float PI = 3.14159;
int s1, r = 5;
double s2;
s1 = r * r * PI;
s2 = r * r * PI;
printf("s1 = %d,s2 = %f\n", s1, s2);
system("pause");
return 0;
}
运行结果:
s1 = 78,s2 = 78.539749
在计算表达式r*r*PI
时,r 和 PI 都被转换成 double 类型,表达式的结果也是 double 类型。但由于 s1 为整型,所以赋值运算的结果仍为整型,舍去了小数部分,导致数据失真。
强制类型转换
自动类型转换是编译器根据代码的上下文环境自行判断的结果,有时候并不是那么“智能”,不能满足所有的需求。
如果需要,程序员也可以自己在代码中明确地提出要进行类型转换,这称为强制类型转换。
自动类型转换是编译器默默地、隐式地进行的一种类型转换,不需要在代码中体现出来;强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预。
强制类型转换的格式为:(type_name) expression
强制类型转换例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
int sum = 103; //总数
int count = 7; //数目
double average; //平均数
average = (double)sum / count;
printf("Average is %lf!\n", average);
system("pause");
return 0;
}
运行结果:
Average is 14.714286!
sum 和 count 都是 int 类型,如果不进行干预,那么sum / count
的运算结果也是 int 类型,小数部分将被丢弃;虽然是 average 是 double 类型,可以接收小数部分,但是心有余力不足,小数部分提前就被“阉割”了,它只能接收到整数部分,这就导致除法运算的结果严重失真。
既然 average 是 double 类型,为何不充分利用,尽量提高运算结果的精度呢?为了达到这个目标,我们只要将 sum 或者 count 其中之一转换为 double 类型即可。上面的代码中,我们将 sum 强制转换为 double 类型,这样sum / count
的结果也将变成 double 类型,就可以保留小数部分了,average 接收到的值也会更加精确。
在这段代码中,有两点需要注意:
- 对于除法运算,如果除数和被除数都是整数,那么运算结果也是整数,小数部分将被直接丢弃;如果除数和被除数其中有一个是小数,那么运算结果也是小数。
-
( )
的优先级高于/
,对于表达式(double) sum / count
,会先执行(double) sum
,将 sum 转换为 double 类型,然后再进行除法运算,这样运算结果也是 double 类型,能够保留小数部分。注意不要写作(double) (sum / count)
,这样写运算结果将是 3.000000,仍然不能保留小数部分。
类型转换只是临时性的
无论是自动类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。请看下面的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
double total = 400.8; //总价
int count = 5; //数目
double unit; //单价
int total_int = (int)total;
unit = total / count;
printf("total=%lf,total_int=%d,unit=%lf\n", total, total_int, unit);
system("pause");
return 0;
}
运行结果:
total=400.800000, total_int=400, unit=80.160000
注意看第 9行代码,total 变量被转换成了 int 类型才赋值给 total_int 变量,而这种转换并未影响 total 变量本身的类型和值。如果 total 的值变了,那么 total 的输出结果将变为 400.000000;如果 total 的类型变了,那么 unit 的输出结果将变为 80.000000。
自动类型转换 VS 强制类型转换
在C语言中,有些类型既可以自动转换,也可以强制转换,例如 int 到 double,float 到 int 等;而有些类型只能强制转换,不能自动转换,例如 void * 到 int *,int 到 char * 等。
可以自动转换的类型一定能够强制转换,但是,需要强制转换的类型不一定能够自动转换。现在的数据类型,既可以自动转换,又可以强制转换。
可以自动进行的类型转换一般风险较低,不会对程序带来严重的后果,例如,int 到 double 没有什么缺点,float 到 int 顶多是数值失真。只能强制进行的类型转换一般风险较高,或者行为匪夷所思,例如,char * 到 int * 就是很奇怪的一种转换,这会导致取得的值也很奇怪,再如,int 到 char * 就是风险极高的一种转换,一般会导致程序崩溃。
使用强制类型转换时,程序员自己要意识到潜在的风险。
类型转换发生在寄存器中,所以不会改变内存的值
#include <stdio.h>
#include <stdlib.h>
void main()
{
float a = 10.123;
printf("a = %f, &a = %x\n", a, &a);
int b;
b = (int)a;
printf("a = %x, &a = %f, b = %d , &b = %x\n", &a, a,b,&b);
system("pause");
}
可以看到就算对a进行了强制类型转换,但a的值并没有发生改变,也就是说,类型转换发生的地方不在内存中,是在寄存器中进行的,因为类型转换也属于运算的一种,运算所发生的场所就是在寄存器中,也就是CPU中
类型转换中的大小数据转换情况
#include <stdio.h>
#include <stdlib.h>
//小数据转大数据
void main()
{
unsigned short a = 256;
char b = a;
printf("%d",b);
}
结果
0
这个是因为大数据转小数据的时候会发生截取
小数据转大数据时出现的不同情况
情况1,小数据为带符号,大数据也带符号
#include <stdio.h>
#include <stdlib.h>
//小数据转大数据
void main()
{
char a = -1; //内存为ff
short b = a; //内存为ffff
printf("a = %x,b = %x", &a, &b);
printf("a = %d,b = %d", a, b);
//a = -1,b = -1
}
结果
a = -1,b = -1
情况2,小数据为带符号,大数据无符号
#include <stdio.h>
#include <stdlib.h>
//小数据转大数据
void main()
{
char a = -1; //内存为ff
unsigned short b = a; //内存为ffff
printf("a = %x,b = %x", &a, &b);
printf("a = %d,b = %d", a, b);
//a = -1, b = 65535
}
结果
a = -1, b = 65535
情况3,小数据为无符号,大数据有符号
#include <stdio.h>
#include <stdlib.h>
//小数据转大数据
void main()
{
unsigned char a = 255; //内存为ff
short b = a; //内存为00ff
printf("a = %x,b = %x", &a, &b);
printf("a = %d,b = %d", a, b);
//a = 255,b = 255
}
结果:
a = 255,b = 255
情况4,小数据为无符号,大数据为无符号
#include <stdio.h>
#include <stdlib.h>
//小数据转大数据
void main()
{
unsigned char a = 255; //内存为ff
unsigned short b = a; //内存为00ff
printf("a = %x,b = %x", &a, &b);
printf("a = %d,b = %d", a, b);
//a = 255,b = 255
}
结果:
a = 255,b = 255
总结:小数据转大数据是根据小数据是否是有符号,并且符号位是否为1还是0来确定转到大数据的时候大数据的值。
- 当小数据类型为有符号数据类型,并且符号位为1时,大数据前面会全部填充为1
- 当小数据类型为无符号数据类型,不管符号为是否为1,大数据前面全部都会填充为0,因为大数据类型能容纳小数据类型的所有范围
个人猜想,程序在编译器中会先把所有数据转成二进制数据,如把2先转成01,-1转成1111 1111,255转成1111 1111,然后再根据数据类型对数据进行判断是正数还是负数,如果是有符号类型的,那么1111 1111明显是负数,所以这个值为-1,所以转到大数据类型也应该是-1,所以转大数据前面会添加1,但如果是小数据类型是无符号类型的,那么1111 1111就为255,转成大数据类型明显是能存放的下的,所以这时候转成大数据时前面只需要补0,那么转成大数据也只会是255
sizeof 操作符
获取某个数据类型的长度可以使用 sizeof 操作符,这个是操作符,并不是函数
#include <stdio.h>
#include <stdlib.h>
int main()
{
short a = 10;
int b = 100;
int short_length = sizeof a;
int int_length = sizeof(b);
int long_length = sizeof(long);
int char_length = sizeof(char);
printf("short=%d,\nint=%d,\nlong=%d,\nchar=%d\n", short_length, int_length, long_length, char_length);
system("pause"); #用来暂停窗口
}
在 32 位环境以及 Win64 环境下的运行结果为:
short=2,
int=4,
long=4,
char=1
在 64 位 Linux 和 Mac OS 下的运行结果为:
short=2,
int=4,
long=8,
char=1
sizeof 用来获取某个数据类型或变量所占用的字节数,如果后面跟的是变量名称,那么可以省略( )
,如果跟的是数据类型,就必须带上( )
。
需要注意的是,sizeof 是C语言中的操作符,不是函数,所以可以不带( )
char a = ‘A’ sizeif(a) 与sizeof(‘A’)不一样
#include <stdio.h>
#include <stdlib.h>
void main()
{
char a = 'A';
printf("a = %d,A = %d\n", sizeof(a), sizeof('A'));
system("pause");
}
这是因为char a = 'A’为窄字符,sizeof(‘A’)编译器当成了宽字符来进行显示,因为窄字符版本的C语言编译器已经不能满足现在的需求了,所以现在比较新的编译器都默认支持了宽字符,这样为了是能显示更多的字符,而不是再显示ASCII码表的数据,使用宽字符可以显示中文字,使用宽字符时要在前面加上L