引用与指针
一、引用
引用类型同指针类型一样,属于C++的一种复合类型。引用也就是为对象起了一个别名,引用类型引用另外一种类型,而非引用类型。
1.1 引用的定义
一般在初始化变量时,初始值会被拷贝到新建的对象中。而在定义引用时,程序把引用和它的初始值绑定到一起,而不是将值拷贝给引用。一旦初始化完成,引用将和它的初始值永远绑定在一起,不能再重新绑定到另一个对象,所以定义引用一定要初始化,且初始值为对象。
int ival = 1024;
int& refVal = ival, ival2 = 110;//refVal 是ival的另一个名字
int& refVal4 = ival, & refVal5 = ival2;
引用只能绑定到对象上,而不能与对象或者某个表达式计算的结果绑定到一起,引用的类型都要与之绑定的对象严格匹配。
int& refVal1;//错误:引用必须要初始化
int& refVal2 = 10;//错误:初始值必须为对象,10只是一个字面值。
double dval;
int& refVal3 = dval;//错误:引用类型同引用引用的类型不一致
因为引用本身并不是一个对象,所以不能定义引用的引用,但是有指向指针的指针。
int ival3 = 1024;
int& refVal6 = ival3;
int&& refVal7 = refVal6;//错误:不存在引用的引用
int ival3 = 1024;
int& refVal6 = ival3;
int& refVal7 = refVal6;//但是这么写正确的,看引用的操作
1.2 引用的操作
定义一个引用后,对其进行的所有操作,都是在与之绑定的对象上进行的。为引用赋值,实际上是把值赋给了与引用绑定的对象;获取引用的值,实际上获取的也是与引用绑定的对象的值。
int ival = 1024;
int& refVal = ival;
refVal = 2048;//实际上赋值给了ival
cout << ival << endl;//2048
cout << refVal << endl;//2048
同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
int ival3 = 1024;
int& refVal6 = ival3;
int& refVal7 = refVal6;//refVal6是ival3 的另一个名字,实际上也是ival3
1.3 引用与const
const限定符
const
限定符对变量起到了一个限定的作用,使用它定义的变量,其值不能被改变,值不能改变的变量,称其为常量。比如const int ci = 1024;
这个定义,ci
就是一个常量,一旦定义,任何有可能会改变ci
值的操作都是错误的,而不改变ci
值的操作,都是正确的,如算数运算等。所以所有常量的定义,都必须初始化,否则就会自相矛盾(常量一旦定义,就不能再做改变其值的操作嘛),比如这样定义就是错误的const int ci;
(ci
不允许除了初始化之外的赋值操作)。
int i = 1;
和const int j = 1;
,j
只有被限制不能做出改变其值的操作,其余都和i
一样。利用一个对象a初始化另一个对象b,对象a的值不会被改变,所以对象初始化时是不是const无关紧要。
int i = 1;
const int ci = i; // 正确:i的值被拷贝给ci
int j = ci; // 正确:ci的值被拷贝给j
这里有一个很有趣的操作:
const double d = 1.25;
int i = d;
cout << i << endl; // i = 1;
给人的感觉好像是把变量d
的值1.25
转成了1
,其实不然。int i = d;
这个操作,是把变量d中的值拷贝一份,再将拷贝的值强转成int类型,最后再赋值给变量i。对于值的拷贝不会改变这个值,且拷贝出来的只是一个值,至于对拷贝出来的副本如何操作,与被拷贝的值已经没有任何关系了。
对const的引用(常量引用)
可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称其为对常量的引用。与普通引用不同的是,对常量的引用(对常量的引用简称常量引用,如const int& rci = 123;
rci就是一个常量引用)不能被用作修改它所绑定的对象。
const int ci = 1024;
const int& rci = ci; // 正确,ci是常量对象,rci是常量引用
rci = 44; // 错误,常量引用不能修改所引用的对象
常量对象的值不能被改变,那么绑定在常量对象上面的引用,也不允许通过其改变常量值,由于非常量引用具有改变绑定对象值的权限,所以常量对象只能被常量引用所绑定。
const int ci = 1024;
int& ri = ci; // 错误,ci是常量对象,ri是非常量引用,ri具有改变绑定对象值的权利,所以不能让其绑定常量对象
【注】所以说,常量对象只能被常量引用绑定。
那上面这句话反过来说“常量引用只能绑定常量对象”是否成立呢?那肯定是不行的。因为常量引用的限制,只是对引用的限制,而非是对引用绑定的对象的限制。常量引用不但可以绑定非常量对象,也可以绑定非常量字面值、表达式。
int i = 42;
const int& ri = i;
const int& r1 = 123;
const int& r2 = i * 2;
const int& r3 = r1 * r2; // 这里注意,r1和r2只是引用,本身并不是对象,而是绑定着i对象
进一步对上面概念加深理解:
double d = 1.1;
const int& rd = d; // 正确
cout << d << endl; // 值为:1.1
cout << rd << endl; // 值为:1
d
和rd
输出的值不一样,那是不是说明,rd
引用绑定的对象并不是d
?确实是如此,编译器在出来上面代码时,做了如下的处理:
double d = 1.1;
const int i = d;
const int& rd = i;
所以,常量引用绑定非常常量对象时,首先会将非常量对象的值赋给一个临时的常量对象,然后再将常量引用绑定到这个临时的常量对象上面。
那下面代码,为什么i
的值改变,ri
的值没有变,也就能理解了。(再次申明,常量引用只是对引用的限制)
int i = 42;
const int ri = i;
i = 1;
cout << i << endl; // 值为:1
cout << ri << endl; // 值为:42
1.4 引用作为函数参数和返回值
二、指针
指针是指向另外一种类型的复合类型,与引用类似,指针也实现了对其他对象的间接访问。但是与引用相比又有许多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无须在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
2.1 指针的定义与操作
在定义时,*
用来定义一个指针,&
用来定义一个引用;在非定义语句或表达式中,*
被称为解引用符,用来访问指针所指的对象,&
称为取地址符,用来获取一个对象的地址。指针也是对象。
int a = 10, *pa, &ra = a, **ppa, *pnull;
pa = &a;
*pa = 110;
ppa = &pa;
cout << a << endl;//110
cout << *pa << endl;//110
cout << ra << endl;//110
cout << **ppa << endl;//110,相当于*(*ppa)
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针,但是可以定义指向指针的指针,如上面的ppa就是指向指针的指针,所指对象为pa。
且指针类型必须与其所指对象类型严格匹配。
int b = 11, &rb = b;
int *rpb;
long *pa;
pb = &b;//错误:int和long类型不匹配
虽然不能定义指向引用的指针,但是下面操作是正确的。引用rb引的是变量b,一旦对引用初始化,引用就不再只是一个引用,而是其所引变量的别名,对已初始化引用的所有操作,都是对其所引变量的操作,包括取地址取的也是所引变量的地址。所以,不能定义指向引用的指针,这句话的前提是引用没有初始化,但引用必须初始化,嗯嗯,这就搞得人很压抑。
int b = 11, &rb = b;
int *rpb;
rpb = &rb;//这是OK的
cout << *rpb << endl;//11
由于指针是一个对象,所以可以定义指向指针的引用,或者说是引用指针的引用,都一样。
int i = 12;
int *p;
int *&r = p;
r = &i;
*r = 121;
cout << *r << endl;//121
cout << *p << endl;//121
对于两个类型相同的合法指针,可以使用相等操作符(==)或不相等操作符(!=)来比较他们,比较的结果是布尔类型。若两个指针存放的地址值相同,则他们相等;反之不相等。两个指针存放的地址值相同(两个指针相等)有三种可能:a,他们都为空;b,他们都指向同一个对象;c,他们都指向同一对象的下一个对象。
【注】这里的所有操作都是使用合法指针,如果采用非法指针作为条件判断将会引起不可估计的后果,导致你很难定位到问题。
2.2 指针值
指针的值(即地址)应该属于下列4中状态之一。
- 1、指向一个对象,即指针值为所指对象的地址值。
- 2、指向紧邻对象所占空间的下一个位置。
- 3、空指针,意味着指针没有指向任何对象,也就是没有初始化,如上面的pnull。
- 4、无效指针,上述情况之外的其他值。一定要注意这种情况,拷贝或以其他方式访问无效指针的值都会引发错误,编译器也发现不了这种错误,所以一定要小心使用指针。
2.3 空指针
空指针,不指向任何对象,在试图使用一个指针之前可以先检查他是否为空。以下是生成空指针的方法:
int* p1 = nullptr;
int* p2 = 0;
int* p3 = NULL;
nullptr
是C11引入的一种方法,是一种特殊的字面值,它可以被转成任意其他的指针类型。让一个指针的值为空一般都用这种方式。NULL
是头文件cstdlib
中定义的一个预处理变量,其值为0。现在应该尽量避免使用这种方式来为指针赋空值。- 0,这就涉及到一个知识点。现在很多的电脑都是64位的操作系统,所以其内存总线、数据总线的位数是64位,这里单说内存总线。内存总线的每个位,只能表示0或者1,所以每个内存单元的地址值表示是64位的整型数,这也就是为什么输出C++的一个地址值是一个8位16进制的数。当给一个指针赋值为0时,说明该指针存放的地址地址值并不存在,也就没有指向任何一个对象,所以为空。
在使用一个指针时,可以判断该指针是否为空。下面有一个C++中的隐式转换,if
条件后面跟着的表达式,如果值为0,会自动换成false
;如果值为非零(不管正负),自动转成true
。
int* p1 = nullptr;
if (!p1) {
// 如果是空指针
cout << p1 << endl;//00000000
}
2.4 void* 指针
void*
是一种特殊的指针类型,可以将指向任意非常量的指针转换成void*
,指向任意对象的指针转换成const void*
。
int a = 110;
double b = 19;
int* p = nullptr;
double* pb = 0;
p = &a;
pb = &b;
void* v = p;
v = pb;
一个void*
存放一个地址,我们也能知道地址值,但是我们对该地址中到底是什么类型的对象并不了解,也无法确定该如何操作这个对象,所以不能直接操作void*
指针所指的对象。
void* v = p;
cout << v << endl;//可以输出指针所值内存的地址值
cout << *v << endl;//报错:不能直接操作void*所指的对象
虽说不能直接操作void*
所指的对象,但也不是说操作不了,如果预先知道void*
所指对象的类型,就可以使用强转将其转成所指对象的指针类型。如下面操作,也可以使用C中的强转方式int* q = (int*)v;
来转换,但是这样比较危险,对于指针的强转尽量使用下面这种。
int *q = reinterpret_cast<int*>(v);
cout << q << endl;//ok
cout << *q << endl;//这个也ok
void*
以及void**
真正使用起来不但妖,而且异常的骚,后面函数指针和数组指针会再详细介绍这个玩意。
2.5 指针和const
常量指针
如果在定义指针对象的时候,数据类型前用const修饰,被定义的指针对象就是指向常量的指针变量,指向常量的指针变量称为常量指针。常量指针不能用于改变所指对象的值。
int a, b;
const int ca = 0;
const int* p = &ca; //常量指针,p是一个指针,所指内容是一个常量
// 注意以下几种操作
p = &b; // 操作成功,常量指针可以指向一个常量,也可以指向一个变量
*p = 9; // 操作错误,指向常量的指针变量没有改变所指对象内容的权限
b = 1; // 操作正确,可以通过其他途径改变所指对象的值,前提是对象是一个非常量
常量指针本身是一个变量,而只是将其所指对象当做一个常量来处理,且常量指针没有规定其所指对象必须是一个常量,所以p
既可以指向常量ca
,也可以指向变量b
。常量指针不能改变所指对象的值,但是如果所指对象是一个变量,是可以通过其他途径改变的,如变量b
的值。
指针常量
如果在定义指针对象的时候,指针前面用const修饰,被定义的指针对象就是指针常量。指针常量是一个常量,一旦初始化就不能再改变其值。
int a, b;
const int ca = 0;
int* const p = &b; //指针常量,使用前一定要被初始化
//注意一下几种操作
*p = 9; //操作成功,指针常量可以通过指针改变所指对象的值
p = &a; //操作失败,指针常量一旦被初始化,就不能再改变其值
- 常量指针本身是一个变量,const修饰的是所指对象,将其所指对象当做一个常量来处理。
- 指针常量本身是一个常量,const修饰的是指针本身,与指针所以对象没关系。
2.6 指针作为函数参数和返回值
2.7 指针和数组
三、指针与引用的总结
3.1 引用使用总结
- 1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
- 2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
- 3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
- 4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。
3.2 指针使用建议
- 1)代码编写时注意
malloc
/free
,new
/delete
成对使用,free
或delete
之后将相应的指针设置为nullptr
。即使这样做了,由于异常可能会导致释放内存的free
/delete
语句得不到执行,也会发生内存泄露。 - 2)C++代码代码中多注意使用智能指针。
- 3)避免使用野指针,因为野指针不能通过
if (nullptr == p)
的判断语句来预防,所以在创建指针变量时尽量做到要初始化,不知道指向哪就初始化为nullptr
。 - 4)
c/c++
中,局部变量是存放在栈中的,它的特点是随函数调用时创建随函数结束时销毁,因此在程序中将局部变量的地址返回后赋值给一个指针,这个指针指向的是一个已经被回收的内存,这也是一种野指针。所以,不要在函数中返回局部变量的地址,如果代码的逻辑非要是一个局部变量的地址,那么该局部变量一定要申明为static
类型。 - 5)动态内存分配函数分配内存的时,有可能会分配失败,返回
nullptr
。所以,在使用内存分配函数分配内存的时候,应该用if(p ==nullptr)
或if(p != nullptr)
进行防错处理。 - 6)在含有指针参数的函数,也是有可能会误用到
NULL
指针,当调用该函数时传递的指针是个空指针时,如果没有if(p ==nullptr)
或if(p != nullptr)
进行防错处理,也就凉凉了。
3.3 指针与引用比较
- 引用必须要初始化,初始值必须为一个对象,且初始化完后不能再重新绑定(只能绑定一个)。
- 引用本身并不是一个对象,它只是其引用对象的另一个名字。
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初值(但是最好还是初始化),和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值