一文带你深入浅出C语言数据

目录

前言

常量与变量

1.概念

2.局部与全局变量

1.作用域

2.生命周期

3.全局变量与局部变量可以同名

3.初识各类常量

1.const修饰的常变量

2.#define定义的标识符常量

3.枚举常量

基本数据类型详解

1.关于位,字节和字的区别

2.int类型

1.变量声明

2.注意一下变量的命名

3.关于进制的显示

3.其他整型

4.关于原码反码与补码(这节之后有深入讲解)

注意:计算机存储和运算时用的都是补码而非原码

5.整数溢出问题

1.问题是什么

2.别急,这就涉及到有符号整数的范围规定了

3.让我们回到int范围的问题上来 

6.大小端字节序问题

1.字节序

2.什么叫大端小端

3.为什么有大端和小端

4.如何测试大小端

7.char类型

1.基本介绍    

2.非打印字符

3.何时使用ASCII码?何时使用转义字符?

4.有符号还是无符号char

8.浮点数类型

 1.简介

2.看看C标准的规定(了解一下)

3.表示形式

4.打印

5.浮点型的存储

9.隐式类型转换 

1.整型提升是什么

2.整型提升的意义

3.如何进行整型提升

4.算术转换

10.一些例题

1.第一题

2.第二题

3.第三题

4.第四题

5.第五题

6.第六题

其他数据类型一览

1.指针类型 

 2.构造类型

3.空类型

       敬请期待更好的作品吧~


前言

        本文致力于用简单浅显的方式讲解C与数据的知识,简单地介绍常量变量,基本数据类型、隐式类型转换及例题,大小端字节序等的知识,希望的是能够用知识间的联系建立起博客间的联系,在讲到某些内容的时候,本文不细讲的地方会在我写的别的博客里深入讲解,将知识串联起来。

一文带你深入浅出C语言数据_第1张图片 给你点赞,加油加油!

常量与变量

1.概念

        在程序中,我们使用的量无非就是常量和变量。

        听过数学里的常数没?其实差不多。所谓常量就是预先设定好值并且在程序执行过程中不发生变动的量,比如我要计算圆的的面积,那在编写程序的时候我就先定义一个常量PI为3.14嘛,而这个值在整个程序运行过程中不会改变。

#define PI 3.14

         变量顾名思义就是会随着某些条件变化的量,其实就是会在程序运行过程中根据某些条件或设计而改变的量。比如我要计算两个整数之和,定义一个整型sum初始化为零,当执行完求和语句后sum的值就变成了60嘛。

int sum = 0;
sum = 10 + 50;

2.局部与全局变量

1.作用域

        作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字(标识符)并不总是有效可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

        作用域有块作用域,函数作用域,函数原型作用域和文件作用域。

        在存储的类别会继续讲到作用域,这里简单理解一下。

        变量的作用域简单来说就是变量能发挥用处的范围,世间万物都有自己能起到作用和起不到作用的地方嘛,就比如说老师的作用域在哪?学校呗!

        局部变量,就是作用域在局部范围的变量。

        我们来看看下面这段代码,想一想变量a, b的作用域分别是什么?

int main()
{
    int a = 10;
    printf("%d", a);

    {
        int b = 10;
        printf("%d", b);
        printf("%d", a);
    }
    printf("%d", a);
    printf("%d", b);
    return 0;
}

        我们让代码跑起来看看,发现发生错误无法运行,警告如下:

一文带你深入浅出C语言数据_第2张图片

        为什么b会未声明呢?我不是在main函数花括号里面的花括号里定义了b吗?奇怪捏。

        这就要注意了,这里的b作用域就是这个花括号,出了花括号b就没用了。

一文带你深入浅出C语言数据_第3张图片

         这里可能有人就要问了,b不是定义并赋值了吗,b就算不能再使用了那应该还存在吧?怎么会未声明呢?搞得好像b不曾来过一般。(在后面会细讲好吧,别急呢米娜桑~)

        那a的作用域就是main函数的花括号范围咯,真聪明!

        实际上,局部变量作用域是块作用域,看的就是离它定义处最近的花括号,花括号括起来的范围就是它的“活动范围”,所谓的块就是花括号括起来的代码区域。

        全局变量,就是作用域为整个工程项目的变量。

        一句话,全局变量就是这条道上的爷,想搁哪沾花惹草那都不是事儿。

        首先,全局变量在整个文件范围内都有用,而一个文件里可能有若干个函数,这个变量也就是能够在这若干个函数内使用。(其实就是文件作用域)

        看看这段代码,全局变量c穿梭在两个函数之间。

int c = 0;//全局变量可以在函数外面定义并初始化

//求两数之和函数
int add(int a, int b)
{
    c = a + b;//在这个函数里就用到了全局变量c接收求和的值
    return c;
}

