C\C++指针详解(上篇)
之前学习Java语言中并没有指针概念,虽然Java的底层也是由C语言实现的,但是将指针进行封装,程序员并没有办法使用。后来在学习C\C++的过程中,发现指针却是必不可少的类型,因此将自己过去总结笔记整理出来,自己可以复习,也可以方便大家学习交流,指针部分的知识如果详细叙述实在是过长,本文先介绍指针的一些基本内容,后面会在继续追加一些数组指针、二级指针、指针运算等方面的知识。如果文中有错误或不足之处还请大家在评论区指出,共同进步。
一、什么是指针
指针(pointer)是指向(piont to)另一种类型的复合类型(复合类型是指基于其他类型定义的类型),这也就表明了指针的特性与作用:指针指向一个对象,如果没有这个对象,凭空创造出来一个指针其实也没有什么意义。与引用操作类似,指针也实现了对其他对象的间接访问。指针自己本身也是一个对象,它的内部存储着计算机存储器中另一块区域的地址,因此我们可以通过指针,读取它内部存储的地址,来找到另一块区域里面存放的数据。
二、定义指针及获取对象地址
定义指针的方法是:指针类型 *变量名,如int *ip1;
int表示定义的这个指针指向的数据类型,*表示这是个指针,ip1则是这个指针的名字,如果要在一条语句中同时定义多个指针,则每个指针前面都要加上*,比如int *ip1, *ip2;
获取对象的地址,将其存放在指针中,就要用到取地址操作符 ‘&’,比如
int i = 10;
int *ip = &i;//取得int类型的对象i的地址,将其放入指向int类型的指针ip中
这里我们就可以说ip里存放着i的地址,或者说ip是指向i的指针。
注意!!!指针的类型与它要指向的数据类型务必要一致!
int *ip;//定义一个指向int类型的指针ip
double d = 3.14;
ip = &d;//错误,不能将double类型的对象地址赋给int类型的指针
千万不要忽略了指针类型,这在后面可是有大用处的。
三、利用指针访问、修改对象
如果指针已经指向了一个对象,那我们可以通过使用(解引用操作符 ‘*’)来访问该对象,先来看例子:
int i = 10;
int *p = &i;//定义一个int类型指针p,并用i的地址初始化它
cout << *p << endl;//这里输出的结果就是p所指向的对象(即i)的数据,所以会输出10
当时笔者在初次接触到解引用操作符的时候也是有些混淆,’*'不是应该代表这是个指针吗,为什么*p反而输出了它指向的对象呢?
我们可以这样理解,在定义一个指针的时候,需要首先指出这个指针所指向对象的类型,即int *p;
这里的’*'代表p是一个指针,前面还有它所对应的数据类型,即int
;在进行解引用操作*p
的时候,'*'前面是没有数据类型的
,只有后面的p,而p我们前面也已经定义过了,它相当于是指针的名字,所以*p代表着对p这个指针进行解引用操作,读取p这个指针中存放的地址所指向的那个对象的数据。
那在内存中到底是如何通过对指针进行解引用操作而访问它指向的数据呢,这里我们需要先了解一些相关的知识,下面是三种最常见的基本数据类型在C++中所占据的内存大小。
类型 | 大小 | 备注 |
---|---|---|
char | 1个字节 | 一个char类型的对象在内存中占1个字节的内存空间 |
int | 4个字节 | 一个int类型的对象在内存中占据4个字节的内存空间 |
double | 8个字节 | 一个double类型的对象在内存中占据8个字节的内存空间 |
我们需要明确的一点是在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址是所使用字节中最小的地址。大家可以这样理解这句话,指针作为一个对象,不管什么类型的指针,总归都是指针,那在同一个操作系统下,它的大小是固定的(32位机器指针大小为4字节/64位机器则为8字节),char类型自己只占用一个字节,那指针只需要存储这一个字节的地址就够了,但是double类型占据8个字节,double类型的指针也只能存放一个字节的地址编号,所以就只能存储这个double类型对象占据的8个字节中**最小的字节(或者说是最开始的那个字节)**的地址编号。
举出三个常见对象在内存中存储并定义指针指向它们的简单例子如下:
char c = 'a';
char *pc = &c;//pc指针中存放着c的地址
int i = 10;
int *pi = &i;//pi中存放着i的地址
double d = 3.14;
double *pd = &d;//pd中存放着d的地址
在内存中的简图如下:
那既然都是指针,内部只存放一个字节的地址,char类型还好说,只占一个字节,刚好就是它本身的地址,但int、double这种多字节的对象,在解引用操作时该怎么访问呢?
其实在上一章节结束时就已经埋下了伏笔:一定要注意指针类型!
指针类型决定了指针在解引用操作时,一次访问几个字节(即一次访问内存的大小)。在编译时,编译器发现,要对一个指针进行解引用操作,而且这个指针还是一个int类型,那么编译器就会自动以该指针中存储的指针为起始,一次性访问接下来一共4个字节内存空间的内容;同理如果编译器发现这是个double类型的指针,就会以这个指针存放的地址为起始,一次性访问内存中接下来的8个字节的内存空间,从而完成对该对象的访问。
在修改的时候也要注意,弄明白到底是在修改指针的值,还是修改指针所指向的对象的值
int i1 = 5;
int i2 = 10;
int *pi1 = &i1;//此时pi1被初始化,指向了i1
int *pi2 = &i2;//pi2被初始化,指向i2
pi1 = pi2;//对pi1赋值,将pi2的值赋给pi1,此时pi1和pi2同时指向i2,这是对指针进行修改,即改变了指针pi1指向哪个对象
*pi1 = 20;//此时修改的是pi1指向的对象,即i2的数值,等价于(i2 = 20)
想要搞清楚一条赋值语句到底是在修改指针的值,还是要修改指针所指向对象的值,只要看赋值语句左边的对象,pi1就是个指针,所以当pi1在赋值号左端时,修改的是pi1内部存放的值,它内部存放的是所指向对象的地址,因此修改它意味着,改变了pi1去指向哪个对象。
而赋值号左端是*pi1代表的是要先进行指针解引用操作,*pi1解引用操作后得到的是pi1指向的对象,即整数10(因为上面的操作已经让pi1和pi2都指向i2了),这时再进行修改,就是在对整数10进行修改了,所以*pi1 = 20是将20存储在pi1指向的那块内存空间,也就是通过指针间接对i2进行了修改。
四、指针需要注意的问题
指针本身就是一个对象,允许程序员对指针进行赋值和拷贝操作。
指针在其生命周期中可以先后指向几个不同的对象,并非像引用操作只能绑定给一个对象。
int a = 1; int b = 2; int c = 3; int *p = &a;//p指向a p = &b;//让p指向b p = &c;//再让p指向c
指针无需在定义时赋初始值,它会被一个随机值初始化,但是我们并不鼓励这样做,因为使用未经初始化的指针是引发运行时错误(runtime error)的一大原因。因为如果我们访问这个指针内部存放的随机值,相当于我们去访问一个本不该在这个位置存在的对象,如果这里恰好有个对象,我们又对它进行访问、修改等操作,就会造成非法访问的严重问题。
int main(){ int *p;//p指针并未初始化,里面存放的是随机值 *p = 20;//错误!!!通过p里面的随机值当作地址找到了一块空间,这块空间并不属于当前程序,再对它进行解引用并修改,造成了严重的非法访问 return 0; }
基于3.的原因,建议大家初始化所有指针,并且应该尽量在定义了对象之后,再定义指向它的指针,如果实在是不知道这个指针要指向什么对象,那我们在定义指针之后就将它初始化为nullptr或者0。(在C++11标准中加入nullptr,建议大家在C++程序中最好使用nullptr,尽量不再去使用NULL)。