前言
对于习惯使用C进行开发的朋友们,在看到c++中出现的&符号,可能会犯迷糊,因为在C语言中这个符号表示了取地址符,取地址符常常用来用在函数传参中的指针赋值。引用是C++引入的新语言特性,是C++常用的一个重要内容之一。在C++中它却有着不同的用途,掌握C++的&符号,是提高代码执行效率和增强代码质量的一个很好的办法。
一、引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量**共用同一块内存空间。
引用的声明方法:类型标识符 &引用名=目标变量名;
类型& 引用变量名(对象名) =引用实体;
引用的符号和C语言中取地址的符号一样为&,在某类型名的后面加上引用符号(&)就成为了引用类型,设计引用的目的是简化指针的使用,但是C++中引用不能完全替代指针,引用不能引用多个对象,所以C++也没有摆脱指针,JAVA则完全摆脱了指针。在C++中,引用和指针相辅相成,引用的底层是通过指针实现的。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
return 0;
}
注意:引用类型必须和引用实体是同种类型的
【说明】
(1)&在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。ra=1; 等价于 a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。
二、引用的特性
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
int main()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& x = a;
int& y = a;
x++;
y++;
a++;
printf("%p %p %p\n", &a, &ra, &rra);
}
三、常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。和const修饰变量一样,被const修饰的引用只能够读,而不能够写,即不能修改引用变量的值。
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& rra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
对于a来说,a的权限是只读,而ra的权限是读写。所以a的权限变大大了,所以编译会报错,对于rra来说,他的权限是只读,和a的权限一样,属于权限的平移,所以可行;对于int& b = 10来说,右边的10为常量,常量只能进行读操作,而左边b的权限是可读可写,所以权限放大了,编译会报错;所以需要左边的前面需要用const来修饰,对于double d = 12.34;我们用int& rd = d;因为类型转换会将12.34转换成整形的临时变量给int& d;临时变量具有常性,所以权限放大了,编译会报错,所以需要加const进行修饰。
【注意】
1.权限可以缩小和平移,但不可以放大。一个只读的变量,不能用可读可写的变量做引用,这样权限放大了。这里的权限指的是读和写的权限,并且只针对于指针和引用。
2.对于上面的代码,int& b = 10;数字10只存在于指令中,在内存中并不会占用空间,所以我们对其进行引用时,10会先赋给一个临时变量,所以我们对这个临时变量进行引用,而临时变量具有常性,即临时变量是只读的,使用我们需要用常引用这个临时变量。
四、引用的使用场景
1.做参数
以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为 这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择)。
引用做参数的好处:
1.减少拷贝,提高效率。在C语言中我们知道,形参是实参的临时拷贝,那么就会有时间和空间的消耗,而引用是实参的别名,相当于我们对实参进行操作,省去了数据拷贝的过程。
2.引用可以直接改变实参,作为输入型参数的时候就不需要传递指针;输出型参数,函数中修改形参,实参也修改了。
void Swap1(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
void Swap2(int left, int right)
{
int tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 1, b = 2;
Swap1(a, b);
Swap2(a, b);
return 0;
}
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给 形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效 率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的 形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
引用传参,形参是实参的别名,不需要拷贝,也不开辟空间。
传值或者传址传参,要发生临时拷贝,形参的实参的拷贝。
//减少拷贝
//一般引用做参数都用const引用
void Func(const int& x)
{
}
int main()
{
int a = 0;
//权限平移
int& ra = a;
//指针和引用中赋值中,权限可以缩小,但不可以放大
const int b = 1;
//拷贝
a = b;
//我引用你,权限放大,不行
//int& rb = b;
//我引用你,我的权限缩小,可以
const int& rra = a;
//rra++;
a++;
//权限平移
const int& rrb = b;
Func(a);
Func(b);
Func(rra);
Func(rrb);
return 0;
}
指针和引用中赋值中,权限可以缩小,但不可以放大。
const引用做参数,有没有const修饰的变量都可以传递,这里只有权限平移或缩小。
常引用做参数:
我们知道引用做参数可以提高效率以及可以直接改变实参,那么如果我们只想提高效率而不改变参数可不可以用引用呢?比如我们我们要传递一个非常大的数据而又不行改变它,这时候用引用可以提高效率,但是安全性又得不到保障,那么这时候常引用的作用就体现出来了;在C++中,一般非输出型参数都是用常引用做参数,就在C语言中,我们传递的两个字符串,把第一个拷贝到另一个字符串中,原字符串就可以加const进行修饰,防止出错。
int main()
{
const int a = 10;
double d = 12.34;
cout << (int)d << endl;
const int& ri = d;//可以
return 0;
}
类型转换,提升,截断都会产生临时变量,临时变量具有常性。
语法上面,ra是a的别名,不开空间
底层实现,引用是使用指针
2.做返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << ret << endl;
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
对于第一、二个函数,n的值存放在静态区(代码段)中,函数栈帧销毁了之后n还在,可以用引用返回、也可以使用传值返回,传值返回是将n的值存放到一个临时变量中再返回。
对于第三个函数来说,函数调用结束,函数栈帧销毁,再使用传值返回就会越界访问,返回值就是一个不确定的值,所以不能这样使用。
对于第四个函数来说,传值返回的是n拷贝到临时变量的值(该过程发生在函数栈帧销毁之前)。所以没有问题。
对于上面第三个函数我们深入理解什么它是错的:
#include <iostream>
using namespace std;
int& Count()
{
int n = 0;
n++;
cout << "&n:" << &n << endl;
return n;
}
void Func()
{
int x = 100;
cout << "&x:" << &x << endl;
}
int main()
{
int& ret = Count();
cout << "ret=" << ret << endl;
Func();
cout << "ret=" << ret << endl;
cout << "&ret:" << &ret << endl;
cout << "ret=" << ret << endl;
return 0;
}c
从上面的结果可以看出,n,x,ret的使用的是同一块内存空间,我们要理解这段代码,首先需要知道函数栈帧销毁的本质是什么:
函数栈帧的销毁并不是把那块空间给销毁了,而是在这个栈帧里面的数据不被保护了,可能被其他数据占用,有以下几种情况:该空间没有被编译器分配给其他函数或变量使用,这在该空间的原来数据没有覆盖,我们还能够拿到该空间原来是的数据;该空间如果被分配给了其他函数,则该空间的数据将会被覆盖;如果没有被分配给其他函数使用,而是被编译器清理了,该空间的数据将是一个随机值。
对于上面我们的代码:1.Count函数中的变量n是一个局部变量,函数调用完毕之后,函数栈帧销毁,栈帧里面的数据将不被保护,我们这里用的是引用返回,返回的是n那块空间的数据。ret是Count函数返回值的引用,而返回值是局部变量n的引用,所以第一次打印n的值是1;第二次我们我们调用了Func函数,这两个函数栈帧的大小刚好相等,所以n看那一块空间被x=100覆盖打印出来ret的值是100;之后我们继续调用cout函数,导致那块空间原有的数据被覆盖,而ret是n的引用,所以cout打印的是n的那块空间的数据,所以打印出来的是一个随机值。三次打印应该都是一个随机值,毕竟这个一个错误的程序,我们需要了解它为什么是错的。
【注意】
内存空间销毁之后,空间还在,只是使用权不是我们的,我们存的数据不被保护,我们还能够访问,只是我们读写的数据都是不确定的。
内存申请和释放,就像住酒店。空间读写数据,就像在房间寄存的东西。
【结论】
出了函数作用域,返回变量不存在了,不能用引用返回,因为引用返回的结果是未定义的。
出了函数作用域,返回变量存在,才能用引用返回。
五、性能比较
1.传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
比如一下例子:
#include <iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
2.值和引用的作为返回值类型的性能比较
值和指针在作为传参以及返回值类型上效率相差很大。
比如下面的例子:
#include <iostream>
#include <time.h>
using namespace std;
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
六、引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比:
引用和指针的不同点:
1.引用概念上定义一个变量的别名,指针存储一个变量地址。
2.引用在定义时必须初始化,指针没有要求
3.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4.没有NULL引用,但有NULL指针
5.在sizeof中含义不同**:引用结果为引用类型的大小,但指针始终是**地址空间所占字节个数(32位平台下占4个节,64位平台下占8个字节)
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7.有多级指针,但是没有多级引用
8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9.引用比指针使用起来相对更安全
七、总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
ULL引用,但有NULL指针
5.在sizeof中含义不同**:引用结果为引用类型的大小,但指针始终是**地址空间所占字节个数(32位平台下占4个节,64位平台下占8个字节)
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7.有多级指针,但是没有多级引用
8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9.引用比指针使用起来相对更安全