第二章:数据类型

第二节:基本数据类型


首先把C语言所有数据类型列举出来:

char 有符号整数(8位)

short 有符号整数(16位)

int 有符号整数(32位)

long 有符号整数(32/64位)

long long 有符号整数(64位)


unsigned char 无符号整数(1字节)

unsigned short 无符号整数(2字节)

unsigned int 无符号整数(4字节)

unsigned long 无符号整数(4/8字节)

unsigned long long 无符号整数(8字节)


char 字符(1字节)

char* 字符串(64字节-如果在64为系统的话)


float 浮点数(4字节)

double 双精度浮点数(8字节)

long double 长双精度(16字节)


以上是C语言中所有的基本数据类型,其他数据类型都是由上述数据类型组合或者延伸而成的。

大家还记得我在前面章节中强调的,“计算机中所有数据最根本的形式都是二进制串,二进制串代表什么意思,由程序员自己决定的”。这句话吗?C语言给了我们程序员一种手段,通过这种手段可以告诉编译器,我们希望将一个二进制串解释成什么东西,怎么解释。这个手段是啥呢,就是数据类型。如果我们把二进制串解释成数字,那么这个二进制串就可以进行算数运算。如果我们把二进制串解释成文字或者图像,显然再进行算术算数运算就不合适了。所以,数据类型背后还隐藏了二进制串的处理方式。本章我们的重点内容就是每种数据类型如何解释二进制串,这种解释方法背后对应的数据处理方法。

我们先来简单的介绍一下,各个数据类型的基本概念。后续我们会详细介绍各个数据类型的以下几个概念点:


  1. 数据类型所占字节数;
  2. 0-1二进制序列和数据类型所代表的数值之间的关系
  3. 和这种数据类型相关的算法
  4. 这个数据类型的范围
  5. 是否回环


现在,我们不追究这几个概念,只是简单的介绍各个数据类型代表那些数据,这个很好理解,和我们人类的认知基本一致。

1. 无符号整数

这种数据类型代表了非负整数,取值范围和数据所占内存字节数的多少有关系。

2. 有符号整数

这种数据类型代表了整数,取值范围和数据所占内存字节数有关。

3. 浮点数

可以说,这个类型的数据代表了数学中的有理数,取值范围和浮点数所占字节数有关。

4. 字符和字符串

这种数据类型没有啥特殊的含义。其中字符数据类型代表了文本字符,他们没有任何计算方法与之对应,只是代表了我们人类能够阅读的一个字(也就是这个字的图片)。字符串就是这种字符的一个顺序堆积(就是一堆字符图片,按照顺序排列成一串)。(一个字符用单引号括起来,例如'A',一个字符串用双引号括起来,例如"maopaihuo-yangdmao"。)


常量、变量、内存位置(内存地址)。

在介绍这几个概念之前,我们需要先再了解一下可执行程序的结构,这个了解,比前面就要详细一些。我们前面说过,一个程序,由程序执行的指令和程序处理的数据两部分组成。现在我们要细分一下。程序的指令部分,一般来说,有这个特点,就是这些指令是固定的,不会改变的,因为程序指令的改变,意味着程序功能的改变。大家理解这个东西,可以和现实中这个事情联系起来。例如你打三国志这个游戏,点开图标后,肯定执行的是三国志这个游戏,如果有人把三国志这个游戏可执行程序的指令部分,替换成其他程序的可执行指令,那么你点开三国志这个游戏,执行的还是三国志这个游戏吗?所以,一个可执行程序的指令部分,一般是不会进行修改的,既然如此,程序中指令部分一般我们说他是不可修改的,专业术语叫只读段,意思很明显了,不能修改。而程序的数据部分,一般来说是必须能够被修改的,好了,这部分我们叫做可读写段。

然后,我们再把程序中所涉及的数据分成两部分,一种一部分是临时数据,一部分是永久数据。怎么说呢,永久数据是程序在整个执行过程中,这个数据都必须存在,在程序执行期间,这个数据的值都有可能改变。还有一种数据,是程序在执行的某个阶段,可能需要这个数据,过了这个执行阶段,这个数据就没有用了,不再需要了,这部分数据我们叫临时数据。

