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

C++指针变量的定义和使用(非常详细)

C++ 指针作为一种表示内存地址的特殊变量,其定义形式也有一定的特殊性:

数据类型* 指针变量名;

其中,数据类型指的是指针所表示的地址上存储的数据的类型。例如,如果我们要定义一个指针来表示某个 int 类型数据的地址,那么指针定义中的数据类型就是 int。这个数据类型由指针所指向的数据决定,可以是 int、string 和 double 等基本数据类型,也可以是自定义的结构体等复杂数据类型。简而言之,指针所指向的数据是什么类型,就用这种类型作为指针变量定义时的数据类型。

数据类型之后的*符号表示定义的是一个指针变量,而指针变量名则是给这个指针指定的名字。

请看下面的例子:
// 定义指针变量p,它可以记录某个int类型数据的地址
int* p;
// 定义指针变量pEmp,它可以记录某个Employee类型数据的地址
Employee* pEmp

选择合适的定义指针变量的方式

实际上,下面两种定义指针变量的形式都是合乎 C++ 语法的:
int* p;
int *p;
这两种形式都可以通过编译,并表示相同的语法含义。然而,这两种形式所反映的编程风格和所强调的意义稍有不同。

int* p强调的是“p 为一个指向 int 类型整数的指针”,这里可以把int*视为一种特殊的数据类型,而整个语句强调的是 p 是这种数据类型(int*)的一个变量。

int *p是把*p当作一个整体,强调的是“这个指针指向的是一个 int 类型的整数”,而 p 就是指向这个整数的指针。

这两种形式没有对错之分,可根据个人编程风格来选择。笔者推荐第一种形式,因为它把指针视为一种数据类型,使得定义指针变量的语句更加清晰明了,可读性更强。

需要注意的是,当我们用第一种形式在一条语句中定义多个指针变量时,可能会产生混淆,例如:
// p是一个int类型的指针变量,而q实际上是一个int类型的变量
// 可能会让人误认为p和q都是int类型的指针
int* p, q;
// 以下形式更清楚一些:*p是一个整数,p是指向这个整数的指针,q也是一个整数
int *p, q;
// 定义两个指向int类型数据的指针p和q
int *p, *q;
在开发实践中,有一条编码规范:一条语句只完成一件事情。按照这条规范,我们可以通过分开定义 p 和 q 来很好地避免上述问题。

如果确实需要定义多个相同类型的指针变量,也可以使用 typedef 关键字将指针类型定义为新的数据类型,然后用这个新的数据类型来定义多个指针变量:
// 将Employee指针类型定义成新的数据类型EMPointer
typedef Employee* EMPointer;
// 用EMPointer类型定义多个指针变量,这些变量都是Employee*类型
EMPointer pCAO,pCBO,pCCO,pCDO;

指针的赋值和使用

在定义一个指针变量之后,指针变量的初始值通常是一个随机值。它可能所指向某个无关紧要的数据,也可能指向重要的数据或者程序代码。如果直接使用这些未初始化的指针,后果可能是不可预期的。虽然有时可能啥事儿都没有,但也可能因此引发严重的问题。因此,在使用指针之前,务必对其赋值以进行初始化,将其指向某个有意义且合法内存位置。

对指针变量进行赋值的语法格式如下:

指针变量 = 内存地址;

可以看到,对指针变量的赋值实际上是将这个指针指向某一内存地址,而该内存地址上存放的就是指针想要指向的数据。

数据通常是用变量来表示的,因此获得变量的内存地址相当于获得数据所在的内存地址,从而可以用这个地址给指针变量赋值。在 C++ 中,我们可以利用&取地址运算符,将该运算符放在某个变量的前面,以获得该变量的内存地址。例如:
//定义一个整型变量,用以表示整数1003
int N = 1003;
//定义整型指针变量pN,用&符号取得整型变量N的地址
//并将其赋值给整型指针变量
int* pN = &N;
这里,我们用&符号取得整型变量 N 的内存地址,即存储 1003 这个整数所在的内存地址,然后将这个地址赋值给整型指针变量 pN,也就是将指针 pN 指向存储 1003 这个数据的内存地址,如下图所示。

C++指针和指针所指向的数据
图:指针和指针所指向的数据

