探秘C++系列——指针(基础篇)


关键词:

  1. 指针的定义语法
  2. 解引用
  3. 改变指针的指向 or 改变指针所指地址存储的内容? 如何区分?
  4. 定义空指针
  5. void*指针的注意事项
  6. &*和*&的区别 , 自右向左阅读法
  7. 指针的自增自减, *p++ ,(*p)++ , *++p ,++*p的区别

首先我们知道变量(对象)要想存放在计算机中,得为它开辟一块内存空间,然后将数据存储在里面。那之后计算机要如何找到这块内存空间呢?

其实程序加载到内存中后,在程序中使用的变量、常量、函数等数据,都有自己唯一的一个编号,这个编号就是这个数据的地址。相当于门牌号,方便他人寻找。因此知道了一个变量存储的地址,就能知道这段地址里存放的值。我们可以把变量的内存地址称为这个变量的指针。这个其实很形象,有个东西可以指向一个变量的位置,那我就能通过这个“指路人”去访问该变量的内容了。在C++中,有一种变量可以专门存储其他变量的地址,这就是指针变量。

定义指针

通过将声明符写成 *d 的形式来定义指针类型,其中 d 是变量名称,然后在它前面指明数据类型。如果在一条语句中定义了多个指针变量,则每个变量前都必须有符号 *。大部分情况下,指针的类型与它指向的对象要严格匹配。下面是定义指针的例子:

 int *p; //定义一个int类型的指针,名字为p
 int *p1, *p2, p3; //这里p1,p2是指针,p3是int类型的变量

与引用类似,指针也是一种复合类型,也实现了对其他对象的间接访问。
但是指针与引用相比又有很多不同点
1)指针本身就是一个对象,有自己的内存空间,允许对指针赋值和拷贝,而且在生命周期内它可以先后指向不同的对象,而引用只能访问它最初绑定的那个对象。
2)指针不是必须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。而引用在定义时初始化。
(在我的另一篇博文探秘c++系列——引用 中也有提及指针与引用的区别,想掌握更多引用知识点的小伙伴可以参考这篇博文。)

int a=1,b=2;
int &r;  //错误写法!必须初始化
int &r=a; //定义引用的正确写法
int *p; //正确写法,定义时可以不初始化,后面再赋值
p=&a; //将a的地址赋给指针p
r=b;//只是将a的值修改为b,引用始终与a绑定
p=&b; //p不再指向a,而是指向了b

上面的例子涉及到了指针变量的赋值。注意,指针只能存放地址要想获取变量的地址,就要使用取地址符& 。这个地方&作为一种运算符,是表达式的一部分,此时不是引用的意思。

另外,由于引用不是对象,没有实际的地址,所以不能定义指向引用的指针
但是有的同学可能会写出这种代码:

int i=1;
int &r=i;
int *p=&r;

这是可以编译通过的。但刚刚不是说了不能定义指向引用的指针吗?
实际上,引用r和int i是绑定在一起的,它俩是一个东西。所以&r就等同于对i取地址,p指向的就是i。

解引用

之前强调了指针存放的是地址,那么输出指针的名字,就必然输出的是存放的地址名。可是如果我想取到这段地址里存的数据要怎么办?

如果指针指向一个对象,可以使用解引用(dereference)符 * 来访问该对象
给解引用的结果赋值,就是给指针所指向的对象赋值。解引用操作仅适用于那些确实指向了某个对象的有效指针。

int a=1;
int *p=&a;
cout<<p<<endl; // 我电脑里输出的是000000C0C15AF5F4
cout<<*p<<endl;  //输出1
*p=0;  //p解引用后就是a,所以这里相当于a=0
cout<<*p<<endl; //输出0
cout<< a<<endl; //输出0

注意,声明指针和解引用时都有借助 “ * ” 这个符号,在不同的场景下,它有不同的含义。
1)当*紧随类型出现 ,也就是作为声明的一部分,比如int *p; 这时候 * 是指针的意思。
2)当 * 作为表达式的一部分(表达式即为一个或多个运算对象的组合,对象间可用运算符连接)如 *p=0; 这里 * 作为一元运算符,是解引用的运算符号。
c++中的 & 也类似,有时候是引用,有时候是取地址符,判别方式和 * 差不多。可见 探秘c++系列——引用 这篇博文中“引用和取地址的区别”部分。
可见引用和指针真的是对好兄弟呀,学习时建议二者一起学习,要关注二者的区别,这也是面试中常考的点。

那么到目前为止,我们学了如何修改指针指向的对象,如何修改指针指的那个对象的内容。这两个操作往往很容易混淆。
最好的办法就是记住赋值永远改变的是等号左侧的对象。
举个例子:

int a = 1,b = 0;
int* p = &a;
cout << p << endl;
p = &b; //等号左边是p(指针),这时后把b的地址赋给了p指针,指针的指向改变
cout << p << endl;
cout << *p << endl;
*p = a; //等号左边是*p,解引用的结果就是p指向的对象b,此时是把a的值赋给b,指针仍然指向b
cout << p << endl;
cout << *p << endl;

输出:
在这里插入图片描述

空指针

