目录
什么是指针?
指针
指针是一种复合类型。它与引用类似,实现了对其他对象的间接访问。
指针与引用的不同
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且指针的生命周期它可以指向几个不同的对象。
- 指针无需在定义时赋初值。如果指针没有初始化和其他内置类型一样,也将拥有一个不确定的值。
指针的作用
获取对象的地址,使用取地址符&
int ival=42;
int *p=&ival;
这里的p指的是一个指向int类型的一个指针,p存放的是ival的一个地址,既然p存放的是ival的一个地址,那么这里为什么用int *p=&ival,而int p=&ival;会报错呢?因为这样会和int p;混淆,编译器不能区分p究竟是一个变量还是一个指针。
int iival;
int ival;
int *p=&ival;
p=&iival;//为什么这里的p不加*
我之前学指针比较迷惑的是为什么在赋值的时候的p不加*。其实指针就是存储内存地址的变量,这样做是为了区分p的值是地址还是指向的一个数值,如果指针p指的是一个数值就加上*,如果是地址就不加*直接赋值就行。
int iival;
int ival;
int *p=&ival;
p=&iival;
*p=0;
这里的p指向的是一个数值,意思就是给指针所指的对象赋值为0
总结: 当定义指针并赋值时加*,当指针仅进行赋值操作时不加*
指针值
什么是指针的值?
指针的值(及地址)应该属于下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述之外的其他值。
&和*组成复合部分的多重含义
int i=42;
int &r=i;//这里的r表示的是引用
int *p;//说明p是一个指针,*是声明的一部分
p=&i;//这里的&是取地址符
*p=i;
int &r2=*p;//这里的*是解引用
空指针
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
以下列出几个生成空指针的方法:
int *p1=nullptr;//等价于int *p1=0;
int *p2=0;//直接将p2初始化为字面常量0
int *p3=NULL;//等价于int *p3=0;
得到空指针最直接的办法就是字面值nullptr来初始化指针。
void* 指针
void* 是一种特殊的指针类型,可用于存放任意对象的地址。与其他指针不同的是,我们对指针中到底是什么类型的并对象不了解。
double obj=3.14,*pd=&obj;//void*可以存放任意对象的地址
void *pv=&obj;//obj可以是任意类型的对象
pv=pd;//pv可以存放任意类型的指针
以void*的视角来看内存空间也就仅仅是空间,没办法访问内存空间所存的对象。不再详细表述
指向指针的指针
指针的指针,即指针所指向的内存空间而这个内存空间存的依然是一个指针,即地址。
我们可以通过*的个数来判断指针的级别,也就是说**表示指向指针的指针,***表示指向指针的指针的指针,以此类推。
int ival=1024;
int *pi=ival;//pi指向的是一个int类型的数
int *ppi=π//ppi指向的是一个int类型的指针
下图描述了它们之间的关系:
一般情况下,指向指针的指针一般与二维数组相结合使用。 假如你还是没有理解什么是指向指针的指针,我再举一个例子:
假设一个zippo为一个二维数组存的数字是{{2,4},{6,8},{1,3},{5,7}};
数组zippo的首地址是0x0064fd38,假设本系统中int占4个字节。那么问题是zippo+1的值,zippo[0]的值,zippo[0]+1的值,*zippo的值,*zippo+1的值,zippo[0][0]的值,*zippo[0]的值,**zippo的值是多少,zippo[2][1]的值是多少,*(*(zippo+2)+1)的值是多少
我们知道zippo数组元素的首地址为0x0064fd38,因为是二维数组所以&zippo[0][0]的值与数组首地址相同,为0x0064fd38。
- zippo+1相当于加上当前能表示的最小的内存单元,即&zippo[0]+1(2*4字节),也就是&zippo[1]相当于&zippo[1][0]的值,为0x0064fd40(因为在16进制中38+8=40)
- zippo[0]的值其实就是数组元素的首地址&zippo[0][0],为0x0064fd38
- zippo[0]+1的值其实就是&zippo[0][0]+1,即&zippo[0][1]的值,为0x0064fd3c(在16进制中38+4=3c)
- *zippo的值,其实相当于*&zippo[0],也就是&zippo[0][0]的值,为0x0064fd38
- *zippo+1的值,类比上一个也就是&zippo[0][0]+1,即&zippo[0][1],为0x0064fd3c
- zippp[0][0]为第一个元素的值为2
- *zippo[0],相当于*&zippo[0][0]的值,也就是&zippo[0][0]对象的值为2
- **zippo的值,其实就是**&zippo[0]的值,即*&zippo[0][0],为zippo[0][0]的值2
- zippo[2][1]的值为数组元素3
- 要算*(*(zippo+2)+1)的值,可以先算*(zippo+2),*(zippo+2)相当于*(&zippo[0]+2),即&zippo[2][0],再加上1,即&zippo[2][1],所以最后要求的其实就是zippo[2][1]的值,为3
最后总结一下'*'的一个用法,'*'其实就是将对象转换为比它更小一级的值。
const 限定符
有时我们希望定义这样一个变量,它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时防止程序一不小心改变了这个值。为了满足这一要求,可以用关键字const对变量加以限定。
const int bufSize = 512;//输入缓冲区大小
这样就把bufSize定义成了一个常量。任何试图为bufSize赋值当行为都将引发错误:
bufSize=512;//错误,试图向const对象写值
因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。
const int i = get_size();//正确,运行时初始化
const int j = 42;//正确,编译时初始化
const int k;//错误,k是一个为经初始化的常量
⚠️注意:在默认情况下,const对象仅在文件内有效
const 的引用
可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &r1=ci;//正确,引用及其对应的对象都是常量
r1 = 42;//错误,r1是对常量的引用
int &r2 = ci;//错误,试图让一个非常量引用指向一个常量对象
⚠️注意:对const的引用可能引用一个非const对象
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
int i = 42;
int &r1 = i;//引用r1绑定对象i
const int &r2 = i;//r2也绑定对象i,但是不允许通过r2来修改i的值
r1 = 0;//r1为非常量,i的值修改为0
r2 = 0;//错误,r2是一个常量引用
指针与const
与引用一样,可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const) 不能用于改变其对象的值。要想存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14;//pi是个常量,它的值不能改变
double *ptr = π//错误,ptr是一个普通指针
const double *cptr = π//正确,cptr可以指向一个双精度常量
*cptr = 42;//错误,不能给*cptr赋值
const 指针
常量指针(const pointer)其实就是允许指针本身定为常量。对常量指针而言,它必须初始化,一旦初始化完成,它的值(指针的地址)就不能改变了。把*放在const关键字之前用以说明指针是一个常量,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int *const curErr = &errNumb;//curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π//pip是一个指向常量对象的常量指针
在这里我们可能还对const的用法有些困惑,下面详细介绍一下两种const类型的指针,顶层const和底层const
顶层const
顶层const指的是常量本身是不可改变的,而底层const指的是常量所指向的值是不可改变的。
为了讨论指针本身是不是常量以及指针所指的是不是一个常量这两个问题。用名词顶层 const(top-level const)表示指针所指的对象是一个常量。
对于更一般的,顶层const可以表示任意对象是常量,这一点对任何数据类型适用,如算术类型、类、指针等。底层const则与指针和引用等符合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显:
int i = 0;
int *const p1 = &i;//不能改变p1的值,这是一个顶层const
const int ci = 42;//不能改变ci的值,这是一个顶层const
const int *p2 = &ci;//允许改变p2的值,这是一个底层const
const int *const p3 = p2;//靠右的const是顶层const,靠左的是底层const
const int &r = ci;//用于声明引用的const都是底层const
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中顶层const不受什么影响:
i = ci;//正确,拷贝ci的值,ci是一个顶层const,对此操作无影响。
p2 = p3;//正确,p2和p3指向的对象类型相同,p3顶层const的部分不影响。
int *p = p3;//错误,p3包含底层const的定义,而p没有
p2 = &i;//正确int*能转换成const int*
int &r = ci;//错误,普通的int&不能绑定到int常量上
const int &r2 = i;//正确const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时课可以不在乎它是一个顶层const,但是必须清楚它指向的对象是一个常量。因此,不能用p3来初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次而言不会有什么影响。