从零开始学C++(第三讲:何谓变量)
本篇说明内容是C++中的主要,基本大面积人对于这一系列内容总是昏的,但这一系列内容又是编程的基础中的基础,必须详细说明。
数字表示
数学中,数只有数值大小的不一样,绝不会有数值占用空间的区别,即数学中的数是逻辑上的一个概念,但电脑不可能。考虑算盘,每个算盘上有好多列算子,每列总分成上下两排算子。上排算子有2个,每个代表5,下排算子有4个,每个代表1(这并不重要)。所以算盘上的每列共有6个算子,每列共可以表示0到14这15个数字(因为上排算子的可能状态有0到2个算子有效,而下排算子则可能有0到4个算子有效,故为3×5=15种组合方式)。
上面的重点可以算盘的每列并无表示0到14这15个数字,而是每列有15种状态,所以被人使用来表示数字而已(这很重要)。因为算盘的每列有15个状态,所以用两列算子就可以有15×15=225个状态,所以可以表示0到224.易做图数字的每一位有0到9这10个图形符号,用两个易做图数字图形符号时就能有10×10=100个状态,所以可以表示0到99这100个数。
这个地方的算盘还有可以一个基于15进制的记数器(可以通过维持一列算子的状态来记录一位数字),它的一列算子就相当于一位易做图数字,每列有15种状态,故能表示根据0到14这15个数字,超出14后就必须通过进位来需要另一列算子的加入以表示数字。电脑与此一样,其并不可能数字电脑,而是电子电脑,电脑中通过一根线的电位高低来表示数字。一根线中的电位要求只有两种状态——高电位和低电位,所以电脑的数字表示形式是二进制的。
和上面的算盘一样,一根电线只有两个状态,当要表示超出1的数字时,就必须进位来需要另一根线的加入以表示数字。所谓的32位电脑可以提供了32根线(被称作数据总线)来表示数据,所以就有2的32次方当多种状态。而16根线就能表示2的16次方当多种状态。
所以,电脑并不可能基于二进制数,而是基于状态的变化,只可就是这种状态可以使用二进制数表示出来而已。即电脑并不认识二进制数,这是下面“类型”一节的基础。
内存
内存可以电脑中能记录数字的硬件,但其存储速度相当快(与硬盘等低速存储设备比较),又不可以较长时间保存数据,所以经常被用做草稿纸,记录多数临时信息。
前面可以说过,32位电脑的数字是通过32根线上的电位状态的组合来表示的,所以内存能记录数字,也可以能维持32根线上各自的电位状态(就可以象算盘的算子拨动后就不会改变位置,除非再次拨动它)。可就是依然存在考虑上面的算盘,如果一个算盘上有15列算子,则一个算盘能表示15的15次方个状态,是很大的数字,但经常实际是不会用到变化当大的数字的,所以促使一个算盘只有两列算子,则只可以表示225个状态,当数字超出时就使用另一个或多个算盘来一起表示。
上面不管是2列算子或者15列算子,总是算盘的粒度,粒度分得过大造成不需要的浪费(好多列算子总不使用),太小又很麻烦(就得多个算盘)。电脑与此一样。2的32次方可表示的数字很大,那么总不会用到,可能直接以32位存储在内存中势必造成相当大的资源浪费。于是如上,要求内存的粒度为8位二进制数,称为一个内存单元,而其大小称为一个字节(Byte)。可以说,内存存储数字,至少总会记录8根线上的电位状态,也可以2的8次方共256种状态。所以可能一个32位的二进制数要存储在内存中,就就得占据4个内存单元,也可以4个字节的内存空间。
大家在纸上写字,是通过肉眼区分出字在纸上的比较横坐标和纵坐标以查找到要看的字或要写字的位置。一样,因为内存就相当于草稿纸,所以也就得某种定位方式来定位,在电脑中,可以通过一个数字来定位的。这就和旅馆的房间号一样,内存单元就相当于房间(假定每个房间只可以住一个人),而前面说的那个数字就相当于房间号。为了向某块内存中写入数据(可以使用某块内存来记录数据总线上的电位状态),就必须知道这块内存对应的数字,何况这种种数字就被称为地址。而通过给定的地址找到对应的内存单元就称为寻址。
所以地址可以一个数字,用以唯一标识某一特定内存单元。此数字那么是32位长的二进制数,也就可以表示4G个状态,也可以说那么的32位电脑总拥有4G的内存空间寻址能力,即电脑最多装4G的内存,可能电脑有超过4G的内存,这种时候就就得增加地址的长度,如用40位长的二进制数来表示。
类型
在本系列最开头时可以说明了什么是编程,而刚才更进一步说明了电脑还有连数字总不认识,只是状态的记录,而所谓的加法也只是人为设计那个加法器以促使两个状态通过加法器的处理而生成的状态正好和数学上的加法的结果一样而已。这一切的一切总只说明一点:电脑所做的工作是什么,全视使用的人以为是什么。
所以为了使用电脑那相当快的“计算”能力(实际是状态的变换能力),人为要求了如何解释那些状态。为了方便其间,对于前面提出的电位的状态,大家使用1位二进制数来表示,则上面提出的状态就可以使用一个二进制数来表示,而所谓的“如何解释那些状态”就变成了如何解释一个二进制数。
C++是高级语言,为了帮助解释那些二进制数,提供了类型这种概念。类型可以人为制订的如何解释内存中的二进制数的协议。C++提供了下面的多数标准类型定义。
signedchar | 表示所指向的内存中的数字使用补码形式,表示的数字为-128到+127,长度为1个字节 |
unsignedchar | 表示所指向的内存中的数字使用原码形式,表示的数字为0到255,长度为1个字节 |
signedshor | 表示所指向的内存中的数字使用补码形式,表示的数字为–32768到+32767,长度为2个字节 |
unsignedshort | 表示所指向的内存中的数字使用原码形式,表示的数字为0到65535,长度为2个字节 |
signedlong | 表示所指向的内存中的数字使用补码形式,表示的数字为-2147483648到+2147483647,长度为4个字节 |
unsignedlong | 表示所指向的内存中的数字使用原码形式,表示的数字为0到4294967295,长度为4个字节 |
signedint | 表示所指向的内存中的数字使用补码形式,表示的数字则视编译器。可能编译器编译时被指明编译为在16位操作系统上运行,则等同于signedshort;可能是编译为32位的,则等同于signedlong;可能是编译为在64位操作系统上运行,则为8个字节长,而范围则如上一样可以自行推算出来。 |
unsignedint | 表示所指向的内存中的数字使用原码形式,其余和signedint一样,表示的是无符号数。 |
bool | 表示所指向的内存中的数字为逻辑值,取值为false或true。长度为1个字节。 |
float | 表示所指向的内存按IEEE标准进行解释,为real*4,占用4字节内存空间,等同于上篇中说到的单精度浮点数。 |
double | 表示所指向的内存按IEEE标准进行解释,为real*8,可表示数的精度较float高,占用8字节内存空间,等同于上篇说到的双精度浮点数。 |
longdouble | 表示所指向的内存按IEEE标准进行解释,为real*10,可表示数的精度较double高,但在为32位Windows操作系统编写程序时,仍占用8字节内存空间。 |
标准类型不止上面的几个,后面还会陆续说到。
上面的长度为2个字节也可以用两个连续的内存单元中的数字取出并合并在一起以表示一个数字,这和前面说的一个算盘表示不了的数字,就进位以加入另一个算盘帮助表示是一样的道理。
上面的signed主要字是可以去掉的,即char等同于signed char,用以简化代码的编写。但也仅限于signed,可能是unsigned char,则在使用时依然存在必须是unsigned char.
现在应该可以明白上篇中为什么数字还要分什么有符号无符号、长整型短整型之类的了,而上面的short、char等也总只是长度不一样,这就由程序员个人根据可能出现的数字变化幅度来进行选用了。
类型只是对内存中的数字的解释,但上面的类型看起来比较简单了点,且语义并不可能很强,即无什么特殊意思。为此,C++提供了自己选择类型,也可以将来继文章中用要说明的结构、类等。
变量
在本系列的第一篇中可以说过,电脑编程的绝大面积工作可以操作内存,而上面说了,为了操作内存,就得使用地址来标识要操作的内存块的首地址(上面的long表示连续的4个字节内存,其第一个内存单元的地址称作这连续4个字节内存块的首地址)。为此大家在编写程序时必须记下地址。
做5+2/3-5*2的计算,先计算出2/3的值,写在草稿纸上,接着算出5*2的值,又写在草稿纸上。为了接下来的加法和减法运算,必须可以知道草稿纸上的两个数字哪个是2/3的值哪个是5*2的值。人可以通过记忆那两个数在纸上的位置来记忆的,而电脑可以通过地址来标识的。但电脑只会做加减乘除,不会去主动记那些2/3、5*2的中间值的位置,也可以地址。所以程序员必须完成这种工作,用那两个地址记下来。
疑问可以这个地方只有两个值,也许好记多数,但可能多了,人是很难记住哪个地址对应哪个值的,但人对符号比对数字要敏感得多,即人很简单记下一个名字而不可能一个数字。为此,程序员就个人写了一个表,表有两列,一列是“2/3的值”,一列是对应的地址。可能式子稍微复杂点,当那个表可能就有个二三十行,而每写一行代码就要去翻查相应的地址,可能来个几万行代码那是人总不可以忍受。
C++作为高级语言,很正常地提供了上面疑问的处理之道,可以由编译器来帮程序员维护那个表,要查的时候是编译器去查,这也可以变量的功能。
变量是一个映射元素。上面说到的表由编译器维护,而表中的每一行总是这种表的一个元素(也称记录)。表有三列:变量名、对应地址和相应类型。变量名是一个标识符,所以其命名规则完全按照上一篇所说的来。当要对某块内存写入数据时,程序员使用相应的变量名进行内存的标识,而表中的对应地址就记录了这种地址,进而用程序员给出的变量名,一个标识符,映射成一个地址,所以变量是一个映射元素。而相应类型则告诉编译器应该如何解释此地址所指向的内存,是2个连续字节或者4个?是原码记录或者补码?而变量所对应的地址所标识的内存的内容叫做此变量的值。
有如下的变量解释:“可变的量,其相当于一个盒子,数字就装在盒子里,而变量名就写在盒子外面,这种电脑就知道大家要处理哪一个盒子,且不一样的盒子装不一样的东西,装字符串的盒子就不可以装数字。”上面可以我第一次学习编程时,书上写的(是BASIC语言)。对于初学者也许很简单理解,也不可以说错,可就是造成的误解用导致将来的程序编写地千疮百孔。
上面的解释隐含了一个意思——变量是一块内存。这是严重不正确的!可能变量是一块内存,当C++中著名的引用类型用被弃置荒野。变量实际并不可能一块内存,只是一个映射元素,这是致关重要的。
内存的种类
前面可以说了内存是什么及其用处,但内存是不可以随便使用的,因为操作系统个人也要使用内存,何况现在的操作系统正常情况下总是多任务操作系统,就好同时执行多个程序,即使只有一个CPU.所以可能不对内存访问加以节制,可能会突破另一个程序的运作。打个比方我在纸上写了2/3的值,而您无经我同意且无通知我就用那个值擦掉,并写上5*2的值,结果我后面的所有计算也就出错了。
所以为了使用一块内存,就得向操作系统申请,由操作系统统一管理所有程序使用的内存。所以为了记录一个long类型的数字,先向操作系统申请一块连续的4字节长的内存空间,接下来操作系统就会在内存中查看,看是不是还有连续的4个字节长的内存,可能找到,则返回此4字节内存的首地址,接下来编译器编译的指令用其记录在前面说到的变量表中,最后就可以用它记录多数临时计算结果了。
上面的过程称为需要操作系统分配一块内存。这看起来很不错,可就是可能只为了4个字节就需要操作系统搜索一下内存状况,当可能就得100个临时数据,就需要操作系统分配内存100次,很明显地效率低下(无谓的99次查看内存状况)。所以C++找到了这种疑问,并且操作系统也提出了相应的处理做法,最后提出了如下的处理之道。
栈(Stack)
每一个程序执行前,预先分配一固定长度的内存空间,这块内存空间被称作栈(这种说法并不准确,但因为实际涉及到线程,在此为了不用疑问复杂化才这种说明),也被叫做堆栈。当在需要一个4字节内存时,实际是在这种已分配好的内存空间中获取内存,即内存的维护工作由程序员个人来做,即程序员个人区分可以使用哪些内存,而不可能操作系统,直到已分配的内存用完。
很明显,上面的工作是由编译器来做的,不用程序员操心,所以就程序员的角度来看什么事情总没发生,或者就得像原来那样向操作系统申请内存,接下来再使用。
但工作只是根据操作系统变到程序个人而已,要维护内存,依然要耗费CPU的时间,可就是要简单多了,因为不用标记一块内存是不是有人使用,而专门记录一个地址。此地址以上的内存空间可以有人正在使用的,而此地址下面的内存空间可以无人使用的。之所以是下面的空间为无人使用而不可能以上,是当此地址减小到0时就可以知道堆栈溢出了(可能您可以有些基础,请不可以把0认为是虚拟内存地址,对于虚拟内存用会在《C++根据零开始(十八)》中进行说明,这个地方那么解释只是为了方便理解)。何况CPU还专门对此法提供了支持,给出了两条指令,转成汇编语言可以push和pop,表示压栈和出栈,分别减小和增大那个地址。
而最重要的好处可以因为程序一开始执行时就可以分配了一大块连续内存,用一个变量记录这块连续内存的首地址,接下来程序中所有用到的,程序员以为是向操作系统分配的内存总可以通过那个首地址加上相应偏移来得到正确位置,何况这种很明显地由编译器做了。所以实际上等同于在编译时期(即编译器编译程序的时候)就可以分配了内存(注意,实际编译时期是不可以分配内存的,因为分配内存是指程序运行时向操作系统申请内存,何况这种里因为使用堆栈,则编译器用生成多数指令,以促使程序一开始就向操作系统申请内存,可能失败则立刻退出,而可能不退出就表示那些内存可以分配到了,进而代码中使用首地址加偏移来使用内存也可以有效的),但坏处也可以只可以在编译时期分配内存。
堆(Heap)
上面的工作是编译器做的,即程序员并不参与堆栈的维护。但上面可以说了,堆栈相当于在编译时期分配内存,所以一旦计算好某块内存的偏移,则这块内存就只可以当大,不可以变化了(可能变化会导致别的内存块的偏移不正确)。打个比方需要客户敲入定单数据,可能有10份定单,也可能有100份定单,可能一开始就定好了内存大小,则可能造成不需要的浪费,又或者内存不够。
为了处理上面的疑问,C++提供了另一个途径,即允许程序员有两种向操作系统申请内存的方式。前一种可以在栈上分配,申请的内存大小固定不变。后一种是在堆上分配,申请的内存大小可以在运行的时候变化,不可能固定不变的。
当什么叫堆?在Windows操作系统下,由操作系统分配的内存就叫做堆,而栈可以认为是在程序开始时就分配的堆(这并不准确,但为了不复杂化疑问,故那么说明)。所以在堆上就可以分配大小变化的内存块,因为是运行时期即时分配的内存,而不可能编译时期已计算好大小的内存块。