引用是充当另一个变量 别名的变量。引用类型最常用于 函数的参数,使得函数中的变量名成为调用程序中的变量的别名。一个函数能够对传递给他的变量做出永久性的改变。引用更正确的称呼是 左值引用。
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
左值引用
int x = 34;
int &lRef = x;
在该代码中,标识符 IRef 就是一个引用。在声明中,引用是通过 & 符号来指示的,它出现在类型与变量的标识符之间,这种类型的引用称为左值引用。
可以将左值看作是一个关联了名称的内存位置,允许程序的其他部分来访问它。在这里,我们将 “名称” 解释为任何可用于访问内存位置的表达式。所以,如果 arr 是一个数组,那么 arr[1] 和 *(arr+1) 都将被视为相同内存位置的“名称”。
相对而言,右值则是一个临时值,它不能被程序的其他部分访问。为了说明这些概念,请看以下程序段:
int square(int a)
{
return a * a;
}
int main()
{
int x = 0; // 1
x = 12; // 2
cout << x << endl; // 3
x = square(5); // 4
cout << x << endl; // 5
return 0;
}
在该程序中,x 是一个左值,这是因为 x 代表一个内存位置,它可以被程序的其他部分访问,例如上面注释的第 2、3、4 和 5 行。
而表达式 square(5) 却是一个右值,因为它代表了一个由编译器创建的临时内存位置,以保存由函数返回的值。该内存位置仅被访问一次,也就是在第 4 行赋值语句的右侧。在此之后,它就会立即被删除,再也不能被访问了。
对于包含右值的内存位置来说,其本质就是:它虽然没有名称,但是可以从程序的其他部分访问到它。
右值引用
C++11 引入了右值引用的概念,以表示一个本应没有名称的临时对象。右值引用的声明与左值引用类似,但是它使用的是 2 个 & 符号(&&),以下代码使用了右值引用打印了两次 5 的平方:
int && rRef = square(5);
cout << rRef << endl;
cout << rRef << endl;
右值引用不能约束到左值上,所以,以下代码将无法编译:
int x = 0;
int && rRefX = x;
再来看以下初始化语句:
int && rRef1 = square(5);
在初始化完成之后,这个包含值 square(5) 的内存位置有了一个名称,即 rRef1,所以 rRef1 本身变成了一个左值。这意味着后面的这个初始化语句将不会编译:
int && rRef2 = rRef1;
究其原因,就是右侧的 rRef1 不再是一个右值。综上所述,临时对象最多可以有一个左值引用指向它。如果函数有一个临时对象的左值引用,则可以确认,程序的其他部分都不能访问相同的对象。
// 多线程.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int add(int &a);
int main(void) {
int a = 11, b = 22;
int *arr = &a;
int* &aa = arr;//可以创建指针的引用
(*aa)++;
cout << "a = "<< a << endl;
int i = 0;
int &j = i; //j是i的一个别名
int &k = j;
//int &f;//错误,引用必须初始化
k = b;//这是赋值操作,将b的值赋给k,k是j的别名,j是i的别名,也就是说i=b,引用一旦绑定到某个值,就不能再指向其他对象
cout << "i = "<< i << endl;
j++;//更改i和更改j做的是同样的事情
i++;
k++;
cout << "i is " << i << " ,j is " << j << " ,k is " << k << endl;
cout << "&i = " << &i << endl;
cout << "&j = " << &j << endl;
cout << "&k = " << &k << endl;
cout << add(a) << endl;
cout << add(a) << endl;
cout << add(b) << endl;
return 0;
}
int add(int &a) {
return ++a;
}
//结果
/*
a = 12
i = 22
i is 25 ,j is 25 ,k is 25
&i = 008FFDA8
&j = 008FFDA8
&k = 008FFDA8
13
14
23
*/
*/
引用变量和被引用变量的值和地址都是相同的。因为它们共用一个内存空间。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。
int &a = 2; //# 左值引用绑定到右值,编译失败
int b = 2; // # 非常量左值
const int &c = b;// # 常量左值引用绑定到非常量左值,编译通过
const int d = 2; //# 常量左值
const int &e = c; //# 常量左值引用绑定到常量左值,编译通过
const int &b =2; //# 常量左值引用绑定到右值,编程通过
使用引用类型的准则
- 只有左值可以传递给引用参数
- 引用参数不会导致复制,而是该函数获得了对传递的变量的直接访问。
- 一个引用参数一旦绑定到某个变量,就不能再指向其他对象·
- 必须在声明引用时将其初始化
- 不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。可以创建指针的引用。
引用更接近于 const指针(必须在创建时初始化,一旦跟某个变量相关联,就要一直指向与它):
int a=0;
int * const ptr = &a
临时变量、引用参数和const
如果引用参数是const,编译器在下面两种情况下会生成临时变量:
- 实参的类型正确,但不是左值(常规变量和const变量)
- 实参类型不正确,但可以转换为正确的类型
将引用参数声明为const三个理由:
- 避免无意修改原有数据
- 可以处理const和非const类型。const只可以指向const。
- 可以正确生成并使用临时变量
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int sum1( int a,const int b) {
return a + b;
}
int sum2(int &a, const int &b) {
a++;
return a + b;
}
int sum3(const int &a, const int &b) {
return a + b;
}
int main(void) {
const int a = 1;
int b = 2;
//int & c = a;//“初始化”: 无法从“const int”转换为“int &”
cout << "sum=" << sum1(a, b) << endl;
//“int sum2(int &,const int &)”: 无法将参数 1 从“const int”转换为“int &”
//转换丢失限定符
//cout << "sum=" << sum2(a, b) << endl;
//使用 const 引用,可以接受const和非const参数
cout << "sum=" << sum3(a, b) << endl;
//临时变量---实参类型不正确,但可以转换为正确的类型
double pi = 3.14;
//int & ppi = pi;//无法从“double”转换为“int &”
const int & ppi = pi;
cout << "ppi = "<< ppi << endl;//ppi = 3,此时产生了临时变量
/*
*相当于:
const int temp = pi;
const int & ppi = temp;
*/
//实参的类型正确,但不是左值(常规变量和const变量)
//“int sum2(int &,const int &)”: 无法将参数 1 从“int”转换为“int &”
// 非常量引用的初始值必须为左值
//cout << "sum=" << sum2(666, b) << endl;
//右值引用
//int &d = 666;//左值引用失败
int &&d = 666;
cout << "d = "<< ++d << endl;//d = 667
return 0;
}
引用作为返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
#include <iostream.h>
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
temp=(float)(r*r*3.14);
return temp;
}
void main() //主函数
{
float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
//不能从被调函数中返回一个临时变量或局部变量的引用
float c=fn2(10.0); //第3种情况,系统不生成返回值的副本
//可以从被调函数中返回一个全局变量的引用
float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
//可以从被调函数中返回一个全局变量的引用
cout<<a<<c<<d;
}
引用作为返回值,必须遵守以下规则:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)引用与一些操作符的重载:
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << “hello” << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回 一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一 个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这 就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。
引用和多态
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
class A;
class B:public A{……};
B b;
A &Ref = b; // 用派生类对象初始化基类对象的引用
Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。
引用总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
(4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。
引用和指针的区别
1.指针有自己的一块空间,是一个变量,而引用只是一个别名,不能单独存在;
2.使用sizeof看一个指针的大小是4(32位系统)或8(64位系统)个字节,而引用则是被引用对象的大小;
3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;
4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
6.指针可以有多级指针(**p),而引用只有一级;
7.指针和引用使用++运算符的意义不一样;
8.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。