对于程序中的数据,还有另外一种分发,就是这个数据是直接表示一个值呢(不能修改),还是这个数据可以被修改(在不同的程序阶段,数据的值可能不同),这时就需要一个名字来表示这个数据,也就是所谓变量(所谓的名字就是一个数学上的变量,这个变量的值当前我们可能不在乎,以后用的时候,我们再给这个变量赋值)。这就是所谓的常量和变量。常量我们在乎的是值,变量我们在乎的是这个变量的名字(在数学上我们是这样的)。但是,在计算机里面,常量和变量的特点,就产生了不同的结果。先说常量,常量因为我们在乎的是其值,且这个值只有在我们说这个值的时候有用,比方说,我们说有一个萝卜是10斤,那么10这个数字就是一个常量,这个常量只在这句话中有效,放到其他任何地方,就没有意义了。总结一下,常量只在当前自己所在的语句中有效,且只有效一次,不会改变。(这里提一下特殊情况,比方说,我买了10斤糖,那么这个10和刚才10斤萝卜的10,我们可以认为这是两个常量,而且是两个不同的常量,在计算机程序中,确实如此)。根据常量的特点,不能被修改,且只在当前语句有效,这个特点是不是和指令的特点非常相似,因此,常量就和指令被放在一起,一般被放在只读段里面。

再说说变量,变量我们在数学上,在乎的是变量的名字,比如,有一个整数变量a,那么我买了a斤萝卜,我买了a斤糖。那这个时候,两句话中的a是同一个变量,至于这个a到底是多少,具体情况下,可能有不同的值。这是在数学上我们这么看的,那么我们现在再挪到计算机里面。如果a的值是5,那么这个a不管再什么地方,他都是5,这个可以理解把。变量,意味着这个a的值是可以改变的,只要a这个值改变了,所有使用a的地方,a就会被这个值所替代。有了上述特点,那么a这个变量在计算机中,就应该找一个固定的地方,保存这个a的值,如果有那一句话,需要a的值,直接就到保存a的值的地方,把a的值取出来就可以了。这个是不是就符合我们数学上关于变量的使用规则了。在计算机中,我们使用一段内存来保存a的值,既然如此,那么变量还有一个特征,就是变量一定被保存在某个内存位置,这个内存位置必须在可读可写段内,因为变量有内存位置,我们就可以说,变量有其固定的内存地址。

那么我们再加深一下,常量有没有内存地址?绝对意义上来说,常量有内存地址,因为常量属于程序的一部分,且和程序的指令放在一起,其必然被放到了内存中,既然在内存中,有内存地址就是再自然不过了。但很多人又说常量没有内存地址,其实这个说法肯定是不对的,但这样理解,又没有错。说法不对,但这样理解起来真香。为啥呢?因为常量的内存地址没有使用价值,常量只在使用常量的一条指令中有效(注意一条),在其他指令中无效,即使是值相等的两个常量,因为在不同的指令中被使用,也被看做是两个不同的常量。这样就导致,常量的内存地址在程序中没有了任何价值,因为无法在其他语句中第二次使用这个常量(这说明常量只能使用一次)。总结一下,常量不是没有自己的内存地址,是这个内存地址没啥用处。

现在再进行总结,程序分两部分,指令和数据,数据分常量和变量,变量分临时变量和永久变量(可以叫全局变量)。如下:

程序

指令

数据

常量

变量

临时变量

全局变量

程序分成指令和数据,数据有常量和变量,变量有临时变量和全局变量。

其中指令和常量被放在只读内存里面。其他的被放在可读写内存段里面。

现在我们细说下临时变量和全局变量。首先,每个变量都必须有一块内存和其对应,这是毫无疑问的,也就是每个变量都必须占用一段内存。如果临时变量在整个程序执行期间,都占用一块内存的话,是不是很浪费,这里合理的做法就是,临时变量在使用期间,占用内存,不使用了,这段内存就可以给其他变量使用了。对于永久变量,或者说全局变量,没办法了,只能给这家伙永久分配一段内存。

这个事实就造成了内存使用方法上的不同,对于全局变量,程序在运行开始的时候,这段内存必须留出来。而对于临时变量,程序执行到要使用这个临时变量了,再给他分配内存,不需要了,就把这段内存收回就可以了。

根据这个使用办法,我们下面这个做法就是最合理的啦。怎么做呢,首先,对于全局变量,我们在程序中,写入一些信息,这些信息表明,这个程序执行过程中,有多少全局变量,需要多少内存,这些内存应该在程序的那个位置。这样处理全局变量比较合适。在操作系统将程序从硬盘装入内存的同时,这些全局变量的内存就分配好了,也就是说,全局变量在程序开始准备执行阶段,就准备好了(如何准备的,其他课程再讲,这里先理解这些)。而对于临时变量,我们在程序的指令中插入一些专门为临时变量分配内存的指令,也就是在需要临时变量的时候,程序指令就为这个临时变量分配内存,在不需要这个临时变量的时候,对应的程序指令就释放这些内存。这是不是就合理了?但这增加了程序指令的数量,降低了程序效率,但又节省了内存(这就说明,干啥都是要付出代价的,就看值不值了)。我们现在把程序指令动态控制的这段内存取个名字,叫做栈(注意,这个名字将贯穿你的程序员生涯)。临时变量以及其他临时信息存储的地方,就在栈里面。(记住,临时信息的存储都在栈中)


