引用
引用可以对别名进行引用!
#include<iostream>
using namespace std;
int main()
{
int a = 0; // 李逵
int& b = a; // 铁牛
int& c = b; // 在铁牛的基础上取名为黑旋风
return 0;
}
引用的特性:
- 引用在定义的时候必须初始化;
- 一个变量可以有多个引用(可以对引用再进行引用);
- 引用一旦引用一个实体,再不能引用其他实体
java当中没有指针! 因此每个节点中存放的是下一个节点的引用!
C++不能这样实现,因为C++中的引用是不能被修改的!
同一个域的引用不能同名;不同域的引用可以同名!
引用的使用场景
-
做参数(输出型参数)
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
- 输出型参数:此时形参的改变会影响实参(引用)
- 输入型参数 :形参的改变不会影响实参!
struct + 结构体得名字才是结构体的类型!(C语言中)
但是C++可以直接使用结构体的名字!
引用做参数还可以提高效率(后面详解)(大对象/深拷贝对象)
#include<iostream>
using namespace std;
void swap(int& p, int& q)
{
int tmp = p;
p = q;
q = tmp;
}
int main()
{
int aa = 10; // 李逵
int bb = 20;
int& a = aa; // 铁牛
// int& c = b; // 在铁牛的基础上取名为黑旋风
int& b = bb;
cout << aa <<' '<<bb<< endl;
//cout << bb << endl;
swap(aa, bb);
cout << aa << ' ' << bb << endl;
return 0;
}
做返回值
分析下面两个代码:
int Count1()
{
int n = 0;
n++;
// ...
return n;
}
(n拷贝给临时变量,临时变量再拷贝给ret!)
不能直接返回n的值,因为n在栈区,出作用域直接销毁,因此会创建临时变量!(详细解释如下:)
中间会生成临时变量,临时变量可能由寄存器代替,会把临时变量放到寄存器里面,作为表达式的返回值,再给ret!
但是寄存器一般只有4/8个字节,数据量大就放不下!
在C/C++中,当你从一个函数返回一个局部变量(如n)的值时,编译器会创建一个临时变量来存储这个值。这个临时变量通常存放在栈区(stack)中。
具体来说,当函数Count1执行完毕后,局部变量n会被销毁,但在返回时,编译器会将n的值复制到一个临时变量中。这个临时变量的生命周期会持续到它被使用完为止,通常是在调用函数的上下文中。
因此,虽然n在函数结束时会被销毁,但返回的值(即临时变量的值)仍然可以在调用该函数的地方使用。这个临时变量的存储位置仍然是在栈区,直到它不再被需要。
int Count2()
{
static int n = 0;
n++;
// ...
return n;
}
此时n位于静态区,出作用域不会被销毁!
那么此时n会创建临时变量吗?
会!编译器不看变量在栈区还是静态区,只要是传值返回,都会创建临时变量!(傻瓜处理)
此时,用引用做返回值就不会生成临时变量!
- 减少拷贝,提高效率(大对象影响很大)
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
值和引用的作为返回值类型的性能比较:
#include <time.h>
#include <iostream>
using namespace std;
struct A { int a[10000]; }; // 定义结构体A,包含一个大小为10000的整数数组
A a; // 创建一个全局变量a,类型为A
// 值返回
A TestFunc1() { return a; } // 定义一个函数TestFunc1,返回全局变量a的副本
// 引用返回
A& TestFunc2() { return a; } // 定义一个函数TestFunc2,返回全局变量a的引用
void TestReturnByRefOrValue() {
// 以值作为函数的返回值类型
size_t begin1 = clock(); // 记录开始时间
for (size_t i = 0; i < 100000; ++i) // 循环调用TestFunc1 100,000次
TestFunc1();
size_t end1 = clock(); // 记录结束时间
// 以引用作为函数的返回值类型
size_t begin2 = clock(); // 记录开始时间
for (size_t i = 0; i < 100000; ++i) // 循环调用TestFunc2 100,000次
TestFunc2();
size_t end2 = clock(); // 记录结束时间
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time: " << end1 - begin1 << " ms" << endl; // 输出TestFunc1的执行时间
cout << "TestFunc2 time: " << end2 - begin2 << " ms" << endl; // 输出TestFunc2的执行时间
}
int main() {
TestReturnByRefOrValue(); // 调用性能测试函数
return 0; // 返回0表示程序正常结束
}
分析下面用引用做返回值的代码 :
这里ret与n的关系是拷贝! (栈帧的销毁不会影响ret)
这个代码的输出结果为随机值!
n所在的空间已经被销毁!(归还它的使用权) 此时访问的数据的结果是不确定的!访问的是随机值!
- 如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的结果是侥幸正确的!
- 如果Count函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值!
相当于访问的是野指针!
int& Count2()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
//int ret2 = Count2();
int& ret2 = Count2();
cout << ret2 << endl;
return 0;
}
这种情况下:count()等于n的别名,ret2是count2的别名,相当于给别名取别名!
此时ret2也是n的别名!
- ret是n的别名,当函数调用结束后,栈帧销毁,如果没有清理栈帧,此时两次打印ret的值为11和21;(访问的是一个已经被销毁的空间的变量);
- 第二次调用函数所占的空间一样大,将原来的10覆盖为20,
- 如果栈帧被清理,此时打印的就是随机值!
但是如果此时调用任何一个其他的函数,ret的值就为一个随机值!
因为调用下一个函数的时候,需要建立函数栈帧,正好将上面的空间覆盖,覆盖后为什么样子不确定,此时再访问这块空间就为一个随机值!(再次调用count()是在同一块空间的同一块位置将其值从10覆盖为20)
因此!上面的做法不可取!返回局部作用的变量的引用非常危险!
我们采用以下做法:
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
此时栈帧销毁,n位于静态区,n一直存在,不会影响返回!
总结:
- 基本任何场景都可以使用引用传参;
- 谨慎用引用做返回值,出了函数作用域,对象不在了,就不能引用返回,还在就可以用引用返回!
可以使用引用返回的:static修饰的,全局变量、malloc开辟的,常量值.
常引用
第一种
const int a = 0;
int& b = a;
这种写法是错误的!权限不能扩大!a的值为常量不能改变 ;
第二种
const int c = 0;
int d = c;
这种写法可以!因为c拷贝给d,此时d的改变不影响c;
第三种
int main()
{
int a = 10;
int& b = a; // 平移
const int& z = a; // 缩小
a++;
z++; // 这种写法是错误的,z不能修改!
cout << a << endl;
cout << z << endl;
return 0;
}
引用过程中,权限可以平移或者缩小,但是不能放大!
这里a++的时候z也能++,但是不能使用z++!
第四种:
const int& m = 10;
权限进行了平移,两边都不能修改!
第五种:
double a = 1.11;
int b = a;
int& c = a; // 这种行不通!!!
const int& d = a;
double& e = a;
b实际上是强制类型转换,发生类型转换(类型提升,截断等)的时候会产生临时变量,类似于传值返回!
实际上是把dd给临时变量,再将临时变量给ri,因此,中间会产生一个int的临时变量!
对应的,下面的实际上是将dd给临时变量,再把临时变量给rii!
临时变量具有常性!相当于被const修饰!
因此这样子不满足不是因为类型不行,而是因为权限放大!
第六种
上面这种写法不可取!因为func1()返回的是一个临时变量,临时变量具有常属性!此时这样子做会造成权限的方法!
解决方法:前面加上const即可!
当函数返回一个基本数据类型的值时,返回的是该值的副本。这个副本是一个临时变量,不能被修改,所以我们一般用一个变量来接受这个函数的返回值!(传值返回)
第七种:
上面这两种都可以!(没有产生临时对象)
分析下面这种特殊情况:
当j和i进行比较的时候,实际上i的值没有发生改变,但是会产生一个临时变量,将i转化为double类型的临时变量,此时再用这个临时变量和j进行比较!
指针和引用的区别
语法层面上:
int main()
{
int a = 10;
// 语法层面上,不开空间,是对a取别名
int& b = a;
b = 20;
// 语法层面上,开空间,存储a的地址
int* ps = &a;
*ps = 30;
return 0;
}
先看指针:
- lea是取地址的意思:取a的地址放到eax中(eax是一个寄存器);
- 然后把eax的值放入pa中,pa存放的是地址;(内存的速度太慢了,一般都会借助寄存器,缓存当中转!)
- 解引用的时候将pa的值放到eax中;再对eax解引用将30放进去!
可以发现:从底层汇编指令实现的角度来看:引用是类似指针的方法实现的!
语法层面上我们认为引用不开辟空间,但是从底层来看引用还是会开辟空间!(底层和上层可能是不一样的!)
引用和指针的区别:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址;
2. 引用在定义时必须初始化,指针没有要求;
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体;
4. 没有NULL引用,但有NULL指针;(引用不能指向 NULL
是因为引用必须始终绑定到一个有效的对象,以确保安全性和简洁性。引用的设计旨在避免指针所带来的复杂性和潜在错误,因此不允许引用指向 NULL
。如果需要表示“无对象”的状态,应该使用指针。)
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节);
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
7. 有多级指针,但是没有多级引用 ;
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理 ;
9. 引用比指针使用起来相对更安全 ;