第三章 数据结构

目录

第三章 数据结构

1.数据和容器

2.基本类型

3.指针类型

4.复合类型

5.数组类型

6.特殊类型

 

 

1.数据和容器

数据或者可以说是信息是什么,我认为信息就是能够复原为具体物理感官的东西,或者是没有物理实体的抽象概念,比如数学等产物。数据的种类有很多,图像,声音,甚至触感,然后是数学,文字,控制信息等。有一个问题,数据是否可以存储,如果可以存储,他必然有存储并复原的有效方式,如果可以存储,必然有存储所需要的物理空间。答案是可以!

我们可以用以下的公式去认识数据和电脑的关系:

图片 –(编码)-> 数据 -(解码)-> 图片

编码借助相关的物理设备,把光信号(人类能看到的),转化为电信号(电脑能识别的)。

解码也需要借助相关的物理设备,反方向处理。

光信号需要一一对应电信号,否则电信号是不可能还原出一模一样的光信号的。当然,因为人眼睛的光识别能力有限,只需要所谓的16777216个不同状态就能表示一个点可识别的任意颜色任意亮度的光。因此,一个光点就是一个小于2千万的任意数,实际上,任何数据,最终都是等价于某个范围的自然数,自然数还是图像数据,对于电脑处理来说没有任何不同,所以对图像的处理等于对数的处理。因此不管数据本身代表什么,要对他进行任意编辑,实际上也是简单的数学运算。

因此,我们只需要掌握两样,数和存放数的容器。

数,是无限的,用有限的物理空间不能存放无限的数,因此电脑的“数的类型”实际上等于数的范围的不同。电脑的单位物理空间是二态的,就好象一个萝卜一个坑,一个坑永远只有两个状态,一个是有萝卜的坑,一个是没有萝卜的坑。如果我们把二态空间串联起来,就好象自然数的表示方法,个位十位百位千位,不同位置表示的数是不同的,那么就能组成无穷无尽的大数(当然总空间是有限的电脑空间是做不到的)。问题是自然数是逢十进一,而电脑连续空间是逢2进一。这就需要经过编码来转换。转换方法其实不是很难,就是用自然数除以2取余数,从低到高排列。例如:

76 / 2 = 38 / 2 = 19 / 2 = 9 / 2 = 4 /2  = 2 /2 = 1 / 2

二进制结果是:1001100

这只是一种编码方案,也可以用其他方案,因为只需要满足一一对应这个要求便可,可以把1111定义为0,0000定义为1,只要0和1是不同的状态而你又知道便可。当然,一种方案的设计,还要看运算的设计是否简便。如:

  0000(1)

+0000(1)

------------

  0000(1)

通过简单的“进位加法”电路无法完成这个编码方案(因为0000等于1,而1+1不等于1),而这个电路是比较容易设计的。而原方案:

  0001(1)

+0001(1)

-------

  0010(2)

这个方案就能用很自然的进位加法去运算。

编码方案有很多种,有兴趣自己上网搜索一下,有很详细的介绍。这里就不深究了。

然后我们看看能存放76的电脑空间需要多少?他对应1001100,只需要7个物理单位便可:只要小于76的自然数,都可以用7个单位空间便可。这不是一个很大的空间。

1.1电脑空间大小的表示方法

一个单位空间称为“位”,用b表示。

8个单位空间称为“字节”,用B表示。

1024个字节等于1千字节,用KB表示。

1024KB等于1兆字节,用MB表示。

1024MB等于1吉字节,用GB表示。

1024GB..中文读法我也不清楚,用TB表示。

现在很多硬盘都是1TB的,很多内存是4GB的,很多手机内存是512MB的。可想而知,电脑容量是非常惊人的。

一个普通大小的图片文件大概就是300KB左右,一个MP3文件是4MB左右,一个dvd级别的电影文件是600MB左右。网速1Mb/s等于125KB每秒传送速度。各位不要混淆1Mb和1MB的区别了,商人最爱玩小花招。

1.2电脑是如何管理存储空间的

电脑空间可以看作是一块长长的队列,而电脑可以在这个队列的任意一点上存储数据,或者读出数据。电脑是如何做到的?首先电脑对空间按字节分配,每一个字节有一个访问它的地址编号。比如1KB的容量,地址是0.到1023,要想读第一个,只要给出0这个地址,电脑就能找到这个位置。要想读取中间哪一个,就给出地址511.

第二个问题,电脑一次可以操作多大的容量?电脑不可能一次操作全部的数据,而只能每次处理一点,不断的重复读取来访问所有数据,所以就有一次可以操作多大容量这个问题。根据电脑等级不同,有些电脑一次读取一个字节,有些可以读取两个字节,有些可以读取四个字节。理论上读取四个字节的大可以按照四个字节为一小节,分配一个的地址便可以,因为这样便能访问到所有空间,但是为了统一,电脑容量的地址都是按字节为一个节点来编址的。

总结:容量是一个连续空间,这个空间是经过编址的。

1.3 c++的数据类型

学了那么多基础知识,回到c++的学习中来吧。我说过这条公式:程序=数据结构+算法。现在我们就来学习一下c++的数据类型。

