引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
定义:类型&引用变量名(对象名) = 引用实体; 例如以下代码,在变量名前加一个
&
,意思是一个引用类型,b
是a
的别名,也就是a
有了一个外号,但还是a
本身:
int a = 70;
int& b = a; //引用:b是a的别名
我们接下来看看引用后的地址是否会发生改变: 例如以下例子:
int main()
{
int a = 70;
int& b = a; //引用:b是a的别名
int& c = a; //引用:c是a的别名
c = 80;
cout << a << endl;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
代码运行图:在这个代码中,定义了一个变量a
为70
,int& b = a
; 这里b
是a
的引用,给a
取了一个外号b
,int& c = a
; 这里c
是a
的引用,又给a
取了一个外号是c
,因此我们对c
还是对b
进行修改,a
都会发生改变,这是因为编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
注意:引用类型必须和引用实体是同种类型的
int a = 10;
int& ra = a; // 正确,ra初始化为a
int& ra; // 错误,引用必须在定义时初始化
int a = 10;
int& ref1 = a;
int& ref2 = a;
ref1++; // a的值变为11
cout << a << endl; // 输出11
ref2--; // a的值变为10
cout << a << endl; // 输出10
pp
可以改变p
指向的地址而引用更像一个const
指针:定义后不能改变指向的对象,就像const
指针定义后不能改变指向
但可以通过这个“const
指针”来修改原对象,就像通过const
指针可以修改原对象
int a = 10;
int b = 20;
int& ref = a;
ref = b; // 错误!引用ref已经引用a,不能再引用b
cout << ref << endl; // 输出10,ref依然引用a
如图:ref引用了a,这里的值发生改变是因为b赋值给了ref
当引用用来做参数时将会对代码进行大大的优化,并且具有可读性,如:当我们看了很多遍的交换了两个数的函数:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int ra = 88;
int rb = 99;
Swap(&ra, &rb);
return 0;
}
形参是实参的一份临时拷贝,所以如果想交换需要,传地址,不能传值。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int ra = 88;
int rb = 99;
Swap(ra, rb);
return 0;
}
a
和b
分别是ra
和rb
的别名,当你调换a
和b
的纸时,其实是修改了ra
和rb
的地址的值,这样的好处就是,当你看代码时,引用a
和b
给人一种感觉,就是操作ra
和rb
本身。这隐藏了底层是通过地址操作原变量ra
和rb
的实现细节。从使用者的角度看,代码读起来就像直接交换ra
和rb
,而不是通过复杂的地址操作实现。
这里使用了引用挺好的,不用担心指针的解引用,地址相关操作,但是,前面我们知道,引用一旦指向一个实体,就无法改变指向,例如,有关链表操作,当我们要删除一个节点,是不是要改变前面节点的指针,让他指向后面节点,而引用恰恰不能改变,因此,引用也不是完全替代指针的
回归正题,这里还有一个小注意点:作用域的不同,因此,在Swap函数里,取别的名字都可以,任由发挥,结果都相同。
void Swap(int& x, int& x)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int ra = 88;
int rb = 99;
Swap(ra, rb);
return 0;
}
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
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 < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++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;
}
按值传递(TestFunc1):
调用
TestFunc1(a)时,会将
a进行拷贝,生成一个临时对象**a_copy**。**a_copy**作为参数传递给TestFunc1
。TestFunc1
内部操作的实际上是a_copy
,对a_copy
的修改不会影响实参a
。TestFunc1
返回时,临时对象a_copy
会被销毁。TestFunc1
以值方式传递结构体A
作为参数。这会导致每次调用都会对A进行值拷贝,对于一个包含10000
个int
成员的大结构体,拷贝开销很大。
按引用传递(TestFunc2):
调用TestFunc2(a)
时,不会进行值拷贝,直接传递a
的引用。TestFunc2
内部操作的仍然是实参a
本身。TestFunc2
返回时,不需要销毁任何对象。TestFunc2以引用方式传递A。这种方式下,函数内直接操作的就是实参a本身,不会有任何拷贝开销。
总结:
TestFunc1
值传递,效率低是因为值拷贝开销大TestFunc2
引用传递,效率高是因为避免了值拷贝,直接操作的就是实参a
本身
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
我们先看看下面代码会输出什么结果?
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;
}
在Vs编译运行图:结果是7,真的是正确吗?
问题分析: 如果函数返回时,返回的对象已经超出了函数作用域(即已经被销毁),那么不能返回该对象的引用,必须返回值。
在第一个示例中:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
这里函数返回了局部变量
c
的引用,但c
在函数返回后就已经被销毁了,所以这是一个未定义行为,输出结果是不确定的。而在第二个示例中:
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;
}
这里同样是返回了局部变量c
的引用,但是在main
函数中又调用了一次Add
函数,这时第一次调用返回的引用ret
已经指向了一个不存在的对象,所以输出结果也是未定义的。
函数返回引用时必须确保返回的对象在调用者作用域内仍然存在,否则就会产生未定义行为。这是
C++
中函数返回引用需要特别注意的地方。
答案思考:
在Visual Studio
上运行这段代码,输出结果是:
Add(1, 2) is :7
这个结果确实是未定义行为,但在某些情况下可能会输出7
。之所以会出现这种情况,是因为Visual Studio的编译器在处理这种未定义行为时可能会做一些特殊的优化或处理,导致在某些环境下能够得到一个看似合理的结果。但这种行为是不可靠的,因为它依赖于具体的编译器实现细节。在不同的编译器或环境下,输出可能会完全不同。
正确的做法:是要么返回值,要么返回一个在调用者作用域内仍然存在的对象的引用。这样可以确保代码的行为是可预测和可移植的。
&
符号声明,如int& ref = x;
*
符号声明,如int* ptr = &x;
ref = 10;
*
)才能访问其指向的实体,如*ptr = 10;
0x0
。让我们看看例子来说明引用和指针的区别:
假设我们有一个整型变量x
,值为10。
使用引用:
int x = 10;
int& ref = x; // 声明引用ref,它是x的别名
ref = 20; // 通过ref修改x的值
cout << "x = " << x << endl; // 输出 x = 20
ref
是x
的引用,它们共享同一块内存空间。通过ref
修改值,实际上是在修改x
的值。 输出x
的值为20,因为x
的值已经被修改了。
使用指针:
int x = 10;
int* ptr = &x; // 声明指针ptr,存储x的地址
*ptr = 20; // 通过ptr解引用并修改x的值
cout << "x = " << x << endl; // 输出 x = 20
ptr
是一个指向x
的指针,存储了x
的内存地址。通过*ptr
解引用并修改值,实际上是在修改x
的值。输出x
的值为20,因为x
的值已经被修改了。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看下引用和指针的汇编代码对比得出:在汇编中引用的底层逻辑还是指针,经过编译转换成汇编,还是进行指针的操作
引用和指针的不同点:
sizeof
中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4
个字节)从上述代码中,我们可以得出以下关于常引用的结论:
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
对于常量对象a
,我们可以使用常引用const int& ra = a;
来引用它。这样做可以避免对常量进行修改,直接使用非常引用int& ra = a;
会在编译时报错,因为不允许对常量进行非常引用。
2. 字面量常引用:
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
我们可以使用常引用const int& b = 10;
来引用字面量常量。这样做可以避免创建临时变量, 直接使用非常引用int& b = 10;
会在编译时报错,因为字面量不能被非常引用。
3. 类型不匹配的常引用:
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
根据类型不同的变量,如double d = 12.34;
,我们可以使用常引用const int& rd = d;
来引用它,直接使用非常引用int& rd = d;
会在编译时报错,因为类型不匹配。