引用(reference)是其中C++语言复合类型之一。
C++11中新增了一种引用:所谓的“右值引用(rvalue reference)”,之后再详细介绍。这种引用主要用于内置类。严格来说,我们使用术语“引用(reference)”,指的是“左值引用(lvalue reference)”
引用(reference)实际上就是给对象起了个外号,操作一个变量的引用也就相当于操作变量本身,这一点跟指针很类似,只是相比指针更直观。
一、基本操作
1.引用必须要被初始化
定义引用时,程序把引用和它的初始值绑定(binding)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起(换言之,无法令引用重新绑定到另一个对象)。
int x = 10, y = 9;
int &xx = x;
int &refVal2; //报错:引用必须被初始化
int &refVal3 = 10; //报错,引用类型的初始值必须是个对象
xx = y; //无法令引用重新绑定到另一个对象
xx++;
std::cout << "x Address:" << &x << std::endl; //x Address:0x72fe2c
std::cout << "y Address:" << &y << std::endl; //y Address:0x72fe28
std::cout << "xx Address:" << &xx << std::endl; //xx Address:0x72fe2c
std::cout << "x:" << x << std::endl; //x:11
std::cout << "xx:" << xx << std::endl; //xx:11
2.引用本身并不是对象。
不能建立引用的引用和指向引用的指针,但可以建立指针的引用。
int x = 10;
int *p = &x;
int *&refp = p;
(*refp)++;
cout << *p << endl; //11
cout << *refp << endl; //11
cout << x << endl; //11
3.建立数组的引用。
很多资料都强调,不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。其实是无法直接像引用基本类型那样,声明数组的引用,稍加处理就可以建立数组的引用了。
int (&xx)[3]
的解读:首先,xx是一个引用;其次,xx是一个存放3个元素的数组引用;最后,xx是一个存放3个整数类型的数组引用。
int x[] = {1,2,3};
int (&xx)[sizeof(x)/sizeof(x[0])] = x; //A reference to current array x
xx[1]++;
std::cout << "xx[1]:" << xx[1] << std::endl; //xx[1]:3
std::cout << "x Address:" << &x << std::endl; //x Address:0x72fe20
std::cout << "xx Address:" << &xx << std::endl; //x Address:0x72fe20
下面介绍一种标准点的写法(或者更容易理解的写法)。在此之前,先来了解类型别名(type alias),它是某种类型的同义词,便于将复杂的类型名字变得简单明了,易于理解和使用。
传统的方法是使用关键字typedef
,例如
typedef int Jack; // 帮int取名叫Jack
Jack age = 10; // 等同于int age = 10;
现在,展示容易理解的写法,定义arrayref
代表的类型是存放[sizeof(x)/sizeof(x[0])]
个整数类型的数组类型。
int x[] = {1,2,3};
typedef int arrayref[sizeof(x)/sizeof(x[0])];
arrayref &xx = x; //A reference to current array x
xx[1]++;
std::cout << "xx[1]:" << xx[1] << std::endl; //xx[1]:3
std::cout << "x Address:" << &x << std::endl; //x Address:0x72fe20
std::cout << "xx Address:" << &xx << std::endl; //x Address:0x72fe20
二、函数
A.作为函数参数
基本用法:可以直接对实参进行操作,比如对两个数进行交换。
void swap(int &a,int &b){
int temp;
temp = a;
a = b;
b = temp;
}
传引用参数的好处:
1.安全,引用对象(指向的实参)不会在函数内部改变。指针参数和引用参数都在栈中开辟了内存空间存放的是由主调函数放进来的实参变量的地址。但是,被调函数内部指针存放的内容可以被改变,即可能改变指向的实参,所以并不安全,而引用则不同,它引用的对象的地址一旦赋予,则不能改变。
void compare(int *p, int &ref){
cout << "In compare function:" << endl;
cout << "p Address:" << p << endl;
cout << "ref Address:" << &ref << endl;
int y = 11;
p = &y;
ref = y;
cout << "p Address:" << p << endl;
cout << "ref Address:" << &ref << endl;
}
int main() {
int x = 10;
int *p = &x;
int &xx = x;
cout << "p Address:" << p << endl;
cout << "xx Address:" << &xx << endl;
compare(p,xx);
return 0;
}
输出结果为:
p Address:0x79fe3c
xx Address:0x79fe3c
In compare function:
p Address:0x79fe3c
ref Address:0x79fe3c
p Address:0x79fdfc
ref Address:0x79fe3c
- 拷贝大的类型对象或者容器对象比较低效,这时推荐使用引用形参。比如
string
类。
bool compare(string &s1, string &s2){
return s1.size() == s2.size();
}
3.有的类型(包括IO类型在内)根本不支持拷贝操作。这时,函数只能通过引用形参访问该类型的对象。
B.作为函数返回值
当函数返回引用类型的时候,没有复制返回值,而是返回对象的引用(即对象本身)。实际上, 就是返回一个变量的内存地址,既然是内存地址的话,那么肯定可以读写该地址所对应的内存区域的值,即就是“左值”,可以出现在赋值语句的左边。
typedef double arr[5];
double& setValues(arr &ref,int i )
{
return ref[i]; // 返回第 i 个元素的引用
}
int main() {
using namespace std;
double vals[5] = {10.1, 12.6, 33.1, 24.1, 50.0};
arr &refarr = vals;
cout << "Before:" << endl;
for ( int i = 0; i < 5; i++ ) cout << vals[i] << " ";
cout << endl;
setValues(refarr,1) = 20.23; // 改变第 2 个元素
cout << "After:" << endl;
for ( int i = 0; i < 5; i++ ) cout << vals[i] << " ";
cout << endl;
return 0;
}
一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子,我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。
该如何实现呢?一种方法是定义一个新的数据类型,让它包含两个值。另一种简单的方法,我们可以给函数传入一个额外的引用实参,令其保存字符出现的次数:
string::size_type find_char(const string &s, char c, string::size_type &occurs){
auto ret = s.size();
occurs = 0;
for(decltype(ret) i = 0; i != s.size(); ++i){
if(s[i] == c){
if(ret == s.size()) ret = i;
++occurs;
}
}
return ret; //出现次数通过occurs隐式地返回
}
三、const的引用
A.初始化和对const的引用
可以把引用绑定到const
对象上,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
int i = 42;
const int &r1 = i; //允许将const int&绑定到一个普通int对象上
const int &r2 = 42; //正确:r1是一个常量引用
const int &r3 = r1 * 2; //正确:r3是一个常量引用
int &r4 = r1 * 2; //错误:r4是一个非常量引用
上面,同样的初始化对于非const引用是不合法的,将导致编译错误。原因有些微妙,需要适当做些解释。
引用在内部存放的是一个对象的地址,它是该对象的别名。对于不可寻址的值,如文字常量,以及不同类型的对象,编译器为了实现引用,必须生成一个临时对象,将该对象的值置入临时对象中,引用实际上指向该对象(对该引用的操作就是对该临时对象的操作),但用户不能访问它。
double dval = 3.14;
const int &ri = dval;
此处ri
引用了一个int
型的数。对ri
的操作应该是整数运算,但dval
却是一个双精度浮点数。因此为了确保ri
绑定一个整数,编译器把上述代码编程如下形式:
const int temp = dval;
const int &ri = temp;
再放一些例子
double dval = 3.14159;
//下3行仅对const引用才是合法的
const int &ir = 1024;
const int &ir2 = dval;
const double &dr = dval + 1.0;
上面的代码,转化成
double dval = 3.14159;
//不可寻址,文字常量
int tmp1 = 1024;
const int &ir = tmp1;
//不同类型
int tmp2 = dval;//double -> int
const int &ir2 = tmp2;
//另一种情况,不可寻址
double tmp3 = dval + 1.0;
const double &dr = tmp3;
B.函数和const的引用
1.函数形参
如果函数无须改变引用形参的值,最好将其声明为常量引用。
bool isShorter(const string &s1,const string &s2){
return s1.size() < s2.size();
}
- 函数返回值
由于返回值直接指向了一个生命期尚未结束的变量,因此,对于函数返回值(或者称为函数结果)本身的任何操作,都在实际上,是对那个变量的操作,这就是引入const类型的返回的意义。当使用了const关键字后,即意味着函数的返回值不能立即得到修改!
如下代码,将无法编译通过,这就是因为返回值立即进行了++操作(相当于对变量z进行了++操作),而这对于该函数而言,是不允许的。如果去掉const,再行编译,则可以获得通过,并且打印形成z:7
的结果。
const int& abc(int a, int b, int c, int& result){
result = a + b + c;
return result;
}
int main() {
int a = 1; int b = 2; int c=3;
int z;
abc(a, b, c, z)++; //wrong: returning a const reference
cout << "z: " << z << endl;
return 0;
}
参考文献
1.《C++ Primer(第5版)》 Stanley B.Lippman, Josee Lajoie, Barbara E.Moo