int main()
{
    int a = 10;
    printf("%d ", a);

    int b = 10;
    printf("%d ", b);

    printf("%d", c); //main函数里也用到了c

    int e = add(a, b);
    printf("%d ", e);

    return 0;
}

        这里顺便插一嘴,上面定义了一个函数嘛,那么请问函数形参的作用域是哪呢

        这形参不是搁圆括号里头嘛,我寻思这函数形参也不是全局变量吧,前面不是说局部变量找花括号,那这在花括号外头啊,咋判断呀?

        小伙汁,年纪轻轻的可别被这“奇装异服”给骗了️‍♀️,虽然函数形参声明是在花括号外头,圆括号里头,但是它的作用域是块作用域,也就是后面的花括号呗。

        这就好比如啥呢,“洋装虽然穿在身,我心仍是中国人”,就算我可以到处去国外旅游,但是一到要我发光发热的时候那肯定只能为祖国发光发热嘛——虽然我函数形参待在圆括号里头,但是我真正发挥用处的地方是在花括号里面呢。

        一个项目包含若干个文件,也就是说全局变量可以在各个文件中使用,只是需要标明一下告诉编译器我要在别的文件用这个文件里的全局变量呗。(讲的很片面了解一下即可)

extern int a = 100;

      这里其实就粗略了解一下即可,更多细节以后会在存储的类别里再给大家讲讲。

2.生命周期

        变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。

        生命周期也可以用在人身上嘛,人的生老病死这么一个过程的时间段呗->->。

        变量啥时候创建的呢?在定义变量的时候开辟了相应的内存空间来存储变量的值,这个时候变量就“诞生”了。

        那变量啥时候“翘辫子”呢?其实吧这和它作用域有关系,局部变量生命周期就是从程序执行到局部变量处开始,局部变量被创建被使用,直到程序执行出了作用域,局部变量被销毁。

        再看看前面这个例子,出了花括号后b就被销毁了,自然在花括号外是被认为是未声明的。

一文带你深入浅出C语言数据_第4张图片

一文带你深入浅出C语言数据_第5张图片

        全局变量生命周期就是程序执行的整个过程。

     

3.全局变量与局部变量可以同名

        综合上面所讲的内容,我们来看看下面的问题。

比如:

一文带你深入浅出C语言数据_第6张图片

会打印哪一个值呢?还是编译失败呢?

在C语言中,

 1. 不允许在同一个作用域中定义多个相同名称的变量

   比如:在一个班级中存在两个名字相同的学生王帅,当老师点王帅回答问题时,那个回答就冲突了

  2. 允许在不同的作用域中定义多个相同名称的变量

   比如:两个班级中各有一个王帅,老师在A班中点王帅回答问题,不会有任何歧义

  3. 不同作用域中定义的变量,在访问时采用就近原则

   比如:你们村有一个小伙伴名字叫刘德华,那你在你们村喊刘德华时,你们村的刘德华就会跑过来响应你,

      而我们世界级别人见人爱的天王他不会理你,因为距离远听不见,但是两个刘德华可以同时存在这个 世界上,只要不在一个村,就不会冲突。

 根据以上描述可知,对于上面那份代码:

  1. 全局作用域中的num和main中的num可以同时存在,不会冲突,因为不是同一个作用域

  2. 在main函数中访问num时,采用就近原则,因此访问的是main中的num,相当于将全局作用域中的num屏蔽了

3.初识各类常量

 这里就初步认识一下各类常量,以后会分别深入讲解的。

C语言中的常量分为以下以下几种:

字面常量     (字面常量就是单纯的常量比如3.14,1000,'a')

const 修饰的常变量

#define 定义的标识符常量

枚举常量

1.const修饰的常变量

        比如说原本有一个变量float a = 20.5;,我可以任意修改它吧,a = 60.5;。

         这时候如果我想让它不能再被修改了呢?那就const float a = 10.5;

        一旦使用了const修饰后,这个变量的值就不能直接修改了, 具有类似于常量的性质。

        举个例子就是,我有一个盒子,我想装游戏机就装,想装鸭舌帽就装,完全凭我需要来使用这个盒子的空间对吧,那要是有一天我突然想让这个盒子只装我的RMB¥咋办,简单,买个一上锁就搞定了,这下这个盒子里装的的东西就不能直接改变了嘛(最起码也得先解开锁)。

        const就像那把锁头一样嘛,锁住了变量对应的那块空间呗。

        那这玩意儿到底是变量还是常量?我们来简单检验一下

const int n = 10;

int arr[n] = {0};

代码走起来,发现出现错误

        因为数组括号内应该是常量表达式,也就是说这里的n它仍然是变量,本质未变,只是具有了与常量类似的性质。

2.#define定义的标识符常量

        我想要在程序中反复用到一个常量,并且它对于我这个程序有着特定的意义,比如我要求长方体体积,我需要长宽高对吧,为了方便区分,我就想给它们起个名字,高就是height,宽就是width,长就是length,然后呢我这长宽高是给定的常量,那这时候我就可以这样

#define height 100

#define width 50

#define length 200

        这样就定义了三个常量,并且每个常量都给它们定了个名字(也就是标识符),这样我在写程序的时候就可以用标识符来代表我要使用的常量

int V = height * width * length;

        这些标识符在程序运行之前就会自动被替换为对应的变量值,也就是在预处理阶段时变成这样:int V = 100 * 50 * 200;

有什么好处?

1.能很清楚我要用到的常量代表什么意义

2.在多次中多次用到该常量的时候,如果我想要把它的值改变一下,只需要在#define定义的地方把数值改动一下就可以了,不需要在程序中一个一个地改.

3.枚举常量

        什么是枚举?一一列举呗,也就是把常量一一列举出来。

        我们生活中有什么常量是可以一一列举出来的?

        比如性别

enum sex
{
    MALE,  //男性
    FEMALE  //女性
}

        这里创建了一个数据类型enum sex,它的可能取值为MALE或FEMALE,而这个MALE和FEMALE就是所谓的枚举常量,它们的值不可改变。(不细讲,在自定义类型会讲)