我们现在用一个程序来演示一下上述内容:


/*
冒牌程序员-毛哥
*/
/**************variable.c*****************/
#include<stdio.h>

//定义了一个字符串变量s,同时将字符串常量”yangdamao-maopaihuo”这个字符串常量赋值给变量s。
char* s="yangdamao-maopaihuo";
char c='A'; //定义了一个字符变量c,同时将字符常量’A’的值赋值给变量c。
/*
以上两个变量都是全局变量,生成的可执行程序中将记录这些变量的类型,变量在内存中的位置信息。
*/

int main()
{
    int a; //定义了一个临时有符号整数变量a,程序运行到这个点的时候,有指令为这个a变量分配内存。

    a=100;  //这个语句中使用了一个常量100,这个常量100被赋值给了变量a。

    printf("a=%d\n",a); //打印变量a的值

    float f = 10.1;     //定义了一个浮点数变量f,同时给这个变量赋值10.1,10.1是常量。
    printf("f = %f\n",f);   //打印变量f的值。

    printf("s = %s\n",s);   //打印变量s的值。

    printf("c = %c\n",c);   //打印变量c的值

    return 0;
}


说明:

上面的程序,大家可能看不懂printf语句,尤其是其中的%d,%f,%c是干啥的,先别急,这就是学习计算机技术恶心的地方,讲A可能必须用到B的知识,但是这个时候B的知识显然大家还不知道。如果先讲B,再讲A,也有同样的问题,这里只能这样了。为了好理解,我在这里简单说下,%d,%f,%c是占位符,这个位置最终会被逗号后面的变量的值替代。‘\n’表示换一行的意思,大家可以删除‘\n’后看看效果就明白了。


以上程序中,全局变量s和c会在生成的程序中有信息记录,因为整个程序运行期间,需要使用这些数据(这里只是一个例子,实际上本例看不出来这俩货的特殊性)。我们说计算机语言都是形式语言,全局变量和临时变量必然在形式上有区别,这里的区别在哪里呢?看一下,s和c这俩货是不是在最外围,没有被任何大括号包围。所以从形式上说,全局变量就是没有被任何大括号包围的变量。而被大括号包围的就是临时变量,不管多少层,只要有一层,您老就是临时变量。临时变量会对程序指令产生影响,因为需要程序指令为临时变量赋值和分配内存。

变量定义的格式:

变量类型	变量名;

可以将变量的赋值和定义合并在一起写:

变量类型 变量名=常量;

可以几个变量一起定义:

变量类型 变量名1<=常量>,变量名2<=常量>; //尖括号内的内容标识可以有,也可以没有,通常专业的说是可选的。

例如:

int a, b, c=100,d;

注意,每一句话后面都要有一个分号,分号是C语言中,标识一句话结束的符号。(重点)

现在说常量,常量单独存在是没有任何意义的,比如,单写一个10在程序中,没有任何意义。常量只有和其他语句配合,才有意义。(从上边的程序可以看出来,常量一般都是用来给变量赋值的)。常量的几种写法:


  1. 例如100,啥都没有,或者10.1,也啥都没有,表示数字,根据是否有小数点,是否有符号,来区分常量是整数,有符号整数或者浮点数。
  2. ‘A’,用单引号包围起来的,是一个字符常量,只针对char这种数据类型有效。
  3. “yangdamao-maopaihuo”,用双引号包起来,是一个字符串常量,只针对char*这种数据类型有效。


总结:啥也不带,光溜出来就是数字,如果光溜出来了,又不是数字,就报错。单引号内是字符,双引号内是字符串。

指针数据类型及其含义:

由上面的描述我们其实知道了,不管是指令,还是数据,在程序执行期间都被放到了内存中,既然放到了内存中,那么必然有内存地址。但是,有些东西的内存地址是没有用的,例如指令,常量,这两个东西在程序中一般不被修改,我们也不用知道他们在内存中的那个位置。所以指令和常量就变成我们通常所说的,没有内存地址这个概念了。但是对于变量来说,内存地址就有用了。我们修改一个变量的值,可以通过变量的名字来修改。上面程序中(variable.c)中,a=100;这句话其实就修改了变量a的值。但是有时候通过变量名字修改变量的值是不可能的(后面我们会看到,在那种情况下,通过变量名字修改变量的值是不可能的)。这个时候,我们就需要变量的地址了。

C语言专门为每种变量类型设计了一个指向变量内存地址的数据类型,叫做指针。例如:

int *p;