数据类型是数据组织的方式,一个数据类型说明了两样属性:

一、容量的大小

二、编码的方式

容量大小好理解,就是1个字节,还是两个字节。我们知道电脑一次处理的字节数是有限的,那么是否等于数据类型的大小一定要小于电脑一次处理的限制数呢?不是的,编译器会根据数据类型的大小,自动编译出对应的目标代码,指导电脑多次读取数据,而不需要各位操心这些细节问题。

第二点编码的方式,电脑的编码是多种多样的,图像的编码,声音的编码,数字的编码,每一种编码存储的方式是相同的,但是你必须按照原始对象的编码方式来编辑他,否则是无意义的。就如数字只需要简单加一,他就是增大,而对于声音数据,你必须按照正弦余弦的波形函数求得变化的量来调整音量,图像数据也是一样,你要调整亮度,也需要按照编码所对应的方式去调整他。比如一个光点是由三个字节来表示的,其中红色光信号用一个字节表示,绿色,蓝色也是如此。这就是光的三原色,可以混合成任意色彩。其中一个字节的大小范围是0到255,用0表示最浓度最低,255表示浓度最高,那么要得到存正的红色,只需要把蓝色和绿色调节到0,红色调节为255;要想图像跳亮,就把三原色的浓度同时降低,画面就会发白;要是三原色都是255,那就混合成纯粹的黑色了。之所以可以通过调节数字来调整图像,在于我们知道图像的编码方式,假如他是用4组颜色来编码的,那么你还是用三原色的方式去调整,就不会出现你预想的结果。

需要注意的是:数据类型暗示了编码的方式,但是并不能完全阻止你胡作非为,要怎样去改变数据,是否按照数据类型所暗示的编码方式去改变数据,那是你的自由。这是因为数据最终都是以数的形式存储起来的,如果你一开始并不知道他的编码方式,你就不能正确的编辑他。

c++的数据类型分为以下几种:

一、基本数据类型:包括某个范围的整数,某个范围的浮点数(小数),布尔数(只有“真”和“假”两个值的数,在逻辑数学里面有),字符。

二、指针类型:用来保存内存空间的地址。

三、复合类型:通过拼接其他数据类型组成一个更大容量的新数据类型。

四、数组类型:通过指定长度,把指定类型的单个空间组成的一条连续队列。

五、特殊类型。

1.4 常量和变量

变量是通过数据类型定义的一块存储空间,他符合类型的定义,并增加了一个属性。总的来说一个变量说明了三样东西:

一、容量的大小

二、编码的方式

三、所在容量空间的位置

类型并没有分配空间,而只是一个规范,所以在容量空间没有实际位置。通过类型去定义变量,就实际分配了一块空间,然后你就能存储和读取这块空间。你可能觉得将一件事分成两步有点麻烦,但是这样做是有好处的,因为你可以定义一个类型后,随意制作出任意相类似的变量,他们的操作方式是类似的(由编码方式决定),唯一的不同就是代表的存储空间不同。

c++定义变量的方式很简单:

类型 标识符;

不过要注意细节,类型是c++所支持的所有数据类型,只要用类型的名字去替换"类型"二字所在的位置便可。

然后中间有一个空格,也可以有多个空格,或者你按“回车键”也可以,因为这条语句是以分号结尾,而不是以一行的结束作为结尾,但是除了空格回车等空白符(也就是看不到文字的符号)你不能属于其他不相干的字符。

紧接着是“标识符”,标识符是一个命名规范:第一个字符不能是数字和标点符号,可以是数字,也可以是中文(编译器支持才可以用),接着第二个和往后的字符限制就小点,可以是数字。按照这个规范组成的名称是被接受的。当然不能同名。

最后是分号结尾,需要注意分号是英文的分号,不是中文的分号,这对电脑来说是有分别的。

然后是常量,常量是不能修改的变量。所谓不能修改说的是在初始化(初次设定)数值后,再也不能改变这个数值。

常量定义方式:

类型 const 标识符;

就是在中间插入一个英文const(意思就是常量)。const左右都需要有空白符。

1.5这一节的总结

首先,让各位了解电脑的存储空间是怎么一回事,然后介绍c++中是如何利用这些存储空间的。通过定义变量就能够存储,读取,修改相对应的内存空间。通过编码的说明,希望各位能够理解,一个数据代表了什么是人为设定的,并不是存储形态上有什么分别。要合理的操作数据,就必须按照当初的编码格式来进行相对应的操作。在c++中,类型扮演了指示编码格式的角色,虽然我没有讲任何一种类型,但是他们定义变量的方式都是一致的。变量只是类型基础上分配了容量空间而已,我们往后的重点是放在类型上,只要我们知道类型的特性,就能知道变量的特性。

2.基本类型

2.1整数类型

 

名称 最大值 最小值 所占空间
char 127 -128 1
short 32767 -32768 2
int 约21亿 约-21亿 4
long long 9乘10的19次方 负的9乘10的19次方 8

 

