ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 3.3 指针 (Pointers) 我们已经明白变量其实是可以由标识来存取的内存单元。但这些变量实际上是存储在内存中具体的位置上的。对我们的程序来说,计算机内存只是一串连续的单字节单元(1byte cell),即最小数据单位,每一个单元有一个唯一地址。 计算机内存就好像城市中的街道。在一条街上,所有的房子被顺序编号,每所房子有唯一编号。因此如果我们说芝麻街27号,我们很容易找到它,因为只有一所房子会是这个编号,而且我们知道它会在26号和28号之间。 同房屋按街道地址编号一样,操作系统(operating system)也按照唯一顺序编号来组织内存。因此,当我们说内存中的位置1776,我们知道内存中只有一个位置是这个地址,而且它在地址1775和1777之间。 ### 地址操作符/去引操作符 Address/dereference operator (&) 当我们声明一个变量的同时,它必须被存储到内存中一个具体的单元中。通常我们并不会指定变量被存储到哪个具体的单元中—幸亏这通常是由编译器和操作系统自动完成的,但一旦操作系统指定了一个地址,有些时候我们可能会想知道变量被存储在哪里了。 这可以通过在变量标识前面加与符号ampersand sign (&)来实现,它表示"...的地址" ("address of"),因此称为地址操作符(adress operator),又称去引操作符(dereference operator)。例如: `ted = &andy;` 将变量andy的地址赋给变量ted,因为当在变量名称andy 前面加ampersand (&) 符号,我们指的将不再是该变量的内容,而是它在内存中的地址。 假设andy 被放在了内存中地址1776的单元中,然后我们有下列代码: andy = 25; fred = andy; ted = &andy; 其结果显示在下面的图片中: ![](https://box.kancloud.cn/2015-09-06_55ebf16860966.gif) 我们将变量andy 的值赋给变量fred,这与以前我们看到很多例子都相同,但对于ted,我们把操作系统存储andy的内存地址赋给它,我们想像该地址为1776 (它可以是任何地址,这里只是一个假设的地址),原因是当给ted 赋值的时候,我们在andy 前面加了ampersand (&) 符号。 存储其它变量地址的变量(如上面例子中的ted ),我们称之为指针(pointer)。在C++ 中,指针pointers 有其特定的优点,因此经常被使用。在后面我们将会看到这种变量如何被声明。 ### 引用操作符Reference operator (*) 使用指针的时候,我们可以通过在指针标识的前面加星号asterisk (*)来存储该指针指向的变量所存储的数值,它可以被翻译为“所指向的数值”("value pointed by")。因此,仍用前面例子中的数值,如果我们写: beth = *ted; (我们可以读作:"beth 等与ted所指向的数值") beth 将会获得数值25,因为ted 是1776,而1776 所指向的数值为25。 ![](https://box.kancloud.cn/2015-09-06_55ebf169c6a1a.gif) 你必须清楚的区分ted 存储的是1776,但*ted (前面加asterisk * ) 指的是地址1776中存储的数值,即25。注意加或不加星号*的不同(下面代码中注释显示了如何读这两个不同的表达式): beth = ted; // beth 等于 ted ( 1776 ) beth = *ted; // beth 等于 ted 所指向的数值 ( 25 ) **地址或反引用操作符Operator of address or dereference (&)** 它被用作一个变量前缀,可以被翻译为“…的地址”("address of"),因此:&variable1 可以被读作 variable1的地址("address of variable1" )。 **引用操作符Operator of reference (*)** 它表示要取的是表达式所表示的地址指向的内容。它可以被翻译为“…指向的数值” ("value pointed by")。 * mypointer 可以被读作 "mypointer指向的数值"。 继续使用上面开始的例子,看下面的代码: andy = 25; ted = &andy; 现在你应该可以清楚的看到以下等式全部成立: andy == 25 &andy == 1776 ted == 1776 *ted == 25 第一个表达式很容易理解,因为我们有赋值语句andy=25;。第二个表达式使用了地址(或反引用)操作符(&) 来返回变量andy的地址,即 1776。第三个表达式很明显成立,因为第二个表达式为真,而我们给ted赋值的语句为ted = &andy;。第四个表达式使用了引用操作符 (*),相当于ted指向的地址中存储的数值,即25。 由此你也可以推断出,只要ted 所指向的地址中存储的数值不变,以下表达式也为真: `*ted == andy` ### 声明指针型变量Declaring variables of type pointer 由于指针可以直接引用它所指向的数值,因此有必要在声明指针的时候指明它所指向的数据类型。指向一个整型int或浮点型float数据的指针与指向一个字符型char数据的指针并不相同。 因此,声明指针的格式如下: `type * pointer_name;` 这里,type 是指针所指向的数据的类型,而不是指针自己的类型。例如: int * number; char * character; float * greatnumber; 它们是3个指针的声明,每一个指针指向一种不同数据类型。这三个指针本身其实在内存中占用同样大小的内存空间(指针的大小取决于不同的操作系统),但它们所指向的数据是不同的类型,并占用不同大小的内存空间,一个是整型int,一个是字符型char ,还有一个是浮点型float。 需要强调的一点是,在指针声明时的星号asterisk (*) 仅表示这里声明的是一个指针,不要把它和前面我们用过的引用操作符混淆,虽然那也是写成一个星号 (*)。它们只是用同一符号表示的两个不同任务。 ~~~ | // my first pointer #include int main ( ) { int value1 = 5, value2 = 15; int * mypointer; mypointer = &value1; *mypointer = 10; mypointer = &value2; *mypointer = 20; cout << "value1==" << value1 << "/ value2==" << value2; return 0; } | value1==10 / value2==20 | ~~~ 注意变量value1 和 value2 是怎样间接的被改变数值的。首先我们使用 ampersand sign (&) 将value1的地址赋给mypointer 。然后我们将10 赋给 mypointer所指向的数值,它其实指向value1的地址,因此,我们间接的修改了value1的数值。 为了让你了解在同一个程序中一个指针可以被用作不同的数值,我们在这个程序中用value2 和同一个指针重复了上面的过程。 下面是一个更复杂一些的例子: ~~~ | // more pointers #include int main () { int value1 = 5, value2 = 15; int *p1, *p2; p1 = &value1; // p1 = address of value1 p2 = &value2; // p2 = address of value2 *p1 = 10; // value pointed by p1 = 10 *p2 = *p1; // value pointed by p2 = value pointed by p1 p1 = p2; // p1 = p2 (value of pointer copied) *p1 = 20; // value pointed by p1 = 20 cout << "value1==" << value1 << "/ value2==" << value2; return 0; } | value1==10 / value2==20 | ~~~ 上面每一行都有注释说明代码的意思:ampersand (&) 为"address of",asterisk (*) 为 "value pointed by"。注意有些包含p1 和p2 的表达式不带星号。加不加星号的含义十分不同:星号(*)后面跟指针名称表示指针所指向的地方,而指针名称不加星号(*)表示指针本身的数值,即它所指向的地方的地址。 另一个需要注意的地方是这一行: `int *p1, *p2;` 声明了上例用到的两个指针,每个带一个星号(*),因为是这一行定义的所有指针都是整型int (而不是 int*)。原因是引用操作符(*) 的优先级顺序与类型声明的相同,因此,由于它们都是向右结合的操作,星号被优先计算。我们在 [section 1.3: Operators](http://www.prglab.com/cms/pages/c-tutorial/c-basics/operators.php) 中已经讨论过这些。注意在声明每一个指针的时候前面加上星号asterisk (*)。 ### 指针和数组Pointers and arrays 数组的概念与指针的概念联系非常解密。其实数组的标识相当于它的第一个元素的地址,就像一个指针相当于它所指向的第一个元素的地址,因此其实它们是同一个东西。例如,假设我们有以下声明: int numbers [20]; int * p; 下面的赋值为合法的: `p = numbers;` 这里指针p 和numbers 是等价的,它们有相同的属性,唯一的不同是我们可以给指针p赋其它的数值,而numbers 总是指向被定义的20个整数组中的第一个。所以,p只是一个普通的指针变量,而与之不同,numbers 是一个指针常量(constant pointer),数组名的确是一个指针常量。因此虽然前面的赋值表达式是合法的,但下面的不是: `numbers = p;` 因为numbers 是一个数组(指针常量),常量标识不可以被赋其它数值。 由于变量的特性,以下例子中所有包含指针的表达式都是合法的: ~~~ | // more pointers #include int main () { int numbers[5]; int * p; p = numbers; *p = 10; p++; *p = 20; p = &numbers[2]; *p = 30; p = numbers + 3; *p = 40; p = numbers; *(p+4) = 50; for (int n=0; n<5; n++) cout << numbers[n] << ", "; return 0; } | 10, 20, 30, 40, 50, | ~~~ 在数组一章中我们使用了括号[]来指明我们要引用的数组元素的索引(index)。中括号[]也叫位移(offset)操作符,它相当于在指针中的地址上加上括号中的数字。例如,下面两个表达式互相等价: a[5] = 0; // a [offset of 5] = 0 *(a+5) = 0; // pointed by (a+5) = 0 不管a 是一个指针还是一个数组名, 这两个表达式都是合法的。 ### 指针初始化Pointer initialization 当声明一个指针的时候我们可能需要同时指定它们指向哪个变量, int number; int *tommy = &number; 这相当于: int number; int *tommy; tommy = &number; 当给一个指针赋值的时候,我们总是赋给它一个地址值,而不是它所指向数据的值。你必须考虑到在声明一个指针的时候,星号 (*) 只是用来指明它是指针,而从不表示引用操作符reference operator (*)。记住,它们是两种不同操作,虽然它们写成同样的符号。因此,我们要注意不要将以上的代码与下面的代码混淆: int number; int *tommy; *tommy = &number; 这些代码也没有什么实际意义。 在定义数组指针的时候,编译器允许我们在声明变量指针的同时对数组进行初始化,初始化的内容需要是常量,例如: `char * terry = "hello";` 在这个例子中,内存中预留了存储"hello" 的空间,并且terry被赋予了向这个内存块的第一个字符(对应’h’)的指针。假设"hello"存储在地址1702,下图显示了上面的定义在内存中状态: ![](https://box.kancloud.cn/2015-09-06_55ebf16b3ad73.gif) 这里需要强调,terry 存储的是数值1702 ,而不是'h' 或 "hello",虽然1702 指向这些字符。 指针terry 指向一个字符串,可以被当作数组一样使用(数组只是一个常量指针)。例如,如果我们的心情变了,而想把terry指向的内容中的字符'o' 变为符号'!' ,我们可以用以下两种方式的任何一种来实现: terry[4] = '!'; *(terry+4) = '!'; 记住写 terry[4] 与*(terry+4)是一样的,虽然第一种表达方式更常用一些。以上两个表达式都会实现以下改变: ![](https://box.kancloud.cn/2015-09-06_55ebf16ca9a14.gif) ### 指针的数学运算Arithmetic of pointers 对指针进行数学运算与其他整型数据类型进行数学运算稍有不同。首先,对指针只有加法和减法运算,其它运算在指针世界里没有意义。但是指针的加法和减法的具体运算根据它所指向的数据的类型的大小的不同而有所不同。 我们知道不同的数据类型在内存中占用的存储空间是不一样的。例如,对于整型数据,字符char 占用1 的字节(1 byte),短整型short 占用2 个字节,长整型long 占用4个字节。 假设我们有3个指针: char *mychar; short *myshort; long *mylong; 而且我们知道他们分别指向内存地址1000, 2000 和3000。 因此如果我们有以下代码: mychar++; myshort++; mylong++; 就像你可能想到的,mychar的值将会变为1001。而myshort 的值将会变为2002,mylong的值将会变为3004。原因是当我们给指针加1时,我们实际是让该指针指向下一个与它被定义的数据类型的相同的元素。因此,它所指向的数据类型的长度字节数将会被加到指针的数值上。以上过程可以由下图表示: ![](https://box.kancloud.cn/2015-09-06_55ebf16e1f4af.gif) 这一点对指针的加法和减法运算都适用。如果我们写以下代码,它们与上面例子的作用一抹一样: mychar = mychar + 1; myshort = myshort + 1; mylong = mylong + 1; 这里需要提醒你的是,递增 (++) 和递减 (--) 操作符比引用操作符reference operator (*)有更高的优先级,因此,以下的表达式有可能引起歧义: *p++; *p++ = *q++; 第一个表达式等同于*(p++) ,它所作的是增加p (它所指向的地址,而不是它存储的数值)。 在第二个表达式中,因为两个递增操作(++) 都是在整个表达式被计算之后进行而不是在之前,所以*q 的值首先被赋予*p ,然后q 和p 都增加1。它相当于: *p = *q; p++; q++; 像通常一样,我们建议使用括号()以避免意想不到的结果。 ### 指针的指针Pointers to pointers C++ 允许使用指向指针的指针。要做到这一点,我们只需要在每一层引用之前加星号(*)即可: char a; char * b; char ** c; a = 'z'; b = &a; c = &b; 假设随机选择内存地址为7230, 8092 和10502,以上例子可以用下图表示: ![](https://box.kancloud.cn/2015-09-06_55ebf16f37e67.gif) (方框内为变量的内容;方框下面为内存地址) 这个例子中新的元素是变量c,关于它我们可以从3个方面来讨论,每一个方面对应了不同的数值: c 是一个(char **)类型的变量,它的值是8092 *c 是一个(char*)类型的变量,它的值是7230 **c 是一个(char)类型的变量,它的值是'z' ### 空指针void pointers 指针void 是一种特殊类型的指针。void 指针可以指向任意类型的数据,可以是整数,浮点数甚至字符串。唯一个限制是被指向的数值不可以被直接引用(不可以对它们使用引用星号*),因为它的长度是不定的,因此,必须使用类型转换操作或赋值操作来把void 指针指向一个具体的数据类型。 它的应用之一是被用来给函数传递通用参数: ~~~ | // integer increaser #include void increase (void* data, int type) { switch (type) { case sizeof(char) : (*((char*)data))++; break; case sizeof(short): (*((short*)data))++; break; case sizeof(long) : (*((long*)data))++; break; } } int main () { char a = 5; short b = 9; long c = 12; increase (&a,sizeof(a)); increase (&b,sizeof(b)); increase (&c,sizeof(c)); cout << (int) a << ", " << b << ", " << c; return 0; } | 6, 10, 13 | ~~~ sizeof 是C++的一个操作符,用来返回其参数的长度字节数常量。例如,sizeof(char) 返回 1,因为 char 类型是1字节长数据类型。 ### 函数指针Pointers to functions C++ 允许对指向函数的指针进行操作。它最大的作用是把一个函数作为参数传递给另外一个函数。声明一个函数指针像声明一个函数原型一样,除了函数的名字需要被括在括号内并在前面加星号asterisk (*)。例如: ~~~ | // pointer to functions #include int addition (int a, int b) { return (a+b); } int subtraction (int a, int b) { return (a-b); } int (*minus)(int,int) = subtraction; int operation (int x, int y, int (*functocall)(int,int)) { int g; g = (*functocall)(x,y); return (g); } int main () { int m,n; m = operation (7, 5, addition); n = operation (20, m, minus); cout return 0; } | 8 | ~~~ 在这个例子里, minus 是一个全局指针,指向一个有两个整型参数的函数,它被赋值指向函数subtraction,所有这些由一行代码实现: `int (* minus)(int,int) = subtraction;` `这里似乎解释的不太清楚,有问题问为什么(int int)只有类型,没有参数,就再多说两句。 这里 int (*minus)(int int)实际是在定义一个指针变量,这个指针的名字叫做minus,这个指针的类型是指向一个函数,函数的类型是有两个整型参数并返回一个整型值。` `整句话“int (*minus)(int,int) = subtraction;”是定义了这样一个指针并把函数subtraction的值赋给它,也就是说有了这个定义后minus就代表了函数subtraction。因此括号中的两个int int实际只是一种变量类型的声明,也就是说是一种形式参数而不是实际参数。 `