vector 的遍历可使用下标或迭代器实现,同理,也可用下标或指针来遍历数组。
指针是指向某种类型对象的复合数据类型,是用于数组的迭代器:指向数组中的一个元素。
在指向数组元素的指针上使用解引用操作符 *(dereference operator)和自增操作符 ++(increment operator),与在迭代器上的用法类似。
对指针进行解引用操作,可获得该指针所指对象的值。
而当指针做自增操作时,则移动指针使其指向数组中的下一个元素。
什么是指针
指针的概念很简单:指针用于指向对象。与迭代器一样, 指针提供对其所指对象的间接访问,只是指针结构更通用一些。与迭代器不同的是,指针用于指向单个对象,而迭代器只能用于访问容器内的元素。
具体来说,指针保存的是另一个对象的地址:
string s("hello world"); string *sp = &s; // sp holds the address of s
第二条语句定义了一个指向 string 类型的指针 sp,并初始化 sp 使其指向 string 类型的对象s。
*sp 中的 * 操作符表明 sp 是一个指针变量,&s 中的 & 符号是取地址操作符,当此操作符用于一个对象上时,返回的是该对象的存储地址。
取地址操作符只能用于左值,因为只有当变量用作左值时,才能取其地址。
同样地,由于用于 vector 类型、string 类型或内置数组的下标操作和解引用操作生成左值,因此可对这两种操作的结果做取地址操作,这样即可获取某一特定对象的存储地址。
建议:尽量避免使用指针和数组
指针和数组容易产生不可预料的错误。其中一部分是概念上的问题:指针用于低级操作,容易产生与繁琐细节相关的(bookkeeping)错误。其他错误则源于使用指针的语法规则,特别是声明指针的语法。
许多有用的程序都可不使用数组或指针实现,现代C++程序采用vector类型和迭代器取代一般的数组、采用string类型取代C风格字符串。
指针的定义和初始化
每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。例如,一个 int 型指针只能指向 int 型对象。指针变量的定义
C++ 语言使用 * 符号把一个标识符声明为指针:
vector<int> *pvec; // pvec can point to a vector<int> int *ip1, *ip2; // ip1 and ip2 can point to an int string *pstring; // pstring can point to a string double *dp; // dp can point to a double<Tips>:
理解指针声明语句时,请从右向左阅读。
从右向左阅读 pstring 变量的定义,可以看到
string *pstring;
语句把 pstring 定义为一个指向 string 类型对象的指针变量。类似地,语句
int *ip1, *ip2; // ip1 and ip2 can point to an int
把 ip1 和 ip2 都定义为指向 int 型对象的指针。
在声明语句中,符号 * 可用在指定类型的对象列表的任何位置:
double dp, *dp2; // dp2 is a ponter, dp is an object: both type double
该语句定义了一个 double 类型的 dp 对象以及一个指向 double 类型对象的指针dp2。
另一种声明指针的风格
在定义指针变量时,可用空格将符号 * 与其后的标识符分隔开来。下面的写法是合法的:
string* ps; // legal but can be misleading
也就是说,该语句把 ps 定义为一个指向 string 类型对象的指针。
这种指针声明风格容易引起这样的误解:把 string* 理解为一种数据类型,认为在同一声明语句中定义的其他变量也是指向 string 类型对象的指针。然而,语句
string* ps1, ps2; // ps1 is a pointer to string, ps2 is a string
实际上只把 ps1 定义为指针,而 ps2 并非指针,只是一个普通的 string 对象而已。
如果需要在一个声明语句中定义两个指针,必须在每个变量标识符前再加符号 * 声明:
string* ps1, *ps2; // both ps1 and ps2 are pointers to string
连续声明多个指针易导致混淆
连续声明同一类型的多个指针有两种通用的声明风格。其中一种风格是一个声明语句只声明一个变量,此时,符号 * 紧挨着类型名放置,强调这个声明语句定义的是一个指针:
string* ps1; string* ps2;
另一种风格则允许在一条声明语句中声明多个指针,声明时把符号 * 靠近标识符放置。这种风格强调对象是一个指针:
string *ps1, *ps2;
<Tips>:关于指针的声明,不能说哪种声明风格是唯一正确的方式,重要的是选择一种风格并持续使用。
一般建议第二种声明风格:将符号 * 紧贴着指针变量名放置。
指针可能的取值
一个有效的指针必然是以下三种状态之一:
保存一个特定对象的地址;
指向某个对象后面的另一对象;或者是0值。若指针保存0值,表明它不指向任何对象。
未初始化的指针是无效的,直到给该指针赋值后,才可使用它。
下列定义和赋值都是合法的:
int ival = 1024; int *pi = 0; // pi initialized to address no object int *pi2 = & ival; // pi2 initialized to address of ival int *pi3; // ok, but dangerous, pi3 is uninitialized pi = pi2; // pi and pi2 address the same object, e.g. ival pi2 = 0; // pi2 now addresses no object
《强烈关注:》避免使用未初始化的指针
很多运行时错误都源于使用了未初始化的指针。
就像使用其他没有初始化的变量一样, 使用未初始化的指针时的行为几乎总会导致运行时崩溃。而且,导致崩溃的这一原因很难发现。
对大多数的编译器来说,如果使用未初始化的指针,会将指针中存放的不确定值视为地址,然后操纵该内存地址中存放的位内容。
使用未初始化的指针相当于操纵这个不确定地址中存储的基础数据。因此,在对未初始化的指针进行解引用时,通常会导致程序崩溃。
C++ 语言无法检测指针是否未被初始化,也无法区分有效地址和由指针分配到的存储空间中存放的二进制位形成的地址。
建议程序员在使用之前初始化所有的变量,尤其是指针。
如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。
如果必须分开定义指针和其所指向的对象,则将指针初始化为 0。因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。
指针初始化和赋值操作的约束
对指针进行初始化或赋值只能使用以下四种类型的值:
-
0 值常量表达式,例如,在编译时可获得 0 值的整型 const 对象或字面值常量 0。
-
类型匹配的对象的地址。
-
另一对象末的下一地址。
-
同类型的另一个有效指针。
把 int 型变量赋给指针是非法的,尽管此 int 型变量的值可能为 0。但允许把数值 0 或在编译时可获得 0 值的 const 量赋给指针:
int ival; int zero = 0; const int c_ival = 0; int *pi = ival; // error: pi initialized from int value of ival pi = zero; // error: pi assigned int value of zero pi = c_ival; // ok: c_ival is a const with compile-time value of 0 pi = 0; // ok: directly initialize to literal constant 0
除了使用数值0或在编译时值为 0 的 const 量外,还可以使用 C++ 语言从 C 语言中继承下来的预处理器变量 NULL,该变量在 cstdlib 头文件中定义,其值为 0。
如果在代码中使用了这个预处理器变量,则编译时会自动被数值 0 替换。因此,把指针初始化为 NULL 等效于初始化为 0 值:
// cstdlib #defines NULL to 0 int *pi = NULL; // ok: equivalent to int *pi = 0;<更加常用>
<Tips>:正如其他的预处理器变量一样,不可以使用 NULL 这个标识符给自定义的变量命名。
预处理器变量不是在 std 命名空间中定义的,因此其名字应为 NULL,而非 std::NULL。
double dval; double *pd = &dval; // ok: initializer is address of a double double *pd2 = pd; // ok: initializer is a pointer to double int *pi = pd; // error: types of pi and pd differ pi = &dval; // error: attempt to assign address of a double to int *由于指针的类型用于确定指针所指对象的类型,因此初始化或赋值时必须保证类型匹配。指针用于间接访问对象,并基于指针的类型提供可执行的操作,例如, int 型指针只能把其指向的对象当作 int 型数据来处理,如果该指针确实指向了其他类型(如 double 类型)的对象,则在指针上执行的任何操作都有可能出错。
void* 指针
C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址:
double obj = 3.14; double *pd = &obj; // ok: void* can hold the address value of any data pointer type void *pv = &obj; // obj can be an object of any type pv = pd; // pd can be a pointer to any type
void* 表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。
void* 指针只支持几种有限的操作:与另一个指针进行比较;
向函数传递 void* 指针或从函数返回 void* 指针;
给另一个 void* 指针赋值。
不允许使用 void* 指针操纵它所指向的对象。
指针操作
指针提供间接操纵其所指对象的功能。与对迭代器进行解引用操作一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)将获取指针所指的对象:
string s("hello world"); string *sp = &s; // sp holds the address of s cout <<*sp; // prints hello world
对 sp 进行解引用将获得 s 的值,然后用输出操作符输出该值,于是最后一条语句输出了 s 的内容 hello world。
生成左值的解引用操作
解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值:
*sp = "goodbye"; // contents of s now changed
因为 sp 指向 s,所以给 *sp 赋值也就修改了 s 的值。
也可以修改指针 sp 本身的值,使 sp 指向另外一个新对象:
string s2 = "some value"; sp = &s2; // sp now points to s2
给指针直接赋值即可修改指针的值——不需要对指针进行解引用。
关键概念:给指针赋值或通过指针进行赋值
对于初学指针者,给指针赋值和通过指针进行赋值这两种操作的差别确实让人费解。
谨记区分的重要方法是:
如果对左操作数进行解引用,则修改的是指针所指对象的值;
如果没有使用解引用操作,则修改的是指针本身的值。如图所示,帮助理解下列例子:
指针和引用的比较
虽然使用引用(reference)和指针都可间接访问另一个值, 但它们之间有两个重要区别。第一个区别在于:引用总是指向某个对象:定义引用时没有初始化是错误的。
第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。
引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。
考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:
int ival = 1024, ival2 = 2048; int *pi = &ival, *pi2 = &ival2; pi = pi2; // pi now points to ival2
赋值结束后,pi 所指向的 ival 对象值保持不变,赋值操作修改了 pi 指针的值,使其指向另一个不同的对象。现在考虑另一段相似的程序,使用两个引用赋值:
int &ri = ival, &ri2 = ival2; ri = ri2; // assigns ival2 to ival
这个赋值操作修改了 ri 引用的值 ival 对象,而并非引用本身。赋值后,这两个引用还是分别指向原来关联的对象,此时这两个对象的值相等。
指向指针的指针
指针本身也是可用指针指向的内存对象。指针占用内存空间存放其值,因此指针的存储地址可存放在指针中。下面程序段:
int ival = 1024; int *pi = &ival; // pi points to an int int **ppi = π // ppi points to a pointer to int
定义了指向指针的指针。C++ 使用 ** 操作符指派一个指针指向另一指针。这些对象可表示为:
对 ppi 进行解引用照常获得 ppi 所指的对象,在本例中,所获得的对象是指向 int 型变量的指针 pi:
int *pi2 = *ppi; // ppi points to a pointer
为了真正地访问到 ival 对象,必须对 ppi 进行两次解引用:
cout << "The value of ival\n" << "direct value: " << ival << "\n" << "indirect value: " << *pi << "\n" << "doubly indirect value: " << **ppi << endl;这段程序用三种不同的方式输出 ival 的值。
首先,采用直接引用变量的方式输出;
然后使用指向 int 型对象的指针 pi 输出;
最后,通过对 ppi 进行两次解引用获得 ival 的特定值。
使用指针访问数组元素
C++ 语言中,指针和数组密切相关。特别是在表达式中使用数组名时,该名字会自动转换为指向数组第一个元素的指针:
int ia[] = {0,2,4,6,8}; int *ip = ia; // ip points to ia[0]
如果希望使指针指向数组中的另一个元素,则可使用下标操作符给某个元素定位,然后用取地址操作符 & 获取该元素的存储地址:
ip = &ia[4]; // ip points to last element in ia
指针的算术操作
与其使用下标操作,倒不如通过指针的算术操作来获取指定内容的存储地址。
指针的算术操作和迭代器的算术操作以相同的方式实现(也具有相同的约束)。
使用指针的算术操作在指向数组某个元素的指针上加上(或减去)一个整型数值,就可以计算出指向数组另一元素的指针值:
ip = ia; // ok: ip points to ia[0] int *ip2 = ip + 4; // ok: ip2 points to ia[4], the last element in ia
在指针 ip 上加 4 得到一个新的指针,指向数组中 ip 当前指向的元素后的第4 个元素。
通常,在指针上加上(或减去)一个整型数值 n 等效于获得一个新指针,该新指针指向指针原来指向的元素之后(或之前)的第 n 个元素。
<note>:指针的算术操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一单元时才是合法的。
如果指针指向一对象,我们还可以在指针上加1从而获取指向相邻的下一个对象的指针。
假设数组 ia 只有 4 个元素,则在 ia 上加 10 是错误的:
// error: ia has only 4 elements, ia + 10 is an invalid address
int *ip3 = ia + 10;
只要两个指针指向同一数组或有一个指向该数组末端的下一单元,C++ 还支持对这两个指针做减法操作:
ptrdiff_t n = ip2 - ip; // ok: distance between the pointers
结果是 4,这两个指针所指向的元素间隔为 4 个对象。
两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。
与 size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在cstddef 头文件中定义。
size_t 是 unsigned 类型,而ptrdiff_t 则是signed 整型。
允许在指针上加减 0,使指针保持不变。
更有趣的是,如果一指针具有 0 值(空指针),则在该指针上加 0 仍然是合法的,结果得到另一个值为 0 的指针。也可以对两个空指针做减法操作,得到的结果仍是 0。
解引用和指针算术操作之间的相互作用
在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新指针:
int last = *(ia + 4); // ok: initializes last to 8, the value of ia[4]
这个表达式计算出 ia 所指向元素后面的第 4 个元素的地址,然后对该地址进行解引用操作,等价于 ia[4]。
<note>:
加法操作两边用圆括号括起来是必要的。如果写为: |
last = *ia + 4; // ok: last = 4, equivalent to ia[0]+4
意味着对 ia 进行解引用,获得 ia 所指元素的值 ia[0],然后加 4。
由于加法操作和解引用操作的优先级不同,上述表达式中的圆括号是必要的。简单地说,优先级决定了有多个操作符的表达式如何对操作数分组。解引用操作符的优先级比加法操作符高。
与低优先级的操作符相比,优先级高的操作符的操作数先被组合起来操作。如果没有圆括号,解引用操作符的操作数是 ia,该表达式先对 ia 解引用,获得 ia 数组中的第一个元素,并将该值与 4 相加。如果表达式加上圆括号,则不管一般的优先级规则,将 (ia + 4) 作为单个操作数,这是 ia 所指向的元素后面第4个元素的地址,然后对这个新地址进行解引用。
下标和指针
我们已经看到,在表达式中使用数组名时,实际上使用的是指向数组第一个元素的指针。这种用法涉及很多方面,当它们出现时我们会逐一指出来。
其中一个重要的应用是使用下标访问数组时,实际上是使用下标访问指针:
int ia[] = {0,2,4,6,8}; int i = ia[0]; // ia points to the first element in ia
ia[0] 是一个使用数组名的表达式。在使用下标访问数组时,实际上是对指向数组元素的指针做下标操作。只要指针指向数组元素,就可以对它进行下标操作:
int *p = &ia[2]; // ok: p points to the element indexed by 2 int j = p[1]; // ok: p[1] equivalent to *(p + 1), // p[1] is the same element as ia[3] int k = p[-2]; // ok: p[-2] is the same element as ia[0]
vector 类型提供的 end 操作将返回指向超出 vector 末端位置的一个迭代器。这个迭代器常用作哨兵,来控制处理vector 中元素的循环。类似地,可以计算数组的超出末端指针的值:
const size_t arr_size = 5; int arr[arr_size] = {1,2,3,4,5}; int *p = arr; // ok: p points to arr[0] int *p2 = p + arr_size; // ok: p2 points one past the end of arr // use caution -- do not dereference!
本例中,p 指向数组 arr 的第一个元素,在指针 p 上加数组长度即可计算出数组arr 的超出末端指针。p 加 5 即得 p 所指向的元素后面的第五个 int 元素的地址——换句话说,p + 5指向数组的超出末端的位置。
<Note>:C++ 允许计算数组或对象的超出末端的地址,但不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的。
计算并存储在p2中的地址,与在 vector 上做 end 操作所返回的迭代器具有相同的功能。由end 返回的迭代器标志了该 vector 对象的“超出末端位置”,不能进行解引用运算,但是可将它与别的迭代器比较,从而判断是否已经处理完vector 中所有的元素。
同理,p2 也只能用来与其他指针比较,或者用做指针算术操作表达式的操作数。对 p2 进行解引用将得到无效值。
对大多数的编译器来说,会把对 p2 进行解引用的结果(恰好存储在 arr 数组的最后一个元素后面的内存中的二进制位)视为一个int 型数据。
输出数组元素
用指针编写以下程序:
const size_t arr_sz = 5; int int_arr[arr_sz] = { 0, 1, 2, 3, 4 }; // pbegin points to first element, pend points just after the last for (int *pbegin = int_arr, *pend = int_arr + arr_sz; pbegin != pend; ++pbegin) cout << *pbegin << ' '; // print the current element
这段程序使用了一个我们以前没有用过的 for 循环性质:只要定义的多个变量具有相同的类型,就可以在 for 循环的初始化语句中同时定义它们。本例在初始化语句中定义了两个 int 型指针pbegin 和 pend。
C++ 允许使用指针遍历数组。和其他内置类型一样,数组也没有成员函数。因此,数组不提供 begin 和 end 操作,程序员只能自己给指针定位,使之分别标志数组的起始位置和超出末端位置。
可在初始化中实现这两个指针的定位:初始化指针 pbegin 指向 int_arr 数组的第一个元素,而指针pend 则指向该数组的超出末端的位置:
指针 pend 是标志 for 循环结束的哨兵。for 循环的每次迭代都会使pbegin 递增 1 以指向数组的下一个元素。
第一次执行 for 循环时,pbegin 指向数组中的第一个元素;第二次循环,指向第二个元素;这样依次类推。
当处理完数组的最后一个元素后,pbegin 再加 1 则与 pend 值相等,表示整个数组已遍历完毕。
指针是数组的迭代器
聪明的读者可能已经注意到这段程序迭代器的一段程序非常相像,该程序使用下面的循环遍历并输出一个 string 类型的vector 的内容:
// equivalent loop using iterators to reset all the elements in ivec to 0 for (vector<int>::iterator iter = ivec.begin(); iter != ivec.end(); ++iter) *iter = 0; // set element to which iter refers to 0
这段程序使用迭代器的方式就像上个程序使用指针实现输出数组内容一样。
指针和迭代器的这个相似之处并不是巧合。实际上,内置数组类型具有标准库容器的许多性质,与数组联合使用的指针本身就是迭代器。
指针和 const 限定符
指向 const 对象的指针
到目前为止,我们使用指针来修改其所指对象的值。但是如果指针指向 const 对象,则不允许用指针来改变其所指的const 值。
为了保证这个特性,C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:
const double *cptr; // cptr may point to a double that is const
这里的 cptr 是一个指向 double 类型 const 对象的指针,const 限定了cptr 指针所指向的对象类型,而并非 cptr 本身。
也就是说,cptr 本身并不是 const。
在定义时不需要对它进行初始化,如果需要的话,允许给 cptr 重新赋值,使其指向另一个 const 对象。但不能通过cptr 修改其所指对象的值:
*cptr = 42; // error: *cptr might be const
把一个 const 对象的地址赋给一个普通的、非 const 对象的指针也会导致编译时的错误:
const double pi = 3.14; double *ptr = π // error: ptr is a plain pointer const double *cptr = π // ok: cptr is a pointer to const
不能使用 void* 指针保存 const 对象的地址,而必须使用const void* 类型的指针保存const 对象的地址:
const int universe = 42; const void *cpv = &universe; // ok: cpv is const void *pv = &universe; // error: universe is const
允许把非 const 对象的地址赋给指向 const 对象的指针,例如:
double dval = 3.14; // dval is a double; its value can be changed cptr = &dval; // ok: but can't change dval through cptr
尽管 dval 不是 const 对象,但任何企图通过指针 cptr 修改其值的行为都会导致编译时的错误。
cptr 一经定义,就不允许修改其所指对象的值。如果该指针恰好指向非 const 对象时,同样必须遵循这个规则。
<Note>:不能使用指向 const 对象的指针修改基础对象,然而如果该指针指向的是一个非 const 对象,可用其他方法修改其所指的对象。
事实是,可以修改 const 指针所指向的值,这一点常常容易引起误会。考虑:
dval = 3.14159; // dval is not const *cptr = 3.14159; // error: cptr is a pointer to const double *ptr = &dval; // ok: ptr points at non-const double *ptr = 2.72; // ok: ptr is plain pointer cout << *cptr; // ok: prints 2.72在此例题中,指向 const 的指针 cptr 实际上指向了一个非 const 对象。尽管它所指的对象并非 const,但仍不能使用 cptr 修改该对象的值。
本质上来说,由于没有方法分辩 cptr 所指的对象是否为 const,系统会把它所指的所有对象都视为const。
如果指向 const 的指针所指的对象并非 const,则可直接给该对象赋值或间接地利用普通的非const 指针修改其值:毕竟这个值不是 const。
重要的是要记住:不能保证指向 const 的指针所指对象的值一定不可修改。
<Tips>:
如果把指向 const 的指针理解为“自以为指向 const 的指针”,这可能会对理解有所帮助。
在实际的程序中,指向 const 的指针常用作函数的形参。将形参定义为指向const 的指针,以此确保传递给函数的实际对象在函数中不因为形参而被修改。
const 指针
除指向 const 对象的指针外,C++ 语言还提供了 const 指针——本身的值不能修改:
int errNumb = 0; int *const curErr = &errNumb; // curErr is a constant pointer
我们可以从右向左把上述定义语句读作“curErr 是指向 int 型对象的const 指针”。
与其他 const 量一样,const 指针的值不能修改,这就意味着不能使curErr 指向其他对象。
任何企图给 const 指针赋值的行为(即使给 curErr 赋回同样的值)都会导致编译时的错误:
curErr = curErr; // error: curErr is const
与任何 const 量一样,const 指针也必须在定义时初始化。
指针本身是 const 的事实并没有说明是否能使用该指针修改它所指向对象的值。
指针所指对象的值能否修改完全取决于该对象的类型。例如,curErr 指向一个普通的非常量int 型对象 errNumb,则可使用 curErr 修改该对象的值:
if (*curErr) { errorHandler(); *curErr = 0; // ok: reset value of the object to which curErr is bound }
指向 const 对象的 const 指针
还可以如下定义指向 const 对象的 const 指针:
const double pi = 3.14159; // pi_ptr is const and points to a const object const double *const pi_ptr = π"
本例中,既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向(即 pi_ptr 中存放的地址值)。
可从右向左阅读上述声明语句:“pi_ptr 首先是一个 const 指针,指向 double 类型的 const 对象”。
指针和 typedef
在 typedef中使用指针往往会带来意外的结果。下面是一个几乎所有人刚开始时都会答错的问题。假设给出以下语句:
typedef string *pstring; const pstring cstr;
请问 cstr 变量是什么类型?简单的回答是 const pstring 类型的指针。进一步问:const pstring 指针所表示的真实类型是什么?很多人都认为真正的类型是:
const string *cstr; // wrong interpretation of const pstring cstr
也就是说,const pstring 是一种指针,指向 string 类型的 const 对象,但这是错误的。
错误的原因在于将 typedef 当做文本扩展了。
声明 const pstring 时,const 修饰的是 pstring 的类型,这是一个指针。
因此,该声明语句应该是把 cstr 定义为指向 string 类型对象的const 指针,这个定义等价于:
// cstr is a const pointer to string string *const cstr; // equivalent to const pstring cstr
建议:理解复杂的 const 类型的声明
阅读 const 声明语句产生的部分问题,源于const 限定符既可以放在类型前也可以放在类型后:
string const s1; // s1 and s2 have same type, const string s2; // they're both strings that are const用 typedef 写 const 类型定义时,const 限定符加在类型名前面容易引起对所定义的真正类型的误解:
string s; typedef string *pstring; const pstring cstr1 = &s; // written this way the type is obscured pstring const cstr2 = &s; // all three decreations are the same type string *const cstr3 = &s; // they're all const pointers to string把 const 放在类型 pstring 之后,然后从右向左阅读该声明语句就会非常清楚地知道cstr2 是 const pstring 类型,即指向 string 对象的 const 指针。
不幸的是,大多数人在阅读 C++ 程序时都习惯看到 const 放在类型前面。于是为了遵照惯例,只好建议编程时把const 放在类型前面。但是,把声明语句重写为置 const 于类型之后更便于理解。