1个字节,2个字节,4个字节,8个字节所能表示的数的范围是不同的,上面的编码策略就是把一半空间表示正数,一半表示负数(0被视为正数那一半,所以正数最大值小1).需要注意的是:c++的整数类型大小并没有统一,你用这个公司出产的编译器编译出来的目标代码,int是4个字节的,用另一个公司的编译器编译出来的目标代码,int所占的空间有时8个空间的,这个说不准.所以具体占的多少,还要自己看编译器的说明书,或者自己测试一下.通过 sizeof(类型) 语句可以返回该类型的字节数,来确定大小.

另外,通过signed 来明确指定正负各一半的编码,或者用unsigned指定完全正数的编码.例如:

signed short范围等于 short.

unsigned short范围是 0到65535的正整数.

为什么要有signed这个标志,主要是为了和unsigned作对称.另外,某些编译器的对正数编码的设定也是比较混乱的,如果设置signed标志,就能明确指定,不会造成混乱.

这里还需要强调一句,电脑里面的数的类型,都是一个特定范围的数,而不等价于数学上的数.如果超出范围的使用这些数据类型,必然会导致逻辑错误.这个称为"溢出异常".

实践时间:

int a;

short b;

unsigned long long c;

变量a是int类型,所占大小为4字节,编码是正负21的整数,a在容量空间中是有位置的,那么他的位置是什么呢?我没有指定,所以这一步工作是编译器自动帮我们寻找系统中合适空间的,因为并不是只有我们一个程序在运行,所以系统负责分配未使用的空间,以避免不同程序覆盖相同的位置的数据。

只要我们定义了变量,那就是确定有一个位置和空间了。我们可以通过“&变量名”的方式取得当前变量所占空间的第一个字节的地址,结合变量类型的大小,我们就能确定所占空间在哪里了。是否可以自己指定变量的位置?这个是可以的,不过暂时不告诉你。变量定义后,位置就是不能改变的,要换一个位置,只能是新定义一个同类变量,然后把数据复制过去,然后把原变量所占的空间归还给系统。

2.2 初始化、声明和定义

int a;这里我定义了int类型的变量a,你可能会问,这个a是容器吧,那么里面装的是什么?因为我还没有设定他的值,所以他的值是随意的,系统会分配一个空闲空间给这个变量,但是并不会把空间之前的数据给抹除掉,所以数据是随意的,在乎之前这块地方的用户设置了什么值。

这往往不是我们所希望的,我们希望设置我们自己想要的值。这就要借助初始化(初次设定)。

初始化语法:

类型 标识符 = 常量;

例如:

int a = 123;

就是变量a后面添加等号(等号两边的空白符可要,可不要),然后是你想设定的数字,最后分号结尾。常量有两种,一种是不能修改的变量,一种是文字常量。比如123这个文字,编译器会转换为值等于123的常量,然后把这个数值复制到变量a里面。文字常量是编译器方便我们在源代码中录入数据而提供的一种便利,否则我们就要自己将文字转化为数值才能设定变量的值了。要注意,文字123和数值123的编码形式是不相同的。后面介绍字符的编码便会看到和整数的编码是不同的。

c++认可的文字常量有几种,一般都是很直观的,比如123,-9,0.1等,等涉及相关类型的时候再一一说明.

除了用文字常量来初始化,也可以用普通常量来初始化.

比如:

int const a = 3;

int b = a;

初始化说完了,这里我要开始说另一个问题.比如a和b都是同一个作用域,那么按道理他们是相互"知道"对方,可以利用对方的.但是你看:

int b = a;

int const a = 3;

将a和b的定义顺序换一下,编译器就会提示错误.这是为什么呢?其实这个是C++语言的一个不足之处,就算元素都在同一个作用域,他们理所当然是可以共享的,但是编译器也要求你,在你使用到该元素之前,需要先“声明”一下真的有这个元素。编译器不会统计同一个作用域到底有多少个元素,他只会从头到尾一步一步的查找用过的元素是否已经定义了。比如,当编译器工作到int b = a;这行的时候,他就发现a是之前从来没有出现过的元素,编译器不会等统计完所有元素后,才来分析是否真的没有定义,而是立刻认为a是没有定义的。即使a后面定义了,这不能避免这个错误的出现。

为了弥补这个错误,大家需要掌握一个叫做“声明”的语法技巧。所谓的声明是和“定义”相对的,声明就是告诉编译器:我后面保证会定义,所以你不要报错。也就是,声明并不是定义,声明不会产生实质的代码,不会像定义变量那样产生存储空间,也不会产生可执行的指令。声明和定义的区别有时候不是那么明显,各位还是来实际看看声明和定义的形式吧:

int const a;

int b = a;  备注:我用的编译器b处于程序作用域和文件作用域都会产生错误,但是处于函数作用域就没有问题的,证明a是已经声明了,编译器不会提示未定义错误.

int const a = 3;

如果在一个文件中,出现多个同名的变量(或常量)的定义,最后一个或者第一个带有初始化的定义,就是真正的定义,其余都是声明。需要注意的是:带初始化的定义,必定是真正的定义。如果有两个或以上的定义,就是错误.

为什么int b在函数作用域外部的作用域无法利用a来初始化,这个我也不太清楚.有兴趣请你继续深入.这里就不深究了.