这句话就定义了一个变量p,这个变量p的值是一个内存地址,这个内存地址,也就是内存位置内,存储了一个int类型的变量的值,至于这个变量的名字,无所谓(当然,通过上面的定义,我们不知道p指向的到底是那块内存,因为变量p还没有被初始化,也就是,他的值是未确定的)。我们可以通过修改p的值,使得p指向不同的内存,我们可以使用这个p来修改p指向的内存位置的值。修改的方法如下:

*p=20;
//给p前面加一个*,就表示p指向的内存中,二进制串表示的值,置于二进制串如何解释成这个值,由指针类型决定
//这句话就是将20这个值,保存到p所指向的内存中


这样,p指向的那块内存中,就保存了20这个4个字节的int类型的有符号整数。下面的程序演示这个事实:

/**************pointer.c************************/
#include<stdio.h>
int main()
{
    int a,b; //定义了两个int类型的变量a,b
    int *p; //定义了一个int类型的指针p

    p=&a;   //p的值是a变量所在内存的内存地址。
    *p=20;  //修改p指向的内存中,保存int类型的整数20的值到p执行的内存中

    p=&b;   //p指向了b变量所在内存的内存地址。
    *p=30;  //b变量的值变成了30,因为p此时指向b变量所在内存的内存地址。

    printf("a = %d b = %d\n",a,b);//看看a和b的值分别是多少

    /*p此时就是b变量的内存地址,看看p的值和b的内存地址是否一致。(这里&符号叫取地址符,在变量前面放一个这
    玩意,就代表了这个变量所在内存的内存地址,注意,只有变量可以这么干 
    */
    printf("p = %p, b variable’s address is %p\n",p,&b);

    return 0;
}

程序的注释:

以上两个程序中,//和/*    */分别叫程序的注释符号。

//

这个符号出现,从这个符号后面开始,到这个符号所在行结束,这之间的所有文字符号,都是没有意义的(对编译器来说没有意义)。但是,我们人是可以看见这些文字的,既然我们人能看见这些文字,那么这些文字就可以承载一些信息,这些信息虽然对编译器没有用,但我们人可以看呀,所以,被这些文字承载的信息就可以传递给我们。这些文字传递那些信息呢?可以传递任何信息,比方说,你可以把某个绝密配方写在这里,你也可以把你电脑的开机密码写在这里。当然,除非你假酒喝多了,耗子药吃多了,你才会这么干。一般来说,这些文字承载的信息,是对程序的解释,所以我们把他叫程序的注释。


/*

程序注释

程序注释

*/

显然/* 和 */之间的所有文字,都是程序的注释。这个叫块注释,//叫行注释。

全局变量和临时变量

前文提到了全局变量和临时变量,说全局变量是整个程序运行期间都要使用的变量,一次变量信息被写入到可执行程序中。而临时变量,或者叫自动变量,或者叫本地变量,这类变量是临时使用的,因此是有程序代码在程序执行期间,动态的从程序的栈中为这类变量分配内存的,在这些临时变量不再被使用的时候,就从栈中把这部分内存收回,做其他用。那么现在问题来了,临时变量啥时候被分配,啥时候被收回呢?也就是临时变量起作用的域是哪里?我们用程序进行说明。

/***********冒牌程序员-毛哥 field.c*****/
#include<stdio.h>

int global_x;
/*
变量在哪里定义,就在那里有效,全局变量没有定义在任何大括号中,所以,这个变量在任何地方都有效,除非被屏蔽。
*/
int main()
{
    int local_x;
    /*
    这个变量被定义在main函数的函数体大括号中,因此,这个变量就在main函数的函数体大括号内有效,除非被屏蔽
    */
    local_x = 10;
    {
        int local_y;
        /*
        这个变量local_y被定义在一个嵌套的大括号内,那么这个变量只能在这个嵌套大括号内有效,除非被屏蔽。
        出了这个大括号,这个local_y变量就不能再使用了。
        */
        local_x = 100;
        local_y = 200;
        float local_x;    
        /*
        注意,这个local_x会屏蔽掉上一层大括号内定义的临时变量local_x。同时使得上一层大括号内定义的local_x
        不能在本大括号内被使用,注意,是已经不能被使用了!!!!
        */
        local_x = 300.333;
        printf("看看现在local_x的值:%f\n",local_x);
        /*
        通过上述打印结果,可以看出我们这里所使用的local_x,是本层大括号内定义的local_x变量。
        */
    }   //出了嵌套大括号,变量local_y就无效了,同时这个嵌套打括号内的临时变量local_x也失效。
    /*
    出了float local_x定义的大括号后,int local_x的定义重新被使用,这个时候,就可以重新使用int local_x这个变量了。
    */
    printf("local_x = %d\n",local_x);

}

从上面的程序可以看出,临时变量和全局变量的作用域。另外开需要说明的是,一个大括号不管括了多少语句,都可以看做是一条C语句。