在继续讨论之前,我有一个问题:为什么要根据类型,开辟一块空间,直接将内存整体使用不好吗?

不好。

任何时刻,都不是你一个程序在运行,还有很多其他程序也在运行。你整块用了,让别人怎么办?

另外,你全都用了,一定需要在任何时刻,全部都用完吗?对于暂时不用,但是给你了,对计算机来讲,就是浪费。

C中为何要有类型?本质上对内存进行合理化划分,按需索取

那么,问题又来了,我使用部分内存,使用多少由什么决定?其实是由你的场景决定,你的计算场景,决定了你使用什么类型的变量进行计算。你所使用的类型,决定了你开辟多少字节的空间大小。

所以,C语言中,为什么会有这么多的类型?就是为了满足不同的计算场景。

比如,整形计算,字符计算,字符串计算,浮点数计算等。

本质:用最小成本,解决各种多样化的场景下的问题

基本数据类型详解

类型的意义:

1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。

2. 如何看待内存空间的视角。

1.关于位,字节和字的区别

它们都是描述计算机数据单元或存储单元的术语,这里主要指存储单元。

        位(bit)是最小的存储单元,即一个二进制位,仅能存储0或1。

        字节(byte)是常用的存储单元,基本上大部分机器1字节均为8位,至少在衡量存储单位的时候是这样的。

        字(word)是设计计算机时给定的自然存储单位,目前的个人计算机有1字长为32位的,计算机字长越大,其数据转移就越快,允许的内存访问也更多。

数据类型关键字

一文带你深入浅出C语言数据_第7张图片

_Complex和_Imaginary分别代表复数和虚数

2.int类型

        有符号整型,必须为整数。

        取值范围与计算机系统有关,一般而言,存储一个int要占用一个机器字长,16位的机器就是用两个字节来存储int,取值范围就是-32768~32767。而目前个人计算机一般是32位的,也就是用4个字节存储int。

1.变量声明

        类型关键字 + 变量名;(其他变量声明都是这样)

        int a;

        int b, c, d, e;

        可以单独声明,也可以同时声明多个变量。

2.注意一下变量的命名

        名字必须是由字母,数字或下划线组成,不能有特殊字符,同时也不能以数字开头。

同时变量名不能是关键字。

如:int double = 10.0;(错)

       int 2people = 100;(错)

       int _2peo = 100;(对)

        变量获得值,有三种途径,一是赋值,如a = 10; ,二是通过函数获得值,如scanf(),三是初始化获得值,可以直接在声明中完成,如int weight  = 100; ,不过要注意不要把未初始化和初始化变量搁在一块,容易误会,如int q, p =100; 。

一文带你深入浅出C语言数据_第8张图片

3.关于进制的显示

转换说明

十进制--%d

八进制--%o

十六进制--%x

若要显示八进制和十六进制的前缀0,0x和0X,必须分别使用%#o,%#x,%#X。

3.其他整型

        当int无法满足各种需求的时候,就需要其他整型了。

        C语言提供3个附属关键字修饰基本整数类型int,有short,long和unsigned(无符号即非负数)。

        为了适应不同的机器,C语言只规定了short占用内存不能多于int,long占用内存不能少于int,而int又与机器字长有关。现在大多都以16或32位存储int(依计算机自然字长定),32位存储long,16位存储short,为存储64位的整数引入了long long类型。

        C标准只对基本数据类型规定了允许的最小大小。

        对于16位机,short和int的最小大小是16位。对于32位机,long的最小大小是32位。

        unsigned short和unsigned int最小是16位,unsigned long最小是32位,而long long和unsigned long long就是64位。

        这也太多了吧,到底该如何选择整型来使用呢?

        对于unsigned类型,常用于计数,因为计数一般是非负数,同时unsigned可以表示更大的正数。

        对于那些long占用内存大于int的系统,一般超出int而在long范围内的数用long类型,但是使用long会减慢运算速度,所以需要斟酌一下,如非必要尽量不用long。

        对于那些long占用内存与int相同的系统,当需要用到32位的整数时使用long可能会更好,能提高可移植性,即使把程序从32位机移植到16位机后仍然可以正常工作。

        为什么呢?因为16位机的long是32位的,int是16位的,原程序若选用int存储32位的整数,移植之后会溢出,而选用long则刚好接收,不用担心溢出。

        类似地,如果是确实需要用到64位的整数才考虑用long long,不然会拖慢运行速度。

        对于常量,通常程序代码中的数字都会被存储为int类型,如果超出了int范围的话编译器会将其视为long int ,再超出就视为unsigned long,要是再超出就视为long long甚至是unsigned long long。

        八进制和十六进制常量若超出范围则是这样:int-->unsigned int-->long-->unsigned long-->long long-->unsigned long long。

        要是有某些特殊需求要把一些较小常量用long存储呢?可以在值的末尾加上后缀l(小写L)或L。

        比如0x55L,100L。LL就是long long后缀,再加上个u或U就是unsigned long long后缀了。

汇总表

4.关于原码反码与补码(这节之后有深入讲解)

        有符号整数在计算机中怎么存储与计算呢?我们知道计算机是用二进制数存储一切数据,那怎么去用二进制数表示负数呢?

        前人想到了取最前面的一位作为符号位来规定正负数,我们下面拿一个字节来举例说明