声明的作用是告诉编译器我将在后面定义,声明本身不产生内容。而定义的作用是产生具体的内容。

数据定义的声明和定义的方法已经告诉大家了,但是还有其他种类的声明和定义,这个以后提到的时候我会顺便给他的声明和定义的两种不同格式。

2.3浮点(小数)类型

浮点类型用来存储带小数的数字。不过需要注意的是,小数本身对应的是有理数,而有理数是什么?就是分数,然后分数实质上是两个数字,一个分子一个分母,要将两个数合并为单一数的形式,实际上是隐含了一个分母。我们常用的小数的分母是什么?是10. 比如0.1 等于1/10. 0.11 等于11/(10×10)= 11/100.并不是所有分数都能转化为以10为基础的分数的,比如1/3就等于0.3333……,总是无法在有限的空间精确表示。而电脑的空间总是有限的。电脑使用的是二进制,所以他的小数部分也是以2为基础的,凡是无法精确转化为2做基础的分数,就会产生精确度问题。比如1/10无法被2进制小数精确表示,也就是简单的0.1在电脑中居然是无法表示的。正因为精确问题那么普遍,这就要求我们运用小数的时候,要学会怎样避免精度不足而导致的问题。

浮点类型是值得花时间去研究的。但是这里我不想太深入这些细节。你现在只需要知道,浮点经常是近似的,而不是精确的。

名称 最大 最小 所占字节
float 正38位 负38位 4
double 正308位 负308位 8
long double 正4932位 负4932位 10

 

各位知道,int占4个字节,可以保存21亿的正负整数,也就是10位数,而float占4个字节却可以表示38位,这从侧面说明了浮点数中的很多表示是不精确的,其中有很多是近似数。

float a = 2.1;

double const pi = 3.1415926;

可以用十进制小数去初始化浮点类型,但是,a存储的数未必等于2.1,而是等于2.1的近似数。当然,也有一些十进制小数是a可以精确表示的,比如0.5。

2.4 字符类型

各位知道,声音和图像存进电脑后,都是一样的,不同的只是占用空间的大小而已。字符和文字也是通过编码成数字的方式存储在电脑中。首先,字符存储有两个方面,一个是字形,也就是这个字的图像样式,第二个是字码。什么是字码,比如0到9十个数字,我们只需要10个不同的编码就能一一对应的上。比如0000(0)0001(1)0010(2)0011(3)0100(4)0101(5)0110(6)0111(7)1000(8)1001(9)1010(10)1011(11)1100(12)1101(13)1110(14)1111(15)。只要4个位,就能表示16个不同的编码,足够表示0到9这10个数字。“ANSI字符编码”就采取类似的方式,去统一了26个英文字母,和10个数字符号,和其他标点符号,总共有128个字符,只需要7个位便可以表示。中国也有汉字的编码标准,比如“GB码”,利用16个位来表示6万多的字符空间(汉字比较多),兼容汉字和ANSI编码。而字形需要保存这个字的形体,可以用图形函数描述,也可以直接用图像来描述。因此字形的存储是相对比较庞大的。

我们现在关心的不是字形,而是字码。

名称 最大值 最小值 所占字节
char 127 -128 1
wchar_t     2或4

c++语言只是用char(字符)类型来存储字符。char类型存储的是数字,还是字符,这个完全是看你怎么理解。

char a = ‘a’;

char b = 97;

wchar_t  c= L‘汉’;

用单引号包含的单个字符,是字符常量的格式。char存放不下一个汉字(要两字节以上),所以需要用wchar_t来存放汉字。其中a和b是相等的,’a’ 字符的ANSI编码就等于97。要注意,大写A和小写a实际上是两个完全不同的字符,大写‘A’的编码是65.要想了解ANSI编码,网上有ANSI编码的表格,各位可以看看。

wchar_t(宽字符)类型可以用于表示更大范围的字符编码,尤其是对汉字这种有数万个编码单位的字库来说,是很有价值的。宽字符的文字常量和普通字符文字常量形式上的差别是:在前面多加一个L。

2.5 布尔类型

布尔类型只有一个,那就是bool。布尔类型只有两个值,true(真)和false(假)。其中true和false在c++中可以作为文字常量来初始化布尔变量,例如:

bool 太阳从东边升起 = true;

bool 太阳从西边升起 = false;

 

理论上,布尔类型只需要一个位就能表示,因为只有两个值,分别对应一个位上的两个状态便可,但是为了程序运行效率(针对位的处理速度较慢),编译器一般会让布尔类型占一个字节的大小。一般这样规定,这个字节的编码形式等于0的时候为假,否则都是真。布尔类型是用来处理逻辑关系的类型,比如“太阳是东边升起”这一个命题有两个可能,一是真,二是假。根据命题的真假进一步执行相关的指令,是电脑具有“智能”的一个重要特征。通过复杂的判断组合,电脑能够处理各种各样的情况,执行对应的处理代码,这个就是电脑解决对人类来说都是复杂问题的硬件基础。在c++语言中,有很多指令是针对布尔变量来设计的,这在以后会接触到。现在只需要了解布尔类型的存储特性。

名称 值一 值二 所占大小
bool true false 1字节

2.6 基本类型总结

