首页 > 编程笔记 > C++笔记 阅读:54

C++指针用法详解(附带实例)

要想弄明白什么是指针,就必须弄清楚数据在内存中是如何被存储的,又是如何被读取的。

通常来说,系统会按字节对每个内存单元进行编号,这些内存单元就好比是许多带有编号的小房间,要想使用内存,就需要知道房间编号。假设我们定义了一个整型变量 i,编译器会为其分配 4 个字节——编号为 4001~4004 的存储单元,如下图所示:


图 1 整型变量 i

接下来我们又定义了一个整型变量 j,则 j 在内存中的起始地址是 4005,同样占 4 个字节,即编号 4005~4008 的内存单元中保存的是 j,如下图所示。


图 2 整型变量 i 和 j

可见,编译系统会为每个变量都分配了一个能满足其类型大小的内存单元地址,访问该地址就能找到对应变量。汇编语言中直接通过地址访问内存单元,C、C++、Java 等高级语言中通过变量名访问对应内存单元,得到其中保存的变量值。这是因为代码经过编译后,会将变量名转换为该变量在内存中的存放地址。

例如,语句“i+j;”的计算过程如下:根据变量名与地址的对应关系,找到变量 i 的地址 4001,从 4001 开始读取 4 个字节数据放到 CPU 寄存器中,再找到变量 j 的地址 4005,从 4005 开始读取 4 个字节的数据放到 CPU 另一个寄存器中,最后通过 CPU 的加法中断计算得出结果。

既然通过地址可以访问指定的内存单元,因此可以说该地址“指向”该内存单元。为了好记忆,我们将变量的地址形象化地称为该变量的“指针”,意思是通过它能找到对应的内存单元。

C++指针变量

一个变量的地址称为该变量的指针。如果有一个变量,专门用来存放另一个变量的地址,那就是指针变量。

C++ 中有专门用来存放内存单元地址的变量类型,就是指针类型。

指针变量可以像普通变量一样声明、赋值和引用。

1) 指针的声明

声明指针变量的一般形式如下:
数据类型 *指针变量名
其中,“*”表示该变量是一个指针变量,“数据类型”表示该指针指向的变量的数据类型。例如:
int *p_iPoint;  // 声明一个整型指针
float *a, *b  // 声明两个浮点型指针

2) 指针的赋值

与普通变量赋值不同,给指针变量赋值只能赋值地址,不能是其他数据,否则将引起错误。

C++ 中一般用“&变量名”表示某个变量的地址,如 &a 表示变量 a 的地址,&b 表示变量 b 的地址。其中,“&”称为取地址符。因此,给一个指针变量赋值可以有以下两种方法。

① 声明指针时进行初始化赋值。例如:
int i = 100;
int *p = &i;  // 定义一个指针变量 p,指向 i 的地址

② 先声明指针,再进行赋值。例如:
int i = 100;
int *p;
p = &i;  // 将 i 的地址赋给指针 p
变量的实际地址编写代码时是无法知晓的,当程序运行时系统会为变量分配内存空间,此时才能获取变量地址。

注意观察这两种赋值语句的区别。如果先声明指针变量之后再赋值,则赋值时指针变量前不用再加“*”。

没有初始化的指针变量俗称“野指针”。野指针并非不能使用,但是有一定错误危害(可能指向不合法的内存空间),良好的编程习惯是在定义指针变量时将其初始化为 NULL,使其暂时指向空值。这样的指针称为指向空的指针。

C++ 指针变量的引用

引用指针变量的形式为:
*指针变量
含义是引用指针变量所指向地址内的值。

通过变量名访问一个变量是直接的,而通过指针访问一个变量是间接的。

例如:
#include <iostream>
using namespace std;
int main()
{
    int a = 100;
    int *p = &a;  // 定义一个指针变量 p 并初始化
    // 以十进制形式输出 a 的地址
    printf("%d\n", *p);
}
执行结果为:

