目录
传值,传地址,传引用区别:
传值,传地址都理解,传值就是将一个结果传回去,传地址则是将一个变量的地址传回去。那传引用是什么?
下面这位好汉是李逵,小名铁牛,江湖人称“黑旋风”,这些不同的名字都指代同一个人。
我们每个人都有正名,小时候有乳名,写信有笔名,朋友给我们取的绰号……这些也都是指代着同一个人,只是称呼换了一个而已,但本质没有变。
引用:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用表示方法:类型& 引用变量名(对象名) = 引用实体;
为了更加直接的理解引用,我们来看一下下面第一段代码
注意:引用类型必须和引用实体是同种类型的
可以看见两个的地址是一样的,没有开辟新的内存空间,是同一片空间的不同表示方式。
引用特性:
-
引用在定义时必须初始化
-
一个变量可以有多个引用
-
引用一旦引用一个实体,再不能引用其他实体
我们可以看到这里引用没有进行初始化就报错了
可以看到,对一个实体进行多次引用,但已然表示同一片空间,而且对引用变量再进行引用依然还是表示同一片空间
接下来大家可以看一下这一段代码的结果
int main()
{
int a = 10;
int& ra = a;
cout << "a -->" << &a << endl;
cout << "ra -->" << &ra << endl;
int b = 20;
ra = b;
cout << "ra -->" << &ra << endl;
return 0;
}
他们还是指向同一片空间,这也证实了,引用不能引用其他实体,这也是引用与指针较大的一个区别
那么此刻,ra的值是多少呢?
接下来让我们看一下他的值
可以看到,虽然不能指向另一片空间,但为什么值却是b的值呢?是因为在这里是将b的值赋给ra,同时也赋给a,而并不是引用,所以ra的值也跟着改变了
常引用
那么引用出了引用变量,那能常引用吗?答案是肯定的。
接下来让我们看一段代码,大家来猜猜这段代码正确吗?
int main()
{
int& a = 10;
cout << "a = " << a << endl;
return 0;
}
答案是错误的,这里的常引用指的不是引用常量,而是引用由const修饰的常变量,那么接下来让我们看以下两段段代码,大家来猜一猜看哪段代码是正确的。
// 代码1
int main()
{
const int a = 10;
int& ra = a;
cout << "ra = " << ra << endl;
return 0;
}
// 代码2
int main()
{
const int a = 10;
const int& ra = a;
cout << "ra = " << ra << endl;
return 0;
}
答案揭晓:第二段代码是正确的。
那么是为什么呢?const修饰的整形是不能修改的,而int& 代表着原类型是int型,是一个可修改的类型,就与常引用形成了矛盾,那么在进行常引用的时候,我们只要加上const进行修饰限制,此处就不会报错了。
使用场景
1、做参数
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10;
int b = 20;
cout << "a = " << a << endl;
cout << "b = " << b << endl << endl;
Swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl << endl;
return 0;
}
2、做返回值
我们来看一下下段代码,来猜一下输出结果会是多少呢?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
答案是3
接下来我们再提升下难度,这段代码的结果是多少呢?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
诶?怎么成7了呢?ret是引用的Add(1, 2) 返回的实体啊,怎么成7了呢?接下来用一张图来阐述以下在这期间发生了什么。
那么接下来我们再来修改以下代码:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
printf("\n");
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
此时的值又会发生什么变化呢?
为什么第二次的输出值变成随机值了呢?
通过上一幅图来分析
在第一次调用完,将栈帧释放后,此时的ret的引用实体c地址还没有被覆盖,第二次调用之后仍然没有被覆盖,但当此时再去调用printf的时候,系统也会创建栈帧,此时栈帧的大小与Add函数开辟的栈帧大小也绝大可能是不相同的,也就意味着,原先c处地址块极大概率会被覆盖,从而就诞生了随机值。
那么接下来我们一起看一下值传递与引用传递在时间上的差距:
struct A{ int a[10000]; };
void Func1(A a){};
void Func2(A& a){};
void TimeTest()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
Func1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
Func2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TimeTest();
return 0;
}
时间差距还不是很明显,我们再将数据放大至10W
struct A{ int a[100000]; };
可以看见传值大时间消耗远远大于传引用的时间消耗,这是因为传值会将数据一个一个的保存在栈帧里面,以及开辟空间的时间消耗,因此增加了时间消耗,而传引用不会开辟新空间来保存数据,他指向原数据,因此没有开辟空间的时间消耗,所以传引用的时间消耗远远小于传值的时间消耗。
引用和指针的区别:
在语法上引用没有开辟新空间,而是别名,但是在底层汇编的角度上,引用也是一个特殊的指针,是按照指针的方式来实现的,其存储的地址引用实体的地址
引用和指针的不同点:
-
引用概念上定义一个变量的别名,指针存储一个变量地址。
-
引用在定义时必须初始化,指针没有要求
-
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
-
没有NULL引用,但有NULL指针
-
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
-
有多级指针,但是没有多级引用
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
-
引用比指针使用起来相对更安全
总结:
传值会创建独立空间,在函数调用结束释放栈帧的时候,会通过临时变量将值传送回来,其地址就具有了不可测性,一旦栈销毁就无法溯源,并且比传引用消耗更多时间与空间
传地址于传引用相比最大的区别就是写法上不同,引用能实现的功能指针也能实现
传引用可以通过返回值来修改原值,但是当引用实体地址被覆盖的时候,这种方法就会失误,会导致随机值。