指针与地址


在〈变量〉谈过,变量提供一个有名称的内存存储空间,变量可包含的信息包含变量数据类型、变量内存地址与变量存储值。

如果想知道变量的地址为何,可以使用&取址运算符(Address-of operator),例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 

    cout << "n 的值:" << n << endl
         << "n 地址:" << &n << endl; 

    return 0; 
}

执行结果:

n 的值:10
n 地址:0x61febc

这个程序中,定义了一个int整数变量nn的地址是0x61febc,使用 16 进制表示法,若int长度为 4 个字节,从0x61febc后 4 个字节都是配置给n的空间,现在这个空间中存储值为 10。

直接访问变量会对分配到的空间作访问,指针(Pointer)是一种变量,指针可存储特定的内存地址,要定义指针,使用以下的语法:

type *ptr;

ptr可存储地址,而type为该地址存储值的类型,实际定义的方式如下:

int *n;
float *s;
char *c;

虽然定义指针时,C++ 习惯将*前置在变量名称前,不过n的类型是int*s的类型是float*,而c的类型是char*,指针的类型决定了地址上的数据如何解释,以及如何进行指针运算(Pointer arithmetic)。

可以使用&运算符获取变量地址并指定给指针,例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    int *p = &n ; 

    cout << "n 变量的地址:" << &n << endl
         << "p 存储的地址:" << p << endl; 

    return 0; 
}

执行结果:

n 变量的地址:0x61feb8
p 存储的地址:0x61feb8

以上的程序使用&来获取变量n存储的地址,然后指定给指针p,因此p存储的值就与&n取出的值相同。

可以使用提取 (Dereference)运算符*来提取指针存储地址处的对象。例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    int *p = &n;

    cout << "指针 p 存储的地址:" << p << endl
         << "提取 p 存储地址处的对象:" << *p << endl;

    return 0; 
}

*p提取了p存储的地址处之对象,这个对象就是n变量,因此执行结果如下:

指针 p 存储的地址:0x61feb8
提取 p 存储地址处的对象:10

*p提取了变量n,将值指定给*p时,就是指定给变量n,例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    int *p = &n ; 

    cout << "n = " << n << endl
         << "*p = " << *p << endl; 

    *p = 20; 

    cout << "n = " << n << endl
         << "*p = " << *p << endl;

    return 0; 
}

执行结果:

n = 10
*p = 10
n = 20
*p = 20

如果定义指针但不指定初值,指针存储的地址是未知的,访问未知地址的内存内容是危险的,例如:

int *p;
*p = 10;

这会造成不可预知的结果,最好为指针设定初值,如果指针一开始不存储任何地址,可设定初值为 0,或者是使用nullptr,例如:

int *p = nullptr;

在指针定义时,可以靠在变量旁边,也可以靠在类型关键字旁边,或者是置中,例如:

int *p1;
int* p2;
int * p3;

这三个定义方式都是可允许的,C++ 开发者倾向用第一个,因为可以避免以下的错误:

int* p1, p2;

这样的定义方式,初学者可能以为p2也是指针,但事实上并不是,以下的定义p1p2才都是指针:

int *p1, *p2;

有时只希望存储地址而不关心类型,可以使用void*来定义指针,例如:

void* p;

由于void*类型的指针没有任何类型信息,只用来持有地址,不可以使用*运算符对void*类型指针提取值,编译器也不会允许将void*指针直接指定给具有类型信息的指针,必须使用reinterpret_cast明确告知编译器,这个动作是你允许的,例如:

#include <iostream> 
using namespace std; 

int main() { 
    int n = 10; 
    void *p = &n ; 

    int *iptr = reinterpret_cast<int*>(p);
    cout << *iptr << endl; // 显示 10

    return 0; 
}

reinterpret_cast用于指针,它告诉编译器,你就是要以指定类型重新解释p地址处的数据。

const定义的变量指定值后,就不能再改变变量值,也无法对该变量取址:

const int n = 10;
int *p = &n; // error,  invalid conversion from `const int*' to `int*'

const定义的变量,必须使用对应的const类型指针才可以:

const int n = 10;
const int *p = &n;

同样地,也就不能如下试图改变地址处的数据:

*p = 20; // error: assignment of read-only location '* p'

const定义的变量指定值后,就不能再改变变量值,也无法对该变量取址,编译会不通过,不过必要时,可以用const_cast叫编译器住嘴:

const int n = 10;
int *p = const_cast<int*>(&n);

在〈算术运算、类型转换〉最后也提到过const_cast,这只是叫编译器住嘴罢了,后续代码也是别对 pi 地址处的数据做变动,以避免执行时期不可预期的结果。

要留意的是,const int *p定义的p并不是常数,可以存储不同的地址。例如:

#include <iostream> 
using namespace std; 

int main() { 
    const int n = 10;
    const int m = 20;

    const int *p = &n;
    cout << p << endl;

    p = &m;
    cout << p << endl;

    return 0; 
}

执行结果:

0x61feb8
0x61feb4

如果想令指针存储的值无法变动,必须创建指针常数,先来看看来源变量没有const的情况:

int n = 10;
int m = 20;

int* const p = &n;
p = &m;  //  error: assignment of read-only variable 'p'

如果nmconst修饰,那么就必须如下创建指针常数:

const int n = 10;
const int m = 20;

const int* const p = &n;
p = &m; // error: assignment of read-only variable 'p'




展开阅读全文