您现在的位置是:首页 >技术教程 >【详解C++中的引用】网站首页技术教程
【详解C++中的引用】
一、什么是引用
引用就是给一个变量取别名。
注意:这个引用不会新开辟一块空间,而是和原来的变量公用一块空间。
举个例子:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
二、引用规则
引用规则:引用实体类型+&+引用别名 = 引用实体。
比如下面:
int main()
{
int a = 10;
//引用
int& ba = a;
ba = 20;
printf("%d ", a);
return 0;
}
上面代码为例:
引用对象类型是int + & + 引用别名(ba) = 引用对象(a)
C++中的 “&”符号跟类型在一起是不在是取地址,而是”引用“。
现在ba这个就是a的别名,和a是同一块内存空间,改变了ba的内容,就等于改变了a的内容。
同时,一个变量可以有多个引用。
相当于一个人可以有多个别名一样。
就像是:假如你在家被叫做小红,在外面被叫燕燕。你的妈妈叫小红吃饭,然后小红去吃饭了,那燕燕是不是也吃了,你是不是也吃了呢?
三、引用特性
1.引用类型必须是和引用实体是同一类型。
比如:
报错的原因是:引用对象的类型和引用实体的类型不一致。
引用的对象a是int类型,而给它取别名却是double类型,这是不允许的。
2.引用在定义时必须初始化
比如:
这也是不允许的。
3. 引用一旦引用一个实体,就不能引用其他实体。
比如:
int main()
{
int a = 10;
//引用
int& pa = a;
int x = 20;
pa = x;
return 0;
}
这段代码,有错误吗?
没有错误,但是pa = x这行代码,并不是引用更改,因为前面已经提过:
引用一旦引用一个实体,就不能引用其他实体。
所以这行代码的意思是:将x 的值赋值给pa。
此时a的值是20了。
四、使用场景
1.做函数参数
引用可以做函数的参数,作用相当于指针,可以改变变量本身。
比如:
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d ,b = %d
", a, b);
Swap(a, b);
printf("交换后:a = %d ,b = %d
", a, b);
return 0;
}
传参传一个实参过去,但是接收时使用引用,此时该引用就是形参的别名,形参的别名改变会改变形参本身。
这个是使用引用一个好的地方。
做返回值的作用:
1.做输出型参数,节省空间。
我们传形参时,用引用接收,也不再需要使用地址,不再需要开辟指针空间来接收,直接改变引用即可改变形参。
2. 提高效率,当传过来的形参是大对象/深拷贝对象时,能够极大地提高效率。
2.做返回值
先看不用引用做返回值,用普通的返回值:
案例1,错误代码
int test()
{
int n = 10;
n++;
return n;
}
int main()
{
int ret = test();
printf("%d
", ret);
return 0;
}
这样的返回值是我们常见的返回值。
这里需要提一点:
test函数并不是直接返回n的。
因为test函数在调用结束后会销毁它所在的栈帧,连同n会一起销毁,所以编译器会先保存n的值到一个寄存器中,再销毁栈帧,然后返回寄存器的值给ret。
过程如下:
所以就出现了一个问题。
当我们用上面的代码,返回的是n的引用(别名)时,这就不安全了。因为返回的是n的引用,不会创建临时空间给n,而是直接返回n。 但是返回之后n所在的函数栈帧会被销毁,所以连同n一起销毁了,但是此时ret是n这块已经不属于自己的空间的拷贝,所以ret是违法的。
打印出来的ret,可能是随机值,也可能是n原来的值,但如果是n原来的值,这只是侥幸,因为n原来的空间暂时没有被使用。但如果n这块空间被其他函数使用了,此时ret就有可能是随机值。
所以在上面的例子中,不能使用引用来返回。
再看下面的例子:
案例2 ,正确代码
int& test()
{
static int n = 10;
n++;
return n;
}
int main()
{
int ret = test();
printf("%d
", ret);
return 0;
}
注意,n是被static修饰过的变量。此时可以用引用进行返回了,因为函数test的销毁不会销毁n,n是在静态区开辟的空间,而函数是在栈区开辟的空间,两者互不影响。
返回n的引用是绝对安全的。
再看下面:
案例3,错误代码
int& test()
{
int n = 10;
n++;
return n;
}
int main()
{
int& ret = test();
printf("%d
", ret);
return 0;
}
此时与案例二相似,但是n不是被static修饰过的,而且ret也是引用,相当于返回n的引用后,再用引用接收n的引用,此时ret也还是n的别名,而n是在栈区开辟的空间,销毁后,此时n的空间不再属于自己,打印ret,相当于打印不属于自己的n,这是违法的行为。
当这段栈空间被其他东西用之后,n的值可能是随机值了。
再看下面的案例:
案例4,正确代码
int& test()
{
static int n = 10;
n++;
return n;
}
int main()
{
int& ret = test();
printf("%d
", ret);
return 0;
}
此时n 是被static修饰过的,所以栈帧空间的销毁不会影响n,打印ret(ret是n的引用),会正确打印出来。
总结:
1.使用引用做返回值时,引用返回不会开辟临时空间保存返回值
2.而不管改变量是在栈区还是在静态区,不用引用都会开辟临时空间保存返回值。
但是使用引用必须保证返回值是绝对的安全。
五、常引用
1.在引用的过程中,权限不能放大,只能缩小或平移。
1.在引用的过程中,权限不能放大,只能缩小或平移。
1.在引用的过程中,权限不能放大,只能缩小或平移。
案例1:
错误示例
int main()
{
//权限不能放大,不正确
const int a = 0; // 常变量,不可修改
int& b = a; // 起了一个别名,必须也是常引用
return 0;
}
a是一个被const修饰后的常变量,不可修改。
而b是a的引用,此时b并没有被const修饰,意味着b可以修改,但是a已经不能被修改了,b是a的别名同样不可修改,这是规定。
所以权限不能放大
案例2:
正确实例
int main()
{
const int a = 0; // 常变量,不可修改
//权限的平移
const int& b = a;
return 0;
}
变量a 和引用b都被const修饰了,它们的权限是一样的,所以权限可以平移。
案例3:
正确案例:
int main()
{
//权限可以缩小
int g = 0;
const int& h = g;
return 0;
}
g是一个变量,h是g的引用,但是h被const修饰后,意味着h不能修改。
所以这是一个权限的缩小。
2.只要发生类型转换,都会产生临时变量。
而临时变量不可修改,具有常性。
2.只要发生类型转换,都会产生临时变量。
而临时变量不可修改,具有常性。
2.只要发生类型转换,都会产生临时变量。
而临时变量不可修改,具有常性。
类型转换包括:隐式类型转换,截断,整型提升等。
比如:
int main()
{
double d = 1.1;
int a = d;
return 0;
}
这里会发生隐式类型转换。d是double类型拷贝给a ,会在中间生成一个int类型的临时变量,然后把d放入该临时变量中,然后该临时变量再拷贝给a。所以把d拷贝给a是不会改变d本身的。
再看下面的代码:
错误示例
int main()
{
double d = 1.1;
int& a = d;
return 0;
}
为什么a不能作为d的引用?
本质上还是权限放大的问题。
d是double类型的变量,a是int类型的引用,中间创建一个int类型的临时拷贝,而临时拷贝具有常性,给了a之后,a是引用,不具有常性,就相当于权限的放大。
正确示范
int main()
{
double d = 1.1;
const int& a = d;
return 0;
}
加了const修饰后,引用a就具有了常性,相当于权限的平移了。
ps:为什么类型转换会产生临时变量?
看下面的例子:
int main()
{
int a = 1;
double b = 1.1;
if (b > a)
{
cout << "hehe" << endl;
}
return 0;
}
此时会打印hehe,因为表达式左右两边如果类型不同,会发生整型提升或截断。
这里b和a相比,a会发生整型提升到double类型再与b比较。
而整型提升的过程会生成一个临时变量,这个临时变量就是a提升后的结果(并不是a本身提升)。
所以比较前后a和b的值都不会发生改变。
六、引用和指针的区别
从汇编的角度看,引用的底层也是用指针实现的。
但是在语法层面,引用不开空间,指针会开辟一块空间。
总结
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
int main()
{
int a = 10;
//指针存储a的地址
int* pa = &a;
//b是a的引用(别名)
int& b = a;
return 0;
}
- 引用在定义时必须初始化,指针没有要求。
int main()
{
int a = 1;
//引用必须初始化
int& b = a;
//指针可不初始化
int* p;
return 0;
}
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
int main()
{
int a = 10;
int b = 20;
int& d = a;
//指针可以改变指向的对象
int* p = &a;
p = &b;
//但是引用不可改变指向的对象
//这个并不是改变引用d的实体对象,而是把b拷贝给d
d = b;
return 0;
}
- 没有NULL引用,但有NULL指针。
int main()
{
//可以存在空指针
int* p = NULL;
//不存在空引用,引用必须初始化一个实体
//错误代码
int& b;
return 0;
}
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
int main()
{
double a = 10;
double* p = &a;
cout << sizeof(p) << endl;
double& b = a;
cout << sizeof(b) << endl;
return 0;
}
-
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
-
有多级指针,但是没有多级引用。
-
访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
-
引用比指针使用起来相对更安全。