CodeStore 2011-07-14
在介绍C++中的指针开始之前,我们一定先要了解数组的概念以及用法,大家可以看看这篇文章,《浅析C++中的动态多维数组》,供参考。
数组
在C++中是通过变量来对内存进行访问的,但根据前面的说明,C++中只能通过变量来操作内存,也就是说要操作某块内存,就必须先将这块内存的首地址和一个变量名绑定起来,这是很糟糕的。
比如有100块内存用以记录100个工人的工资,现在要将每个工人的工资增加5%,为了知道各个工人增加了后的工资为多少,就定义一个变量float a1;,用其记录第1个工人的工资,然后执行语句a1 += a1 * 0.05f;,则a1里就是增加后的工资。由于是100个工人,所以就必须有100个变量,分别记录100个工资。因此上面的赋值语句就需要有100条,每条仅仅变量名不一样。
上面需要手工重复书写变量定义语句float a1;100遍(每次变一个变量名),无谓的工作。因此想到一次向操作系统申请100*4=400个字节的连续内存,那么要给第i个工人修改工资,只需从首地址开始加上4*i个字节就行了(因为float占用4个字节)。
为了提供这个功能,C++提出了一种类型——数组。数组即一组数字,其中的各个数字称作相应数组的元素,各元素的大小一定相等(因为数组中的元素是靠固定的偏移来标识的),即数组表示一组相同类型的数字,其在内存中一定是连续存放的。在定义变量时,要表示某个变量是数组类型时,在变量名的后面加上方括号,在方括号中指明欲申请的数组元素个数,以分号结束。因此上面的记录100个工资的变量,即可如下定义成数组类型的变量:
float a[100];
上面定义了一个变量a,分配了100*4=400个字节的连续内存(因为一个float元素占用4个字节),然后将其首地址和变量名a相绑定。而变量a的类型就被称作具有100个float类型元素的数组。即将如下解释变量a所对应内存中的内容(类型就是如何解释内存的内容):a所对应的地址标识的内存是一块连续内存的首地址,这块连续内存的大小刚好能容纳下100个float类型的数字。
因此可以将前面的float b;这种定义看成是定义了一个元素的float数组变量b.而为了能够访问数组中的某个元素,在变量名后接方括号,方括号中放一数字,数字必须是非浮点数,即使用二进制原码或补码进行表示的数字。如a[ 5 + 3 ] += 32;就是数组变量a的第5 + 3个元素的值增加32.又:
long c = 23; float b = a[ ( c – 3 ) / 5 ] + 10, d = a[ c – 23 ];
上面的b的值就为数组变量a的第4个元素的值加10,而d的值就为数组变量a的第0个元素的值。即C++的数组中的元素是以0为基本序号来记数的,即 a[0]实际代表的是数组变量a中的第一个元素的值,而之所以是0,表示a所对应的地址加上0*4后得到的地址就为第一个元素的地址。
应该注意不能这样写:
long a[0];
定义0个元素的数组是无意义的,编译器将报错,不过在结构或类或联合中符合某些规则后可以这样写,那是C语言时代提出的一种实现结构类型的长度可变的技术。
还应注意上面在定义数组时不能在方括号内写变量,即
long b = 10; float a[ b ];//是错误的
因为编译此代码时,无法知道变量b的值为多少,进而无法分配内存。可是前面明明已经写了b = 10;,为什么还说不知道b的值?那是因为无法知道b所对应的地址是多少。
因为编译器编译时只是将b和一个偏移进行了绑定,并不是真正的地址,即b所对应的可能是Base - 54,而其中的Base就是在程序一开始执行时动态向操作系统申请的大块内存的尾地址,因为其可能变化,故无法得知b实际对应的地址(实际在 Windows平台下,由于虚拟地址空间的运用,是可以得到实际对应的虚拟地址,但依旧不是实际地址,故无法编译时期知道某变量的值)。
但是编译器仍然可以根据前面的long b = 10;而推出Base - 54的值为10啊?重点就是编译器看到long b = 10;时,只是知道要生成一条指令,此指令将10放入Base - 54的内存中,其它将不再过问(也没必要过问),故即使才写了long b = 10;编译器也无法得知b的值。
上面说数组是一种类型,其实并不准确,实际应为——数组是一种类型修饰符,其定义了一种类型修饰规则。关于类型修饰符,后面将详述。
字符串
要查某个字符对应的ASCII码,通过在这个字符的两侧加上单引号,如'A'就等同于65.而要表示多个字符时,就使用双引号括起来,如:"ABC".而为了记录字符,就需要记录下其对应的ASCII码,而ASCII码的数值在-128到127以内,因此使用一个 char变量就可以记录一个ASCII码,而为了记录"ABC",就很正常地使用一个char的数组来记录。如下:
程序无论执行多少遍,在申请内存时总是申请固定大小的内存,则称此内存是静态分配的。前面提出的定义变量时,编译器帮我们从栈上分配的内存就属于静态分配。每次执行程序,根据用户输入的不同而可能申请不同大小的内存时,则称此内存是动态分配的,后面说的从堆上分配就属于动态分配。
很明显,动态比静态的效率高(发票长度的利用率高),但要求更高——需要电脑和打印机,且需要收银员的素质较高(能操作电脑),而静态的要求就较低,只需要已经印好的发票联,且也只需收银员会写字即可。
同样,静态分配的内存利用率不高或运用不够灵活,但代码容易编写且运行速度较快;动态分配的内存利用率高,不过编写代码时要复杂些,需自己处理内存的管理(分配和释放)且由于这种管理的介入而运行速度较慢并代码长度增加。
静态和动态的意义不仅仅如此,其有很多的深化,如硬编码和软编码、紧耦合和松耦合,都是静态和动态的深化。
地址
前面说过“地址就是一个数字,用以唯一标识某一特定内存单元”,而后又说“而地址就和长整型、单精度浮点数这类一样,是数字的一种类型”,那地址既是数字又是数字的类型?不是有点矛盾吗?
如下:浮点数是一种数——小数——又是一种数字类型。即前面的前者是地址实际中的运用,而后者是由于电脑只认识状态,但是给出的状态要如何处理就必须通过类型来说明,所以地址这种类型就是用来告诉编译器以内存单元的标识来处理对应的状态。
指针
已经了解到动态分配内存和静态分配内存的不同,现在要记录用户输入的定单数据,用户一次输入的定单数量不定,故选择在堆上分配内存。假设现在根据用户的输入,需申请1M的内存以对用户输入的数据进行临时记录,则为了操作这1M的连续内存,需记录其首地址,但又由于此内存是动态分配的,即其不是由编译器分配(而是程序的代码动态分配的),故未能建立一变量来映射此首地址,因此必须自己来记录此首地址。
因为任何一个地址都是4个字节长的二进制数(对32位操作系统),故静态分配一块4字节内存来记录此首地址。检查前面,可以将首地址这个数据存在unsigned long类型的变量a中,然后为了读取此1M内存中的第4个字节处的4字节长内存的内容,通过将a的值加上4即可获得相应的地址,然后取出其后连续的4个字节内存的内容。但是如何编写取某地址对应内存的内容的代码呢?
前面说了,只要返回地址类型的数字,由于是地址类型,则其会自动取相应内容的。但如果直接写:a + 4,由于a是unsigned long,则a + 4返回的是unsigned long类型,不是地址类型,怎么办?
C++对此提出了一个操作符——“*”,叫做取内容操作符(实际这个叫法并不准确)。其和乘号操作符一样,但是它只在右侧接数字,即*( a + 4 )。此表达式返回的就是把a的值加上4后的unsigned long数字转成地址类型的数字。
但是有个问题:a + 4所表示的内存的内容如何解释?即取1个字节还是2个字节?以什么格式来解释取出的内容?如果自己编写汇编代码,这就不是问题了,但现在是编译器代我们编写汇编代码,因此必须通过一种手段告诉编译器如何解释给定的地址所对内存的内容。
C++对此提出了指针,其和上面的数组一样,是一种类型修饰符。在定义变量时,在变量名的前面加上“*”即表示相应变量是指针类型(就如在变量名后接“[]”表示相应变量是数组类型一样),其大小固定为4字节。如:
unsigned long *pA;
也就是说,某个地址的类型为指针时,表示此地址对应的内存中的内容,应该被编译器解释成一个地址。
因为变量就是地址的映射,每个变量都有个对应的地址,为此C++又提供了一个操作符来取某个变量的地址——“&”,称作取地址操作符。其与“数字与”操作符一样,不过它总是在右侧接数字(而不是两侧接数字)。
“&”的右侧只能接地址类型的数字,它的计算(Evaluate)就是将右侧的地址类型的数字简单的类型转换成指针类型并进而返回一个指针类型的数字,正好和取内容操作符“*”相反。
上面正常情况下应该会让你很晕,下面释疑。
unsigned long a = 10, b, *pA; pA = &a; b = *pA; ( *pA )++;
上面的第一句通过“*pA”定义了一个指针类型的变量pA,即编译器帮我们在栈上分配了一块4字节的内存,并将首地址和pA绑定(即形成映射)。然后“&a”由于a是一个变量,等同于地址,所以“&a”进行计算,返回一个类型为unsigned long*(即unsigned long的指针)的数字。
应该注意上面返回的数字虽然是指针类型,但是其值和a对应的地址相同,但为什么不直接说是unsigned long的地址的数字,而又多一个指针类型在其中搅和?因为指针类型的数字是直接返回其二进制数值,而地址类型的数字是返回其二进制数值对应的内存的内容。因此假设上面的变量a所对应的地址为2000,则a;将返回10,而&a;将返回2000.
再来看取内容操作符“*”,其右接的数字类型是指针类型或数组类型,它的计算就是将此指针类型的数字直接转换成地址类型的数字而已(因为指针类型的数字和地址类型的数字在数值上是相同的,仅仅计算规则不同)。
所以:
b = *pA;
返回pA对应的地址,计算此地址的值,返回类型为unsigned long*的数字2000,然后“*pA”返回类型unsigned long的地址类型的数字2000,然后计算此地址类型的数字的值,返回10,然后就只是简单地赋值操作了。同理,对于++( *pA )(由于“*”的优先级低于前缀++,所以加“()”),先计算“*pA”而返回unsigned long的地址类型的数字2000,然后计算前缀++,最后返回unsigned long的地址类型的数字2000.