一个字节八个二进制位

一文带你深入浅出C语言数据_第9张图片

        原码:第七位作为符号位,当该位值为1时表示负数,为0时表示非负数,这就是原码。

        比如:-100原码

一文带你深入浅出C语言数据_第10张图片

        反码:非负数反码与原码相同,负数反码为原码除了符号位以外按位取反。

        按位取反就是对于单独的一个位的值,如果原来是0就变成1,原来是1就变成0。

        比如:-100的反码

一文带你深入浅出C语言数据_第11张图片

        补码:非负数补码与原码相同,负数补码为反码+1。

        比如:-100的补码

一文带你深入浅出C语言数据_第12张图片

注意:计算机存储和运算时用的都是补码而非原码

        补码利于计算机准确计算,而原码便于我们识别数值。

        在计算机系统中,数值一律用补码来表示和存储。

        原因在于,使用补码,可以将符号位和数值域统一处理。
        同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

        以一个字节范围来举例:

        计算5+6 和 -5+6

一文带你深入浅出C语言数据_第13张图片

        溢出的第九位去掉,只取八位

一文带你深入浅出C语言数据_第14张图片

以上就是原反补码的简单介绍了,接下来我们继续深入了解一下。

5.整数溢出问题

1.问题是什么

        实际上溢出行为是C标准未定义行为,不同编译器可能状况不一样,就拿VS来研究一下。

        在VS下unsigned int,int和long都是四字节,short是两字节。

        无论上网一搜还是课本一查,都发现发现int范围为-2148483648~2147483647,而unsigned int范围0~4294967295,那你有没有想过为什么有符号整数正负范围不对称?

        看看下面演示代码,思考一下为什么是这样。

一文带你深入浅出C语言数据_第15张图片

在VS下,int溢出以后从最小值-2147483648开始,分析一下:

一文带你深入浅出C语言数据_第16张图片

        你可能会觉得有点不对劲,按照2147483647 + 1后的补码逆推一下原码会得到0000000000000000000000000000000,那不就是0了吗??

        欸等会儿,0的原码也是这个呀,那为什么printf函数打印a+1得到的却是-2147483648??

2.别急,这就涉及到有符号整数的范围规定了

        我们一点一点来讲,在还没有出现反码和补码的时候,人们发现用原码存储和计算会出现一些奇怪的结果。

(这里以下都用一个字节来演示)

        比如2-1,计算机没有减法这一概念,在执行算术减法时会用加法来计算,即2+(-1),则有

一文带你深入浅出C语言数据_第17张图片

        得到结果居然是-3!

        多少有点离谱了,要是只用原码一遇到负数或减法就得出大问题。

        所以大伙就说用反码试试吧,那好咱就来试试

        还是2+(-1)

一文带你深入浅出C语言数据_第18张图片

        不断往前进1得到了九位二进制数,但是我们只要八位,多出的一位舍去,就得到了1。

        这下不就对上了嘛,计算无误。

        那是不是这样就解决一切问题了呢?我们来看看下面这个例子

        对于1+(-1),得到的11111111取原码就是10000000。

一文带你深入浅出C语言数据_第19张图片

        小伙汁,你看看原码10000000是什么数?是不是想说-0?是不是有点不对劲,怎么会有-0这种东西呢?0既不是负数也不是正数,况且原码00000000已经能表示0了,难不成来个+0=-0=0吗?滑天下之大稽!

        后来大伙捣鼓来捣鼓去,最终搞出了补码,那我们试试补码能不能解决问题。

        对于1+(-1),得到九位二进制数,只要八位,舍去第九位,得到00000000就是0,这不就成了嘛。

一文带你深入浅出C语言数据_第20张图片

        可是原码10000000的数到底是啥?emm,反正不能是-0,实际上我们这里只能由果溯因,

        应该问一下为什么一个字节范围是-128~127而不是-127~127。首先-127~127一共有多少个数?答案是255个,而八位二进制数最多能表示256个十进制数,这不就漏了一个嘛,我们看一下:

一文带你深入浅出C语言数据_第21张图片

        发现了没有?漏了哪一个呢?对,就是10000000,对它按位取反得11111111,再加1得1 00000000,就变成了九位,只要八位,去掉第九位就得到00000000即它的补码。

        现在我们看看-128如何,原码应当为1 10000000有九位,第九位作为符号位,按位取反得1 01111111,再加1得1 10000000发现补码与原码相同。一个字节只要八位,原码去掉第九位就得到了10000000对吧,这不就对上了吗。

        1000 0000  和-128原码丢弃最高位后余下的8位相同,而且即使去掉第九位后的-128和一个字节范围内的其他数(-127~127)运算也不会影响结果, 所以才敢这么表示-128。

        也就是说-128八位原码与补码都为10000000。

        比如:-128 + 125 = -3

一文带你深入浅出C语言数据_第22张图片

        那么127+1结果会是多少呢?答案是-128!01111111+00000001=10000000不就是前面讲了这么一长串的-128嘛。

        这也就意味之数值溢出后从最小值重新记起。

3.让我们回到int范围的问题上来 

        其实原理上差不多,意思就是达到最大正数值后再+1数值溢出了呗,具体参照先前讲的内容就行了。

