学习了博主的《漫谈继承技术》系列博文之后,相信大家都有所收获吧!这次博主将和大家一起探讨《灵活而奇特的C++语言特性》,主要包括引用、常量(const)、常量表达式(constexpr)、静态(static)、外部(expert)、类型定义(typedef)、类型别名(aliases)、类型转换、作用域解析、统一初始化、显示转换运算符、特性(attribute)、用户自定义文本、头文件、可变长度参数列表和预处理器宏。尽管这个知识清单显得有点凌乱,但是这些话题都是博主经过精心挑选,是容易混淆的语言特性。本篇我们来学习一下引用,增进大家对《灵活而奇特的C++语言特性》的理解。
在C++中,引用是变量的别名。所有对引用的修改都会改变被引用的变量的值。可以将引用当作隐形指针,这个“指针”没有取变量地址和解除引用的麻烦。说到引用,那咱们就先来谈谈的引用的特性:
①声明一个引用时,必须同时对它进行初始化,使它指向一个已存在的对象。
②一旦一个引用被初始化后,就不能改为指向其他对象。也就是引用天生就是常量,不能改变自己,但是可以改变它所引用的对象。
③ const修饰的引用可以引用常量或表达式,但是不能修改它所引用的对象的值。
④假设T是数据类型。T &类型的引用或T类型的变量既可以用来初始化constT &类型的引用,也可以用来初始化T &类型的引用。const T类型的变量或const T &类型的引用则只能用来初始化const T &类型的引用,除非进行强制类型转换。
可能对于初学者来说上面的陈述有些抽象,那咱们举个栗子吧。
#include<iostream>
usingnamespacestd;
int main(intargc,char**argv)
{
intnValue1 = 10;
intnValue2 = 20;
constintnValue3 = 30;
//int&nRef; /*引用变量必须初始化,否则不能通过编译*/
//声明引用时初始化,使其指向已存在的对象nValue
int&nRef = nValue1;
//一旦一个引用被初始化后,就不能改为指向其他对象
//此语句是一条赋值语句,使nRef引用变量的值变为nValue2
nRef =nValue2;
cout<<"nValue1: "<< nValue1 << endl;
cout<<"nValue2: "<< nValue2 << endl;
cout<<"nRef: "<< nRef << endl;
//const修饰的引用可以引用常量或表达式
constint&nCRef = nValue1 + 10;
//nCRef= 20;/*不能修改const修饰的引用所引用的对象的值*/
//T&类型的引用或T类型的变量可以用来初始化T &类型的引用
int&nRef1 = nRef;
int&nRef2 = nValue2;
//T&类型的引用或T类型的变量可以用来初始化const T &类型的引用
constint&nCRef1 = nRef;
constint&nCRef2 = nValue2;
//constT类型的变量或const T &类型的引用可以用来初始化const T &类型的引用
constint&nCRef3 = nCRef;
constint&nCRef4 = nValue3;
/*constT类型的变量或const T &类型的引用可以用来初始化T &类型的引用*/
//int&nRef3 = nCRef;
//int&nRef4 = nValue3;
//但是可以使用强制类型转换
int&nRef3 = (int&)nCRef;
int&nRef4 = (int&)nValue3;
//也可以使用const_cast来去掉对象的const限制
int&nRef5 = const_cast<int&>(nCRef);
int&nRef6 = const_cast<int&>(nValue3);
return0;
}
程序运行结果:
根据上面代码所举的栗子以及程序运行结果,相信大家对引用的特性已经掌握的差不多啦。我们再来探讨一下数组引用和引用数组。
数组引用
咱们先来一起使用一下数组引用,了解一下它的一些特性。
#include<iostream>
usingnamespacestd;
int main(intargc,char**argv)
{
//定义符号常量
constintsize = 10;
//定义整型数组,并初始化
intnArray[size] = { 0 };
//nRArray:数组引用,它的size的大小必须和引用的对象数组的大小保存一致,否则编译出错。
int(&nRArray)[size]= nArray;
//给数组元素赋值为1~10
inti = 0;
for(auto&var : nRArray)
{
var =++i;
}
//通过引用输出数组中各元素的值
cout<<"nRArray = { ";
for(auto&var : nRArray)
{
cout<< var <<", ";
}
cout<<"}" << endl;
//通过数组输出数组中各元素的值
cout<<"nArray = { ";
foreach(auto&varin nArray)
{
cout<< var <<", ";
}
cout<<"}" << endl;
return0;
}
程序运行结果:
数组引用可以完全当作数组来看,它满足数组的所有性质:
①数组名代表整个数组空间
②数组名是数组首元素的地址
③数组名是常量,不能更改
结合上面的程序,相信大家已经理解了数组引用的概念以及用法。聪明的小伙伴可能会问:“我们在学习指针的时候有数组指针和指针数组,那么类比一下,既然有数组引用,那么有没有引用数组呢?”。不过,我只能说有这种类比的思想是好的,C++暂不支持引用数组的语法。可能有的小伙伴会觉得博主在欺骗他/她,那咱们写个简单的程序试一试吧!
#include<iostream>
usingnamespacestd;
int main(intargc,char**argv)
{
intnValue1 = 10;
intnValue2 = 20;
constintsize = 2;
int&nCArray[size] = { nValue1, nValue1 };
return0;
}
编译器会提示一下错误:
其实引用是指针的封装形式,至少你可以这样去理解,引用的内部实现和指针一样,任何使用引用的地方都可以用指针来代替,它们的汇编代码完全一样。只是我们在使用指针的时候容易犯错而造成内存泄露和悬挂指针的问题,然而引用就不会了出现这些错误,不可能存在无效引用,也不需要显式地解除引用。可能有小伙伴正在怀疑博主刚才说的话:“使用引用的地方都可以用指针来代替,并且它们的汇编代码完全一样”。咱们举个简单的栗子吧。
#include<iostream>
usingnamespacestd;
int main(intargc,char**argv)
{
intnValue1 = 10;
intnValue2 = 20;
int*nPtr = &nValue1;
int&nRef = nValue2;
*nPtr =15;
nRef = 25;
return0;
}
上面语句的汇编代码如下:
看,博主没有忽悠你吧!既然我们已经学了指针和引用,那它们之间有什么区别呢?下面我们一起来总结下指针与引用之间的区别吧。
①指针变量可以更改指向,而引用一旦初始化之后就不能更改其指向。
②指针变量自增自减是地址在改变,这个涉及到步长的概念,步长与对象的类型有关,而引用变量自增自减则是将引用空间里的值加1。
③指针变量可以不初始化(为了安全,建议定义时初始化),但是引用变量必须初始化。
④sizeof(指针)在32位操作系统上永远为4,在32位操作系统上永远为8,而sizeof(引用)则是引用空间的大小。
⑤当指针作为函数参数时,不用指定数组的长度,但是引用必须指定长度。
如:voidfun1(int *nPtr);//不用指定长度
void fun2(int(&nRArray)[size]);//必须指定长度,且长度必须和实参数组的长度保存一致
了这么多引用的语法特性,那么我们在写程序的时候,都在什么时候使用引用呢?引用可以作为类的数据成员、函数的参数以及函数的返回值。使用引用主要是为了效率和正确性。效率体现在复制较大对象或者结构需要较长的时间。按引用传递只是把指向对象或结构的“指针”传递给函数。正确性体现在并非所有对象都允许按值传递,即使允许按值传递的对象,也可能不支持正确的深度复制(deep coping)。为了支持深度复制,动态分配内存的对象必须提供自定义的复制构造函数和赋值运算符重载函数。
最后提醒大家:无法声明引用的引用,或者指向引用的指针。博主这里偷个懒,就不给大家举例子啦。
相信你已经深入理解了引用的概念,并能很好的使用它。然而我们还有一个更好玩东西:右值引用和移动语义。限于篇幅,我就不在这里给 大家介绍右值引用和移动语义相关的知识了,我会在《灵活而奇特的C++语言特性——引用(下)》中给大家一起探讨。
如果想了解更多关于C++语言特性相关的知识,请关注博主《灵活而奇特的C++语言特性》系列博文,相信你能够在那里寻找到更多有助你快速成长和加深你对C++语言特性相关的知识和一些特性的理解和掌握。当然,如果你想了解关于继承方面的技术,请关注博主《漫谈继承技术》系列博文。