100


关于指针的说明:
1) 指针变量名是 p,而不是 *p。*p = &i 的含义是取变量 i 的地址,赋给指针变量 p。例如,输出变量的地址。实现代码如下:
#include <iostream>
using namespace std;
int main()
{
    int a = 100;
    int *p = &a;  // 定义一个指针变量 p 并初始化
    // 以十进制形式输出 a 的地址
    printf("%d\n", p);
}
程序运行结果为:

7339540

本例通过 printf() 函数直接将地址输出。变量是由系统分配空间,因此变量的地址不是固定不变的。

2) 指针变量不能直接进行赋值。例如,下面的代码无法编译通过:
int a = 100;
int *p;
p = 100;  // 编译错误,指针变量不能直接赋值
上述代码编译时,会提示 error C2440: '=': cannot convert from 'const int' to 'int *' 错误。

如果强行赋值,使用指针运算符**提取指针所指变量时会出错,例如:
int a = 100;
int *p;
p = (int*)100;  // 通过强制转换将 100 赋值给指针变量
printf("%d", p);  // 输出地址
printf("%d", *p);  // 输出指针指向的值,出错语句

3) 不能将 *p 当做变量使用。例如:
int a = 100;
int *p;
*p = 100;  // 指针没有获得地址
printf("%d", p);  // 输出地址,出错语句
printf("%d", *p);  // 输出指针指向的值,出错语句
上述代码可以编译通过,但运行时会出错。

例如,使用指针比较两个数的大小,实现代码如下:
#include <iostream>
using namespace std;
int main()
{
    int *p1, *p2;
    int *p;
    int a, b;
    cout << "input a:" << endl;
    cin >> a;
    cout << "input b:" << endl;
    cin >> b;
    p1 = &a; p2 = &b;  // p1 指向 a 的地址,p2 指向 b 的地址
    // if 语句,如果 a>b,交换 p1 和 p2 所指向的地址
    if(a > b)
    {
        p = p1;
        p1 = p2;
        p2 = p;
    }
    cout << "a=" << a;
    cout << ",";
    cout << "b=" << b;
    cout << endl;
    cout << "较大的数:" << *p1 << "较小的数:" << *p2 << endl;
}
执行结果为:

input a:
33
input b:
15
a=33,b=15
较大的数:15较小的数:33

C++指针运算符和取地址运算符

初学者非常容易混淆 * 和 &,下面来细致解释一下。

1) *和&的区别

& 是取地址运算符,作用是获取某个变量的内存单元地址。

例如,变量 i 的值为 100,存储在编号为 4009 的内存单元中,通过代码 p = &i,指针变量 p 可得到 i 的内存地址 4009,如图 3 所示。


图 3 取地址

* 是指针运算符,作用是获取存在某个地址内的变量值。例如,指针变量 p 存储的是内存地址 4009,通过 *p 可得到编号 4009 的内存单元中存储的变量值 100,如下图所示。


图 4 通过地址取值

例如,定义整型变量 a 并赋值,再定义一个指针变量 p,并将 a 的值赋给指针变量,最后用 cout 输出整型变量 a 的值和指针变量 p 的值。具体代码如下:
#include <iostream>
using namespace std;
int main()
{
    int a=100;
    int *p=&a;
    cout<<"a="<<a<<endl;
    cout<<"p="<<p<<endl;
}

2) “&*”和“*&”的区别

“&”运算符和“*”运算符的优先级别相同,按自右而左的方向结合:
注意,&*p 中的 p 只能是指针变量,如果将“*”放在普通变量名前,编译时会出现逻辑错误。例如,下面的代码编译时,会提示 error C2100: illegal indirection 错误。
#include <iostream>
using namespace std;
int main()
{
    int a=100;
    int *p;
    printf("%d",&*a);    //非法指向错误。a不是指针变量
}

C++指针的自增、自减运算

指针变量存储的是地址,因此对指针做运算就等于对地址做运算。