一文带你深入浅出C语言数据_第23张图片

         这样的话上面一行printf()的输出就弄清楚了,那下面一行的printf()输出为什么会是-1,0,1呢? 

        问题出在哪呢?

        对,b是无符号整型而使用了%d,导致出现意外结果。

        我们结合前面讲的原反补码来分析一下为什么会这样。

        首先提一下前面没讲到的,无符号整型没有所谓所谓原反补码之说,最高位不用来作符号位,所以可以表示的正数值范围更大,无符号整型对应的二进制数就是存在计算机里的二进制数,不需要经过原码到补码的变化过程。相对而言,有符号整型存在计算机里和用于参与计算的都是补码。

        有符号整型int最大正数值是2147483647 < 4294967295,而%d就是告诉计算机我要输出的类型是有符号整型,最高位看成符号位,那好,上图。

一文带你深入浅出C语言数据_第24张图片

        也就是说当成有符号类型的话,4294967295对应二进制数看成补码,求出的原码对应的值就是-1,所以说会输出-1。

        那b+1输出0,b+2输出1就同理了吧。

        其实要用printf的话最好就把转换说明(比如%d之类的)和数据类型对应好,不要使用不匹配的转换说明,否则可能会出大问题。

        在使用%u后输出数值就正常了。

一文带你深入浅出C语言数据_第25张图片

6.大小端字节序问题

1.字节序

        字节序,简而言之,就是以字节为单位数据在内存中的存放顺序。

         要注意的是,数据在内存中是以二进制形式存储的,在显示的时候一般都以十六进制形式显示,我们后面讲解时都是用十六进制显示。

2.什么叫大端小端

        大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。

        小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

        为什么要叫这个名字呢?据说当时的命名者根据《格列夫游记》里两个党派关于吃鸡蛋时是从大头敲开还是从小头敲开发生不小的争执的故事而取名的,其实这样命名也比较形象,高位看成大头端,低位看成小头端,则可以有:

一文带你深入浅出C语言数据_第26张图片

         浮点型也有大小端字节序问题,其实只要是占用内存超过两个字节的数据类型都有大小端字节序问题。

3.为什么有大端和小端

        为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short 型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在如何将多个字节安排的问题

        因此就导致了大端存储模式和小端存储模式。 例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为 高字节,0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高 地址中,即 0x0011 中。小端模式,刚好相反。

        我们常用的 X86 结构是小端模式,而 KEIL C51 则 为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式 还是小端模式。具体是大端还是小端我们可以设计小程序测试一下。

 

4.如何测试大小端

        我们只要取出第一个字节存储的值,看看是不是1就行了。

一文带你深入浅出C语言数据_第27张图片

 实现代码:

#include 

int check_sys()
{
 int i = 1;
 return (*(char *)&i);//把类型转换成char*后解引用只读取第一个字节的值
}                     //要是小端就返回1,大端就返回0

int main()
{
 int ret = check_sys();
 if(ret == 1)
 {
     printf("小端\n");
 }
 else
 {
     printf("大端\n");
 }
 return 0;
}

7.char类型

1.基本介绍    

        char类型用来存储字符,但要注意,char是整数类型,实际上存储还是整数。

        还记得前面讲的吗,计算机存储数据都是数字数据,由无数的0和1按一定规定排序组成的,那为啥要叫字符类型?为什么说是用它来存储字符呢?

        实际上是这样的,计算机使用数字编码来处理字符,而我们用特点的整数表示特定的字符,最常用的编码是ASCII编码,比如65代表大写字母A,所谓的存储A实际上存储的是整数65。

        标准ASCII码范围是0~127,而C语言规定char类型占用内存为1字节。

        在C语言中,用单引号括起来的单个字符被称为字符常量,编译器会自动将其转化成对应的代码值,如'A'。

        因为字符是以数值的形式存储的,所以给char类型变量赋int型整数也是没问题的,只是要注意一下范围0~127。

        比如char cat = 65;如果系统能识别ASCII码的话就会认为是把A赋给的cat,但是最好不要这样赋,我也说了如果二字,有些系统不一定用的是ASCII编码,所以用字符'A'赋值最妥当。

一文带你深入浅出C语言数据_第28张图片

2.非打印字符

        注意到了吗,表里有些奇怪的字符根本就打印不出来,如行为字符退格,换行,振铃等,那怎么表示这些字符呢?

方法一:使用ASCII码,比如振铃对应ASCII码值为7,那就char beep = 7;。

方法二:使用特殊的符号序列表示字符,即转义序列也叫转义字符。 

        转义字符顾名思义就是转变意义的字符,一般由\+某个字符组成。

一文带你深入浅出C语言数据_第29张图片

        活跃位置指的是显示设备(如屏幕)中下一个字符将出现的位置,就是平时所说的屏幕光标位置。

一文带你深入浅出C语言数据_第30张图片

        接下来介绍一下常用的转义字符

        首先,问大家一个问题,回车和换行有什么区别?

        可能有人会觉得回车不就是换行吗...

        但其实回车和换行...

        不是一回事!!

        换行:活跃位置移动至下一行

        回车:\r 把活跃位置移到当前行的开始处,会把前面打印的清除掉。

一文带你深入浅出C语言数据_第31张图片

        \n就是将活跃位置移到下一行的开始处,也就是换行+回车。

一文带你深入浅出C语言数据_第32张图片

        \t把活跃位置移到下一个水平制表点(通常是第1个,第9个,第17个,第25个等)

