2.3 复合类型
2.3.1 引用
2.3.2 指针
1 指针
指向是指向另一种类型的复合类型
与引用类似
都实现了对其它对象的间接访问
与引用不同
指针本身是对象,允许对其赋值和拷贝
在其生命周期内,可以指向几个不同的对象
不需要在定义时赋初值,在块作用域若未初始化,则拥有一个不确定的值
2 获取对象的地址
指针存放某个对象的地址,获取对象地址,需用取地址符(&)
int val =42; int *p = &val; // p指针存放变量val的地址,或者说p是指向val的指针
除了两种例外情况(后面总结),其它所有的类型都要和它所指向的对象严格匹配,若指针指向了一个其它类型的对象,则该对象操作将发生错误【编译不通过】
3 指针值
指针的值 (即地址) 应属下列四种状态之一
1、指向一个对象
2、指向紧邻对象所占空间的下一位置
3、空指针,指针没有指向任何对象
4、无效指针,即上述情况之外的指针
拷贝或访问无效指针将引发错误,且编译器不会检查。这与使用未初始化的变量一样。第2,3种的指针有效,但没有指向任何对象,所以访问此类指针对象的行为不允许。
指向紧邻对象所占空间的下一位置如何理解?
例:声明一个 int32 数组,数组在初始化时会自动分配地址以及元素个数。假定起始地址为001E,int* p=arr,即指向该数组的指针,指向其首部001E。
按照位计算,4字节容量的int就是占有32位,即紧邻对象位置001E+32=003E再下一个位置就是003E+32=005E。一般不用按照占位运算,而是通过指针加减法,让编译器根据指针所指向的数据的大小进行移动
实际上就是通过指针算术运算,改变其指向的内存空间地址
4 利用指针访问对象
如果指针指向一个对象,则可用一个解引用符(*)来访问对象
对指针解引用会得到指向的对象,对解引用结果赋值就是给指向对象赋值
5 空指针
空指针不指向任何对象,在使用指针前需检查是否为空
几个生成空指针的方法
int *pl = nullptr; // 等价于 int *pl =0; int *p2 =0; //直接将p2初始化为字面常量0 需include cstdlib int *p3 = NULL; // 等价于 int *p3 =0;
nullptr 初始化指针是得到空指针最直接的方法,也是C++11新标准引入的办法。它是一种特殊的字面值,可以被转换成任意的指针类型,也可以通过字面值 0 来生成空指针
NULL是预处理变量,在头文件cstdlib中定义,它的值就是0。预处理变量不属于命名空间std,由预处理器管理,它会将预处理变量转换为实际值,因此用NULL初始 化指针和用0初始化指针是一样的
【建议】
在新标准下的C++程序最好使用nullptr,把 int (0) 变量直接赋给指针是错误操作
建议初始化所有指针,并且尽量定义对象后再定义指向它的指针。若不清楚指针指向何处,则初始化为nullptr或者0, 便于程序检测
6 赋值和指针
指针和引用都可以对其它对象的间接访问,而最重要的区别是,引用本身并非一个对象,无法绑定到另外对象上
指针没有这样的限制,给指针赋值就是令它存放一个新对象
确定改变指针值,还是所指对象值,只需:赋值改变的永远是等号左边的对象
7 其它指针操作
只要指针拥有一个合法值,就能用在表达式中,和采用算术值作为条件
int ival =1024; int *pi =0; // pi合法,是一个空指针 int *pi2 = &ival; // pi2是一个合法的指针,存放着ival的地址 if (pi) // pi的值是0,条件的值是false if (pi2) // pi2指向ival,值不是0,条件的值是true
任何非0指针对应的条件值都是true
对于两个类型相同的合法指针,可以用相等操作符(=)或不相等操作符(!=)来
比较,比较结果是布尔类型
如果两个指针存放地址值相同,则它们相等;反之不相等。两个指针相等有两种可能:它们都为空、 或都指向了同一地址
8 void*指针
void* 是一种特殊指针类型,可用于存放任意对象地址
一个 void* 指针存放着—个地址,但并不了解该地址中的对象类型
以 void 的视角看,内存空间仅仅是内存空间,无法访问内存空间中的对象
2.3.2 节练习
【练习 2.18】 编写代码分别更改指针的值以及指针所指对象的值 int a =1 , b =1; int *ptr = &a; *ptr =2; // 更改指针所指对象的值 *ptr = &b; // 更改指针的值 【练习 2.19】 说明指针和引用的主要区别 指针和引用类似都提供间接操作对象的方式,主要区别在于指针是对象而引用不是,因为指针不必初始化赋值,引用必须;指针可以拷贝赋值,引用不呢;指针生命周期内可以指向多个对象,引用只能和初始化的对象绑定; 【练习2.20】 请叙述下面这段代码的作用 int i =42; // 用int字面值常量初始化变量i int *pl = &i; // 指针pl指向i,或把i的内存地址赋值给指针pl *pl = *pl * *pl; // 为变量i赋值,值为 42*42 【练习2.21】 请解释下述定义。在这些定义中有非法的吗?如果有,为什么? int i =0; (a) double* dp = &i; // 非法,除了两种特殊情况,指针类型和对象类型需一致 (b) int *ip = i; // 非法,无法用整型的字面值常量给指针赋值 (c) int *p = &i; // 合法 【练习2.22】 假设p是一个int型指针,请说明下述代码的含义 if (p) // 判断指针的值是否为0,即判断指针是否为空指针 if (*p) // 判断指针指向的对象是否为0,判断指针指向对象值是否为0 【练习2.23】 给定指针p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断思路;如果不能,也请说明原因 可以:用*p将其值输出,若编译器报错,证明p指向一个无效对象,要么p=0,要么p未进行初始化,此时可以用if(p == NULL)进行判断即可 不可以:无法检测其实有效指针,且无法判断其指向的地址是否合法; 【练习2.24】在下面这段代码中为什么p合法而lp非法? int i =42; void *p = &i; long *lp = &i; void* 指针为任意类型指针,可转换成任意指针类型;而lp为long类型指针,指针指向对象的类型必须和其类型一致
2.3.3 理解复合类型的声明
1 定义多个变量
变量定义 包括一个基本类型 + 声明符,在同一条定义语句中,基本类型只有一个,但声明符却不同。即,一 条定义语句可定义出不同类型变量
int i =1024, *p = &i, &r = i;
基本数据类型是int而非int* , * 仅修饰了 p , 对声明语句中其它变量,不产生作用
int* p; //合法但是容易产生误导
涉及指针或引用的声明,一般有两种写法
1、把修饰符和变量标识符写在一起
这种形式着重强调变量具有的复合类型
int *pl, *p2; // pl和p2都是指向int的指针
2、把修饰符和类型名写在一起,并且每条语 句只定义一个变量
这种形式着重强调声明定义了一种复合类型
int* pl; // pl是指向int的指针 int* p2; // p2是指向int的指针
2 指向指针的指针
一般声明符中修饰符个数没有限制。当多个修饰符连写时,需按照其逻辑关系解释
以指针为例,指针是内存中的对象,像其它对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中
通过*的个数可以区分指针的级別。即,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推
通过*的个数可以区分指针的级別。即,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推 int ival =1024; int *pi = &ival; // pi 指向一个 int 型的数 int **ppi = π // ppi指向一个 int 型的指针
3 指向指针的引用
引用不是对象,故不存在指向引用的指针,但指针是对象,所以存在指针的引用
int i =42; int *p; // p是一个int型指针 int *&r = p; // r是一个对指针p的引用 r = &i; // r引用了一个指针,因此给r賦值&i就是令p指向i *r =0;
理解 r 类型,办法是从右向左阅读 r 定义
离变量名最近的符号决定变量类型,因此r是一个引用。声明符其余部分用以确定 r 引用的类型。* 说明 r 引用的是一个指针。基本数据类型部分指出 r 引用的是一个 int 指针
2.3.3节练习
【练习2.25】 说明下列变量的类型和值 (a) int* ip, i, &r = i; // 指针(不确定) int整型(0) 引用(0) (b) int i, *ip =0; // int整型(0) 空指针(0) (c) int* ip, ip2; // 指针(不确定) int整型(0)