1、 数据的类型
(1)空间第一个字节的地址
代码、变量、常量空间,往往都是连续的一片字节空间,到底哪一个字节的地址才代表整个空间的地址(指针)呢,我们前面说过,第一个字节的地址是整个空间的指针。
(2)如何访问一片连续字节空间
只要知道如下几件事即可。
1)知道第一个字节的地址
2)知道访问到第几个字节结束
3)知道如何解析里面存放的数据
如此就能正确访问连续字节中的数据,不管是访问函数、变量、常量,都是这样来访问的。
例子1:
访问int变量的空间,int是四个字节组成的一片连续空间,如何访问呢?
1)知道第一个字节的地址
2)int的大小为4个字节,显然就知道了访问到那个字节结束
3)数据类型为整型,数据的存储结构就是整形的存储结构,访问空间时,需要按照“整形存储结构”去解析空间。
例子2:
访问float变量的空间,float也是四个字节组成的一片连续空间,如何访问呢?
1)知道第一个字节的地址
2)知道大小为四个字节
3)知道是float类型,那么数据就是以浮点形式存储的,读写空间时就必须按照浮点存储方式来解析空间。
疑问:浮点数是如何存储的?
是以科学计数法来存储的,比如
转为科学计数法就是 指数
12.3 ——————————————————————> 123 * 10的-1次方
疑问float为例,float空间总共32位:
31 30 29 28 27 26 252 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 0
· 最高位(31):符号位,0表示正数,1表示负数
· 23~30:指数(1)
· 0~22:整数(123)
比如向float的空间写数据时,按照这个结构来存储12.3的。
(3)回顾C语言的数据类型
对于绝大多数高级语言来说,都是有数据类型的,以c为例,就有大量的数据类型。
凡是有数据类型的语言,我们都成为强类型语言。
1)基础数据类型
(a)整形:char short int long,这些还可以与unsigned进行组合,unsigned char,...
(b)浮点:float double,注意浮点都是有符号的,浮点不能和unsigned进行组合,不存在无符号浮点数一说,
2)组合数据类型
由基础型演变而来。
(a)结构体、联合体类型
/* 结构体类型 */ struct Student { int num; //基础类型 char name; //基础类型 ... }; /* 联合体类型 */ union Student { int num; //基础类型 char name; //基础类型 ... };
(b)指针类型
int *,float *,struct Student *, union Student *。 int **,float **,...。
疑问:有联合体指针吗?
答:有,不过用的很少,后面课程讲联合体时,还会提到的。
(4)数据类型的意义
1)给程序员看到
比如你一看到变量类型,就知道应该放什么类型的数据到变量里面。
2)数据类型决定了空间大小和存储结构
比如:
(a)例子1
int a; a = 100;
空间大小:4字节
存储结构:整形存储结构
编译器在编译时,就知道给a预安排多大的空间,存储结构是怎样的。
float a; a = 100.7;
空间大小:四个字节
存储方式:浮点存储方式
(b)例子2:
struct Student { int num; char name; float score; ... }; struct Student stu;
空间大小:基本>=各成员之和,我们后面学习了结构体的对齐后,其实是能算出具体大小的。
存储结构:属于结构体存储结构
结构体存储时,是按照“构体成员的顺序”来进行存储的,每个成员的存储方式则又由成员的类型来决定。
在很多同学的想象中,总认为成员之间是紧挨着的,其实不是的,中间其实可能有间隔,这个涉及到结构体的对齐,这个我们以后再讲。
不同结构体类型的成员类型、成员数量不同,所以不同结构体类型的存储结构是不一样的,就算是同一个结构体类型,将成员顺序改变后,也不一样。
3)让编译器帮忙检查类型错误
如果编译器检查到类型有问题,就会帮我们报错或者报警告,提醒我们数据的使用可能有问题。
报错、报警告是好事,因为通过这些信息可以快速的帮我们排查问题,解决问题。
如果没有类型检查的话,编译器就不会进行类型检查,那么与数据类型相关的错误,就只能由程序员自己来预防,当时显然这种方式很不靠谱。
(5)=两边的类型问题
等号的左边叫左值,右边叫右值,不管=是用于初始化,还是用于赋值,要求右值必须与左值类型一致。
左值:必须是一个“可写”的变量空间
右值:不定,可以是一个数,一个变量,一个常量
int a = 0; //初始化:定义时就给值的就是初始化。 a = 100; //赋值:定义之后给值的,就是赋值
类型一致有两种情况,第一种是天然一致,第二种是强行一致。
1)天然一致
int a = 100;
变量a的类型:int
数据100的类型:100
天然一致,其实就是什么类型的数据,就应该放到对应类型的空间中。
2)强制转换,强行一致
(a)显式强制转换
int a = (int)12.45;
float被强制转为int后再,在将值给a。
float a = 100.0; int b = (int)a;
float的100被强制转为int后再,然后再给b。
int a = 103435455; int b = (int)&a;
int *的&a被强制转换int,然后在给b。
当然,并不是所有类型之间都可以强制转换的,比如:
float a= 134.454 int *p = (int *)a;
将浮点强转为指针,这是毫无意义的,所以c不允许这种强制转换。
(b)隐式强制转换
编译器在编译时,会默认将右值类型强制转换左值的类型,这就是隐式强制转换。
int a = 12.56;
float 12.56被隐式转为int后,再给a。
int a = 12.56; char b = a;
int a被隐式转为char后,在都给b。
int a = 10; char *p = a;
int被隐式转为char *。
int a = 10; char *p = &a;
int *被隐式强制转为char *。
像int a = 12.56这种基本类型的隐式强制转换,编译器在编译时是不会报警告的,但是像指针这种很重要的类型,编译时一定会报警告提醒你,两边类型不一致,让你警惕,因为编译器很担心这是你的误写。
实际上我们并不提倡使用隐式强制转换,如果你确实需要进行“强制转换”,我们应该使用()进行显式强制转换,显式强制转换的目的:
· 让代码意思更明确,具有更强的可读性
否则其他程序员在看到你的代码时,可能认为因为误写才导致类型不一致的,而()可以明确表明,我这里就是要进行强制转换,程序有这个需求。
· 不要让编译器误解
对于指针等重要的类型来说,虽然它会帮助你进行隐式强制转换,但是它一定会提警告,提醒你这里类型不一致,你需要注意。
我们说编译器提警告是好事,因为这有利于程序员排查错误,为了不要让编译器报一堆的警告,干扰我们排查其它错误,我们也应该使用(),明确的告诉编译器,这里就是要进行强制准换,此时编译器就不要再报警告。
总之在编程时,如果能够让类型“天然一致”的话最好,否则就应该进行显式强制转换。
(6)函数传参和返回值的类型问题
1)传参
传参时也有一个“=”的存在,编译器在翻译传参时,与翻译=是一样的,所以传参等效于有一个=。
形参 = 实参
形参为左值,实参为右值。
如果传参时类型不一致的话,最好使用显式强制转换,不要使用隐式的。
一般时,都要求实参和形参类型一致。
2)返回值:
返回值与返回类型要一致,否者要强转,我们最好使用显式强制转换,不要使用隐式的。
int fun(void) { return (int)123.56; }
(7)强制转换时,到底干了什么事
不管是隐式还是显式强制转换,强制转换时,其实就是强行的采用某类型去解释数据,然后将解释后的数据copy给左值。
1)例子1
float a = 134.57 short b = (short)a;
按照short类型要求的“空间大小、存储结构”去解释a中的数据,然后copy给b。
short只有2个字节,float为4个字节,按照short类型解释时,只会强行解释a中头两个字节的空间,所以会丢失两个字节空间的内容。
而且存储类型也会从float转变为short(短整形)。
疑问:a的内容会发生改变吗?
答:不会,只是读取a的内容时,会进行类型的强制解释,但是a中原有的内容不会改变,改变的只是复制的副本。
2)例子2:
char a = 'a'; int b = (int)a;
强行按照int去解释a中数据,然后copy给b。
a只有一个字节,b有四个字节,按照int型强行解释a空间时,a的一个字节是不够的,所以会把后面紧挨着的其它的三个字节也会解释进来。
这一个强制转换不会丢失数据,但是数据也可能会发生改变,因为会多解释了其它空间的内容。
3)例子3:
float a = 100.12; int b = (int)a;
强行按照int去解释a中数据,然后copy给b。
a和b都是四个字节,空间大小都是一致的,但是存储结构不一致,此时数据也会发生变化,最终b中数据放的是100。
4)例子4
struct Student { int num; char name; float score; }; struct Student stu = {100, "zhangsan", "98.0"}; struct Teacher { char name; int num; }; struct Teacher tea; tea = (struct Teacher)stu;
强行按照struct Teacher类型去解释stu空间的内容,然后copy给tea。
struct Teacher的空间大小<struct Student,而且存储结构体也不一样的,所以强行按照struct Teacher去解释stu时,不仅数据会丢失,数据的存储结构也改变。
在struct Student类型中,头四个字节解释为一个成员,但是按照struct Teacher解释时,第一个字节为一个成员,其实每个字节的内容并没有改变,只是各自的解释方式不同了。
5)总结强制类型转换
通过前面的例子可以发现,进行强制转换时,并没有改变空间每个字节中的01011001这些内容,只是各自按照不同的类型去解释而已。
强制解释时,会出现如下问题:
(a)可能丢失部分空间中数据
(b)可能会多包含其它空间的数据
(c)存储结构会发生变化
所以进行强制转换时,一定要小心,只有当以上可能结果都被我们接受时,而且确实需要强制转换时,我们才会进行强制转换。
在正常情况下,我们更多是还是要,尽量的让类型天然的一致。