引用
1 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
也就是说,给出下面这段代码,变量a,b,c的地址是相同的:
int main()
{
int a = 0;
int& b = a;
int& c = b;
return 0;
}
2 引用的特性
- 引用在定义时必须初始化
int main()
{
int a = 1;
//1、引用必须在定义的时候初始化
int& b;//这样写是错的
return 0;
}
- 一个变量可以有多个引用
//先前给的代码就有这个特性
int main()
{
int a = 0;
int& b = a;
//2、一个变量可以有多个引用
int& c = b;
return 0;
}
- 引用一旦引用一个实体,再不能引用其他实体(和指针区分开)
int main()
{
int a = 1;
//此时b为a的引用变量
int& b = a;
int c = 5;
//将c的值赋值给b,而不是使b变成c的引用变量
b = c;
return 0;
}
3 常引用
3.1 权限放大/缩小
情形一:
如果是下面这种情形,编译不通过
void TestConstRef()
{
const int x = 5;
//int& y = x;//x为常量,编译不通过
}
要想为const int x
取别名就需要用常引用
const int x = 5;
//int& y = x;//x为常量,编译不通过
const int& y = x;
情形二:
但如果是下面这种情形,编译又能通过
int a = 10;
const int& b = a;
对比情形一、二,我们能总结出取别名原则:对原引用变量,权限只能缩小或者不变,不能放大
void TestConstRef()
{
const int x = 5;
//int& y = x;//x为只读,y为读写,权限放大
const int& y = x;//权限不变
int a = 10;
const int& b = a;//a为读写,b为只读,权限缩小
}
情形三:
如果想为常量取别名,就必须在引用变量前加上const
//int& c = 20;//该语句编译时会出错,b为常量
const int& c = 20;
情形四:
要为变量取别名,引用变量和原引用变量的类型必须相同
double d = 10.5;
//int& rd = d; // 该语句编译时会出错,类型不同
但如果rd
被const
修饰,编译却能通过,不过编译器会提示:从“double”
转换到“const int”
,可能丢失数据
double d = 10.5;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
c++是在c语言的基础上发展的,因此延续了c语言的一些特性,如变量的隐式类型转换:int float double char
这些变量类型虽然不同,但是可以相互赋值(大给小截断,小给大提升)
而浮点数和整形数的存储机制是不一样的,不是简单的截断可以实现。如浮点型变量转换成整型变量则需要通过一个临时变量,将浮点数部分给丢掉。
也就是说之所以const int&
类型的rd
能对double
类型的d
进行引用,是因为rd
其实并不是对d
进行引用,而是对其临时变量进行引用。
4 使用场景
4.1 做参数
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
void Swap(double& x, double& y)
{
double tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 2, b = 3;
Swap(a, b);
double c = 1.1, d = 2.2;
Swap(c, d);
return 0;
}
用引用进行穿参的意义是什么呢?
以值作为参数
或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
以引用作为参数
相当于传递了实参(与指针相似),也就节省了拷贝的时间,提高了效率。
我们可以写一个程序,通过较大的数据以两种穿参方式多次调用函数进行比较二者之间的效率差距。
#include <time.h>
//定义一个大小为40000字节的结构体类型
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;
}
输出结果如下:
由此可见,通过形参传值,在这样大量的数据的情况下下调用函数,由于需要对数据进行拷贝而导致效率低下;而通过引用传值,大大提高了程序的效率,这就是引用传参的意义。
4.2 做返回值
int& Count()
{
int n = 0;
n++;
cout << "&n:" << &n << endl;
return n;
}
int main()
{
int& ret = Count();
cout << "&ret:" << &ret << endl;
return 0;
}
上面这段代码,我们可以理解成在返回值的时候也会产生一个临时变量,我们给它取个名字叫tmp,不过tmp的类型是int&,也就是n的别名,然后再传给ret,此时ret就是tmp的别名,也就是n的别名。我们可以运行程序查看结果,发现ret和n的地址是相同的
。
传值返回:会有一个拷贝。
传引用返回:没有拷贝,函数直接返回变量的别名。
上面这段代码还有一个问题:Count()调用结束之后,为Count()函数开辟的栈帧已经销毁了,也就是说我们是非法访问。而非法访问并不是说将先前Count()函数的空间摧毁,而是使用权不再属于Count()函数,也就随时有可能被覆盖,下面为大家举一个例子(根据不同的编译器产生的结果也不同,这里博主使用的是VS2019)。
int& Count()
{
int n = 0;
n++;
cout << "&n:" << &n << endl;
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;//第一次打印是1
cout << "&ret:" << &ret << endl;
cout << ret << endl;//第二次打印是随机值
return 0;
}
至于为什么会这样,我们可以理解成main()函数中的cout
语句时需要调用函数,而调用函数所创建的栈帧将原来为Count()函数创建的栈帧覆盖了。
至于是什么时候才覆盖,还需要看运气:执行cout语句的时候,和其他函数一样是需要传参
的,而ret就是它的参数,此时ret还没有被覆盖掉,因此第一次一定能输出正确值。直到第一次开始执行cout语句的时候才将原来的栈帧覆盖掉。
因此是第一次执行时输出结果是n的正确值1,而第二次是随机值。
再举一个有意思的例子:
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
想必在理解了上面的栈帧覆盖的问题后,对于这个结果并不感到奇怪:在第一次调用Add()函数的时候,c的值是3,调用结束后栈帧摧毁,此时ret是c的别名;然而第二次调用的时候,c的值被覆盖成7,因此输出结果是7。
由此可见:传引用返回的意义是减少拷贝,但又不是所有情况都可以传引用返回,比如我们刚刚所举的两个例子。
再举个可以使用引用返回的例子:
int& Add(int a, int b)
{
static int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
cout << "Add(1, 2) is :" << ret << endl;
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
上面的代码中,变量static int c
是存放在静态区中的,并不会因为栈帧的覆盖而改变其值,因此三次打印结果都为7.
结论:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
5 引用和指针的区别
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
语法的角度:
引用变量ra是a的别名,没有独立空间,和其引用实体共用同一块空间。
指针pa存储a的地址,开辟了4/8个字节。
但是在底层的角度确是相同的:
就像是相同的原料在不同的品牌商的手下加工,而给人带来的感觉不同。
引用和指针的不同点:
1. 引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型 实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全