例如,定义指针变量和整型变量,并将整型变量的地址赋给指针变量,再进行指针自增、自减运算,输出结果,比较增减前后的数值。代码如下:
#include <iostream>
using namespace std;
int main()
{
    int a=100;
    int *p=&a;            //定义整型指针并赋初值
    printf("address:%d\n",p);
    p++;                  //指针自增运算
    printf("address:%d\n",p);
    p--;                  //指针自减运算
    printf("address:%d\n",p);
    p--;                  //指针再一次自减运算
    printf("address:%d\n",p);
}
运行结果为:

address:7339540
address:7339544
address:7339540
address:7339536

p 指向变量 a 的地址,所以程序首先输出的是 a 的地址 7339540,执行一次自增运算(p++)和两次自减运算(p--)后,输出的地址分别是 7339544、7339540、7339536。可见,指针进行一次自增或自减运算,其地址不是增加或减少 1 个字节,而是增加或减少 4 个字节,这是因为指针 p 的数据类型为整型,而整型的字节宽度为 4。

总结一下,指针指向的是某种数据类型的地址,由于不同数据类型有着不一样的字节宽度,所以指针自增或自减一次,会增加或减少不同的字节数,而不是单纯的加 1 或减 1。通过 sizeof 运算符可快速得知不同数据类型的字节宽度。

下面将指针类型改为 double,看一下指针自增、自减运算的结果。程序代码如下:
#include <iostream>
using namespace std;
int main()
{
    double a=100;
    double *p=&a;        //定义 double 型指针并赋初值
    printf("address:%d\n",p);
    p++;                 //指针自增运算
    printf("address:%d\n",p);
    p--;                 //指针自减运算
    printf("address:%d\n",p);
}
运行结果为:

address:7339536
address:7339544
address:7339536

可见,指针进行一次自增或自减运算,会增加或减少 8 个字节,也就是 double 类型的字节宽度。

C++指向空的指针与空类型指针

前面介绍过,为避免野指针的危害,可以将指针先指向空值(NULL)。例如:
int *p=NULL;            //定义一个指向空的指针

除此之外,指针本身还可以是任意数据类型,包括空类型(void)。例如:
void *p;                //定义一个空类型指针
这里,空类型(void)的含义是指针变量指向的地址内可以存放任意的数据类型,具体要存放哪种数据类型,可以通过后续赋值确定。赋值后,还需要将其强制转化为对应的数据类型,才能得到正确的结果。

举个例子,定义一个整型指针并赋空值 NULL,再定义一个空类型指针,然后尝试将不同类型的变量赋给它,并在输出时进行强制类型转换。程序代码如下:
#include <iostream>
using namespace std;
int main()
{
    int *pl = NULL;
    int i = 4;
    pl = &i;
    float f = 3.333f;
    bool b = true;
    void *pV = NULL;
    cout<<"依次赋值给空指针"<<endl;
    pV = pl;
    cout<<"pV = pl -------"<<*(int*)pV<<endl;    //将 pV 强制转化为 int 型指针并输出存放的值
    cout<<"pV = pl -------"<<*(float*)pV<<endl;  //将 pV 强制转化为 float 型指针并输出存放的值
    pV = &f;
    cout<<"pV = &f -------"<<*(float*)pV<<endl;  //将浮点数 f 的地址赋给 pV
    cout<<"pV = &f -------"<<*(int*)pV<<endl;   //将 pV 强制转化为 int 型指针并输出存放的值
    return 0;
}
执行结果为:

依次赋值给空指针
pV = pl -------4
pV = pl -------5.60519e-045
pV = &f -------3.333
pV = &f -------1079332831

本例中,NULL 表示空值,被赋空的指针无法被使用,直到被赋予其他的值。

另外,空类型指针被赋值后,还需要转化为对应类型的指针才能得到期望结果;若转换为其他类型的指针,结果将不可预知。

相关文章