空指针(null pointer) 不指向任何对象,在试图使用一个指针前代码可以先检查它是否为空。这是一种编码的好习惯。得到空指针最直接的办法是用字面值 nullptr 来初始化指针。
旧版本程序通常使用 NULL(预处理变量,定义于头文件 cstdlib 中,值为0)给指针赋值,但在C++11中,最好使用 nullptr 初始化空指针。
下面列出几个生成空指针的例子:

int *p1 = nullptr;  // 等价于 int *p1 = 0;
int *p2 = 0;        // 直接将p2初始化为字面常量0
// 需要首先 #include <cstdlib>
int *p3 = NULL;     // 等价于 int *p3 = 0;

可以看到,我们也可以直接把一个字面值常量0赋给指针表示一个空指针,这是一个特例,但是需要注意的是,下面这种赋值是错误的

int a=0;
int *p4=a; //错误,不能把int变量赋给指针,即使变量的值为0

只有把字面值常量0赋给指针才能表示空指针!!!

void*指针

void* 是一种特殊的指针类型,可以存放任意对象的地址。

double obj = 3.14, *pd = &obj;
int a = 1;
// 正确:void*能存放任意类型对象的地址
void *pv = &obj; // obj可以是任意类型的对象
pv = pd; // pv可以存放任意类型的指针
pv = &a;  //把a的地址赋给pv

但不能直接操作 void* 指针所指的对象。因为我们并不知道 这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作
void*指针的操作比其他指针要少,只能和另一个指针比较,向函数传递或者被函数返回,给同类型的指针赋值
但是,不能用void指针操作它所指向的对象,不能对void指针进行解引用,不能对它进行算数操作
void*指针只有经过强制转换后才能操作
来看具体的例子(接着上面的程序):


cout<<*pv<<endl; //报错,不会输出3.14,因为不能自动把void*指针转成double* 
cout<<*((double*)pv)<<endl; //正确,先把void*转成double* ,再解引用
double *pd1 = pv;               // 错误,不可以直接赋值
double *pd2 = (double*)pv;      // 正确,必须进行显示类型转换
cout << *pd2 << endl;           // 3.14
cout << (pv == pd2) << endl;    // 1  ,可以将void*指针和其他指针比较地址值是否相同
void *p2=pv;  //正确,void*指针可以给同为void*类型的指针赋值

&* 和*&的区别

这俩哥们儿的区别估计是很多人学习指针时遇到的噩梦。但只要掌握了技巧,也能很轻松地理解这个问题。
&和*运算符优先级相同,按自右向左的方向结合
那么我们就从右向左阅读就行啦!!!
来,上例子:

int a = 1 , *p=&a;
cout << &*p << endl;
cout << *&a << endl;
cout << &a << endl;
cout << p << endl;

我们先来看&*p,从右向左看 , *p对p解引用,得到的就是a,再与取地址符&结合,最终应该输出a的地址。
而 *&a 同理, 先取地址再解引用,兜兜转转回到原地,得到的还是a本身。
不信?我们来看输出结果验证一下:
在这里插入图片描述
上面这个例子, *和&作为表达式的一部分,分别是解引用和取地址的意思。我们知道,如果作为声明的一部分,它们就成了指针和引用了。之前说过没有指向引用的指针,因为引用不是对象,没有自己的内存空间,只能说指针可以指向引用绑定的那个对象。
但是反过来,可以有指向指针的引用对不对?来看例子:

int i = 42;
int *p;         // p是一个int型指针
int *&r = p;    // r是一个对指针p的引用
r = &i;         // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0;         // 解引用r得到i,也就是p指向的对象,将i的值改为0

还是从右往左阅读的法则:
离变量名最近的符号(此例中是 &r 的符号 &)对变量的类型有最直接的影响,因此 r 是一个引用。声明符的其余部分用以确定 r 引用的类型是什么,此例中的符号 * 说明 r 引用的是一个指针。最后,声明的基本数据类型部分指出 r 引用的是一个 int 指针。

指针的自增自减

指针的自加自减是指向下一个/前一个存放数据的地址,也就是按照它所指向的数据类型的直接长度进行增或减。
比如说int指针的自增,因为int在64位计算机中占 4byte(32 bit,可以把每个bit想象成一个个格子),所以自增后就是地址数值上向后移动32bit(32个格子),也就是移动到下一个紧挨的数据存放的位置。
指针的自增自减在数组中使用的比较多。我们知道数组的名字代表数组首元素的地址,假设数组名叫arr,那么arr就是一个指向数组首元素的指针,arr++就是将指针移到数组的第二个元素。

常见问题:
*p++ ,(*p)++ , *++p ,++*p的区别
假设有个数组{1,1,1,1}
1、*p++ 先取指针p指向的值(数组第一个元素1),再将指针p自增1;
【* 和++ (单目运算)两个处于同一优先级,结合方向是自右向左,但是前提是当++在变量前面的时候才处理同一优先级,当++在变量之后时,可以将++的优先级看成最低级的】

2、(*p)++ 先取指针p指向的值(数组第一个元素1),再将该值自增1(数组第一个元素变为2)

3、*++p 先将指针p自增1(此时指向数组第二个元素), 操作再取出该值

4、++*p 先取指针p指向的值(数组第一个元素1),再将该值自增1(数组第一个元素变为2)

依然是从右往左读的原则,1是个特例。

猜你喜欢

转载自blog.csdn.net/qq_46044366/article/details/119759211
今日推荐