在程序中,我们的数据都有其存储的地址。在程序每次的实际运行过程中,变量在物理内存中的存储位置不尽相同。不过,我们仍能够在编程时,通过一定的语句,来取得数据在内存中的地址。
地址也是数据。存放地址所用的变量类型有一个特殊的名字,叫做“指针变量”,有时也简称做“指针”。
地址只是一个刻度一般的数据,为了针对不同类型的数据,“指针变量”也有不同的类型,比如,可以有 int 类型的指针变量,其中存储的地址(即指针变量存储的数值)对应一块大小为 32 位的空间的起始地址;有 char 类型的指针变量,其中存储的地址对应一块 8 位的空间的起始地址。
事实上,用户也可以声明指向指针变量的指针变量。
假如用户自定义了一个结构体:
struct q {int a;int b;int c;
};
则 q 类型的指针变量,对应着一块 3 × 32 = 96 bit 的空间。
C/C++ 中,指针变量的类型为类型名后加上一个星号 *。比如,int 类型的指针变量的类型名即为 int*。
我们可以使用 & 符号取得一个变量的地址。
要想访问指针变量地址所对应的空间(又称指针所 指向 的空间),需要对指针变量进行 解引用(dereference),使用 * 符号。
int main() {int a = 123; // a: 123int* pa = &a;*pa = 321; // a: 321
}
对结构体变量也是类似。如果要访问指针指向的结构中的成员,需要先对指针进行解引用,再使用 . 成员关系运算符。不过,更推荐使用“箭头”运算符 -> 这一更简便的写法。
struct q {int a;int b;int c;
};int main() {q x{1, 2, 3}, y{6, 7, 8};q* px = &x;(*px) = y; // x: {6,7,8}(*px).a = 4; // x: {4,7,8}px->b = 5; // x: {4,5,8}
}
指针变量也可以 和整数 进行加减操作。对于 int 型指针,每加 1(递增 1),其指向的地址偏移 32 位(即 4 个字节);若加 2,则指向的地址偏移 2 × 32 = 64 位。同理,对于 char 型指针,每次递增,其指向的地址偏移 8 位(即 1 个字节)。
我们前面说过,数组是一块连续的存储空间。而在 C/C++ 中,直接使用数组名,得到的是数组的起始地址。
int main() {int a[3] = {1, 2, 3};int* p = a; // p 指向 a[0]*p = 4; // a: [4, 2, 3]p = p + 1; // p 指向 a[1]*p = 5; // a: [4, 5, 3]p++; // p 指向 a[2]*p = 6; // a: [4, 5, 6]
}
当通过指针访问数组中的元素时,往往需要用到“指针的偏移”,换句话说,即通过一个基地址(数组起始的地址)加上偏移量来访问。
我们常用 [] 运算符来访问数组中某一指定偏移量处的元素。比如 a[3] 或者 p[4]。这种写法和对指针进行运算后再引用是等价的,即 p[4] 和 *(p + 4) 是等价的两种写法。
使用指针,使得程序编写者可以操作程序运行时中各处的数据,而不必局限于作用域。
在 C/C++ 中,调用函数(过程)时使用的参数,均以拷贝的形式传入子过程中(引用除外,会在后续介绍)。默认情况下,函数仅能通过返回值,将结果返回到调用处。但是,如果某个函数希望修改其外部的数据,或者某个结构体/类的数据量较为庞大、不宜进行拷贝,这时,则可以通过向其传入外部数据的地址,便得以在其中访问甚至修改外部数据。
下面的 q 方法,通过接收两个 int 型的指针,在函数中使用中间变量,完成对两个 int 型变量值的交换。
void q(int *a, int *b) {int t;t = *a;*a = *b;*b = t;
}int main() {int a = 6, b = 10;q(&a, &b);// 调用后,main 函数中 a 变量的值变为 10,b 变量的值变为 6
}
除此之外,程序编写时往往会涉及到动态内存分配,即,程序会在运行时,向操作系统动态地申请或归还存放数据所需的内存。当程序通过调用操作系统接口申请内存时,操作系统将返回程序所申请空间的地址。要使用这块空间,我们需要将这块空间的地址存储在指针变量中。
在 C++ 中,我们使用 new 运算符来获取一块内存,使用 delete 运算符释放某指针所指向的空间。
int* p = new int(1234);
/* ... */
delete p;
上一篇:怎么清空回收站?3分钟解决!
下一篇:SQL分库分表