指针的初始化赋值最好是在定义指针时同时进行。例如,在上面的例子中,我们可以在定义指针 pN 的同时获取变量 N 的内存地址并赋值给该指针,从而使得指针在一开始就有一个合理的初始值,避免未初始化的指针被错误地使用。

如果在定义指针时确实没有合理的初始值,我们可以将其赋值为 nullptr(C++ 11标准中引入的关键字),表示这个指针没有指向任何内存地址,是一个空指针(null pointer),此时指针还不能使用。例如:
//定义一个指针变量 pN,赋值为 nullptr,表示它没有指向任何内存位置
//这里只是定义变量,后面才会对其进行赋值
int* pN = nullptr;

//...

//判断pN是否指向某个数据
//如果pN的值不是以 nullptr 作为初始值,就表示它被重新赋值指向某个数据
if(nullptr != pN)
{
    //使用pN指针访问它所指向的数据
    cout<<"指针pN所指向的数据是:"<<*pN<<endl;
}

我们可以用&运算符获得一个变量(数据)的内存地址,反过来,也可以用*运算符表示一个内存地址上的变量(数据)。

*被称为指针运算符,或称为解析运算符。它所执行的是与&运算符完全相反的操作。如果把*放在一个指针变量的前面,就可以取得这个指针所指向的内存地址中的数据。例如:
//输出pN指向的内存地址0x0016FA38
cout<<pN<<endl;
//通过*运算符获取pN所指向的内存地址中的数据1003并输出
//等同于 cout<<N<<endl;
cout<<*pN<<endl;
//通过指针修改它所指向地址中存储的数据
//等同于 N=1982;
*pN = 1982;
//输出修改后的数据1982
cout<<*pN<<endl;
通过*运算符可以取得 pN 这个指针所指向的数据变量 N,虽然 N 和 *pN 的形式不同,但是它们都代表内存中的同一份数据,都可以对这个数据进行读写操作,并且是等效的。

特别地,如果一个指针指向的是一个结构体类型的变量,与结构体变量使用.符号引用成员变量不同的是,如果是指向结构体的指针,则应该使用->符号引用成员变量。这个符号很像一个指针。例如:
//定义一个结构体变量
Employee Zengmei;
//定义一个指针变量,用 & 运算符取得变量Zengmei的地址,并对赋值给结构指针变量
//也就是将指针pZengmei指向这个结构体变量Zengmei
Employee* pZengmei = &Zengmei;
//用->运算符引用这个结构体指针变量的成员变量
pZengmei->m_strName = "Zengmei";
pZengmei->m_nAge = 28;

尽量避免把两个指针指向同一个变量

当指针变量被正确赋值指向某个变量后,它就会成为一个有效的内存地址,也可以用它对另一个指针赋值。这样,两个指针指向相同的内存地址,指向同一份数据。例如:
// 定义一个整型变量
int a = 1982;
// 得到变量a的内存地址并赋值给指针pa
int* pa = &a;
// 使用pa对另一个指针pb赋值
int* pb = pa;
在这里,我们用已经指向变量 a 的指针 pa 对指针 pb 赋值,这样 pa 和 pb 的值相同,都是变量 a 的地址,也就是说,两个指针指向了同一个变量。

需要特别说明的是,虽然两个指针指向同一个变量在语法上是合法的,但在实际开发中应当尽量避免。因为稍有不慎,这种代码就会给人带来困扰。继续上面的例子:
// 输出pa指向的数据,当前为1982
cout<<*pa<<endl;
// 通过pb修改它所指向的数据,修改为1003
*pb = 1003;
// 再次输出pa指向的数据,已经变为1003
cout<<*pa<<endl;
如果我们只看这段程序的输出,一定会感到奇怪:为什么没有通过 pa 进行任何修改,前后两次输出的内容却不同?

结合前面的代码就会明白,pa 和 pb 指向的是同一个变量 a。当我们通过指针 pb 修改变量 a 后,再通过 pa 来获得变量 a 的数据,自然就是更新过后的数据了。表面上看,似乎没有通过 pa 对变量 a 进行修改,但实际上,pb 早已暗渡陈仓,将变量 a 的数据修改了。

在程序中,最忌讳这种“偷偷摸摸”的行为,因为一旦这种行为导致程序运行错误,将很难被发现。因此,应尽量避免两个指针指向同一个变量,就如同一个人最好不要取两个名字一样。

相关文章