基本类型只有三大类,一、不同范围的整型;二、不同范围的浮点型;三、用于逻辑计算的布尔类型。在c++中,字符型实际上也是整型。基本类型很简单,而复杂类型是基本类型的组合,因此也不会太难理解的。

3.指针类型

指针是保存空间位置,也就是地址编号的类型。保存地址编号有何用?在于c++有一系列的指令可以通过地址去间接操作该地址所指示的内存空间上面的数据。就因为这个特性,类似罗表的指示针之类的作用,所以称为“指针”。

人人都说指针是c++最难搞懂的部分,但是我觉得并不是这样,指针就是保存地址的类型,而地址编号就是自然数,没什么特别的。

3.1指针概述

指针的一般语法是:

类型*

指针的一般语法形式就是任何数据类型后面添加*(星号),当然也包含指针类型,所以指针是可以循环定义的,比如“类型**”,特殊的情况后面介绍。可以把指针类型理解为两部分:

一、星号前的基础类型,用于间接操作

二、指针自身保存的是这个基础类型的地址,而不是这个基础类型的数据

比如:

int a = 1;

int* b = &a;

其中语法“&变量”等于取得这个变量所在空间的地址(所占空间的第一个字节的地址编码),b保存的就是这个地址,而不是a的数据内容“1“。保存地址的作用是为了将来可以间接操作变量a,当然这里我就先不介绍这些细节。

因为我们知道,空间的地址是一个自然数,是一个数字编码,那么这个指针类型实际大小是什么?不同的cpu所能访问的容量空间的大小是不同的,如果有1个字节来编码容量的空间,那么最大可能访问容量是:256 * 1字节=256字节,如果是两字节那就大约是6万字节,也就是60KB左右,相当的小。现在常用的电脑使用4个字节来存储地址的编码,所能访问的容量大约是4GB。你也许会问,我的硬盘有1TB,也就是1024GB,这样电脑是怎么访问这些空间的?

电脑并不会直接访问硬盘,而是只会访问内存,然后通过其他机制去访问硬盘,这里就不介绍了,而c++所谓的指针类型,保存的地址也只是内存的地址。因此常见指针类型的大小就是等于4个字节的。也许你听过64位电脑,它其实是等于指针类型等于8字节(8字节等于64位)的电脑,可以访问天文数字的内存空间。毕竟现在超过4GB内存的电脑也并不罕见了。

回来讨论指针类型。指针类型为什么要包含基础类型,而不是单纯的就是一种指针类型?这是为了间接操作这个地址所指示的位置。我们知道某个地址,但是不知道这个地址所代表的类型,也就不知道他的大小和编码,这样我们就无法操作这个空间。

事实上,真的有一种指针是不包含基础类型的:

void*

void表示“无”,也就是这种指针你只能知道一件事情,你有一个位置编号,你可以认为这应该是一个变量的起始地址,但是你不知道这个变量是一个字节,还是两个字节,还是4个字节,你没有这些信息。你也不知道他是整数,还是浮点数,甚至整数本身也有两套编码,一套是正负各占一半的,一套是纯正的。所以你根本就无从下手去做任何事情。

因此基础类型对指针类型来说,是间接操作该地址上的数据所必不可少的部分。那为什么要有void *指针,有很多理由,但我认为是用于直接操作的时候。指针的目的经常是为了间接操作,但某些时候,地址本身也是有作用的。

练习:

int a = 1;

int* b = &a;

int** c = &b;

int*** d = &c;

注意:&a是取地址操作,我们知道地址是一个自然数编号,因此各位也许认为&a 和&b是差不多的(都是得到一个自然数),但是我们知道指针是包括基础类型的,也就是虽然从大小的角度去看,没有分别,但是他们的含义是不同的。

&a 表示得到一个int 变量的地址, &b 表示得到一个int* 指针的地址,所以他们的含义是不同的。而int* b表示存储的数据是一个int变量的地址,所以可以用来保存&a取得的地址。int**c表示存储的是int*指针变量的地址,因此可以用来保存&b所取得的地址。

只要稍微整理一下,就可以发现这样的规律:我们可以把&a 的类型看作是int *的,也就是取一个变量的地址,等于得到这个变量类型做基础类型的指针(这个类型后面加个*号)。

然后,我们只需要遵从这样的简单法则:等号两边都是相同类型。这便是没有语法错误的。

3.2第二个议题,是否可以取常量的地址?

不可以取文字常量的指针,比如“&1”,但可以取普通常量的地址:

int const a = 1;

int const* b = &a;

int const** c = &b;

我们知道变量的常量形式,就是中间插如一个const,但是你可能发现指针常量会产生两种形式:

int const* x;

int* const y;

可以放在*号左边,也可以放在右边。它们是有区别的么?这里要特别注意,c++就是这些细微之处看出差别的。

回顾常量的定义: 类型 const 标识符;也就是基础类型 + const + 标识符;

指针的定义: 类型 * 标识符;也就是基础类型 + * + 标识符;

我们要得到一个指针常量的合理形式是:先把”类型*”看作一个类型,套入常量的定义中,得到 “类型* const 标识符;”。也就是要定义指针常量,必须把const 插入*号之后。

