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操作,就只能读,不能写。