首页 > 代码库 > 【C++ Primer每日一刷之七】指针操作
【C++ Primer每日一刷之七】指针操作
4.2.3 指针操作
指针提供间接操纵其所指对象的功能。与对迭代器进行解引用操作一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)将获取指针所指的对象:
string s("hello world");
string *sp = &s; // sp holds theaddress of s
cout <<*sp; // prints hello world
对 sp 进行解引用将获得 s 的值,然后用输出操作符输出该值,于是最后一条语句输出了 s 的内容 hello world。
生成左值的解引用操作
解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值:
*sp = "goodbye"; // contents of snow 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 apointer to int
定义了指向指针的指针。C++ 使用** 操作符指派一个指针指向另一指针。
这些对象可表示为:
对 ppi 进行解引用照常获得 ppi所指的对象,在本例中,所获得的对象是
指向 int 型变量的指针 pi:
int *pi2 = *ppi; // ppi points to a pointer
为了真正地访问到 ival 对象,必须对 ppi 进行两次解引用:
cout << "The value ofival\n"
<< "direct value: "<< ival << "\n"
<< "indirect value: "<< *pi << "\n"
<< "doubly indirect value:" << **ppi
<< endl;
这段程序用三种不同的方式输出 ival 的值。首先,采用直接引用变量的方
式输出;然后使用指向 int 型对象的指针 pi 输出;最后,通过对 ppi 进行两
次解引用获得 ival 的特定值。
4.2.4. 使用指针访问数组元素
C++ 语言中,指针和数组密切相关。特别是在表达式中使用数组名时,该名字会自动转换为指向数组第一个元素的指针:
int ia[] = {0,2,4,6,8};
int *ip = ia; // ip points to ia[0]
如果希望使指针指向数组中的另一个元素,则可使用下标操作符给某个元素
定位,然后用取地址操作符 & 获取该元素的存储地址:
ip = &ia[4]; // ip points to lastelement in ia
指针的算术操作
与其使用下标操作,倒不如通过指针的算术操作来获取指定内容的存储地址。指针的算术操作和迭代器的算术操作(第 3.4.1 节)以相同的方式实现(也具有相同的约束)。使用指针的算术操作在指向数组某个元素的指针上加上(或减去)一个整型数值,就可以计算出指向数组另一元素的指针值:
ip = ia; // ok: ip points to ia[0]
int *ip2 = ip + 4; // ok: ip2 points toia[4], the last element in ia
在指针 ip 上加 4 得到一个新的指针,指向数组中 ip 当前指向的元素后的第 4 个元素。
通常,在指针上加上(或减去)一个整型数值 n 等效于获得一个新指针,该新指针指向指针原来指向的元素之后(或之前)的第 n 个元素。
指针的算术操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一单元时才是合法的。如果指针指向一对象,我们还可以在指针上加1 从而获取指向相邻的下一个对象的指针。
假设数组 ia 只有 4 个元素,则在 ia 上加 10 是错误的:
// error: ia has only 4 elements, ia + 10is an invalid address
int *ip3 = ia + 10;
只要两个指针指向同一数组或有一个指向该数组末端的下一单元,C++ 还支
持对这两个指针做减法操作:
ptrdiff_t n = ip2 - ip; // ok: distancebetween the pointers
结果是4,这两个指针所指向的元素间隔为 4 个对象。两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。与 size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在 cstddef 头文件中定义。size_t 是unsigned 类型,而 ptrdiff_t 则是 signed 整型。
这两种类型的差别体现了它们各自的用途:size_t 类型用于指明数组长度,它必须是一个正数;ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数。例如,ip 减去 ip2,结果为 -4。
允许在指针上加减 0,使指针保持不变。更有趣的是,如果一指针具有 0 值(空指针),则在该指针上加 0 仍然是合法的,结果得到另一个值为 0 的指针。也可以对两个空指针做减法操作,得到的结果仍是 0。
解引用和指针算术操作之间的相互作用
在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新指针:
int last = *(ia + 4); // ok: initializeslast to 8, the value of ia[4]
这个表达式计算出 ia 所指向元素后面的第 4 个元素的地址,然后对该地址进
行解引用操作,等价于 ia[4]。
加法操作两边用圆括号括起来是必要的。如果写为:
last = *ia + 4; // ok: last = 4, equivalentto ia[0]+4
意味着对 ia 进行解引用,获得 ia所指元素的值 ia[0],然后加 4。由于加法操作和解引用操作的优先级不同,上述表达式中的圆括号是必要的。我们将在第 5.10.1 节讨论操作符的优先级。简单地说,优先级决定了有多个操作符的表达式如何对操作数分组。解引用操作符的优先级比加法操作符高。
与低优先级的操作符相比,优先级高的操作符的操作数先被组合起来操作。如果没有圆括号,解引用操作符的操作数是 ia,该表达式先对 ia 解引用,获得ia 数组中的第一个元素,并将该值与 4 相加。
如果表达式加上圆括号,则不管一般的优先级规则,将 (ia + 4) 作为单个操作数,这是 ia 所指向的元素后面第4 个元素的地址,然后对这个新地址进行解引用。
下标和指针
我们已经看到,在表达式中使用数组名时,实际上使用的是指向数组第一个元素的指针。这种用法涉及很多方面,当它们出现时我们会逐一指出来。
其中一个重要的应用是使用下标访问数组时,实际上是使用下标访问指针:
int ia[] = {0,2,4,6,8};
int i = ia[0]; // ia points to the firstelement in ia
ia[0] 是一个使用数组名的表达式。在使用下标访问数组时,实际上是对指向数组元素的指针做下标操作。只要指针指向数组元素,就可以对它进行下标操作:
int *p = &ia[2]; // ok: p points to theelement 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 sameelement 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 pointsone past the end of
arr
// use caution -- do not dereference!
本例中,p 指向数组 arr 的第一个元素,在指针 p 上加数组长度即可计算出数组 arr 的超出末端指针。p 加 5 即得 p 所指向的元素后面的第五个 int 元素的地址——换句话说,p + 5 指向数组的超出末端的位置。
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, pendpoints 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 resetall the elements in ivec to 0
for (vector<int>::iterator iter =ivec.begin();iter != ivec.end(); ++iter)
*iter = 0; // set element to which iterrefers to 0
这段程序使用迭代器的方式就像上个程序使用指针实现输出数组内容一样。指针和迭代器的这个相似之处并不是巧合。实际上,内置数组类型具有标准库容器的许多性质,与数组联合使用的指针本身就是迭代器。在第二部分中,我们还会详细介绍容器和迭代器类型。
4.2.5. 指针和const 限定符
第 2.4 节介绍了指针和 const限定符之间的两种交互类型:指向 const对象的指针和const 指针。我们在本节中详细讨论这两类指针。
指向const 对象的指针
到目前为止,我们使用指针来修改其所指对象的值。但是如果指针指向const 对象,则不允许用指针来改变其所指的 const 值。为了保证这个特性,C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性:
const double *cptr; // cptr may point to adouble 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 aplain pointer
const double *cptr = π // ok: cptris a pointer to const
不能使用void* 指针(第 4.2.2 节)保存 const 对象的地址,而必须使用 const void* 类型的指针保存 const 对象的地址:
const int universe = 42;
const void *cpv = &universe; // ok: cpvis 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 changedval through cptr
尽管 dval 不是 const 对象,但任何企图通过指针 cptr 修改其值的行为都会导致编译时的错误。cptr 一经定义,就不允许修改其所指对象的值。如果该指针恰好指向非 const对象时,同样必须遵循这个规则。
不能使用指向 const 对象的指针修改基础对象,然而如果该指针指向的是一个非 const 对象,可用其他方法修改其所指的对象。
事实是,可以修改 const 指针所指向的值,这一点常常容易引起误会。考虑:
dval = 3.14159; // dval is not const
*cptr = 3.14159; // error: cptr is apointer to const
double *ptr = &dval; // ok: ptr pointsat 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 的指针所指对象的值一定不可修改。如果把指向 const 的指针理解为“【自以为指向 const 的指针”】,这可能会对理解有所帮助。
在实际的程序中,指向 const 的指针常用作函数的形参。将形参定义为指向 const 的指针,以此确保传递给函数的实际对象在函数中不因为形参而被修改。
const 指针
除指向 const 对象的指针外,C++语言还提供了 const 指针——本身的值不能修改:
int errNumb = 0;
int *const curErr = &errNumb; // curErris 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 constobject
const double *const pi_ptr = π
本例中,既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向(即 pi_ptr 中存放的地址值)。可从右向左阅读上述声明语句:“pi_ptr首先是一个 const 指针,指向 double 类型的 const 对象”。
【也就是说——指向const 对象的 const 指针——既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向】
指针和 typedef
在 typedef中使用指针往往会带来意外的结果。下面是一个几乎所有人刚开始时都会答错的问题。假设给出以下语句:
typedef string *pstring;
const pstring cstr;
请问 cstr 变量是什么类型?简单的回答是 const pstring 类型的指针。
进一步问:const pstring 指针所表示的真实类型是什么?很多人都认为真正的类型是:
const string *cstr; // wrong interpretationof 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 constpstring cstr
建议:理解复杂的const 类型的声明
阅读 const 声明语句产生的部分问题,源于 const 限定符既可以放在类型前也可以放在类型后:
string const s1; // s1 and s2 have sametype,
const string s2; // they‘re both stringsthat are const
用 typedef 写 const 类型定义时,const 限定符加在类型名前面容易引起对所定义的真正类型的误解:
[View full width]
string s;
typedef string *pstring;
const pstring cstr1 = &s; // writtenthis way the type is obscured
pstring const cstr2 = &s; // all threedecreations are the same type
string *const cstr3 = &s; // they‘reall const pointers to string
把const 放在类型 pstring 之后,然后从右向左阅读该声明语句就会非常清楚地知道 cstr2 是 const pstring 类型,即指向 string 对象的 const 指针。
不幸的是,大多数人在阅读 C++ 程序时都习惯看到 const 放在类型前面。于是为了遵照惯例,只好建议编程时把 const 放在类型前面。但是,把声明语句重写为置 const 于类型之后更便于理解。
【C++ Primer每日一刷之七】指针操作