而如果把常量看作一个类型,套入指针定义中就是“类型 const* 标识符;”。这是一个指针变量,而不是指针常量,这个指针变量的基础类型是int const常量。

正是因为指针定义的这种循环特性,会导致各位在认识指针的时候,会有一种很绕口的感觉。

对于一个指针来说,放在星号左边的是基础类型,这个是间接操作所需要关心的部分;右边如果有const,那就是指针常量,而不是变量。

指针的复杂性在于可以循环定义,但是只要细心,从右到左一层层的分析,就能理解指针的复杂定义。

3.3第三个议题:如何读指针的复杂定义?

有些人喜欢读int* 为整型指针,然后读int const*为整型常量指针,然后读int *const为整型指针常量。总之,我认为很难统一,并且很绕口,容易出错。指针常量和常量指针的差别真的那么明显么?我是不觉得,因此建议除非很明显的类型,否则还是直接给出文字和别人交流。

基本类型* 可以读整型指针或者浮点指针,还有字符指针,wchar_t* 是宽字符指针。至于想说明是那个整型只有把名称念出来了。

基本类型+两个**的指针,可以成为整型双重指针,浮点双重等。

三个星类似,不过几乎用不上。

一般来说,指针常量是很少见的,而指针的基础类型是常量的情况就比较多见,所以人们说某某常量指针,往往是指基础类型是常量,而不是说这个指针是常量。这是一种习惯,不过越复杂就越没学习的必要,还不如直接给文字版减少误会。

4.复合类型

4.1 结构类型概述

复合类型是用来描述复杂对象的。比如我们要描述“图书”,可以这样:

int 页数;

int 字数;

int 出版次数;

int 印刷量;

int 开本数;

我们可以分开5个变量去描述一本书,如果有100本书,那么我们就要5×100 = 500个变量。我们要起500个变量名,光这点就让人很难接受。而且也很容易出错。因此c++允许我们定义复合的类型,把所有描述对象局部的类型组合成可以代表整体的类型。复合类型有两种,一个是struct(结构类型),另一个是class(类类型)。这两个在c++中是大体相同的,只是使用习惯有所不同,以后会细说。这里用struct做讲解。

结构类型的语法:

struct 标识符{

内部类型定义列表;

};

其中,标识符是这个类型的名字;内部类型定义列表类似:

类型 标识符;

类型 标识符;

……

这个列表的元素个数可以是一个到n个。

大家知道,基本类型的名字是c++固定那几个,而指针类型也没有名字,用简单的形式组合直接就用来定义变量,所以这个复合类型是我们第一次定义一个新的类型,这个类型的名字是我们自己用标识符命名的,是我们学c++以来第一次创造个性化的类型。

使用结构类型有两步,一步是先定义这个结构类型,第二步使用这个类型去定义变量。

用结构定义一本书的例子:

struct 书{

int 页数;

int 字数;

int 出版次数;

int 印刷量;

int 开本数;

};

书 汉语词典;

书* 某书 = &汉语词典;

struct 和结构名字的空白符是必须的,结构名字和左花括号,然后内部列表、右花括号个元素之间的空白符是可有可无的,当然插入一些空白符会让排版好看点,最后要记住,必须要分号结尾。用复合类型定义变量的方法和基本类型一致,复合类型就是基本类型的扩展。

通过定义结构类型,我们可以用一个变量取代一堆零散的变量,原本定义100本书,需要500个变量,现在只需要100个,而且这样还能让各个变量之间的关系清晰化。在编程解决生活和工作中的实际问题时,经常要面对复杂的对象,他们不是一个整数就能描述清楚的,通过复合变量将各项要描述的内容组合起来,形成对整体的描述,是非常有效的方法。任何复杂的对象,都能通过足够多的项目描述清楚。

要注意:结构内部列表的单项元素所谓的“类型”本身也可以是结构类型。也就是结构类型定义也有循环定义的特征,因此会比较绕口。

4.2 结构初始化

结构的初始化:

book a = {1,2,3,4,5};

book b = {1,2,3};

只需要用花括号包含,然后按顺序填写初始化的值,每一个值用逗号(英文逗号)分割。那些省略的位置使用默认值。如果结构内的元素本身就是结构怎么初始化,只需要在这个元素对应的位置上用花括号包含要初始化的内容。如:

{1,2,{100,100},4,5}

这里第三个元素是结构,用花括号初始化这个结构元素。也可以平铺直叙:

{1,2,100,100,4,5}

但是这样你不能省略结构元素的某一部分的初始化。如:{1,2,{100},4,5} 不等于{1,2,100,4,5},后一个的4是初始化结构的第三个元素中的第二个元素。

4.3 复合类型的大小

你也许认为复合类型的大小就是等于内部元素相加的和。这并不一定成立!c++编译器会调整内部各个元素所占的位置,元素和元素之间可能会存在一些空隙,因为这样会让电脑运行快一些。因此,复合类型的大小需要你用“sizeof(类型)”这个语句去判断:它是大于等于内部元素的总和。

4.4 复合类型的内部成员的定位

我们定义了一个符合类型,在存取和读取数据的时候,经常需要读取单一的内部成员的数据。

