1.4 数据对象与访问
程序中必须使用一些内存单元存放数据。程序的代码可以读出或改写这些存储单元的内容,对内存的读、写操作称为访问。
程序可以用标识符命名指定存储单元。若定义的内存对象既能读又能写,则称为变量;一旦把数据存入内存单元,程序中就不能修改的对象称为常量。通常,一些直接表示的数值,例如,256、0.025等数值称为常数或直接常量。
C++可以用对象名,也可以通过对象的地址访问对象。
1.4.1 变量定义
变量是存储数据的内存单元。变量定义的作用是要求编译器在内存申请指定类型的存储空间,并以指定标识符命名。
变量说明的语句格式为:
类型 标识符表;
其中,“类型”为各种合法的C++类型;“标识符表”可以为一个或多个用逗号分隔的变量名。
例如,有以下变量说明语句:
int a, b, c; double x;
若一个变量仅被说明而未赋值,则它的存储值是无意义的随机状态。在变量说明的同时可以赋初值:
double value = 0;
在程序中,一个变量被赋值后,就一直保留,直到对它再次赋值修改,例如:
value = 3.21; //… value = value + 100;
1.4.2 访问变量
程序运行时占有内存的实体,包括数据(常量、变量)、代码(函数)都可以称为对象。数据是程序处理的对象,程序是编译器的处理对象。“对象”是一个广义的概念。对对象的访问包括对内存相关单元内容的读和写操作。
内存单元由操作系统按字节编号,称为地址。当程序出现常量或变量说明语句时,编译器按类型分配存储空间,把地址写入标识符表。标识符就是获得分配的内存单元的名字。
例如,对已经说明的变量:
int a;
内存状态示意如图1.2所示。标识符a是变量名,按照类型int分配4字节存储空间,第1字节的地址0x8FC6FFF4称为变量a的地址。地址值是系统分配的,不能由高级程序设计语言决定,但C++可以通过代码获取对象被分配的地址值。
图1.2 一个整型变量
在内存建立一个对象后,可以用名方式和地址方式访问。
1. 名访问
对于数据单元,名访问就是操作对象的内容。名访问又称直接访问。访问形式分为“读”和“写”两种。例如,有赋值表达式:
变量 = 表达式
其中,运算符“=”称为赋值号。其功能是首先计算“表达式”的值,然后写入“变量”代表的存储单元,用新的值代替旧的值。
如果对象名出现在“表达式”中,表示读出对象的值。而对赋值号左边的对象进行写操作,把表达式的运算结果写入对象中。
赋值语句:
a = a + b;
首先读出变量a,b的值,通过加法器求和,然后把结果写入变量a,以新的值覆盖原来的值。语句通过名对变量内容进行操作。如图1.3所示为对变量a,b的读/写过程。
赋值号的左边必须能够确定一个存储单元,它是数据存放的目的地。例如:
2 + 3 = 5
在C++语言中是错误操作。赋值号不是逻辑等号。逻辑等号是“==”。
例如,有说明语句:
int a, b;
看下面语句的意义(见图1.3):
a=10; //把常数10写入变量a b=20; //把常数20写入变量b a=a+b; //读出a和b的值,相加后结果写入a b=b+1; //读出b的值,加1后结果写入b cout<<a<<b; //输出a,b的值
图1.3 访问变量
2. 地址访问
日常,我们可以按“会议室”这个名字找到开会的地方,也可以按地址,如1105 号房间,找到它。1105是地址,换句话说,1105所指的房间是会议室。
同样,也可以按地址找到所需的内存空间。对象的地址用于指示对象的存储位置,称为对象的“指针”。指针所指的物理存储空间称为“指针所指对象”。通过地址访问对象又称为“指针访问”。
例如,变量a的地址是0x0012FF60,则0x0012FF60所指存储单元就是a。这个单元的长度和内容解释方式由类型说明符(如a的类型是int)决定。
C++语言中,指针访问使用运算符“*”。例如:
*(0x0012FF60) //相当于变量a的名访问,但在程序中不能这样直接书写
“*”是一个多义符号。它在算术表达式中是乘法运算符;在地址值之前是指针运算符;在变量说明语句中是指针类型符。应该根据语句的性质和上下文做出正确判断。
那么,我们怎么知道对象在内存中的地址呢?可以用取址运算获得。取址运算符是“&”。例如:
&a //变量a的地址(指针) *(&a) //a的地址所指的对象
【例1-6】测试对变量的不同访问形式。
#include<iostream> using namespace std; int main() { int a=451; cout<<a<<endl; //输出变量值 cout<<(&a)<<endl; //输出变量地址 cout<<*(&a)<<endl; //输出变量值 }
程序运行结果:
451 0012FF60 451
上述显示结果的第2行是十六进制数,即变量a的地址。对象的存放地址是由系统分配的,C++代码只能查看而不能指定对象的地址。当我们再次运行,或在不同的机器上运行这个程序时,将会看到相同的对象值具有不同的对象地址值。
3. 指针变量与间址访问
从例1-4看到,变量a的地址是一个十六进制整数。可以把这个地址值存放在另外一个变量中。能够存放地址值的变量称为“指针类型变量”,简称“指针变量”。在本书叙述中,有时没有严格区分指针和指针变量。
指针类型变量定义形式为:
类型 * 标识符;
其中,“*”为指针类型说明符,说明以“标识符”命名的变量用于存放对象的地址;“类型”是指针变量的关联类型,表示指针变量所指对象的类型。
计算机的CPU(Central Processing Unit,中央处理器)决定了内存寻址方式,所以,不管指针所指对象是什么类型的,指针值本身的规格都一样。例如,16位或32位的整数。关联类型的作用是控制和解释对象的访问。如果一个指针变量关联类型为int,则通过指针变量访问对象时,读取从指针值指示的位置开始的连续4字节,并按整型数据解释。
例如,有说明:
int a = 10, b = 20; int * p1, * p2;
在内存中开辟4个存储单元。整型变量a,b已经赋初值,而指针变量没有初值。若执行以下语句:
p1=&a; //把a的地址写入指针变量p1 p2=&b; //把b的地址写入指针变量p2
执行状态如图1.4所示。图中,用箭头表示指针变量已获取对象的地址,读做“指向”。这里,p1指向a,p2指向b。
对变量的访问可以通过指针变量间接实现。例如,要访问a,首先从p1中读出a的地址值,按地址找到所指对象*p1,从0x0012FF60字节开始,读出4字节的二进制位串,根据关联类型int,解释为整型数。用*p1的这种访问方式,称为间接地址访问,简称为间址访问。
a,b的地址值可以表示为:
&a 或 p1 &b 或 p2
a,b的值可以表示为:
a 或 *(&a) 或 *p1 b 或 *(&b) 或 *p2
【例1-7】用指针变量访问所指对象。
图1.4 指针与所指对象
#include<iostream> using namespace std; int main() { long int a=10,b=20,t; long int*p1=&a,*p2=&b,*pt; //用变量地址值初始化指针变量 cout<<p1<<'\t'<<p2<<endl; //输出地址 cout<<*p1<<'\t'<<*p2<<endl; //输出变量值 t=*p1; *p1=*p2; *p2=t; //交换变量的值 cout << *p1 << '\t' << *p2 << endl; pt=p1;p1=p2;p2=pt; //交换指针值(地址) cout << p1 << '\t' << p2 << endl; cout << *p1 << '\t' << *p2 << endl; cout << a << '\t' << b << endl; }
程序运行结果:
0012FF60 0012FF54 10 20 20 10 0012FF54 0012FF60 10 20 20 10
程序中,用3条语句实现变量值的交换,t是过渡变量:
t = *p1; *p1 = *p2; *p2 = t;
等价于: t = a; a = b; b = t;
交换指针变量的值就是交换地址值,相当于改变指针变量的指向:
pt=p1; p1=p2; p2=pt;
虽然p1,p2分别是a,b的地址,但以下语句是非法的(请读者想想为什么):
pt=&a; &a=&b; &b=pt;
交换变量值和交换指针值如图1.5所示。
图1.5 交换变量值和交换指针值
当要表示一个指针变量不指向任何内存单元(即不存放对象地址)时,可以赋NULL值。NULL是C++的一个预定义常量。一个指针变量如果仅作说明而不赋值,则它的值是不确定及无意义的。下面的操作是绝对不允许的:
int * pp; *pp=50; //错误,pp没有指向合法的内存空间
程序经常用NULL值处理并判断指针变量的指向:
int * ip = NULL; //… if( ip != NULL ) //访问 *ip;
指针变量的关联类型可以为空类型void。例如:
void * vp;
void指针变量能够存放任意对象的地址。因为没有关联类型,编译器无法解释所指对象,因此,在程序中必须对其作强制类型转换,才可以按指定类型使用数据。void指针用于能支持多种数据类型的数据操作,而且会在C++语言提供的库函数中出现。
【例1-8】void指针的强制类型转换。
#include<iostream> using namespace std; int main() { int a=65; int *ip; void*vp=&a; //定义无类型指针,以整变量地址初始化 cout<<*(int*)vp<<endl; //强制类型转换后访问对象 cout<<*(char*)vp<<endl; //转换成字符型指针 ip=(int*)vp; //向整型指针赋值 cout << (*ip) << endl; }
程序运行结果:
65 A 65
程序中,*(int*)vp的操作如下。
第一步,强制类型转换。C++可以用类型符作强制类型转换,“int*”是整型指针类型符,(int*)vp把vp转换成整型指针,即可以用int解释对象。
第二步,用间址符访问指针所指对象。经类型转换之后,用int类型形式读出4字节数据。
类似地,把vp转换成字符型指针,把变量的值解释为字符'A'。
从以上例子看到,指针变量的主要操作有:
= 赋值,对指针变量赋给地址值 * 访问对象
指针本身能否进行算术运算?例如,对例1-6中的指针ip自增:
++ip
是一个合法的C++表达式,偏移量是指针关联类型的长度。但是,上述程序只定义了一个整型变量a,它之后的内存并没有分配给程序,读出*(++ip)一般没什么问题(没有意义的数据),但要对其赋值就是一件危险的事情了。
如果程序定义了一片连续的内存空间(如数组),用指针访问内存,指针变量的算术运算表示指针在这片内存空间的移动,则是很常用的操作。详见第4章数组。
4. 引用
C++允许为对象定义别名,称为“引用”。定义引用说明的语句格式为:
类型 &引用名 = 对象名;
其中,“&”为引用说明符。
引用说明为对象建立引用名,即别名。“=”的意义是在定义时与对象名绑定,程序中不能对引用重定义。一个对象的别名,在使用方式和效果上,与使用对象名一致。
引用仅仅是对象的别名,不开辟新的内存空间。这与对象指针不同。引用常常用于函数参数的传递。例如:
int a; int *pa; int&ra=a; //ra是a的别名,只能在定义时初始化 pa=&a; //pa指向a,这里“&”是取址符
内存状态如图1.6所示。
图1.6 引用与指针
【例1-9】引用测试。
#include<iostream> using namespace std; int main() { int a=2345; int *pa; int &ra = a; pa = &a; cout<<a<<'\t'<<ra<<'\t'<<*pa<<endl; //输出a的值 cout<<(&a)<<'\t'<<(&ra)<<'\t'<<pa<<endl; //输出a的地址 cout<<(&pa)<<endl; //输出指针pa的地址 }
程序运行结果:
2345 2345 2345 0012FF60 0012FF60 0012FF60 0012FF54
想一想,程序中,&a和&ra一样吗?*ra有意义吗?&pa与*pa有什么区别?
1.4.3 常量和约束访问
C++语言中,关键字const可以约束对象的访问性质,使对象值一旦初始化就不允许修改。被约束为只读的对象称为常对象。
1.标识常量
C++语言中,当用关键字const约束基本类型存储单元为只读时,在程序中使用存储单元的名字就像使用常数值一样,即用标识符表示数值,所以称为标识常量,简称常量。
定义标识常量的说明语句形式为:
const类型 常量标识符 = 常量表达式;
例如,以下是正确的标识常量定义:
const double PI=3.14159; const int MIN = 50; const int MAX=2*MIN; //max是值为100的常量
在程序中,可以读出标识常量的值或地址,例如:
girth = 2 * PI * r; cout << ( MIN + MAX ) / 2; cout << &PI << '\t' << &MAX << '\t' << &MIN << '\n';
但是,重定义或修改已说明的标识常量都是错误的,例如:
const double PI=3.14; //错误,重定义常量 MIN=MIN+10; //错误,修改常量
2.指向常量的指针
用const约束指针对所指对象访问时,这个指针称为指向常量的指针。
定义形式:
const 类型 *指针 或者 类型 const*指针
const写在关联类型之前或者紧跟关联类型之后,表示约束所指对象访问。我们习惯一种写法就可以了。
设有说明:
int var = 35; const int MAX = 1000; int *p; const int *P1_const; const int *P2_const;
指向常量的指针变量可以获取变量或常量的地址,但限制用指针间址访问对象方式为“只读”。例如:
P1_const = &var; P2_const = &MAX; *P1_const=100; //错误,不能修改指向常量指针的对象 *P2_const=200; //错误,不能修改指向常量指针的对象 var=*P1_const+*P2_const; //正确,可以读指向常量指针的对象,修改变量的值
C++语言为了保证标识常量的只读性,常量的地址只能赋给指向常量的指针。例如:
p=&MAX; //错误,常量地址不能赋给普通指针
图1.7所示为指向常量的指针访问示意图。其中,“←”表示写(赋值)操作,打上“×”的表示非法操作。
图1.7 指向常量的指针访问
3.指针常量
指针常量的意义是指针变量的值只能在定义的时候初始化,定义后不能修改,即不能改变指针变量的指向。但不影响所指对象的访问特性。
指针常量的定义形式为:
类型 *const指针
const写在“指针”变量名之前,表示约束指针变量本身。例如:
int var1 = 100, var2 = 200; int*const const_P1=&var1; //定义指针常量时初始化 const_P1=&var2; //错误,不能修改指针常量 *const_P1=var2; //可以修改指针常量所指对象的值
如果有以下语句,将出现编译错误:
const int MAX = 1000; int*const const_P2=&MAX; //错误
因为const_P2是一个指针常量,仅仅约束指针值为只读,并没有约束间址访问对象,而MAX是一个标识常量,不能用一个无约束间址访问的指针获取它的地址。
图1.8所示为指针常量访问示意图。
图1.8 指针常量的访问
4.指向常量的指针常量
指向常量的指针常量的含义是,指针本身和所指对象的值在定义之后都限制为只读,不能写。
指向常量的指针常量的定义形式为:
const 指针 或者 类型 const*const 指针
例如:
int var = 128, other_var = 256; const int MAX = 1000; const int * const double_P1 = &var; const int * const double_P2 = &MAX; double_P1=&other_var; //错误,不能写指针常量 *double_P2=500; //错误,不能写指向常量的指针常量 var=other_var; //不影响变量的读/写
图1.9所示为指向常量的指针常量的访问示意图。
图1.9 指向常量的指针常量的访问
5.常引用
冠以const定义的引用,将约束对象用别名方式访问时为只读。常引用的定义形式为:
const类型 & 引用名 = 对象名;
例如:
int a=863; const int&ra=a; //ra是a的常引用 ra=985; //错误,不能通过常引用对对象a执行写操作 a=985; //正确
ra是a的别名,但ra是常引用,若通过ra对a操作,就只能读,不能写。