一文带你深入浅出C语言数据_第33张图片

趣例:有趣的旋转杠

#include
#include
int main()
{
	int index = 0;
	const char* lable = {"|/-\\"};
	while (1)
	{
		index %= 4;//每满四清零
		printf("[%c]\r", lable[index]);
		index++;
		Sleep(50);
	}
}

         怎么样,是不是想起了“大风车吱呀吱呀吱呀转~”

         还有倒计时:

#include
#include
int main()
{
	int i = 10;
	while (i >= 0)
	 {
		Sleep(1000);
		printf("[%2d]\r", i--);
	}
	printf("\n倒计时结束!\n");

	return 0;
}

        因为  \,',"  都有特殊作用,所以在打印的时候无法直接打印,这就要用到转义字符了。

        单纯想打印”,printf(" \" ");

        想打印\,printf(" \\ ");

        想打印',printf("\' ");

        看看最后两个转义字符:

        \0oo,\后面是一个八进制数,比如\130,printf("%c", '\130');得到结果是打印出了大写字符X,为什么呢?\130先把130转变为十进制数也就是88,然后变成对应ASCII码值的字符。

        '\0oo'就代表一个字符,前面的0表示是八进制,省略了也是八进制数。 

         \xhh,\x后面是一个十六进制数,类同上面。

        实际上就是用八进制或十六进制的ASCII码来表示一个字符。

        举个例子就是'\101'等价于65等价于'A'。

巩固一下:

        C语言中有字符类型,但是没有字符串类型,使用双引号括起来的就是字符串。

//程序输出什么?
int main()
 {
	printf("%s\n","c:\test\628\test.c");     
	return 0;
 } 

一文带你深入浅出C语言数据_第34张图片

        提示:八进制里没有8,'\628'明显不存在,只能是'\62',对应字符'2'。

3.何时使用ASCII码?何时使用转义字符?

        一般选择后者,假设使用'\f'或'\104',明显转义字符更好记并且可移植性更高,因为不使用ASCII码的系统中仍然有效。

        如果要使用ASCII码,一般写成'\032'比032更好,一是前者直接能看出来你想使用的是字符编码,二是前者能嵌入C的字符串中,如printf("hello!\007\n");,能明显把它们和普通字符区分开。

        在使用printf()打印字符的时候,使用不同的转换说明%c或%d打印结果不同。

一文带你深入浅出C语言数据_第35张图片

4.有符号还是无符号char

        有些C编译器把char当成有符号类型,范围为-128~127,而有些编译器把它当成无符号类型,范围为0~255,可以查阅一下limits.h头文件。

        根据C90标准,C语言允许在char前面加上unsigned和signed修饰以区分出无符号和有符号类型而不用管编译器如何。

        因为char实际上存储的是整数,所以可以用在不超过1字节范围大小的小整数的处理上,以节省空间。

8.浮点数类型

 1.简介

        什么是浮点数呀?简单来说就是有小数点浮动的数。

        浮点类型能表示包括小数在内的更大范围的数。

        计算机中表示方法:

        一般记数法即第一列,指数记数法(计算机中的科学记数法)即第三列。

        这里的e是10,e+数字就是10的几次幂。

一文带你深入浅出C语言数据_第36张图片

 有三种类型:float,double和long double

2.看看C标准的规定(了解一下)

        C标准规定float(单精度)类型必须至少能表示小数点后6位有效数字且取值范围至少是10^-37~10^37,通常占用32位,其中8位用于表示指数的值和符号,剩下24位表示非指数部分及其符号。

        C规定的另一种浮点类型为double(双精度)和float的最小取值范围相同,但它至少必须能表示10位有效数字。一般而言double占用64位而非32位,一些系统将多出来的32位全部用来表示非指数部分,增加了有效数字的位数(提高了精度),另一些系统把其中一些位分给了指数部分以容纳更大的指数,从而增加了可表示数的范围,无论哪种方法,double类型的值至少有13位有效数字。

        long double至少与double的精度相同,一般都是比double精度要求更高。

3.表示形式

        浮点型常量的表示形式比较多,比如-1.56E+12,2.87e-3。

        e后面正号可以省略,可以没有小数点,如2E5,也可以没有指数部分,如19.28,但是不能同时没有小数点和指数部分。

        可以省略小数部分,如100.,也可以省略整数部分,如.45,但是不能同时省略小数部分和整数部分。

        一些例子:3.1415,.666,89.,45e-10。

        默认情况下编译器认为浮点型常量是double类型的精度,可以在浮点数后面加上f或F后缀改成float类型,如23.56f。

        也可以加上l或L后缀改成long double类型,如12.5e5L。

4.打印

        打印浮点数的时候,转换说明一般用%f说明打印十进制数的float和double类型数,打印long double用%lf,如果支持C99标准的话可以有%e打印指数记数法的浮点数。

5.浮点型的存储

先看一个例子: 

int main()
{
     int n = 9;
     float *pFloat = (float *)&n;
     printf("n的值为:%d\n",n);
     printf("*pFloat的值为:%f\n",*pFloat);
     *pFloat = 9.0;
     printf("num的值为:%d\n",n);
     printf("*pFloat的值为:%f\n",*pFloat);
     return 0;
}

结果会是你想的那样吗?

结果其实出乎意料!