如:

struct book{int a; int b; char c; book* d;};

book mybook = {1,2,3,&mybook};

以下在函数作用域内定义:

int a = mybook.a;

book* b = mybook.d;

为什么a,b要在函数作用域定义,在于函数作用域的a,b初始化不需要文字常量而已,和定义复合类型成员的语法要求无关。定位复合类型成员,只需要”复合类型变量.成员”语法,中间不能有空白符,点号是英文的句号。

各位可能发现book类型的定义有点特别,他内部有一个book* 的指针变量成员,这是被允许的。因为指针是存储地址的类型,有固定大小。而内部有一个book变量就是不允许的,因为book如果包含自身,那么容量应该是多大呢?编译器无法分配恰当的空间。同样的逻辑,假如结构a包含结构b,结构b又包含结构a,也是不行的。

4.5 复合类型未谈之事

复合类型在c++中的作用并不如上面所的那么简单,事实上还有很多内容可以讲解。但是从某种意义上来说,另外的事和我们单纯把复合类型作为数据定义来说,没有太大关系。

复合类型把一系列零散的变量组合起来,作为有逻辑相关性的一个整体,对我们描述复杂对象有很大的帮助。在建立复合类型的时候,各位需要两部曲:一、建立一个逻辑上说得过去的整体概念;二、用合适的组合构造复合类型。如果把逻辑上不相干的东西组成复合类型,那实际价值就大打折扣。

5.数组类型

5.1 概述

和指针类型类似,数组的定义也是基于某个类型之上的组合定义。

数组语法:

类型 标识符[个数] ;

数组类型是基础类型n个依次排列的结果。数组类型定义中的“类型”本身也可以是数组,所以这是一个循环定义,不过数组的数组把“[个数]”向右扩展,而不同于指针的指针把“*”号向左扩展。例子:

int a[2] ;

int b[2][3];

int c[1];

int d[0];

这里a相当于两个int。b是两个int[3],而int[3]又等于三个int,也就是等于6个int。从b可以看出,数组的类型从数组名向右扩展的。第一级是2,第二级是3,以此类推。

d的大小是0,在有些编译器上是允许的,不过意义不那么明显就是了,但是小于0的是绝对不允许的。

5.2 数组的初始化

数组的初始化类似复合类型的形式。

int a[4] = {1,2,3,4};

int b[4][2] = { {1,2},{1,2},{1,2},{1,2} };

int c[2][4] = { {1,2,3,4},{1,2,3,4} };

int d[1][2][3] ={ { {1,2,3},{1,2,3} } };

int e[1] = {1};

int f[] = {1,2,3,4,5};

int g[][2] = { {1,2},{1,2} };

a是大小为4的int数组,初始化和复合类型类似,并且也可以省略后端的初始化。b是4个int[2],这相当于结构类型内有结构元素,要针对结构元素用花括号,针对数组的数组也是一样的道理,要用花括号。在第一层有4个int[2],所以需要4组花括号,每一组用逗号分隔。而每一个花括号组初始化一个int[2],int[2]等于两个int,结果用两个int文字常量来初始,每个int文字常量用逗号分隔。

最复杂的d变量也是一样的道理,第一层有一个数组元素,所以要一个花括号组对应;第二层有两个数组元素,所以要用两个花括号组对应;第三层是3个整数,所以用3个文字常量对应(当然也可以省略一部分),层次要对应,就很容易初始化了。

观察e数组,只有一个元素,但是也要用花括号,也就是数组的初始化不管有多少个元素都必须用花括号组

观察f数组,这个数组很特别,没有指定大小,没有指定大小一般是错误的,但是如果你同时给出了初始化,那么编译器就根据初始化列表的元素个数来推断数组的大小。

观察g数组,第一层没有给出大小,观察初始化我们会发现对应2个数组元素。数组带初始化的时候只有第一层可以省略大小,而第二层和以上的层数都无法省略大小。

5.3 数组的大小

数组类型的所占空间的大小和复合类型不同,是确定的,也就是元素大小×个数。用sizeof(元素类型)×个数可以得出数组大小。当然也可以干脆用sizeof(数组)来得到。

5.4 数组元素的定位

需要访问数组的某一个元素,只需要用:

数组[下标]

其中“下标”是自然数。c++中的数组下标是从0开始的,0是第一个元素,而数组大小-1是最后的元素。

例子:

int a[2]  = {1,2};

int b = a[0];

int c = a[1];

int d[2][3];

a[0] 访问数组的第一个元素,a[1]访问数组的第二个元素。如果下标不在范围内会有怎样的结果?c++语法并不会检查这个,所以不是语法错误。但是访问不在范围的元素,等于访问那些不受你控制的容量空间,这样可能导致程序有隐性的错误存在。正因为c++无法侦测到这一点,所以要良好使用数组,就是你的责任了。

如果数组只有一层,那么成为一维数组,如a就是。如果数组有两层,叫做二维数组,如数组d;依此类推。一般超过两维的数组是很少用到的。

char e[x];

