复合类型指的是 *基于其它类型定义的类型*, C++中主要的两个复合类型有:引用和指针
引用
引用可以理解为别名,也就是为变量起了另一个名字。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。
~~~C++
int ival = 1024;
int &refV1 = ival; //合法,为ival起了一个别名refV1
int &refV2; //不合法,引用必须被初始化
~~~
引用在初始化时,会被与初始值绑定在一起,操作引用就是在操纵与引用绑定的对象,也就是说,如果为引用赋值,同时也会改变绑定的值(因为二者本就是一个东西,好像一个同学叫做AAA,我为他取了个外号叫3A,那么3A和AAA指的就是同一个人,并且3A不能再是别人的外号)
需要注意的是,引用本身并不是一个对象,他只是为另外一个对象起了一个新的名字,相当于是一个标签,并且这个标签一旦贴上去,就撕不下来了。所以我们无法定义一个引用的引用,因为引用是要指向一个对象的。
指针
指针与引用类似,也实现了对另一个对象的间接访问。与引用的不同在于
- 指针本身也是一个对象,允许对指针赋值、拷贝,并且指针可以在生命周期内先后指向不同的对象。而引用本身不是一个对象,无法定义引用的引用,也不能指向初始值外的对象。
- 指针可以不在定义时赋值。而对引用而言,这样做是不合法的
也正是因为引用不是一个对象,所以指针是无法指向引用的。
定义指针要写成*d的形式,d是变量名。
需要注意的是,如果在一条语句中定义多个指针,每个变量名前都要加*.
~~~C++
int* p1, p2; //实际是定义了一个int指针p1,和一个int变量p2。符合*只对p1起作用
int *p1, *p2; //两个指针
注意:
符号* 紧跟类型说明符后面 或 变量名前面,这两者是等价的。
推荐使用int *p这种形式,因为
int* p1容易误导人,例如int* p1,p2; 让人以为*是对后面所有声明符起作用的,但实则只对p1有效。
~~~
获取对象的地址
指针存储的是该对象的地址,要想获得一个对象的地址,需要取地址符&
~~~C++
int val = 1;
int *p = &val; //p存放的是val的地址
~~~
需要注意的是,除了两种例外情况(书56页和534页),指针必须指向对应类型的对象,否则报错。例如int指针不能指向一个double对象。
指针值
指针的值有以下4种状态:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,没有指向任何对象
- 无效指针,即除以上三种情况外的指针
试图拷贝、访问无效指针都将报错。
通过指针访问对象
通过解引用符*,可以访问对象。
~~~C++
int val =1;
int *p = &val;
cout<< *p; //输出1
~~~
解引用的意思,其实就是取出指针指向地址所存的值。例如本例,p指向val的地址,而val的地址上存的值是1,所以*p为1.
修改*p,会直接修改地址上存的值,所以val的值也会改变(因为二者都在取同一地址的值,修改也是在一个地址上修改)
空指针
空指针不指向任何对象,有三种初始化空指针的方法
~~~C++
int *p = nullptr;
int *p = NULL;
int *p = 0;
~~~
nullptr是C++11新标准的一个方法,它可以被转换成任何一种其它类型的指针。NULL是一个预处理变量,在cstdlib中定义,其值就是0.但是在现在的程序中,在定义空指针时,最好使用nullptr。
注意:
指针是C中的一大难点,容易造成各类问题。例如,访问未经初始化的变量可能造成意想不到的后果。如果使用了未经初始化的指针,那么指针所占内存空间的当前内容会被看作一个地址值,如果这个地址恰好有内容,我们就很难分辨这个是合法还是非法的了。
因此,建议初始化所有指针
指针的操作
~~~C++
int i = 123;
int *p1 = nullptr; //空指针
int *p2; //未初始化的指针,不要这样做
p1 = &i; //指针p3指向i
p2 = p1; //p2和p3指向了同一位置。
/*
为什么这里不用&p3?
原因在于,之前说过,指针本身存储的是一个地址值,所以p1本身就已经是一个地址值了,所以可以直接通过p2 = p1的方式,将这个地址赋给p2,使p2也指向这个地址。而i是一个int变量,i的值是123,而不是它的地址,所以需要通过取地址符&i,来使得p1指向i对应的地址
/*
~~~
指针只要有合法值,就也可以用于表达式中。
如果指针的值是0,返回false,任何非0指针返回true。
~~~C++
int i = 1024;
int *p = nullptr;
int *p2 = &i;
if(p){ // false
}
if(p2){ // true
}
if( p == p2){ // false
//如果两个指针指向同一地址,则返回true,此处是false
}
~~~
void*
void*可以存储任意对象的地址,但是我们对其中存储的是什么类型的对象并不了解。
通过void*,我们可以拿他与其它指针比较,也可以作为函数的输入输出,或者赋值给另一个void*.
指向指针的指针
指针可以指向指针,*的个数没有限制,我们按照逻辑进行解释即可。通过*的个数,我们可以划分其级别。也就是说,**表示指向一个指针的指针,***表示指向指针的指针的指针。
~~~C++
int v = 1;
int *p = &v;
int **p2 = &p; //注意,这里用的是&p。 因为p的值,是p地址位置所存储的值,这个值是v的地址,而我们现在需要的是p的地址,所以需要取 &p
~~~
通过一个表,来为大家解释一下各个值。
值 | 解释 | |
v | 1 | int v的值是1 |
&v | 0x0001 | v的存储地址(我这里只是代指,真实的地址位数取决于系统) 64位系统本身应该有16位十六进制数(16*6=64位),但是大家打印出来可以发现只显示12位,原因在于pc几乎达不到128T内存,所以有4位十六进制数全0或全1,不显示 |
p | 0x0001 | p指向了v,也就是说p存储的值是v的地址 |
*p | 1 | p存储的地址是v,v存储的值是1,所以解地址取得1 |
&p | 0x0002 | p本身也有一个地址是0x0002,这个地址上存储的值是0x0001 |
**p2 | 1 | 可以理解为 *(*p2),而*p2的值是&v |
*p2 | 0x0001 | *p2存储的值是&v,指向的是p |
p2 | 0x0002 | p2存储的值,是p的地址 |
&p2 | 0x0003 | p2本身的地址 |
只要时刻记住,*xxx是要把&xxx上存储的值A(一个地址值),这个A地址所存储的值给取出来,就不会搞混。
指向指针的引用
int i =1;
int *p;
int *&r = p;
r = &i; //将 p指向i
*r = 0; //修改i为0
可以看到,其实现在r就是p,*r就是*p,r是p的一个别名。
分析复杂度声明语句时,应该从右往左读。例如int *&r, &对r的影响最直接,说明r是一个引用,在往左看,*代表r引用的是一个指针。