3.2 指针类型与地址算法
3.2.1 指针的概念与定义
在对变量进行定义时,编译器都为其分配一块内存单元,该内存单元中存放该变量的值,而该内存单元还具有确定的地址,通过该地址可以实现对该内存单元的访问。指针便是存放一个对象在内存中的地址的。指针是非常有用。通过指针,可以直接对内存中各个不同数据类型的数据进行快速地处理,并为函数之间各类数据传递提供了简捷便利的途径。通过指针可以方便地创建链表和管理动态对象等。正确熟练地使用指针能够编写出简洁有效、功能强大的高质量程序。但是指针的使用比较复杂,易留下一些不易察觉的隐患,最终导致程序的瘫痪。
指针变量是用来存放内存地址的,其内容是内存中的一个地址。通过指针可以实现对内存中相应变量和函数等的访问。每一个指针都有一个相应的类型,该类型是说明指针所指向的内存单元中所存放的数据为数据类型。但是,不同类型的指针在内存中占用的空间大小都是相同的,即指针本身不确定其占用内存区域的大小及数据存放规律。指针的类型告诉编译器在编译指针所指向对象的二进制序列按什么规律进行翻译并翻译多少位。
先介绍两个与指针运算有关的运算符。运算符“*”称为取值运算符,它的作用是访问地址的内存单元,“*指针变量名”是对该内存单元中的数据进行访问;运算符“&”称为取址运算符,“&变量名”是取得某变量的地址。取值运算“*”和取地址运算“&”是一对互逆运算。
定义指针变量的语法结构如下:
type *varible1,*varible2,….,*variblen;
其中type是指针的类型,即该指针变量所指向的内存单元中所存储的数据为数据类型。而存储类型则是指针型变量自身的存储类型。定义语句中变量名前加“*”号,表明该变量被定义为指针变量,要注意此处的“*”号和执行语句中的“*指针名”之间的区别。
注意:在一个定义语句中同时对若干个指针变量进行定义时,在用逗号分隔的定义表中,每个指针变量前都必须加上取值运算符“*”。
在对指针变量进行读取操作之前必须对它进行初始化。一个指针可以被一个具有相同类型的对象进行初始化,这个相同类型的对象可以是一个同类指针变量,也可以对相应类型的变量用取址操作符获得其地址。例如:
int val=512;
int *pt1=&val;
int *pt2=pt1;
C++是一种具有强类型检查机制的语言,所有在初始和赋值中出现的数据都将在编译时受到检查,看它们是否与所需的类型正确匹配。如果不匹配但满足某一转换规则,则编译器会按照这一规则自动进行类型转换;否则是十分不安全的,它将导致一个编译错误。最好不要依赖编译系统的自动转换,而由程序员用显示地强制类型转换来完成指针类型之间的转换。
强制类型转换的形式为
(目标类型 *)变量名例如:
int var1=10
char *str;
int *var=&var1;
str=(char *)var;//error
一个指针可以赋零值(NULL),表明该指针不指向任何一个对象,称为空指针。
C++中还提供了一种通用的指针类型void *,它将使指针有能力指向任何类型的对象。
3.2.2 指针变量的运算规则指针的运算是按指针所持有的地址变量所进行的,其运算的实质是地址的计算。它只包括一部分的算术运算、关系运算和赋值运算。
1、指针的算术运算指针的算术运算只有两种加法和减法(地址值加减)。而指针的加减算术运算也仅支持指针变量与整数相加或用一个指针变量减去一个整数的运算,不允许将两个指针相加,也不允许将浮点数或双精度数等与指针相加减。
假设pt是一个指针变量,指针中保存的内存地址为d,它所指向的数据的数据类型在内存中占用的字节数为n,则算术运算:pt±m的结果值为d±m*n地址值。
例如:若pt是一个指向整型变量的指针,其保存的地址值是2000,而整型数据在内存中所占用的内存单元为2=n,若在算术运算中取m=5,则pt+5获得的地址值为2010,而不是2005。
值得注意的是,在8088/8086系列微机上进行指针的算术运算时,其运算次数是有限的,不能对指针进行无休止的加减法运算。
2、指针的关系运算两个指向同一数据类型的指针之间的关系运算表示它们所指向的内存单元的地址位置之间的关系(前后关系)。内存单元在内存中的排列是有一定顺序的,指向排在较前面的内存单元的指针小于指向排在较后面的内存单元的指针。
关系运算符:<、<=、>、>=,= =、!=。
注意:(1)指向不同类型数据的指针之间的比较是没有意义的,而指针与一般其他类型的常量或变量之间的关系运算也是没有意义的,除非变量中的内容表示的是地址量。
(2)任何类型的指针变量都可以和零进行相等或不相等的关系运算(判是否为空指针)。
(3)由于具有不同的内存模式,在8088/8086系列微机上进行指针的关系运算时,除了指针和零(空指针)之间的相等与不相等运算比较安全之外,其他同类型指针的关系运算有时不一定可靠。要实现指针之间的可靠比较,需要对内存的“分段”、“分页”、“逻辑地址”、“物理地址”,“近地址”、“远地址”和存储管理模式的“实模式”、“保护模式”等概念有一个正确清晰的认识。
3、指针的赋值运算指针的赋值运算可以分为两类:为指针变量本身赋值及为指针所指向的对象赋值。
在为指针本身赋值时,所赋予的值必须是地址常量或地址变量,不能是普通的整数,除非该整数本身就表示地址值。但仍然要进行强制转换。
为指针本身赋值常用的有以下几种形式:
(1)、把一个变量的地址赋予一个指向相同数据类型的指针,如:
double val,*pt;
pt=&val;
(2)、把一个指针的值赋予另一个执行相同数据类型的指针,使这两个指针指向相同的内存单元,如:
float *pt1,*pt2;
pt1=pt2;
(3)、把一个数组的地址赋予一个指向相同数据类型的指针,如:
char faculty[20],*pf;
pf=faaculty;
(4)、将一个指针的地址进行一定的位置偏离后将其值赋予指向相同数据类型的指针,如:
unsigned *pt1,pt2,*pt3;
pt2=pt1+2;
至于为指针所指向的对象赋值,则基本与一般变量的赋值相同。它们之间的唯一区别是:为了取得指针所指向的变量,必须使用取值操作符“*”。将“*”作用在指针变量上,并将这一整体当作一个相应数据类型的变量看待,则可按正常变量的读写规则对该对象进行读写。如:
int val=512;
int *pt=&val;
*pt=128;
3.2.3 指针与数组的关系数组和指针之间具有十分密切的关系,它们都可以处理内存中连续存放的一系列数据,且数组和指针在访问内存时,采用同一的地址计算方法。在某些场合,数组和指针甚到可以互换使用。
对数组元素的访问采用数组名与下标相结合的办法。如
double weight[20];//define
weight[20]=20.0;
数组名实际上是一个存放数组首元素地址的指针。可以利用该指针来实现对数组元素的访问。如上例中以下表达式是等价的:
weight 等价于 weight[0];
weight+2 weight[2];
在使用时,一般用户会觉得采用下标进行访问更自然和直观些,而在对数组元素进行连续访问时,利用指针访问具有更快的速度。因此要合理地选择。
虽然指针和数组名在引用数组元素和获取数组及其某元素的地址时是可以互换的,但在用于指向某一块连续内存空间时,还是有区别的。数组名是不能赋值的(即在其生存期不能改变)。
3.2.4 指针与字符串在数组一节中已经对用字符数组来表示字符串进行了讨论,本节讨论如何用指针表示字符串。
由于字符串趋于严格地逐一访问的形式,因此许多字符串操作通常使用指针和指针运算来实现。
在用指针表示字符串时,字符串的结尾以空字符(NULL)为标志(编译器可以自动添加)。在显示时是从该指针所指地址到其后第一个空字符之间的字符。
[例如3.3] 利用指针表示字符串,并进行显示。
//EX3_3.cpp
3.2.5 指针数组与其他数据类型一样,若干个指向同样数据类型的指针也可以组织起来,形成指针数组。在指针数组中,各元素中保存的不是常规的整数、浮点数、字符型等数据,而是指向某种数据类型的指针,即一系列内存地址。定义一个指针数组语法结构如下:
type *varible[size];
其中type是数组内的指针所指向的数据类型,“*”表示指针类型,varible是数组名,size限定了数组的大小。
为了访问数组中的某个元素,读取其中保存的地址值或将一个地址值赋给它,可以采用与一般数组元素使用相同的方法,只需要注意指针数组中元素的值是一个地址值便可以了,如:
int *intpt[5];
int val=10;
intpt[0]=&val;
如果需要访问指针数组中某元素所包含的指针所指向的数据,则需要再在前面加一个取值操作符“*”,如接上例:
*(intpt[0])=20; //NEX3_3
**intpt=30;
3.2.6 多级指针在指针中保存的是一个指向其他对象的内存地址,该对象可以是普通的整型、浮点型等数据,也可以是一个指针。当一个指针所指向的内存单元中保存的也是一个内存地址时,该指针就称为指向指针的指针,也就是一个多级指针(如指针数组)。一般来讲,指针数组概念比较清晰直观,而多级指针则很容易混淆,必须引起高度重视。
在定义多级指针时,通过在变量名前加多重取值符“*”来表明所定义的是一个多重指针。如,double **pt;
注意:pt仍然不是一个数值,其内部保存的也是一个内存地址,到了**pt内部保存的才是一个双精度型数。即需要进行两次取值操作。图3.3说明了单重指针和多重指针间接取数的差别。
图3.3 单重和多重间接取值
多重指针的重数过多,会造成间接取值操作过多,会给计算带来麻烦,且在概念上容易造成混乱和出错。
3.2.7 void型和const型指针我们已经知道,void修饰符用于修饰一个函数的返回值类型时,表示该函数没有返回值,而void修饰符用于作函数的参数表说明时表明该函数不需要任何参数。在C++中,void修饰符也可以用来修饰指针类型,表明该指针可以是普遍性的(泛指针)。
如果一个指针被定义成void *型,则该指针可以指向任意一个没有用const修饰符(表明该变量在程序运行的过程中是不可变的)或volatile修饰符(表明该变量在程序的执行过程中是易变的)进行修饰的变量。一个void型指针可以转化为任意类型的其他指针。但在对其赋值前,其数据类型是不确定的。在C++中允许定义void型函数指针,但不能用void型指针指向类的成员。
[例3.4] void修饰符的使用例子
void vobject;//错误,不能将void作为一般变量类型修饰符使用
void *pv;//正确,定义了一个void型指针pv
int *pint;//定义了一个指向整型变量的指针pint
int i=10;
pv=&I;//使void指针指向整型变量i
pint=(int *)pv;//利用强制类型转换使用void型指针为指向整型变量的指针赋值
const类型修饰符用于修饰指针时,表明该指针不能指向普通的变量,它是为了指向常量的地址而专门提供的,即将常量的地址赋给const型指针,以避免非法操作。
3.2.1 指针的概念与定义
在对变量进行定义时,编译器都为其分配一块内存单元,该内存单元中存放该变量的值,而该内存单元还具有确定的地址,通过该地址可以实现对该内存单元的访问。指针便是存放一个对象在内存中的地址的。指针是非常有用。通过指针,可以直接对内存中各个不同数据类型的数据进行快速地处理,并为函数之间各类数据传递提供了简捷便利的途径。通过指针可以方便地创建链表和管理动态对象等。正确熟练地使用指针能够编写出简洁有效、功能强大的高质量程序。但是指针的使用比较复杂,易留下一些不易察觉的隐患,最终导致程序的瘫痪。
指针变量是用来存放内存地址的,其内容是内存中的一个地址。通过指针可以实现对内存中相应变量和函数等的访问。每一个指针都有一个相应的类型,该类型是说明指针所指向的内存单元中所存放的数据为数据类型。但是,不同类型的指针在内存中占用的空间大小都是相同的,即指针本身不确定其占用内存区域的大小及数据存放规律。指针的类型告诉编译器在编译指针所指向对象的二进制序列按什么规律进行翻译并翻译多少位。
先介绍两个与指针运算有关的运算符。运算符“*”称为取值运算符,它的作用是访问地址的内存单元,“*指针变量名”是对该内存单元中的数据进行访问;运算符“&”称为取址运算符,“&变量名”是取得某变量的地址。取值运算“*”和取地址运算“&”是一对互逆运算。
定义指针变量的语法结构如下:
type *varible1,*varible2,….,*variblen;
其中type是指针的类型,即该指针变量所指向的内存单元中所存储的数据为数据类型。而存储类型则是指针型变量自身的存储类型。定义语句中变量名前加“*”号,表明该变量被定义为指针变量,要注意此处的“*”号和执行语句中的“*指针名”之间的区别。
注意:在一个定义语句中同时对若干个指针变量进行定义时,在用逗号分隔的定义表中,每个指针变量前都必须加上取值运算符“*”。
在对指针变量进行读取操作之前必须对它进行初始化。一个指针可以被一个具有相同类型的对象进行初始化,这个相同类型的对象可以是一个同类指针变量,也可以对相应类型的变量用取址操作符获得其地址。例如:
int val=512;
int *pt1=&val;
int *pt2=pt1;
C++是一种具有强类型检查机制的语言,所有在初始和赋值中出现的数据都将在编译时受到检查,看它们是否与所需的类型正确匹配。如果不匹配但满足某一转换规则,则编译器会按照这一规则自动进行类型转换;否则是十分不安全的,它将导致一个编译错误。最好不要依赖编译系统的自动转换,而由程序员用显示地强制类型转换来完成指针类型之间的转换。
强制类型转换的形式为
(目标类型 *)变量名例如:
int var1=10
char *str;
int *var=&var1;
str=(char *)var;//error
一个指针可以赋零值(NULL),表明该指针不指向任何一个对象,称为空指针。
C++中还提供了一种通用的指针类型void *,它将使指针有能力指向任何类型的对象。
3.2.2 指针变量的运算规则指针的运算是按指针所持有的地址变量所进行的,其运算的实质是地址的计算。它只包括一部分的算术运算、关系运算和赋值运算。
1、指针的算术运算指针的算术运算只有两种加法和减法(地址值加减)。而指针的加减算术运算也仅支持指针变量与整数相加或用一个指针变量减去一个整数的运算,不允许将两个指针相加,也不允许将浮点数或双精度数等与指针相加减。
假设pt是一个指针变量,指针中保存的内存地址为d,它所指向的数据的数据类型在内存中占用的字节数为n,则算术运算:pt±m的结果值为d±m*n地址值。
例如:若pt是一个指向整型变量的指针,其保存的地址值是2000,而整型数据在内存中所占用的内存单元为2=n,若在算术运算中取m=5,则pt+5获得的地址值为2010,而不是2005。
值得注意的是,在8088/8086系列微机上进行指针的算术运算时,其运算次数是有限的,不能对指针进行无休止的加减法运算。
2、指针的关系运算两个指向同一数据类型的指针之间的关系运算表示它们所指向的内存单元的地址位置之间的关系(前后关系)。内存单元在内存中的排列是有一定顺序的,指向排在较前面的内存单元的指针小于指向排在较后面的内存单元的指针。
关系运算符:<、<=、>、>=,= =、!=。
注意:(1)指向不同类型数据的指针之间的比较是没有意义的,而指针与一般其他类型的常量或变量之间的关系运算也是没有意义的,除非变量中的内容表示的是地址量。
(2)任何类型的指针变量都可以和零进行相等或不相等的关系运算(判是否为空指针)。
(3)由于具有不同的内存模式,在8088/8086系列微机上进行指针的关系运算时,除了指针和零(空指针)之间的相等与不相等运算比较安全之外,其他同类型指针的关系运算有时不一定可靠。要实现指针之间的可靠比较,需要对内存的“分段”、“分页”、“逻辑地址”、“物理地址”,“近地址”、“远地址”和存储管理模式的“实模式”、“保护模式”等概念有一个正确清晰的认识。
3、指针的赋值运算指针的赋值运算可以分为两类:为指针变量本身赋值及为指针所指向的对象赋值。
在为指针本身赋值时,所赋予的值必须是地址常量或地址变量,不能是普通的整数,除非该整数本身就表示地址值。但仍然要进行强制转换。
为指针本身赋值常用的有以下几种形式:
(1)、把一个变量的地址赋予一个指向相同数据类型的指针,如:
double val,*pt;
pt=&val;
(2)、把一个指针的值赋予另一个执行相同数据类型的指针,使这两个指针指向相同的内存单元,如:
float *pt1,*pt2;
pt1=pt2;
(3)、把一个数组的地址赋予一个指向相同数据类型的指针,如:
char faculty[20],*pf;
pf=faaculty;
(4)、将一个指针的地址进行一定的位置偏离后将其值赋予指向相同数据类型的指针,如:
unsigned *pt1,pt2,*pt3;
pt2=pt1+2;
至于为指针所指向的对象赋值,则基本与一般变量的赋值相同。它们之间的唯一区别是:为了取得指针所指向的变量,必须使用取值操作符“*”。将“*”作用在指针变量上,并将这一整体当作一个相应数据类型的变量看待,则可按正常变量的读写规则对该对象进行读写。如:
int val=512;
int *pt=&val;
*pt=128;
3.2.3 指针与数组的关系数组和指针之间具有十分密切的关系,它们都可以处理内存中连续存放的一系列数据,且数组和指针在访问内存时,采用同一的地址计算方法。在某些场合,数组和指针甚到可以互换使用。
对数组元素的访问采用数组名与下标相结合的办法。如
double weight[20];//define
weight[20]=20.0;
数组名实际上是一个存放数组首元素地址的指针。可以利用该指针来实现对数组元素的访问。如上例中以下表达式是等价的:
weight 等价于 weight[0];
weight+2 weight[2];
在使用时,一般用户会觉得采用下标进行访问更自然和直观些,而在对数组元素进行连续访问时,利用指针访问具有更快的速度。因此要合理地选择。
虽然指针和数组名在引用数组元素和获取数组及其某元素的地址时是可以互换的,但在用于指向某一块连续内存空间时,还是有区别的。数组名是不能赋值的(即在其生存期不能改变)。
3.2.4 指针与字符串在数组一节中已经对用字符数组来表示字符串进行了讨论,本节讨论如何用指针表示字符串。
由于字符串趋于严格地逐一访问的形式,因此许多字符串操作通常使用指针和指针运算来实现。
在用指针表示字符串时,字符串的结尾以空字符(NULL)为标志(编译器可以自动添加)。在显示时是从该指针所指地址到其后第一个空字符之间的字符。
[例如3.3] 利用指针表示字符串,并进行显示。
//EX3_3.cpp
3.2.5 指针数组与其他数据类型一样,若干个指向同样数据类型的指针也可以组织起来,形成指针数组。在指针数组中,各元素中保存的不是常规的整数、浮点数、字符型等数据,而是指向某种数据类型的指针,即一系列内存地址。定义一个指针数组语法结构如下:
type *varible[size];
其中type是数组内的指针所指向的数据类型,“*”表示指针类型,varible是数组名,size限定了数组的大小。
为了访问数组中的某个元素,读取其中保存的地址值或将一个地址值赋给它,可以采用与一般数组元素使用相同的方法,只需要注意指针数组中元素的值是一个地址值便可以了,如:
int *intpt[5];
int val=10;
intpt[0]=&val;
如果需要访问指针数组中某元素所包含的指针所指向的数据,则需要再在前面加一个取值操作符“*”,如接上例:
*(intpt[0])=20; //NEX3_3
**intpt=30;
3.2.6 多级指针在指针中保存的是一个指向其他对象的内存地址,该对象可以是普通的整型、浮点型等数据,也可以是一个指针。当一个指针所指向的内存单元中保存的也是一个内存地址时,该指针就称为指向指针的指针,也就是一个多级指针(如指针数组)。一般来讲,指针数组概念比较清晰直观,而多级指针则很容易混淆,必须引起高度重视。
在定义多级指针时,通过在变量名前加多重取值符“*”来表明所定义的是一个多重指针。如,double **pt;
注意:pt仍然不是一个数值,其内部保存的也是一个内存地址,到了**pt内部保存的才是一个双精度型数。即需要进行两次取值操作。图3.3说明了单重指针和多重指针间接取数的差别。
图3.3 单重和多重间接取值
多重指针的重数过多,会造成间接取值操作过多,会给计算带来麻烦,且在概念上容易造成混乱和出错。
3.2.7 void型和const型指针我们已经知道,void修饰符用于修饰一个函数的返回值类型时,表示该函数没有返回值,而void修饰符用于作函数的参数表说明时表明该函数不需要任何参数。在C++中,void修饰符也可以用来修饰指针类型,表明该指针可以是普遍性的(泛指针)。
如果一个指针被定义成void *型,则该指针可以指向任意一个没有用const修饰符(表明该变量在程序运行的过程中是不可变的)或volatile修饰符(表明该变量在程序的执行过程中是易变的)进行修饰的变量。一个void型指针可以转化为任意类型的其他指针。但在对其赋值前,其数据类型是不确定的。在C++中允许定义void型函数指针,但不能用void型指针指向类的成员。
[例3.4] void修饰符的使用例子
void vobject;//错误,不能将void作为一般变量类型修饰符使用
void *pv;//正确,定义了一个void型指针pv
int *pint;//定义了一个指向整型变量的指针pint
int i=10;
pv=&I;//使void指针指向整型变量i
pint=(int *)pv;//利用强制类型转换使用void型指针为指向整型变量的指针赋值
const类型修饰符用于修饰指针时,表明该指针不能指向普通的变量,它是为了指向常量的地址而专门提供的,即将常量的地址赋给const型指针,以避免非法操作。