观察数组e:有点类型电脑对内存容量空间的描述,x是内存容量的大小,也就是内存空间等于一个很大很大的以字节为单位(char也是占一个字节)的一维数组。数组的下标就是内存空间的地址,从0开始到内存总容量-1之间就是内存的可访问空间。

研究一下二维数组d,如何访问第4个元素?

第一个元素是d[0][0];

第二个元素是d[0][1];

第三个元素是d[0][2];

第四个元素是d[1][0];

观察这个排列,可以得到一个规律:一、数组的下标类似数位的进位方式,低位到达规定容量就进位,比如int[2][3]的数组,低位是3,高位是2,低位逢3进1,高位逢2进1;二、从高位向低位观察,高位数字×相对低位容量+次高位数字×相对低位容量……+ 末位数字 + 1 等于该组下标在数组中排列的位置。

如d[1][2] 的位置 = 1 * 3 + 2 + 1= 6 ,也就是该数组的最后一个元素。需要注意的是,你不能用d[5]去访问该数组最后的位置,d的类型是二维数组,只能通过符合这个类型的二维数组的访问形式去访问,也就是有两个下标的形式。

二维数组从直观上等于把同等容量的一维数组平均分成高位数字指定的分数,然后每一份的大小等于低位数字。例如:int f[4][5][6][7][8]这个数组的总容量等于4×5×6×7×8=6720,先分成4分,每一份大小1680.也就是第二份的元素位置是从1680开始的。然后对每一份继续划分,直到分到每一份大小刚好等于8.

也许各位会有疑问:为什么要分成几维,而不是用一维数组就搞定所有问题,因为从本质上来说都是同类元素的队列。我认为是为了方便描述数学上的矩阵等对象,还有是现实中的表格对象也是类似二维数组的。

5.5 数组和指针的融合

回忆一下,指针的定义是:类型* 标识符; 而数组的定义是:类型 标识符[个数]; 也就是当数组和指针定义合并的时候,应该是:类型* 标识符[个数];类似的定义。但是这其实有两种解释:

一、这是一个指针:我们把标识符左边的*看作最优先,而右边的”[个数]”作为次要部分。

二、这是一个数组:把”[个数]”看最优先,左边*看作次要。

当我们把它看作指针,那么这个指针的基础类型就是一个数组。当我们把他看作数组,这个数组的基础类型就是指针。这当然是不同的,如果是指针大小就是固定的4字节,如果是数组,他就是数组元素的大小4×元素个数。

c++ 是没有二义性的,不会存在模棱两可的语法:c++解决这个问题的方法是规定了符号的优先级。在c++中,中括号组的优先级高于星号,所以如果一个定义有中括号又有星号,一律先解释中括号,再解释星号。

所以语法:

类型* 标识符[个数];

是一个数组定义,他的元素类型是指针而已。那么如何定义基础类型是数组的指针?可以用小括号改变优先级,这个方法就类似我们用小括号改变四则运算的优先级:

类型(* 标识符)[个数];

虽然数组和指针的定义刚好相反,数组的第一层是最左边的中括号组,最后一层是最右边的中括号组;而指针第一层是最右边的星号,最后一层是最左边的星号;但是他们都是最靠近标识符的那一个符号最优先。

对于一个复杂的定义,各位要打起十二分精神,根据优先级来慢慢理解。例如:

int (* (* (*a[1]) ) [2])[3];

第一级:a[1] 是一个数组。

第二级:a 左边的第一个星,是一个指针。

第三级:a 左边的第二个星,也是一个指针。

第四级:a 右边第二个中括号组,是数组。

第五级:a 左边第三个星,是一个指针。

第六级:a 右边第三个中括号,是一个数组。

第七级:int。

我们很难理解这么多重关系,究竟目的而在,关键是把握第一层,因为第一层如果是数组,他的大小就是元素×个数的大小。如果第一层是指针,大小只是等于4,不管他基础类型多么复杂。

5.6 数组总结

数组是最常用的同类元素集合,比如一个班有50个学生,可以定义个数等于50的学生数组。数组是相当直观的,同类元素依次排列便是数组的直观模型。

6.特殊类型

数据类型,我们学得7788了,不敢说精通,至少掌握了主要的东西。而特殊类型,是一些有益补充。

void 类型。void 是不存在的,没有大小的类型,因此你不能定义void类型的变量。void类型的意义是做占位符,表示“无”的概念,第二个,用于void* 指针,表示通用指针的概念。

函数类型。在c++中,函数是一段指令,我们还没有学习,c++不允许类似操作变量那样操作函数,不能修改函数的内容,不能复制和存储函数,因此函数类型不能定义变量。但是你可以定义函数类型的指针,指针的作用是保存地址,函数指针也是用于保存函数的第一个字节的地址,通过保存地址有什么作用?为了间接的跳转到这个函数去执行函数的指令。

数组类型的基础元素要求是可以定义变量的,因此不存在void 和函数类型的数组。但是可以存在void* 和 函数* 类型的数组。

复合类型成员指针是一种特殊的指针,他保存的并不是地址,而是改成员在符合类型中的相对位移,以后用到就再谈。

 

 

 

 

 

end.

posted @ 2011-06-18 21:13  诺贝尔  阅读(901)  评论(0编辑  收藏  举报