一文带你深入浅出C语言数据_第37张图片

        这说明什么?如果整型和浮点型的变量的存储方式与读取方式一致的话,第一个*pFloat的值就是9,然而并不是,说明存储方式和读取方式确实不一样。

浮点数存储规则

        num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?

        要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。

详细解读

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

(-1)^S * M * 2^E

(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。

M表示有效数字,大于等于1,小于2。

2^E表示指数位。

举例来说:

十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。

那么,按照上面的格式,可以得出S=0,M=1.01,E=2。

一文带你深入浅出C语言数据_第38张图片

十进制的9.5,写成二进制会是1001.101吗?

不是的,这跟位权有关。

一文带你深入浅出C语言数据_第39张图片

1001.101实际上写成十进制是9.625。

9.5二进制直接就是1001.1

一文带你深入浅出C语言数据_第40张图片

 浮点数并不一定能精确地存储,比如:

 小数点后面无限延伸,无限接近但不相等,也就是只能近似逼近。

IEEE 754规定:

         对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

一文带你深入浅出C语言数据_第41张图片

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

IEEE 754对有效数字M和指数E,还有一些特别规定。

前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

        IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的 xxxxxx部分。比如保存1.01的时候,只保存小数点后面的01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位 浮点数为例,留给M只有23位, 将第一位的1舍去以后,等于可以保存24位有效数字。

        至于指数E,情况就比较复杂。 首先,E为一个无符号整数(unsigned int) 这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即 10001001。

如图,当E真实值为-1时:

 然后,指数E从内存中取出还可以再分成三种情况:

        (1)E不全为0或不全为1,这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。

        比如: 0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为 1.0*2^(-1),其阶码为-1+127=126,表示为 01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进 制表示形式为:

 0 01111110 00000000000000000000000

         (2)E全为0,这时,浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于 0的很小的数字。

        (3)E全为1,这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s).

解释前面的题目:

下面,让我们回到一开始的问题:为什么 9 -> 0000 0000 0000 0000 0000 0000 0000 1001还原成浮点数,就成了 0.000000 ?

        首先,将其拆分,得到第一位符号位SS=0,后面8位的指数 E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 0000 1001。

由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:   

V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-126)

        显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

再看例题的第二部分。        

         请问浮点数9.0,如何用二进制表示?还原成十进制又是多少?

         首先,浮点数9.0等于二进制的1001.0,即1.001×2^3。 那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010。

9.0 -> 1001.0 ->(-1)^01.0012^3 -> S=0, M=1.001,E=3+127=130

所以,写成二进制形式,应该是S+E+M,即这个32位的二进制数

01000001000100000000000000000000

还原成十进制,正是 1091567616 。

9.隐式类型转换 

        有些表达式的操作数在求值的过程中可能需要转换为其他类型

1.整型提升是什么

        C的整型算术运算总是至少以缺省整型类型的精度(至少是以int类型)来进行的。

        为了获得这个精度,表达式中的字符char和短整型short操作数在使用之前被转换为普通整型int,这种转换称为整型提升。

2.整型提升的意义

        表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。

        因此,即使两个char类型的数相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度int字节长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。

        所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。

        换句话说就是char和short类型太小了,不能直接拿来执行运算,得先变长成int字节长度,运算后再截断多余位变回原来字节长度。

        然而在进行类型转换的时候是没有用户命令而自动进行的,没有直接展现出来类型发生过变化,所以叫隐式类型转换。

3.如何进行整型提升

        整型提升是按照变量的数据类型的符号位来提升的,有符号类型整型提升高位补符号位1,无符号类型整型提升高位补0。 

(1)负数的整型提升

char c1 = -1;

变量c1的二进制位(补码)中只有8个比特位:

1111111

因为 char 为有符号的 char

所以整型提升的时候,高位补充符号位,即为1

提升之后的结果是:

11111111111111111111111111111111

(2)正数的整型提升

char c2 = 1;

变量c2的二进制位(补码)中只有8个比特位:

00000001

因为 char 为有符号的 char

所以整型提升的时候,高位补充符号位,即为0

提升之后的结果是:

00000000000000000000000000000001

(3)实例1

int main()
{
 char a = 0xb6;
 short b = 0xb600;
 int c = 0xb6000000;

 if(a==0xb6)
     printf("a");
 if(b==0xb600)
     printf("b");
 if(c==0xb6000000)
     printf("c");
 return 0;
}

在执行表达式a == 0xb6和b == 0xb600时,a和b要进行整型提升

a   0xb6   1011 0110   补码:1100 1010

整型提升后:

1111 1111 1111 1111 1111 1111 1100 1010

而0xb6的补码:0000 0000 0000 0000 0000 0000 1100 1010

        这就不相等了,因为0xb6(182)在char的一个字节中值溢出了(char -128~127)变成了负数,在整型提升后补的又是符号位,所以就与0xb6补码不同。

b   0xb600    1011 0110 0000 0000  补码:1100 1010  0000 0000

整型提升后:

1111 1111 1111 1111 1100 1001  0000 0000

而0xb600的补码:0000 0000 0000 0000 1011 0110 0000 0000

同理。

        a,b整型提升之后,变成了负数,所以表达式 a==0xb6 , b==0xb600 的结果是假,但是c不发生整型提升,则表达式 c==0xb6000000 的结果是真。

(4)实例2

