第七章 指针、数组与引用
7.2 指针
对于类型T来说,T是表示“指向T的指针”的类型。换句话说,T类型的变量能存放T类型对象的地址。例如:
char c = ‘a’;
char* p = &c; //p存放着c的地址,&是取地址运算符
这两条语句的图形化表示是:
对指针的一个基本操作是解引用(dereferencing),即引用指针所指的对象。这个操作也称为间接取值(indirection)。解引用运算符是个前置一元运算符,对应的符号是*。例如:
char c = ‘a’;
char* p = &c; //p存放着c的地址,&是取地址运算符
char c2 = *p; //c2 == ‘a’; *解引用运算符
指针p所指的对象是c,c中存储的值是 ‘a’,因此我们把*p赋给c2等价于给c2赋值 ‘a’。
当指针指向数组中的元素时,C++允许对这类指针执行某些算术运算(见7.4节)。
指针的具体实现应该与运行程序的机器的寻址机制同步。大多数机器支持逐字节访问内存,其他机器则需要从字中抽取字节。很少有机器能直接寻址到一个二进制位。因此,能独立分配且用内置指针指向的最小对象是char类型的对象。有一点请读者注意:bool占用内存空间至少和char一样多(见6.2.8节)。如果想把更小的值存得更紧密,可以使用位逻辑操作(见11.1.1节)、结构中的位域(见8.2.7节)或者bitest(见34.2.2节)。
符号*在用作类型名的后缀时表示“指向”的含义。如果我们想表示指向数组的指针或者指向函数的指针,需要使用稍微复杂一些的形式:
int* pi; //指向int的指针
char** ppc; //指向字符指针的指针
int* ap[15]; //ap是一个数组,包含15个指向int的指针
int(*fp)(char*); //指向函数的指针,该函数接受一个char*实参,返回一个int
int* f(char*); //该函数接受一个char*实参,返回一个指向int的指针
6.3.1节解释了如何声明一个指针,§ iso.A则包含完整的语法信息。
指向函数的指针很有用,相关内容将在12.5节讨论;20.6节将介绍指向类成员的指针。
7.2.1 void*
在某些偏向底层的代码中,我们偶尔需要在不知道对象确切类型的情况下,仅通过对象在内存中的地址存储或传递对象。此时,我们会用到void*。void*的含义是“指向未知类型对象的指针”。
除了函数指针(见12.5节)和指向类成员的指针(见20.6节),指向其他任意类型对象的指针都能被赋给一个void类型的变量。此外,一个void能被赋给另一个void*,两个void能比较是否相等,我们还能把void显式地转换成其他类型。因为编译器事实上并不清楚void所指的对象到底是什么类型,所以对它执行其他操作可能不太安全并且会引发编译器错误。要想使用void,我们必须把它显式地转换成某一特定类型的指针。例如:
void f(int* pi)
{
void* pv = pi; //ok:发生了从int*到void*的隐式类型转换
*pv; //错误:不允许解引用void*
++pv; //错误:不允许对void*执行递增操作(所指的对象尺寸未知)
int pi2 = static_cast<int*>(pv); //显式转换回int*
double* pd1 = pv; //错误
double* pd2 = pi; //错误
double* pd3 = static_cast<double*>(pv); //不安全(见11.5.2节)
}
一般情况下,如果某个指针已经被转换成(强制类型转换)指向一种与实际所指对象类型完全不同的新类型,则使用转换后的指针是不安全的行为。例如,某个机器可能假定double沿着8字节边界分配内存,如果指向int的pi分配内存的方式与之不同,将造成意想不到的后果。这种显式类型转换既不安全也不自然,我们在设计static_cast(见11.5.2节)的时候考虑到了这一点:static_cast的字面形式比较特殊,一旦出现了与之有关的错误,程序员很容易定位。
void*最主要的用途是当我们无法假定对象的类型时,向函数传递指向该对象的指针;它还用于函数返回未知类型的对象。要想使用这样的对象,必须先进行显式类型转换。
用到void*指针的函数通常位于系统的最底层,这些函数的作用大多是操作硬件资源。例如:
void* my_alloc(size_t n); //从特定的堆上分配n个字节的内存空间
在系统的较上层代码中很少用到void*,一旦出现了你就要认真核实是不是存在设计上的错误。当用于优化的目的时,void*能隐藏在类型安全的接口(见27.3.1节)中。
函数指针(见12.5节)和指向类成员的指针(见20.6节)不能被赋给void*。
7.2.2 nullptr
字面值常量nullptr表示空指针,即不指向任何对象的指针。我们可以把nullptr赋给其他任意指针类型,但是不能赋给其他内置类型:
int* pi = nullptr;
double* pd = nullptr;
int i = nullptr; //错误:i不是指针
nullptr只有一个,它可以用于任意指针类型,C++并没有为每种指针类型各设计一个空指针。
在nullptr被引入之前,人们使用数字0表示空指针。例如:
int* x =0; //x的值是nullptr
任何对象都不会分配到地址0上,0(所有位全0的模式)是nullptr最常见的表示形式。0本身是一个int,但是标准类型转换规则(见10.5.2.3节)允许我们把0当成是一个指针常量或指向成员的类型的常量。
在原来的代码中,很多人习惯于定义一个宏NULL来表示空指针。例如:
int* p = NULL; //使用宏NULL
然而,在不同的具体实现中NULL的定义有所差别;例如,NULL可能是0,也可能是0L。在C语言中,NULL通常是(void*)0,这种用法在C++中是非法的(见7.2.1节):
int* p = NULL; //错误:不能给void*赋给int*
使用nullptr的好处很多,首先它的可读性更强,其次当一组重载函数既可以接受指针也可以接受整数时(见12.3.1节),用nullptr能够避免语义混淆。