int main()
{
 char c = 1;
 printf("%u\n", sizeof(c));
 printf("%u\n", sizeof(+c));
 printf("%u\n", sizeof(-c));
 return 0;
}

        实例2中的,c只要参与表达式运算,就会发生整形提升,而表达式 +c ,会发生提升由char变为int,所以 sizeof(+c) 是4个字节。

        表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节.

4.算术转换

long long

double

float

unsigned long int

long int

unsigned int

int

        如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个排名高的操作数的类型后执行运算。

        其实就是从短的向长的转换后再运算以确保精度,排位越高的实际上精度越高。

        如果让高精度的类型转换成低精度的类型会丢失精度,如

int a = 3.14159;

        实际上a的值是3,在转换的过程中小数点后的数字被丢弃。

        而算术转换也是看不见地自动进行的,比如

int a =10;
float  b = 9.34;
double c = a + b;

        a先要转换成float类型,相加之后的值还要再转换成double类型。

        然而我们确实没看到转换的过程,能看到结果,但有时结果或许会和我们想的不太一样。

10.一些例题

1.第一题

//输出什么?
#include 
int main()
{
    char a= -1;
    signed char b=-1;
    unsigned char c=-1;
    printf("a=%d,b=%d,c=%d",a,b,c);
    return 0;
}

        在赋值的时候,赋值符右边的常量是自动分配类型的,比如上面代码中的-1被认为是int型,那么有:

原码:10000000000000000000000000000001

补码:1111111111111111111111111111111111111

由于char只能存储一个字节的值,所以从低位向高位发生截断

a的补码:11111111

        在printf打印的时候使用的转换说明是%d,这时候需要发生整型提升,在这里char默认是signed char,则有:

补码:1111111111111111111111111111111111111

原码:10000000000000000000000000000001

        打印结果为-1

        对于b而言过程与结果同理,打印结果为-1。

对于c

原码:10000000000000000000000000000001

补码:1111111111111111111111111111111111111

由于char只能存储一个字节的值,所以从低位向高位发生截断

c的补码:11111111

        在printf打印的时候使用的转换说明是%d,这时候需要发生整型提升,又是unsigned char,则有:

补码:00000000000000000000000011111111

        被认为是正数,则原码与补码相同,所以打印结果为255。

2.第二题

#include 
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0;
}

对于-128

原码:10000000000000000000010000000

补码:11111111111111111111111110000000

由于char只能存储一个字节的值,所以从低位向高位发生截断

a的补码:10000000

        在printf打印的时候使用的转换说明是%u,这时候需要发生整型提升,是char,则有:

补码:11111111111111111111111110000000

打印的时候被认为是无符号数也就是正数,所以原码与补码相同,则打印结果为4294967168。

要是把%u改成%d得到结果又会是什么呢?

改成%d则认为是有符号数,符号位是1则为负数,对应原码

10000000000000000000000010000000

打印结果为-128

3.第三题

#include 
int main()
{
    char a = 128;
    printf("%u\n",a);
    return 0;
}

对于128

原码:00000000000000000000010000000

正数补码与原码相同

由于char只能存储一个字节的值,所以从低位向高位发生截断

a的补码:10000000

        在printf打印的时候使用的转换说明是%u,这时候需要发生整型提升,是char,则有:

补码:11111111111111111111111110000000

打印的时候被认为是无符号数也就是正数,所以原码与补码相同,则打印结果为4294967168。

4.第四题

int main()
{   
    unsigned int i;
    for(i = 9; i >= 0; i--)
    {
        printf("%u\n",i);
    }
    return 0;
}

打印什么?

答案是死循环。

        最大的坑就是i是无符号整型,并且for的循环条件有=号,当i自减为0时,照样可以进入循环,打印后i--,得到的不是负数,而是一个很大的正数!也就是说i不可能为负数,则永远满足i>=0的条件,一直循环下去。

0-1得

111111111111111111111111

而i是无符号整型,原反补码相同,对应得到的数为4294967295。

5.第五题

int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
   {
        a[i] = -1-i;
   }
    printf("%d",strlen(a));
    return 0;
}

        先看看要打印什么,strlen()求的是字符串长度,所关注的是字符串中'\0'(ASCII码为0)前面有多少个字符。

        可能有人就会觉得那不就是1000了嘛,多简单啊。

        错啦!一定要注意数组元素是char类型的,范围只在-128~127之间。

一文带你深入浅出C语言数据_第42张图片

         也就是说a[i]的取值:-1,-2,......-127,-128,127,126,........1,0,-1,-2......一直循环在-128~127范围内。在第一个0之前有255个数,所以答案就是255。

6.第六题

#include 
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0;
}

        归根到底还是无符号整数溢出问题,i始终是在0~255之间循环,不可能大于255,也就一直满足i<=255的条件,也就一直循环下去即死循环。

其他数据类型一览

1.指针类型 

一文带你深入浅出C语言数据_第43张图片

 2.构造类型

一文带你深入浅出C语言数据_第44张图片

3.空类型

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型。

比如:void test(void),  void *ptr ......


        到这里基本上就把C与数据的基本内容给讲完了,关于其他数据类型如数组,指针,结构和联合等内容还是蛮多的,会单独划分一个大章节来讲述。

       敬请期待更好的作品吧~

感谢观看,你的支持就是对我最大的鼓励,阁下何不成人之美,点赞收藏关注走一波~

一文带你深入浅出C语言数据_第45张图片

你可能感兴趣的:(一文深入浅出C语言,c语言)